2020-06-28 21:44:35 -05:00
using System ;
2022-07-22 15:41:14 +02:00
using System.Collections.Generic ;
2021-12-27 13:46:31 +09:00
using System.Globalization ;
2018-12-19 14:07:05 +08:00
using System.Linq ;
2025-01-15 05:49:25 +00:00
using System.Runtime.CompilerServices ;
2018-12-19 14:07:05 +08:00
using System.Text ;
2018-08-30 13:16:24 -05:00
using System.Text.Encodings.Web ;
2018-12-19 14:07:05 +08:00
using System.Text.RegularExpressions ;
2022-07-22 15:41:14 +02:00
using System.Threading ;
2018-04-03 11:50:41 +09:00
using System.Threading.Tasks ;
2022-02-21 15:46:43 +01:00
using BTCPayServer.Abstractions.Constants ;
2022-03-02 18:28:12 +01:00
using BTCPayServer.Abstractions.Extensions ;
2022-11-25 16:11:13 +09:00
using BTCPayServer.Abstractions.Form ;
2022-07-22 15:41:14 +02:00
using BTCPayServer.Abstractions.Models ;
2025-01-15 05:49:25 +00:00
using BTCPayServer.Abstractions.Services ;
2022-07-18 20:51:53 +02:00
using BTCPayServer.Client ;
2023-07-20 09:03:39 +02:00
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 ;
2022-11-25 18:14:33 +09:00
using BTCPayServer.Forms ;
2023-02-20 11:35:54 +01:00
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 11:50:41 +09:00
using BTCPayServer.Services.Apps ;
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 ;
2025-01-15 05:49:25 +00:00
using Ganss.Xss ;
2022-07-18 20:51:53 +02:00
using Microsoft.AspNetCore.Authorization ;
2022-07-22 15:41:14 +02:00
using Microsoft.AspNetCore.Cors ;
using Microsoft.AspNetCore.Http.Extensions ;
2018-08-30 13:16:24 -05:00
using Microsoft.AspNetCore.Mvc ;
2024-10-17 15:51:40 +02:00
using Microsoft.Extensions.Localization ;
2023-02-23 09:52:37 +01:00
using NBitcoin ;
using NBitcoin.DataEncoders ;
2022-07-22 15:41:14 +02:00
using NBitpayClient ;
2022-11-25 02:42:55 +01:00
using Newtonsoft.Json.Linq ;
2022-07-22 15:41:14 +02:00
using NicolasDorier.RateLimits ;
2023-07-20 09:03:39 +02:00
using StoreData = BTCPayServer . Data . StoreData ;
2018-04-03 11:50:41 +09:00
2022-07-18 20:51:53 +02:00
namespace BTCPayServer.Plugins.PointOfSale.Controllers
2018-04-03 11:50:41 +09:00
{
2022-07-18 20:51:53 +02:00
[AutoValidateAntiforgeryToken]
[Route("apps")]
public class UIPointOfSaleController : Controller
2018-04-03 11:50:41 +09: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 ,
2024-05-09 02:18:02 +02:00
UriResolver uriResolver ,
2023-11-30 10:19:03 +01:00
InvoiceRepository invoiceRepository ,
2022-11-25 18:14:33 +09:00
UIInvoiceController invoiceController ,
2023-04-05 15:42:23 +02:00
FormDataService formDataService ,
2024-10-17 15:51:40 +02:00
IStringLocalizer stringLocalizer ,
2024-11-20 15:47:23 +01:00
DisplayFormatter displayFormatter ,
IRateLimitService rateLimitService ,
2025-01-15 05:49:25 +00:00
IAuthorizationService authorizationService ,
Safe safe )
2022-07-18 20:51:53 +02:00
{
_currencies = currencies ;
_appService = appService ;
2022-07-22 15:41:14 +02:00
_storeRepository = storeRepository ;
2024-05-09 02:18:02 +02:00
_uriResolver = uriResolver ;
2023-11-30 10:19:03 +01:00
_invoiceRepository = invoiceRepository ;
2022-07-22 15:41:14 +02:00
_invoiceController = invoiceController ;
2023-04-05 15:42:23 +02:00
_displayFormatter = displayFormatter ;
2024-11-20 15:47:23 +01:00
_rateLimitService = rateLimitService ;
_authorizationService = authorizationService ;
2025-01-15 05:49:25 +00:00
_safe = safe ;
2024-10-17 15:51:40 +02:00
StringLocalizer = stringLocalizer ;
2023-02-20 11:35:54 +01:00
FormDataService = formDataService ;
2022-07-18 20:51:53 +02:00
}
private readonly CurrencyNameTable _currencies ;
2023-11-30 10:19:03 +01:00
private readonly InvoiceRepository _invoiceRepository ;
2022-07-18 20:51:53 +02:00
private readonly StoreRepository _storeRepository ;
2024-05-09 02:18:02 +02:00
private readonly UriResolver _uriResolver ;
2022-07-18 20:51:53 +02:00
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 ;
2024-11-20 15:47:23 +01:00
private readonly IRateLimitService _rateLimitService ;
private readonly IAuthorizationService _authorizationService ;
2025-01-15 05:49:25 +00:00
private readonly Safe _safe ;
2023-02-20 11:35:54 +01:00
public FormDataService FormDataService { get ; }
2024-10-17 15:51:40 +02:00
public IStringLocalizer StringLocalizer { get ; }
2022-11-25 18:14:33 +09:00
2022-07-22 15:41:14 +02:00
[HttpGet("/")]
2023-03-01 07:27:18 +01:00
[HttpGet("/apps/{appId}/pos")]
2022-07-22 15:41:14 +02:00
[HttpGet("/apps/{appId}/pos/{viewType?}")]
2023-03-20 10:39:26 +09:00
[DomainMappingConstraint(PointOfSaleAppType.AppType)]
2023-03-01 07:27:18 +01:00
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
2022-07-22 15:41:14 +02:00
public async Task < IActionResult > ViewPointOfSale ( string appId , PosViewType ? viewType = null )
{
2023-03-20 10:39:26 +09:00
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 > ( ) ;
2023-01-06 14:18:07 +01:00
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 ( ) ;
2024-05-09 02:18:02 +02:00
var storeBranding = await StoreBrandingViewModel . CreateAsync ( Request , _uriResolver , storeBlob ) ;
2023-12-01 16:13:44 +01:00
2022-07-22 15:41:14 +02:00
return View ( $"PointOfSale/Public/{viewType}" , new ViewPointOfSaleViewModel
{
Title = settings . Title ,
2023-01-30 09:23:49 +01:00
StoreName = store . StoreName ,
2023-12-01 16:13:44 +01:00
StoreBranding = storeBranding ,
2022-07-22 15:41:14 +02:00
Step = step . ToString ( CultureInfo . InvariantCulture ) ,
ViewType = ( PosViewType ) viewType ,
2024-03-14 11:11:54 +01:00
ShowItems = settings . ShowItems ,
2022-07-22 15:41:14 +02:00
ShowCustomAmount = settings . ShowCustomAmount ,
ShowDiscount = settings . ShowDiscount ,
2023-11-13 13:59:14 +01:00
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 ,
2025-01-15 05:49:25 +00:00
Lang = settings . Language ,
HtmlMetaTags = settings . HtmlMetaTags ,
2022-07-22 15:41:14 +02:00
Description = settings . Description ,
} ) ;
}
[HttpPost("/")]
[HttpPost("/apps/{appId}/pos/{viewType?}")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
2023-03-20 10:39:26 +09:00
[DomainMappingConstraint(PointOfSaleAppType.AppType)]
2023-03-01 07:27:18 +01:00
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
2022-07-22 15:41:14 +02:00
public async Task < IActionResult > ViewPointOfSale ( string appId ,
2023-02-20 11:35:54 +01:00
PosViewType ? viewType = null ,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount = null ,
2023-06-16 23:02:14 +09:00
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? tip = null ,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? discount = null ,
2023-07-05 10:23:15 +02:00
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? customAmount = null ,
2023-02-20 11:35:54 +01:00
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 ,
2023-02-20 11:35:54 +01:00
string formResponse = null ,
2022-07-22 15:41:14 +02:00
CancellationToken cancellationToken = default )
{
2024-12-09 09:28:50 +09:00
if ( await Throttle ( appId ) )
2024-11-20 15:47:23 +01:00
return new TooManyRequestsResult ( ZoneLimits . PublicInvoices ) ;
2024-12-09 09:28:50 +09:00
2024-12-13 04:09:55 +01:00
// Distinguish JSON requests coming via the mobile app
var wantsJson = Request . Headers . Accept . FirstOrDefault ( ) ? . StartsWith ( "application/json" ) is true ;
2023-03-20 10:39:26 +09:00
var app = await _appService . GetApp ( appId , PointOfSaleAppType . AppType ) ;
2024-12-13 04:09:55 +01:00
if ( app = = null )
return wantsJson
? Json ( new { error = "App not found" } )
: NotFound ( ) ;
2023-07-26 06:49:05 -05:00
// not allowing negative tips or discounts
if ( tip < 0 | | discount < 0 )
2024-12-13 04:09:55 +01:00
return wantsJson
? Json ( new { error = "Negative tip or discount is not allowed" } )
: RedirectToAction ( nameof ( ViewPointOfSale ) , new { appId } ) ;
2023-07-26 06:49:05 -05:00
2022-07-22 15:41:14 +02:00
if ( string . IsNullOrEmpty ( choiceKey ) & & amount < = 0 )
2024-12-13 04:09:55 +01:00
return wantsJson
? Json ( new { error = "Negative amount is not allowed" } )
: RedirectToAction ( nameof ( ViewPointOfSale ) , new { appId } ) ;
2024-12-09 09:28:50 +09:00
2022-07-22 15:41:14 +02:00
var settings = app . GetSettings < PointOfSaleSettings > ( ) ;
settings . DefaultView = settings . EnableShoppingCart ? PosViewType . Cart : settings . DefaultView ;
2022-10-08 05:41:56 +02:00
var currentView = viewType ? ? settings . DefaultView ;
2023-01-06 14:18:07 +01:00
if ( string . IsNullOrEmpty ( choiceKey ) & & ! settings . ShowCustomAmount & &
2022-10-08 05:41:56 +02:00
currentView ! = PosViewType . Cart & & currentView ! = PosViewType . Light )
2022-07-22 15:41:14 +02:00
{
return RedirectToAction ( nameof ( ViewPointOfSale ) , new { appId , viewType } ) ;
}
2024-12-13 04:09:55 +01:00
2023-02-25 23:34:49 +09:00
var jposData = TryParseJObject ( posData ) ;
2022-10-08 05:41:56 +02:00
string title ;
decimal? price ;
2022-07-22 15:41:14 +02:00
Dictionary < string , InvoiceSupportedTransactionCurrency > paymentMethods = null ;
2024-11-05 03:49:30 +01:00
AppItem choice = null ;
2024-11-26 06:17:40 +01:00
List < AppCartItem > cartItems = null ;
2024-11-05 03:49:30 +01:00
AppItem [ ] 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 ;
2024-11-05 03:49:30 +01:00
if ( choice . PriceType = = AppItemPriceType . Topup )
2022-07-22 15:41:14 +02:00
{
price = null ;
}
else
{
price = choice . Price . Value ;
if ( amount > price )
price = amount ;
}
2022-10-08 05:41:56 +02:00
if ( choice . Inventory is < = 0 )
2022-07-22 15:41:14 +02:00
{
2022-10-08 05:41:56 +02:00
return RedirectToAction ( nameof ( ViewPointOfSale ) , new { appId } ) ;
2022-07-22 15:41:14 +02:00
}
}
else
{
2022-10-08 05:41:56 +02:00
if ( ! settings . ShowCustomAmount & & currentView ! = PosViewType . Cart & & currentView ! = PosViewType . Light )
2022-07-22 15:41:14 +02:00
return NotFound ( ) ;
2023-01-06 14:18:07 +01:00
2022-07-22 15:41:14 +02:00
title = settings . Title ;
2023-08-09 10:31:19 +03:00
// if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
2023-06-16 23:19:47 +09:00
price = amount ;
2024-03-14 11:11:54 +01:00
if ( AppService . TryParsePosCartItems ( jposData , out cartItems ) )
2022-07-22 15:41:14 +02:00
{
2024-03-14 11:11:54 +01:00
price = jposData . TryGetValue ( "amounts" , out var amounts ) & & amounts is JArray { Count : > 0 } amountsArray
? amountsArray . Values < decimal > ( ) . Sum ( )
: 0.0 m ;
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 )
{
2023-08-09 10:31:19 +03:00
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 )
{
2023-08-09 10:31:19 +03:00
case < = 0 :
case { } inventory when inventory < cartItem . Count :
2024-12-13 04:09:55 +01:00
return wantsJson
? Json ( new { error = $"Inventory for {itemChoice.Title} exhausted: {itemChoice.Inventory} available" } )
: RedirectToAction ( nameof ( ViewPointOfSale ) , new { appId } ) ;
2022-07-22 15:41:14 +02:00
}
}
2022-11-18 11:51:33 +01:00
2024-11-05 03:49:30 +01:00
var expectedCartItemPrice = itemChoice . PriceType ! = AppItemPriceType . Topup
2023-08-09 10:31:19 +03:00
? itemChoice . Price ? ? 0
: 0 ;
2024-12-09 09:28:50 +09:00
2023-08-09 10:31:19 +03:00
if ( cartItem . Price < expectedCartItemPrice )
cartItem . Price = expectedCartItemPrice ;
2022-11-18 11:51:33 +01:00
2023-08-09 10:31:19 +03:00
price + = cartItem . Price * cartItem . Count ;
2022-07-22 15:41:14 +02:00
}
2023-07-05 10:23:15 +02:00
if ( customAmount is { } c )
price + = c ;
if ( discount is { } d )
2024-12-09 09:28:50 +09:00
price - = price * d / 100.0 m ;
2023-07-05 10:23:15 +02:00
if ( tip is { } t )
2023-06-16 23:02:14 +09:00
price + = t ;
2022-07-22 15:41:14 +02:00
}
}
2022-11-25 02:42:55 +01:00
2022-07-22 15:41:14 +02:00
var store = await _appService . GetStore ( app ) ;
2023-05-30 10:04:23 +02:00
var storeBlob = store . GetStoreBlob ( ) ;
2022-11-25 02:42:55 +01:00
var posFormId = settings . FormId ;
2023-04-10 11:07:03 +09:00
2024-12-13 04:09:55 +01:00
// skip forms feature for JSON requests (from the app)
var formData = wantsJson ? null : await FormDataService . GetForm ( posFormId ) ;
2023-02-20 11:35:54 +01:00
JObject formResponseJObject = null ;
switch ( formData )
2022-11-25 02:42:55 +01:00
{
case null :
break ;
2023-02-20 11:35:54 +01:00
case not null :
if ( formResponse is null )
2022-11-25 02:42:55 +01:00
{
2023-02-23 09:52:37 +01:00
var vm = new PostRedirectViewModel
2023-02-20 11:35:54 +01:00
{
2024-12-09 09:28:50 +09:00
FormUrl = Url . Action ( nameof ( POSForm ) , "UIPointOfSale" , new { appId , buyerEmail = email } ) ,
2023-04-26 09:45:35 +02:00
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-04-26 09:45:35 +02:00
2023-02-23 09:52:37 +01:00
return View ( "PostRedirect" , vm ) ;
2022-11-25 02:42:55 +01:00
}
2023-01-06 14:18:07 +01:00
2023-02-25 23:34:49 +09:00
formResponseJObject = TryParseJObject ( formResponse ) ? ? new JObject ( ) ;
2023-02-20 11:35:54 +01:00
var form = Form . Parse ( formData . Config ) ;
2023-10-13 03:08:16 +02:00
FormDataService . SetValues ( form , formResponseJObject ) ;
2023-02-20 11:35:54 +01:00
if ( ! FormDataService . Validate ( form , ModelState ) )
2022-11-25 02:42:55 +01:00
{
2023-02-20 11:35:54 +01:00
//someone tried to bypass validation
2023-02-23 09:52:37 +01:00
return RedirectToAction ( nameof ( ViewPointOfSale ) , new { appId , viewType } ) ;
2022-11-25 02:42:55 +01:00
}
2023-01-06 14:18:07 +01:00
2023-07-19 11:54:51 +02:00
var amtField = form . GetFieldByFullName ( $"{FormDataService.InvoiceParameterPrefix}amount" ) ;
2024-03-11 11:05:44 +01:00
if ( amtField is null )
2023-07-19 11:54:51 +02:00
{
form . Fields . Add ( new Field
{
Name = $"{FormDataService.InvoiceParameterPrefix}amount" ,
Type = "hidden" ,
2024-03-11 11:05:44 +01:00
Value = price ? . ToString ( ) ,
2023-07-19 11:54:51 +02:00
Constant = true
} ) ;
}
else
{
amtField . Value = price ? . ToString ( ) ;
}
2023-04-04 04:01:34 +02:00
formResponseJObject = FormDataService . GetValues ( form ) ;
2024-12-09 09:28:50 +09:00
2023-07-19 11:54:51 +02:00
var invoiceRequest = FormDataService . GenerateInvoiceParametersFromForm ( form ) ;
if ( invoiceRequest . Amount is not null )
{
price = invoiceRequest . Amount . Value ;
}
2023-02-20 11:35:54 +01:00
break ;
2022-11-25 02:42:55 +01:00
}
2022-07-22 15:41:14 +02:00
try
{
2023-07-20 09:03:39 +02:00
var invoice = await _invoiceController . CreateInvoiceCoreRaw ( new CreateInvoiceRequest
2022-07-22 15:41:14 +02:00
{
2023-07-20 09:03:39 +02:00
Amount = price ,
2022-07-22 15:41:14 +02:00
Currency = settings . Currency ,
2023-08-09 10:31:19 +03:00
Metadata = new InvoiceMetadata
2023-07-20 09:03:39 +02:00
{
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
2024-12-21 00:16:04 +09:00
: Url . ActionAbsolute ( Request , nameof ( ViewPointOfSale ) , "UIPointOfSale" , new { appId , viewType } ) . ToString ( ) ,
2023-07-20 09:03:39 +02:00
PaymentMethods = paymentMethods ? . Where ( p = > p . Value . Enabled ) . Select ( p = > p . Key ) . ToArray ( )
} ,
2024-12-09 09:28:50 +09:00
AdditionalSearchTerms = new [ ] { AppService . GetAppSearchTerm ( app ) }
2022-07-22 15:41:14 +02:00
} , store , HttpContext . Request . GetAbsoluteRoot ( ) ,
2022-10-08 05:41:56 +02:00
new List < string > { AppService . GetAppInternalTag ( appId ) } ,
2023-02-20 11:35:54 +01:00
cancellationToken , entity = >
2022-07-22 15:41:14 +02:00
{
2023-07-20 09:03:39 +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 ( ) ;
2023-02-25 23:34:49 +09:00
entity . Metadata . PosData = jposData ;
2023-04-05 15:42:23 +02:00
var receiptData = new JObject ( ) ;
if ( choice is not null )
{
2024-07-09 16:56:34 +02:00
var dict = new Dictionary < string , string > { { "Title" , choice . Title } } ;
2024-12-09 09:28:50 +09:00
if ( ! string . IsNullOrEmpty ( choice . Description ) )
dict [ "Description" ] = choice . Description ;
2024-07-09 16:56:34 +02:00
receiptData = JObject . FromObject ( dict ) ;
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 )
{
2023-08-09 10:31:19 +03:00
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 ( ) ;
2024-11-26 06:17:40 +01:00
foreach ( AppCartItem cartItem in posCartItems )
2023-04-05 15:42:23 +02:00
{
2024-12-09 09:28:50 +09:00
if ( ! selectedChoices . TryGetValue ( cartItem . Id , out var selectedChoice ) )
continue ;
2023-08-09 10:31:19 +03:00
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 ;
2024-11-05 03:49:30 +01:00
var key = selectedChoice . PriceType = = AppItemPriceType . Fixed ? ident : $"{ident} ({singlePrice})" ;
2023-08-10 14:57:54 +03:00
cartData . Add ( key , $"{cartItem.Count} x {singlePrice} = {totalPrice}" ) ;
2023-04-05 15:42:23 +02:00
}
2024-03-14 11:11:54 +01:00
if ( jposData . TryGetValue ( "amounts" , out var amounts ) & & amounts is JArray { Count : > 0 } amountsArray )
{
for ( var i = 0 ; i < amountsArray . Count ; i + + )
{
2024-12-09 09:28:50 +09:00
cartData . Add ( $"Custom Amount {i + 1}" , _displayFormatter . Currency ( amountsArray [ i ] . ToObject < decimal > ( ) , settings . Currency , DisplayFormatter . CurrencyFormat . Symbol ) ) ;
2024-03-14 11:11:54 +01:00
}
}
2023-04-05 15:42:23 +02:00
receiptData . Add ( "Cart" , cartData ) ;
}
2023-08-10 14:57:54 +03: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 14:57:54 +03: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 )
{
2023-09-26 15:50:04 +02:00
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 14:57:54 +03: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 11:07:03 +09:00
if ( formResponseJObject is null )
return ;
2023-02-25 14:38:28 +01:00
var meta = entity . Metadata . ToJObject ( ) ;
2023-02-25 23:34:49 +09:00
meta . Merge ( formResponseJObject ) ;
entity . Metadata = InvoiceMetadata . FromJObject ( meta ) ;
2023-01-06 14:18:07 +01:00
} ) ;
2024-12-13 04:09:55 +01:00
var data = new { invoiceId = invoice . Id } ;
if ( wantsJson )
return Json ( data ) ;
2023-05-31 11:27:03 +09:00
if ( price is 0 & & storeBlob . ReceiptOptions ? . Enabled is true )
2024-12-13 04:09:55 +01:00
return RedirectToAction ( nameof ( UIInvoiceController . InvoiceReceipt ) , "UIInvoice" , data ) ;
return RedirectToAction ( nameof ( UIInvoiceController . Checkout ) , "UIInvoice" , data ) ;
2022-07-22 15:41:14 +02:00
}
catch ( BitpayHttpException e )
{
2024-12-13 04:09:55 +01:00
if ( wantsJson ) return Json ( new { error = e . Message } ) ;
2022-07-22 15:41:14 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Html = e . Message . Replace ( "\n" , "<br />" , StringComparison . OrdinalIgnoreCase ) ,
Severity = StatusMessageModel . StatusSeverity . Error ,
AllowDismiss = true
} ) ;
2023-02-20 11:35:54 +01:00
return RedirectToAction ( nameof ( ViewPointOfSale ) , new { appId } ) ;
}
}
2024-12-09 09:28:50 +09:00
private async Task < bool > Throttle ( string appId ) = >
2024-12-09 17:40:29 +01:00
! ( await _authorizationService . AuthorizeAsync ( HttpContext . User , appId , Policies . CanViewInvoices ) ) . Succeeded & &
HttpContext . Connection is { RemoteIpAddress : { } addr } & &
! await _rateLimitService . Throttle ( ZoneLimits . PublicInvoices , addr . ToString ( ) , HttpContext . RequestAborted ) ;
2024-12-09 09:28:50 +09:00
2023-02-25 23:34:49 +09:00
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?}")]
2023-03-01 07:27:18 +01:00
[IgnoreAntiforgeryToken]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.Unset)]
2023-02-23 09:52:37 +01:00
public async Task < IActionResult > POSForm ( string appId , PosViewType ? viewType = null )
2023-02-20 11:35:54 +01:00
{
2023-03-20 10:39:26 +09:00
var app = await _appService . GetApp ( appId , PointOfSaleAppType . AppType ) ;
2023-02-20 11:35:54 +01:00
if ( app = = null )
return NotFound ( ) ;
2023-04-10 11:07:03 +09:00
2023-02-20 11:35:54 +01: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-02-20 11:35:54 +01:00
}
2023-04-10 11:07:03 +09:00
2023-02-23 09:52:37 +01:00
var prefix = Encoders . Base58 . EncodeData ( RandomUtils . GetBytes ( 16 ) ) + "_" ;
var formParameters = Request . Form
2023-02-20 11:35:54 +01:00
. Where ( pair = > pair . Key ! = "__RequestVerificationToken" )
2023-02-23 09:52:37 +01:00
. ToMultiValueDictionary ( p = > p . Key , p = > p . Value . ToString ( ) ) ;
2023-03-17 03:56:32 +01:00
var controller = nameof ( UIPointOfSaleController ) . TrimEnd ( "Controller" , StringComparison . InvariantCulture ) ;
2023-02-20 11:35:54 +01:00
var store = await _appService . GetStore ( app ) ;
var storeBlob = store . GetStoreBlob ( ) ;
var form = Form . Parse ( formData . Config ) ;
2023-04-26 09:45:35 +02:00
form . ApplyValuesFromForm ( Request . Query ) ;
2023-02-23 09:52:37 +01:00
var vm = new FormViewModel
2023-02-20 11:35:54 +01:00
{
StoreName = store . StoreName ,
2024-05-09 02:18:02 +02:00
StoreBranding = await StoreBrandingViewModel . CreateAsync ( Request , _uriResolver , storeBlob ) ,
2023-02-20 11:35:54 +01:00
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 11:07:03 +09:00
2023-02-23 09:52:37 +01:00
return View ( "Views/UIForms/View" , vm ) ;
2023-02-20 11:35:54 +01:00
}
2023-02-23 09:52:37 +01:00
[HttpPost("/apps/{appId}/pos/form/submit/{viewType?}")]
2023-03-01 07:27:18 +01:00
[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 )
2023-02-20 11:35:54 +01:00
{
2023-03-20 10:39:26 +09:00
var app = await _appService . GetApp ( appId , PointOfSaleAppType . AppType ) ;
2023-02-20 11:35:54 +01:00
if ( app = = null )
return NotFound ( ) ;
2023-04-10 11:07:03 +09:00
2023-02-20 11:35:54 +01: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-20 11:35:54 +01:00
{
2023-02-23 09:52:37 +01:00
return RedirectToAction ( nameof ( ViewPointOfSale ) , new { appId , viewType } ) ;
2022-07-22 15:41:14 +02:00
}
2023-02-20 11:35:54 +01: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 11:07:03 +09:00
2023-02-23 09:52:37 +01:00
if ( Request is { Method : "POST" , HasFormContentType : true } )
2023-02-20 11:35:54 +01:00
{
2023-02-23 09:52:37 +01:00
form . ApplyValuesFromForm ( Request . Form . Where ( pair = > formFieldNames . Contains ( pair . Key ) ) ) ;
2023-04-10 11:07:03 +09:00
2023-02-20 11:35:54 +01:00
if ( FormDataService . Validate ( form , ModelState ) )
{
2023-03-17 03:56:32 +01:00
var controller = nameof ( UIPointOfSaleController ) . TrimEnd ( "Controller" , StringComparison . InvariantCulture ) ;
2023-02-23 09:52:37 +01:00
var redirectUrl =
2024-12-21 00:16:04 +09:00
Url . ActionAbsolute ( Request , nameof ( ViewPointOfSale ) , controller , new { appId , viewType } ) . ToString ( ) ;
2023-04-04 04:01:34 +02:00
formParameters . Add ( "formResponse" , FormDataService . GetValues ( form ) . ToString ( ) ) ;
2023-02-20 11:35:54 +01:00
return View ( "PostRedirect" , new PostRedirectViewModel
{
2023-02-23 09:52:37 +01:00
FormUrl = redirectUrl ,
FormParameters = formParameters
2023-02-20 11:35:54 +01:00
} ) ;
}
}
2024-01-16 08:55:38 +01:00
var store = await _appService . GetStore ( app ) ;
var storeBlob = store . GetStoreBlob ( ) ;
2023-02-20 11:35:54 +01:00
viewModel . FormName = formData . Name ;
viewModel . Form = form ;
2023-02-23 09:52:37 +01:00
viewModel . FormParameters = formParameters ;
2024-05-09 02:18:02 +02:00
viewModel . StoreBranding = await StoreBrandingViewModel . CreateAsync ( Request , _uriResolver , storeBlob ) ;
2023-02-20 11:35:54 +01:00
return View ( "Views/UIForms/View" , viewModel ) ;
2022-07-22 15:41:14 +02:00
}
2023-11-30 10:19:03 +01:00
2023-12-04 14:14:37 +01:00
[Authorize(Policy = Policies.CanViewInvoices, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2023-11-30 10:19:03 +01:00
[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 ) ;
2024-05-15 07:49:53 +09:00
var invoices = await AppService . GetInvoicesForApp ( _invoiceRepository , app , from ) ;
2023-11-30 10:19:03 +01:00
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 ) ,
2024-05-15 07:49:53 +09:00
["status"] = i . GetInvoiceState ( ) . Status . ToString ( ) ,
2023-11-30 10:19:03 +01:00
["url"] = Url . Action ( nameof ( UIInvoiceController . Invoice ) , "UIInvoice" , new { invoiceId = i . Id } )
} ) ;
return Json ( recent ) ;
}
2023-01-06 14:18:07 +01:00
2024-03-14 10:25:40 +01:00
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2021-12-11 04:32:23 +01:00
[HttpGet("{appId}/settings/pos")]
2022-06-19 19:55:47 -07:00
public async Task < IActionResult > UpdatePointOfSale ( string appId )
2018-04-03 11:50:41 +09:00
{
2021-12-20 15:15:32 +01:00
var app = GetCurrentApp ( ) ;
if ( app = = null )
2018-04-03 11:50:41 +09:00
return NotFound ( ) ;
2021-12-31 16:59:02 +09:00
2021-12-20 15:15:32 +01:00
var settings = app . GetSettings < PointOfSaleSettings > ( ) ;
2020-05-27 14:20:21 +02:00
settings . DefaultView = settings . EnableShoppingCart ? PosViewType . Cart : settings . DefaultView ;
settings . EnableShoppingCart = false ;
2022-11-02 10:21:33 +01:00
2021-04-08 15:32:42 +02:00
var vm = new UpdatePointOfSaleViewModel
2018-05-24 23:54:48 +09:00
{
2019-01-31 08:56:21 +01:00
Id = appId ,
2021-12-20 15:15:32 +01:00
StoreId = app . StoreDataId ,
StoreName = app . StoreData ? . StoreName ,
2022-06-19 19:55:47 -07:00
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty ( app . StoreDataId , settings . Currency ) ,
2023-09-11 02:59:17 +02:00
Archived = app . Archived ,
2021-12-20 15:15:32 +01:00
AppName = app . Name ,
2018-05-24 23:54:48 +09:00
Title = settings . Title ,
2020-05-26 16:48:47 +02:00
DefaultView = settings . DefaultView ,
2024-03-14 11:11:54 +01:00
ShowItems = settings . ShowItems ,
2018-05-24 23:54:48 +09:00
ShowCustomAmount = settings . ShowCustomAmount ,
2019-02-25 14:11:03 +08:00
ShowDiscount = settings . ShowDiscount ,
2023-11-13 13:59:14 +01:00
ShowSearch = settings . ShowSearch ,
ShowCategories = settings . ShowCategories ,
2019-02-25 14:11:03 +08:00
EnableTips = settings . EnableTips ,
2018-05-24 23:54:48 +09:00
Currency = settings . Currency ,
2018-11-16 20:39:43 -06:00
Template = settings . Template ,
ButtonText = settings . ButtonText ? ? PointOfSaleSettings . BUTTON_TEXT_DEF ,
CustomButtonText = settings . CustomButtonText ? ? PointOfSaleSettings . CUSTOM_BUTTON_TEXT_DEF ,
2018-11-27 14:14:32 +08:00
CustomTipText = settings . CustomTipText ? ? PointOfSaleSettings . CUSTOM_TIP_TEXT_DEF ,
2018-12-19 14:07:05 +08:00
CustomTipPercentages = settings . CustomTipPercentages ! = null ? string . Join ( "," , settings . CustomTipPercentages ) : string . Join ( "," , PointOfSaleSettings . CUSTOM_TIP_PERCENTAGES_DEF ) ,
2025-01-15 05:49:25 +00:00
Language = settings . Language ,
HtmlMetaTags = settings . HtmlMetaTags ,
2019-08-19 07:13:42 +02:00
Description = settings . Description ,
2019-04-11 11:08:42 +02:00
NotificationUrl = settings . NotificationUrl ,
2020-10-13 15:51:28 +08:00
RedirectUrl = settings . RedirectUrl ,
2023-07-20 09:03:39 +02:00
SearchTerm = app . TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"appid:{app.Id}" ,
2021-10-27 07:32:56 -07:00
RedirectAutomatically = settings . RedirectAutomatically . HasValue ? settings . RedirectAutomatically . Value ? "true" : "false" : "" ,
2022-11-25 02:42:55 +01:00
FormId = settings . FormId
2018-05-24 23:54:48 +09:00
} ;
2023-02-20 11:35:54 +01:00
if ( HttpContext . Request ! = null )
2018-05-24 23:54:48 +09:00
{
2021-10-25 16:54:36 +09:00
var appUrl = HttpContext . Request . GetAbsoluteUri ( $"/apps/{appId}/pos" ) ;
2018-05-24 23:54:48 +09:00
var encoder = HtmlEncoder . Default ;
if ( settings . ShowCustomAmount )
{
2023-02-20 11:35:54 +01:00
var builder = new StringBuilder ( ) ;
2021-12-27 13:46:31 +09:00
builder . AppendLine ( CultureInfo . InvariantCulture , $"<form method=\" POST \ " action=\"{encoder.Encode(appUrl)}\">" ) ;
2018-05-24 23:54:48 +09: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 23:54:48 +09:00
var builder = new StringBuilder ( ) ;
2021-12-27 13:46:31 +09:00
builder . AppendLine ( CultureInfo . InvariantCulture , $"<form method=\" POST \ " action=\"{encoder.Encode(appUrl)}\">" ) ;
2018-05-24 23:54:48 +09: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 13:46:31 +09:00
builder . AppendLine ( CultureInfo . InvariantCulture , $" <button type=\" submit \ " name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>" ) ;
2018-05-24 23:54:48 +09:00
builder . AppendLine ( $"</form>" ) ;
vm . Example2 = builder . ToString ( ) ;
}
catch { }
vm . InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3" ;
}
2018-05-25 17:35:01 +09: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}" ;
2024-09-26 12:10:14 +02:00
await FillUsers ( vm ) ;
2022-07-18 20:51:53 +02:00
return View ( "PointOfSale/UpdatePointOfSale" , vm ) ;
2018-04-03 11:50:41 +09:00
}
2021-12-11 04:32:23 +01:00
2022-07-22 15:41:14 +02:00
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2021-12-11 04:32:23 +01:00
[HttpPost("{appId}/settings/pos")]
2018-04-03 11:50:41 +09:00
public async Task < IActionResult > UpdatePointOfSale ( string appId , UpdatePointOfSaleViewModel vm )
2021-12-20 15:15:32 +01:00
{
var app = GetCurrentApp ( ) ;
if ( app = = null )
2021-10-25 16:54:36 +09:00
return NotFound ( ) ;
2021-12-31 16:59:02 +09:00
2023-07-06 04:01:36 +02:00
vm . Id = app . Id ;
2021-03-29 22:26:33 -07:00
if ( ! ModelState . IsValid )
2022-07-18 20:51:53 +02:00
return View ( "PointOfSale/UpdatePointOfSale" , vm ) ;
2021-12-16 17:37:19 +01:00
2021-12-20 15:15:32 +01:00
vm . Currency = await GetStoreDefaultCurrentIfEmpty ( app . StoreDataId , vm . Currency ) ;
2019-02-17 16:53:41 +09:00
if ( _currencies . GetCurrencyData ( vm . Currency , false ) = = null )
2018-04-03 11:50:41 +09:00
ModelState . AddModelError ( nameof ( vm . Currency ) , "Invalid currency" ) ;
try
{
2024-09-26 08:52:16 +02:00
vm . Template = AppService . SerializeTemplate ( AppService . Parse ( vm . Template , true , true ) ) ;
2018-04-03 11:50:41 +09:00
}
2024-09-26 08:52:16 +02:00
catch ( Exception ex )
2018-04-03 11:50:41 +09:00
{
2024-09-26 08:52:16 +02:00
ModelState . AddModelError ( nameof ( vm . Template ) , $"Invalid template: {ex.Message}" ) ;
2018-04-03 11:50:41 +09:00
}
if ( ! ModelState . IsValid )
{
2024-09-26 12:10:14 +02:00
await FillUsers ( vm ) ;
2022-07-18 20:51:53 +02:00
return View ( "PointOfSale/UpdatePointOfSale" , vm ) ;
2018-04-03 11:50:41 +09:00
}
2021-10-29 06:29:02 -04:00
2025-01-15 05:49:25 +00:00
bool wasHtmlModified ;
2022-11-02 10:21:33 +01:00
var settings = new PointOfSaleSettings
2018-04-03 11:50:41 +09:00
{
Title = vm . Title ,
2020-05-26 16:48:47 +02:00
DefaultView = vm . DefaultView ,
2024-03-14 11:11:54 +01:00
ShowItems = vm . ShowItems ,
2018-04-26 22:09:18 +09:00
ShowCustomAmount = vm . ShowCustomAmount ,
2019-02-25 14:11:03 +08:00
ShowDiscount = vm . ShowDiscount ,
2023-11-13 13:59:14 +01:00
ShowSearch = vm . ShowSearch ,
ShowCategories = vm . ShowCategories ,
2019-02-25 14:11:03 +08:00
EnableTips = vm . EnableTips ,
2021-10-25 16:54:36 +09:00
Currency = vm . Currency ,
2018-11-16 20:39:43 -06:00
Template = vm . Template ,
ButtonText = vm . ButtonText ,
CustomButtonText = vm . CustomButtonText ,
2018-11-27 14:14:32 +08:00
CustomTipText = vm . CustomTipText ,
2018-12-19 14:07:05 +08:00
CustomTipPercentages = ListSplit ( vm . CustomTipPercentages ) ,
2019-04-11 09:14:39 +02:00
NotificationUrl = vm . NotificationUrl ,
2020-10-13 15:51:28 +08:00
RedirectUrl = vm . RedirectUrl ,
2025-01-15 05:49:25 +00:00
Language = vm . Language ,
HtmlMetaTags = _safe . RawMeta ( vm . HtmlMetaTags , out wasHtmlModified ) ,
2019-08-19 07:13:42 +02:00
Description = vm . Description ,
2023-08-09 10:31:19 +03:00
RedirectAutomatically = string . IsNullOrEmpty ( vm . RedirectAutomatically ) ? null : bool . Parse ( vm . RedirectAutomatically ) ,
FormId = vm . FormId
2022-11-02 10:21:33 +01:00
} ;
app . Name = vm . AppName ;
2023-09-11 02:59:17 +02:00
app . Archived = vm . Archived ;
2022-11-02 10:21:33 +01:00
app . SetSettings ( settings ) ;
2021-12-20 15:15:32 +01:00
await _appService . UpdateOrCreateApp ( app ) ;
2025-01-15 05:49:25 +00:00
if ( wasHtmlModified )
{
TempData [ WellKnownTempData . ErrorMessage ] = StringLocalizer [ "Only meta tags are allowed in HTML headers. Your HTML code has been cleaned up accordingly." ] . Value ;
} else {
TempData [ WellKnownTempData . SuccessMessage ] = StringLocalizer [ "App updated" ] . Value ;
}
2019-08-01 02:55:41 -04:00
return RedirectToAction ( nameof ( UpdatePointOfSale ) , new { appId } ) ;
2018-04-03 11:50:41 +09:00
}
2021-12-31 16:59:02 +09:00
2018-12-19 14:07:05 +08:00
private int [ ] ListSplit ( string list , string separator = "," )
{
if ( string . IsNullOrEmpty ( list ) )
{
return Array . Empty < int > ( ) ;
2020-01-23 20:19:24 -06:00
}
2021-12-31 16:59:02 +09:00
2021-12-16 17:37:19 +01:00
// Remove all characters except numeric and comma
Regex charsToDestroy = new Regex ( @"[^\d|\" + separator + "]" ) ;
list = charsToDestroy . Replace ( list , "" ) ;
2018-12-19 14:07:05 +08:00
2021-12-16 17:37:19 +01:00
return list . Split ( separator , StringSplitOptions . RemoveEmptyEntries ) . Select ( int . Parse ) . ToArray ( ) ;
2018-12-19 14:07:05 +08:00
}
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 ( ) ;
}
2022-11-02 10:21:33 +01:00
private StoreData GetCurrentStore ( ) = > HttpContext . GetStoreData ( ) ;
2023-01-06 14:18:07 +01:00
2022-07-18 20:51:53 +02:00
private AppData GetCurrentApp ( ) = > HttpContext . GetAppData ( ) ;
2024-09-26 12:10:14 +02:00
private async Task FillUsers ( UpdatePointOfSaleViewModel vm )
{
var users = await _storeRepository . GetStoreUsers ( GetCurrentStore ( ) . Id ) ;
vm . StoreUsers = users . Select ( u = > ( u . Id , u . Email , u . StoreRole . Role ) ) . ToDictionary ( u = > u . Id , u = > $"{u.Email} ({u.Role})" ) ;
}
2018-04-03 11:50:41 +09:00
}
}