btcpayserver/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs

728 lines
37 KiB
C#
Raw Normal View History

2020-06-29 04:44:35 +02:00
using System;
2022-07-22 15:41:14 +02:00
using System.Collections.Generic;
2021-12-27 05:46:31 +01:00
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
2022-07-22 15:41:14 +02:00
using System.Threading;
2018-04-03 04:50:41 +02:00
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
2022-07-22 15:41:14 +02:00
using BTCPayServer.Abstractions.Models;
2022-07-18 20:51:53 +02:00
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
2022-07-22 15:41:14 +02:00
using BTCPayServer.Controllers;
2022-07-18 20:51:53 +02:00
using BTCPayServer.Data;
2022-07-22 15:41:14 +02:00
using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.Forms.Models;
2022-07-22 15:41:14 +02:00
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
2022-07-18 20:51:53 +02:00
using BTCPayServer.Plugins.PointOfSale.Models;
2023-04-05 15:42:23 +02:00
using BTCPayServer.Services;
2018-04-03 04:50:41 +02:00
using BTCPayServer.Services.Apps;
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
using BTCPayServer.Services.Invoices;
2022-07-18 20:51:53 +02:00
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
2022-07-22 15:41:14 +02:00
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
2023-02-23 09:52:37 +01:00
using NBitcoin;
using NBitcoin.DataEncoders;
2022-07-22 15:41:14 +02:00
using NBitpayClient;
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
using Newtonsoft.Json.Linq;
2022-07-22 15:41:14 +02:00
using NicolasDorier.RateLimits;
using StoreData = BTCPayServer.Data.StoreData;
2018-04-03 04:50:41 +02:00
2022-07-18 20:51:53 +02:00
namespace BTCPayServer.Plugins.PointOfSale.Controllers
2018-04-03 04:50:41 +02:00
{
2022-07-18 20:51:53 +02:00
[AutoValidateAntiforgeryToken]
[Route("apps")]
public class UIPointOfSaleController : Controller
2018-04-03 04:50:41 +02:00
{
2022-07-18 20:51:53 +02:00
public UIPointOfSaleController(
2022-07-22 15:41:14 +02:00
AppService appService,
2022-07-18 20:51:53 +02:00
CurrencyNameTable currencies,
StoreRepository storeRepository,
InvoiceRepository invoiceRepository,
UIInvoiceController invoiceController,
2023-04-05 15:42:23 +02:00
FormDataService formDataService,
DisplayFormatter displayFormatter)
2022-07-18 20:51:53 +02:00
{
_currencies = currencies;
_appService = appService;
2022-07-22 15:41:14 +02:00
_storeRepository = storeRepository;
_invoiceRepository = invoiceRepository;
2022-07-22 15:41:14 +02:00
_invoiceController = invoiceController;
2023-04-05 15:42:23 +02:00
_displayFormatter = displayFormatter;
FormDataService = formDataService;
2022-07-18 20:51:53 +02:00
}
private readonly CurrencyNameTable _currencies;
private readonly InvoiceRepository _invoiceRepository;
2022-07-18 20:51:53 +02:00
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
2022-07-22 15:41:14 +02:00
private readonly UIInvoiceController _invoiceController;
2023-04-05 15:42:23 +02:00
private readonly DisplayFormatter _displayFormatter;
public FormDataService FormDataService { get; }
2022-07-22 15:41:14 +02:00
[HttpGet("/")]
[HttpGet("/apps/{appId}/pos")]
2022-07-22 15:41:14 +02:00
[HttpGet("/apps/{appId}/pos/{viewType?}")]
[DomainMappingConstraint(PointOfSaleAppType.AppType)]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
2022-07-22 15:41:14 +02:00
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
{
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
2022-07-22 15:41:14 +02:00
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var numberFormatInfo = _appService.Currencies.GetNumberFormatInfo(settings.Currency) ??
2022-07-22 15:41:14 +02:00
_appService.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -numberFormatInfo.CurrencyDecimalDigits);
viewType ??= settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
var store = await _appService.GetStore(app);
var storeBlob = store.GetStoreBlob();
var storeBranding = new StoreBrandingViewModel(storeBlob)
{
EmbeddedCSS = settings.EmbeddedCSS,
CustomCSSLink = settings.CustomCSSLink
};
2022-07-22 15:41:14 +02:00
return View($"PointOfSale/Public/{viewType}", new ViewPointOfSaleViewModel
{
Title = settings.Title,
StoreName = store.StoreName,
StoreBranding = storeBranding,
2022-07-22 15:41:14 +02:00
Step = step.ToString(CultureInfo.InvariantCulture),
ViewType = (PosViewType)viewType,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
ShowSearch = settings.ShowSearch,
ShowCategories = settings.ShowCategories,
2022-07-22 15:41:14 +02:00
EnableTips = settings.EnableTips,
CurrencyCode = settings.Currency,
CurrencySymbol = numberFormatInfo.CurrencySymbol,
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData
{
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
},
2023-05-23 02:18:57 +02:00
Items = AppService.Parse(settings.Template, false),
2022-07-22 15:41:14 +02:00
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
CustomTipPercentages = settings.CustomTipPercentages,
AppId = appId,
StoreId = store.Id,
Description = settings.Description,
RequiresRefundEmail = settings.RequiresRefundEmail
});
}
[HttpPost("/")]
[HttpPost("/apps/{appId}/pos/{viewType?}")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraint(PointOfSaleAppType.AppType)]
2022-07-22 15:41:14 +02:00
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
2022-07-22 15:41:14 +02:00
public async Task<IActionResult> ViewPointOfSale(string appId,
PosViewType? viewType = null,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount = null,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? tip = null,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? discount = null,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? customAmount = null,
string email = null,
string orderId = null,
string notificationUrl = null,
string redirectUrl = null,
string choiceKey = null,
2022-07-22 15:41:14 +02:00
string posData = null,
string formResponse = null,
2022-07-22 15:41:14 +02:00
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
CancellationToken cancellationToken = default)
{
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
// not allowing negative tips or discounts
if (tip < 0 || discount < 0)
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
2022-07-22 15:41:14 +02:00
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
2022-07-22 15:41:14 +02:00
if (app == null)
return NotFound();
2022-07-22 15:41:14 +02:00
var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
var currentView = viewType ?? settings.DefaultView;
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount &&
currentView != PosViewType.Cart && currentView != PosViewType.Light)
2022-07-22 15:41:14 +02:00
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
}
var jposData = TryParseJObject(posData);
string title;
decimal? price;
2022-07-22 15:41:14 +02:00
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
List<PosCartItem> cartItems = null;
2023-04-05 15:42:23 +02:00
ViewPointOfSaleViewModel.Item[] choices = null;
2022-07-22 15:41:14 +02:00
if (!string.IsNullOrEmpty(choiceKey))
{
2023-05-23 02:18:57 +02:00
choices = AppService.Parse(settings.Template, false);
2022-07-22 15:41:14 +02:00
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
2023-05-23 02:18:57 +02:00
if (choice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup)
2022-07-22 15:41:14 +02:00
{
price = null;
}
else
{
price = choice.Price.Value;
if (amount > price)
price = amount;
}
if (choice.Inventory is <= 0)
2022-07-22 15:41:14 +02:00
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
2022-07-22 15:41:14 +02:00
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
}
}
else
{
if (!settings.ShowCustomAmount && currentView != PosViewType.Cart && currentView != PosViewType.Light)
2022-07-22 15:41:14 +02:00
return NotFound();
2022-07-22 15:41:14 +02:00
title = settings.Title;
// if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
2023-06-16 16:19:47 +02:00
price = amount;
if (currentView == PosViewType.Cart && AppService.TryParsePosCartItems(jposData, out cartItems))
2022-07-22 15:41:14 +02:00
{
price = 0.0m;
2023-05-23 02:18:57 +02:00
choices = AppService.Parse(settings.Template, false);
2022-07-22 15:41:14 +02:00
foreach (var cartItem in cartItems)
{
var itemChoice = choices.FirstOrDefault(item => item.Id == cartItem.Id);
2022-07-22 15:41:14 +02:00
if (itemChoice == null)
return NotFound();
if (itemChoice.Inventory.HasValue)
{
switch (itemChoice.Inventory)
{
case <= 0:
2022-07-22 15:41:14 +02:00
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
case { } inventory when inventory < cartItem.Count:
2022-07-22 15:41:14 +02:00
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
}
}
var expectedCartItemPrice = itemChoice.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup
? itemChoice.Price ?? 0
: 0;
if (cartItem.Price < expectedCartItemPrice)
cartItem.Price = expectedCartItemPrice;
price += cartItem.Price * cartItem.Count;
2022-07-22 15:41:14 +02:00
}
if (customAmount is { } c)
price += c;
if (discount is { } d)
price -= price * d/100.0m;
if (tip is { } t)
price += t;
2022-07-22 15:41:14 +02:00
}
}
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
2022-07-22 15:41:14 +02:00
var store = await _appService.GetStore(app);
var storeBlob = store.GetStoreBlob();
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
var posFormId = settings.FormId;
var formData = await FormDataService.GetForm(posFormId);
2023-04-10 04:07:03 +02:00
JObject formResponseJObject = null;
switch (formData)
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
{
case null:
break;
case not null:
if (formResponse is null)
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
{
2023-02-23 09:52:37 +01:00
var vm = new PostRedirectViewModel
{
FormUrl = Url.Action(nameof(POSForm), "UIPointOfSale", new {appId, buyerEmail = email}),
FormParameters = new MultiValueDictionary<string, string>(Request.Form.Select(pair =>
new KeyValuePair<string, IReadOnlyCollection<string>>(pair.Key, pair.Value)))
2023-02-23 09:52:37 +01:00
};
if (viewType.HasValue)
{
vm.RouteParameters.Add("viewType", viewType.Value.ToString());
}
2023-02-23 09:52:37 +01:00
return View("PostRedirect", vm);
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
}
formResponseJObject = TryParseJObject(formResponse) ?? new JObject();
var form = Form.Parse(formData.Config);
FormDataService.SetValues(form, formResponseJObject);
if (!FormDataService.Validate(form, ModelState))
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
{
//someone tried to bypass validation
2023-02-23 09:52:37 +01:00
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
}
var amtField = form.GetFieldByFullName($"{FormDataService.InvoiceParameterPrefix}amount");
if (amtField is null && price.HasValue)
{
form.Fields.Add(new Field
{
Name = $"{FormDataService.InvoiceParameterPrefix}amount",
Type = "hidden",
Value = price.ToString(),
Constant = true
});
}
else
{
amtField.Value = price?.ToString();
}
formResponseJObject = FormDataService.GetValues(form);
var invoiceRequest = FormDataService.GenerateInvoiceParametersFromForm(form);
if (invoiceRequest.Amount is not null)
{
price = invoiceRequest.Amount.Value;
}
break;
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
}
2022-07-22 15:41:14 +02:00
try
{
var invoice = await _invoiceController.CreateInvoiceCoreRaw(new CreateInvoiceRequest
2022-07-22 15:41:14 +02:00
{
Amount = price,
2022-07-22 15:41:14 +02:00
Currency = settings.Currency,
Metadata = new InvoiceMetadata
{
ItemCode = choice?.Id,
ItemDesc = title,
BuyerEmail = email,
OrderId = orderId ?? AppService.GetRandomOrderId()
}.ToJObject(),
Checkout = new InvoiceDataBase.CheckoutOptions()
{
RedirectAutomatically = settings.RedirectAutomatically,
RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl
: !string.IsNullOrEmpty(settings.RedirectUrl) ? settings.RedirectUrl
: Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), "UIPointOfSale", new { appId, viewType })),
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
? storeBlob.RequiresRefundEmail
: requiresRefundEmail == RequiresRefundEmail.On,
PaymentMethods = paymentMethods?.Where(p => p.Value.Enabled).Select(p => p.Key).ToArray()
},
AdditionalSearchTerms = new [] { AppService.GetAppSearchTerm(app) }
2022-07-22 15:41:14 +02:00
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string> { AppService.GetAppInternalTag(appId) },
cancellationToken, entity =>
2022-07-22 15:41:14 +02:00
{
entity.NotificationURLTemplate =
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl;
entity.FullNotifications = true;
entity.ExtendedNotifications = true;
2022-07-22 15:41:14 +02:00
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
entity.Metadata.PosData = jposData;
2023-04-05 15:42:23 +02:00
var receiptData = new JObject();
if (choice is not null)
{
2023-08-10 13:57:54 +02:00
receiptData = JObject.FromObject(new Dictionary<string, string>
2023-04-05 15:42:23 +02:00
{
2023-08-10 13:57:54 +02:00
{"Title", choice.Title},
{"Description", choice.Description},
2023-04-05 15:42:23 +02:00
});
}
else if (jposData is not null)
{
var appPosData = jposData.ToObject<PosAppData>();
receiptData = new JObject();
if (cartItems is not null && choices is not null)
{
var posCartItems = cartItems.ToList();
var selectedChoices = choices
.Where(item => posCartItems.Any(cartItem => cartItem.Id == item.Id))
2023-04-05 15:42:23 +02:00
.ToDictionary(item => item.Id);
var cartData = new JObject();
foreach (PosCartItem cartItem in posCartItems)
2023-04-05 15:42:23 +02:00
{
if (!selectedChoices.TryGetValue(cartItem.Id, out var selectedChoice)) continue;
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})";
2023-08-10 13:57:54 +02:00
cartData.Add(key, $"{cartItem.Count} x {singlePrice} = {totalPrice}");
2023-04-05 15:42:23 +02:00
}
receiptData.Add("Cart", cartData);
}
2023-08-10 13:57:54 +02:00
receiptData.Add("Subtotal", _displayFormatter.Currency(appPosData.Subtotal, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
2023-04-05 15:42:23 +02:00
if (appPosData.DiscountAmount > 0)
{
2023-08-10 13:57:54 +02:00
var discountFormatted = _displayFormatter.Currency(appPosData.DiscountAmount, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
receiptData.Add("Discount", appPosData.DiscountPercentage > 0 ? $"{appPosData.DiscountPercentage}% = {discountFormatted}" : discountFormatted);
2023-04-05 15:42:23 +02:00
}
if (appPosData.Tip > 0)
{
var tipFormatted = _displayFormatter.Currency(appPosData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
receiptData.Add("Tip", appPosData.TipPercentage > 0 ? $"{appPosData.TipPercentage}% = {tipFormatted}" : tipFormatted);
2023-04-05 15:42:23 +02:00
}
2023-08-10 13:57:54 +02:00
receiptData.Add("Total", _displayFormatter.Currency(appPosData.Total, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
2023-04-05 15:42:23 +02:00
}
entity.Metadata.SetAdditionalData("receiptData", receiptData);
2023-04-10 04:07:03 +02:00
if (formResponseJObject is null)
return;
var meta = entity.Metadata.ToJObject();
meta.Merge(formResponseJObject);
entity.Metadata = InvoiceMetadata.FromJObject(meta);
});
2023-05-31 04:27:03 +02:00
if (price is 0 && storeBlob.ReceiptOptions?.Enabled is true)
{
return RedirectToAction(nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", new { invoiceId = invoice.Id });
}
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Id });
2022-07-22 15:41:14 +02:00
}
catch (BitpayHttpException e)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Html = e.Message.Replace("\n", "<br />", StringComparison.OrdinalIgnoreCase),
Severity = StatusMessageModel.StatusSeverity.Error,
AllowDismiss = true
});
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
}
}
private JObject TryParseJObject(string posData)
{
try
{
return JObject.Parse(posData);
}
catch
{
}
return null;
}
2023-02-23 09:52:37 +01:00
[HttpPost("/apps/{appId}/pos/form/{viewType?}")]
[IgnoreAntiforgeryToken]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
2023-02-23 09:52:37 +01:00
public async Task<IActionResult> POSForm(string appId, PosViewType? viewType = null)
{
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
return NotFound();
2023-04-10 04:07:03 +02:00
var settings = app.GetSettings<PointOfSaleSettings>();
var formData = await FormDataService.GetForm(settings.FormId);
if (formData is null)
{
2023-02-23 09:52:37 +01:00
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
}
2023-04-10 04:07:03 +02:00
2023-02-23 09:52:37 +01:00
var prefix = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)) + "_";
var formParameters = Request.Form
.Where(pair => pair.Key != "__RequestVerificationToken")
2023-02-23 09:52:37 +01:00
.ToMultiValueDictionary(p => p.Key, p => p.Value.ToString());
var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);
var store = await _appService.GetStore(app);
var storeBlob = store.GetStoreBlob();
var form = Form.Parse(formData.Config);
form.ApplyValuesFromForm(Request.Query);
2023-02-23 09:52:37 +01:00
var vm = new FormViewModel
{
StoreName = store.StoreName,
StoreBranding = new StoreBrandingViewModel(storeBlob),
FormName = formData.Name,
Form = form,
AspController = controller,
AspAction = nameof(POSFormSubmit),
RouteParameters = new Dictionary<string, string> { { "appId", appId } },
2023-02-23 09:52:37 +01:00
FormParameters = formParameters,
FormParameterPrefix = prefix
};
if (viewType.HasValue)
{
vm.RouteParameters.Add("viewType", viewType.Value.ToString());
}
2023-04-10 04:07:03 +02:00
2023-02-23 09:52:37 +01:00
return View("Views/UIForms/View", vm);
}
2023-02-23 09:52:37 +01:00
[HttpPost("/apps/{appId}/pos/form/submit/{viewType?}")]
[IgnoreAntiforgeryToken]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
2023-02-23 09:52:37 +01:00
public async Task<IActionResult> POSFormSubmit(string appId, FormViewModel viewModel, PosViewType? viewType = null)
{
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
return NotFound();
2023-04-10 04:07:03 +02:00
var settings = app.GetSettings<PointOfSaleSettings>();
var formData = await FormDataService.GetForm(settings.FormId);
2023-02-23 09:52:37 +01:00
if (formData is null)
{
2023-02-23 09:52:37 +01:00
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
2022-07-22 15:41:14 +02:00
}
var form = Form.Parse(formData.Config);
2023-02-23 09:52:37 +01:00
var formFieldNames = form.GetAllFields().Select(tuple => tuple.FullName).Distinct().ToArray();
var formParameters = Request.Form
.Where(pair => pair.Key.StartsWith(viewModel.FormParameterPrefix))
.ToDictionary(pair => pair.Key.Replace(viewModel.FormParameterPrefix, string.Empty), pair => pair.Value)
.ToMultiValueDictionary(p => p.Key, p => p.Value.ToString());
2023-04-10 04:07:03 +02:00
2023-02-23 09:52:37 +01:00
if (Request is { Method: "POST", HasFormContentType: true })
{
2023-02-23 09:52:37 +01:00
form.ApplyValuesFromForm(Request.Form.Where(pair => formFieldNames.Contains(pair.Key)));
2023-04-10 04:07:03 +02:00
if (FormDataService.Validate(form, ModelState))
{
var controller = nameof(UIPointOfSaleController).TrimEnd("Controller", StringComparison.InvariantCulture);
2023-02-23 09:52:37 +01:00
var redirectUrl =
2023-04-10 04:07:03 +02:00
Request.GetAbsoluteUri(Url.Action(nameof(ViewPointOfSale), controller, new { appId, viewType }));
formParameters.Add("formResponse", FormDataService.GetValues(form).ToString());
return View("PostRedirect", new PostRedirectViewModel
{
2023-02-23 09:52:37 +01:00
FormUrl = redirectUrl,
FormParameters = formParameters
});
}
}
var store = await _appService.GetStore(app);
var storeBlob = store.GetStoreBlob();
viewModel.FormName = formData.Name;
viewModel.Form = form;
2023-02-23 09:52:37 +01:00
viewModel.FormParameters = formParameters;
viewModel.StoreBranding = new StoreBrandingViewModel(storeBlob);
return View("Views/UIForms/View", viewModel);
2022-07-22 15:41:14 +02:00
}
[Authorize(Policy = Policies.CanViewInvoices, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("/apps/{appId}/pos/recent-transactions")]
public async Task<IActionResult> RecentTransactions(string appId)
{
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
return NotFound();
var from = DateTimeOffset.UtcNow - TimeSpan.FromDays(3);
var invoices = await AppService.GetInvoicesForApp(_invoiceRepository, app, from, new[]
{
InvoiceState.ToString(InvoiceStatusLegacy.New),
InvoiceState.ToString(InvoiceStatusLegacy.Paid),
InvoiceState.ToString(InvoiceStatusLegacy.Confirmed),
InvoiceState.ToString(InvoiceStatusLegacy.Complete),
InvoiceState.ToString(InvoiceStatusLegacy.Expired),
InvoiceState.ToString(InvoiceStatusLegacy.Invalid)
});
var recent = invoices
.Take(10)
.Select(i => new JObject
{
["id"] = i.Id,
["date"] = i.InvoiceTime,
["price"] = _displayFormatter.Currency(i.Price, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
["status"] = i.GetInvoiceState().Status.ToModernStatus().ToString(),
["url"] = Url.Action(nameof(UIInvoiceController.Invoice), "UIInvoice", new { invoiceId = i.Id })
});
return Json(recent);
}
2022-07-22 15:41:14 +02:00
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId)
2018-04-03 04:50:41 +02:00
{
var app = GetCurrentApp();
if (app == null)
2018-04-03 04:50:41 +02:00
return NotFound();
2021-12-31 08:59:02 +01:00
var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
settings.EnableShoppingCart = false;
var vm = new UpdatePointOfSaleViewModel
2018-05-24 16:54:48 +02:00
{
Id = appId,
StoreId = app.StoreDataId,
StoreName = app.StoreData?.StoreName,
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.Currency),
Archived = app.Archived,
AppName = app.Name,
2018-05-24 16:54:48 +02:00
Title = settings.Title,
DefaultView = settings.DefaultView,
2018-05-24 16:54:48 +02:00
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
ShowSearch = settings.ShowSearch,
ShowCategories = settings.ShowCategories,
EnableTips = settings.EnableTips,
2018-05-24 16:54:48 +02:00
Currency = settings.Currency,
Template = settings.Template,
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
CustomCSSLink = settings.CustomCSSLink,
EmbeddedCSS = settings.EmbeddedCSS,
Description = settings.Description,
NotificationUrl = settings.NotificationUrl,
2020-10-13 09:51:28 +02:00
RedirectUrl = settings.RedirectUrl,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"appid:{app.Id}",
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "",
Form Builder (#4137) * wip * Cleanups * UI updates * Update UIFormsController.cs * Make predefined forms usable statically * Add support for pos app + forms * pay request form rough support * invoice form through receipt page * Display form name in inherit from store setting * Do not request additional forms on invoice from pay request * fix up code * move checkoutform id in checkout appearance outside of checkotu v2 toggle * general fixes for form system * fix pav bug * UI updates * Fix warnings in Form builder (#4331) * Fix build warnings about string? Enable nullable on UIFormsController.cs Fixes CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. * Clean up lack of space in injected services in Submit() of UIFormsController.cs * Remove unused variables (CS0219) and assignment of nullable value to nullable type (CS8600) * Cleanup double semicolons while we're at tit * Fix: If reverse proxy wasn't well configured, and error message should have been displayed (#4322) * fix monero issue * Server Settings: Update Policies page (#4326) Handles the multiple submit buttons on that page and closes #4319. Contains some UI unifications with other pages and also shows the block explorers without needing to toggle the section via JS. * Change confirmed to settled. (#4328) * POS: Fix null pointer Introduced in #4307, the referenced object needs to be `itemChoice` instead of `choice`. * Add documentation link to plugins (#4329) * Add documentation link to plugins * Minor UI updates Co-authored-by: Dennis Reimann <mail@dennisreimann.de> * Fix flaky test (#4330) * Fix flaky test * Update BTCPayServer/PayoutProcessors/BaseAutomatedPayoutProcessor.cs Co-authored-by: d11n <mail@dennisreimann.de> Co-authored-by: d11n <mail@dennisreimann.de> * Remove invoice and store level form * add form test * fix migration for forms * fix * make pay request form submission redirect to invoice * Refactor FormQuery to only be able to query single store and single form * Put the Authorize at controller level on UIForms * Fix warnings * Fix ef request * Fix query to forms, ensure no permission bypass * Fix modify * Remove storeId from step form * Remove useless storeId parameter * Hide custom form feature in UI * Minor cleanups * Remove custom form options from select for now * More minor syntax cleanups * Update test * Add index - needs migration * Refactoring: Use PostRedirect instead of TempData for data transfer * Remove untested and unfinished code * formResponse should be a JObject, not a string * Fix case for Form type Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: JesterHodl <103882255+jesterhodl@users.noreply.github.com> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com> Co-authored-by: Andreas Tasch <andy.tasch@gmail.com>
2022-11-25 02:42:55 +01:00
FormId = settings.FormId
2018-05-24 16:54:48 +02:00
};
if (HttpContext.Request != null)
2018-05-24 16:54:48 +02:00
{
var appUrl = HttpContext.Request.GetAbsoluteUri($"/apps/{appId}/pos");
2018-05-24 16:54:48 +02:00
var encoder = HtmlEncoder.Default;
if (settings.ShowCustomAmount)
{
var builder = new StringBuilder();
2021-12-27 05:46:31 +01:00
builder.AppendLine(CultureInfo.InvariantCulture, $"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
2018-05-24 16:54:48 +02:00
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example1 = builder.ToString();
}
try
{
2023-05-23 02:18:57 +02:00
var items = AppService.Parse(settings.Template);
2018-05-24 16:54:48 +02:00
var builder = new StringBuilder();
2021-12-27 05:46:31 +01:00
builder.AppendLine(CultureInfo.InvariantCulture, $"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
2018-05-24 16:54:48 +02:00
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
2021-12-27 05:46:31 +01:00
builder.AppendLine(CultureInfo.InvariantCulture, $" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
2018-05-24 16:54:48 +02:00
builder.AppendLine($"</form>");
vm.Example2 = builder.ToString();
}
catch { }
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
}
2018-05-25 10:35:01 +02:00
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
2022-07-18 20:51:53 +02:00
return View("PointOfSale/UpdatePointOfSale", vm);
2018-04-03 04:50:41 +02:00
}
2022-07-22 15:41:14 +02:00
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/settings/pos")]
2018-04-03 04:50:41 +02:00
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
var app = GetCurrentApp();
if (app == null)
return NotFound();
2021-12-31 08:59:02 +01:00
vm.Id = app.Id;
if (!ModelState.IsValid)
2022-07-18 20:51:53 +02:00
return View("PointOfSale/UpdatePointOfSale", vm);
vm.Currency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.Currency);
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
2018-04-03 04:50:41 +02:00
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
2023-05-23 02:18:57 +02:00
vm.Template = AppService.SerializeTemplate(AppService.Parse(vm.Template));
2018-04-03 04:50:41 +02:00
}
catch
{
ModelState.AddModelError(nameof(vm.Template), "Invalid template");
}
if (!ModelState.IsValid)
{
2022-07-18 20:51:53 +02:00
return View("PointOfSale/UpdatePointOfSale", vm);
2018-04-03 04:50:41 +02:00
}
var settings = new PointOfSaleSettings
2018-04-03 04:50:41 +02:00
{
Title = vm.Title,
DefaultView = vm.DefaultView,
2018-04-26 15:09:18 +02:00
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
ShowSearch = vm.ShowSearch,
ShowCategories = vm.ShowCategories,
EnableTips = vm.EnableTips,
Currency = vm.Currency,
Template = vm.Template,
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
CustomCSSLink = vm.CustomCSSLink,
NotificationUrl = vm.NotificationUrl,
2020-10-13 09:51:28 +02:00
RedirectUrl = vm.RedirectUrl,
Description = vm.Description,
EmbeddedCSS = vm.EmbeddedCSS,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? null : bool.Parse(vm.RedirectAutomatically),
FormId = vm.FormId
};
app.Name = vm.AppName;
app.Archived = vm.Archived;
app.SetSettings(settings);
await _appService.UpdateOrCreateApp(app);
TempData[WellKnownTempData.SuccessMessage] = "App updated";
return RedirectToAction(nameof(UpdatePointOfSale), new { appId });
2018-04-03 04:50:41 +02:00
}
2021-12-31 08:59:02 +01:00
private int[] ListSplit(string list, string separator = ",")
{
if (string.IsNullOrEmpty(list))
{
return Array.Empty<int>();
2020-01-24 03:19:24 +01:00
}
2021-12-31 08:59:02 +01:00
// Remove all characters except numeric and comma
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
list = charsToDestroy.Replace(list, "");
return list.Split(separator, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
}
2022-07-18 20:51:53 +02:00
2022-07-22 15:41:14 +02:00
private async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
2022-07-18 20:51:53 +02:00
{
if (string.IsNullOrWhiteSpace(currency))
{
currency = (await _storeRepository.FindStore(storeId)).GetStoreBlob().DefaultCurrency;
}
return currency.Trim().ToUpperInvariant();
}
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
2022-07-18 20:51:53 +02:00
private AppData GetCurrentApp() => HttpContext.GetAppData();
2018-04-03 04:50:41 +02:00
}
}