2020-06-28 21:44:35 -05:00
using System ;
2019-02-19 13:04:58 +09:00
using System.Collections.Generic ;
2023-09-05 15:32:49 +09:00
using System.Configuration ;
2023-02-25 23:34:49 +09:00
using System.Diagnostics.CodeAnalysis ;
2019-02-19 13:04:58 +09:00
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Threading.Tasks ;
2023-05-26 16:49:32 +02:00
using BTCPayServer.Client ;
2019-02-19 13:04:58 +09:00
using BTCPayServer.Data ;
using BTCPayServer.Models.AppViewModels ;
2023-03-17 03:56:32 +01:00
using BTCPayServer.Plugins.Crowdfund ;
using BTCPayServer.Plugins.PointOfSale ;
2022-07-18 20:51:53 +02:00
using BTCPayServer.Plugins.PointOfSale.Models ;
2019-02-19 13:04:58 +09:00
using BTCPayServer.Services.Invoices ;
using BTCPayServer.Services.Rates ;
2019-05-30 07:02:52 +00:00
using BTCPayServer.Services.Stores ;
2023-12-19 20:53:11 +09:00
using Dapper ;
2023-10-18 19:33:43 +09:00
using Ganss.Xss ;
2019-02-19 13:04:58 +09:00
using Microsoft.EntityFrameworkCore ;
2019-09-02 15:37:52 +02:00
using NBitcoin ;
using NBitcoin.DataEncoders ;
2022-06-28 07:05:02 +02:00
using Newtonsoft.Json ;
2019-09-02 15:37:52 +02:00
using Newtonsoft.Json.Linq ;
2020-07-24 09:40:37 +02:00
using StoreData = BTCPayServer . Data . StoreData ;
2019-02-19 13:04:58 +09:00
namespace BTCPayServer.Services.Apps
{
public class AppService
{
2023-03-20 10:39:26 +09:00
private readonly Dictionary < string , AppBaseType > _appTypes ;
2023-05-23 02:18:57 +02:00
static AppService ( )
{
_defaultSerializer = new JsonSerializerSettings ( )
{
ContractResolver = new Newtonsoft . Json . Serialization . CamelCasePropertyNamesContractResolver ( ) ,
Formatting = Formatting . None
} ;
}
private static JsonSerializerSettings _defaultSerializer ;
2020-06-28 22:07:48 -05:00
readonly ApplicationDbContextFactory _ContextFactory ;
2019-02-19 13:04:58 +09:00
private readonly InvoiceRepository _InvoiceRepository ;
2020-06-28 22:07:48 -05:00
readonly CurrencyNameTable _Currencies ;
2023-03-13 02:12:58 +01:00
private readonly DisplayFormatter _displayFormatter ;
2019-05-30 07:02:52 +00:00
private readonly StoreRepository _storeRepository ;
2019-02-19 13:04:58 +09:00
public CurrencyNameTable Currencies = > _Currencies ;
2023-12-19 20:53:11 +09:00
2023-03-17 03:56:32 +01:00
public AppService (
2023-03-20 10:39:26 +09:00
IEnumerable < AppBaseType > apps ,
2023-03-17 03:56:32 +01:00
ApplicationDbContextFactory contextFactory ,
InvoiceRepository invoiceRepository ,
CurrencyNameTable currencies ,
DisplayFormatter displayFormatter ,
2024-02-21 20:53:24 +01:00
StoreRepository storeRepository )
2019-02-19 13:04:58 +09:00
{
2023-04-10 11:07:03 +09:00
_appTypes = apps . ToDictionary ( a = > a . Type , a = > a ) ;
2019-02-19 13:04:58 +09:00
_ContextFactory = contextFactory ;
_InvoiceRepository = invoiceRepository ;
_Currencies = currencies ;
2019-05-30 07:02:52 +00:00
_storeRepository = storeRepository ;
2023-03-13 02:12:58 +01:00
_displayFormatter = displayFormatter ;
2019-02-19 13:04:58 +09:00
}
2023-03-20 10:39:26 +09:00
#nullable enable
2023-03-17 03:56:32 +01:00
public Dictionary < string , string > GetAvailableAppTypes ( )
2019-02-19 13:04:58 +09:00
{
2023-03-20 10:39:26 +09:00
return _appTypes . ToDictionary ( app = > app . Key , app = > app . Value . Description ) ;
2022-04-12 09:55:10 +02:00
}
2023-03-20 10:39:26 +09:00
public AppBaseType ? GetAppType ( string appType )
2022-04-12 09:55:10 +02:00
{
2023-03-20 10:39:26 +09:00
_appTypes . TryGetValue ( appType , out var a ) ;
return a ;
2022-04-12 09:55:10 +02:00
}
2023-03-20 10:39:26 +09:00
public async Task < object? > GetInfo ( string appId )
2022-04-12 09:55:10 +02:00
{
2023-12-20 18:41:28 +09:00
var appData = await GetApp ( appId , null , includeStore : true ) ;
2023-03-17 03:56:32 +01:00
if ( appData is null )
return null ;
2023-03-20 10:39:26 +09:00
var appType = GetAppType ( appData . AppType ) ;
if ( appType is null )
2023-03-17 03:56:32 +01:00
return null ;
2023-12-20 18:41:28 +09:00
return await appType . GetInfo ( appData ) ;
2022-04-12 09:55:10 +02:00
}
2023-03-20 10:39:26 +09:00
2022-06-28 07:05:02 +02:00
public async Task < IEnumerable < ItemStats > > GetItemStats ( AppData appData )
2022-04-12 09:55:10 +02:00
{
2023-03-20 10:39:26 +09:00
if ( GetAppType ( appData . AppType ) is not IHasItemStatsAppType salesType )
throw new InvalidOperationException ( "This app isn't a SalesAppBaseType" ) ;
2023-04-10 11:07:03 +09:00
var paidInvoices = await GetInvoicesForApp ( _InvoiceRepository , appData ,
null , new [ ]
2022-06-28 07:05:02 +02:00
{
2023-03-17 03:56:32 +01:00
InvoiceState . ToString ( InvoiceStatusLegacy . Paid ) ,
InvoiceState . ToString ( InvoiceStatusLegacy . Confirmed ) ,
InvoiceState . ToString ( InvoiceStatusLegacy . Complete )
} ) ;
2023-03-20 10:39:26 +09:00
return await salesType . GetItemStats ( appData , paidInvoices ) ;
2022-06-28 07:05:02 +02:00
}
2023-03-17 03:56:32 +01:00
public static Task < SalesStats > GetSalesStatswithPOSItems ( ViewPointOfSaleViewModel . Item [ ] items ,
2023-04-10 11:07:03 +09:00
InvoiceEntity [ ] paidInvoices , int numberOfDays )
2022-06-28 07:05:02 +02:00
{
2022-04-12 09:55:10 +02:00
var series = paidInvoices
2022-06-28 07:05:02 +02:00
. Aggregate ( new List < InvoiceStatsItem > ( ) , AggregateInvoiceEntitiesForStats ( items ) )
. GroupBy ( entity = > entity . Date )
2022-04-12 09:55:10 +02:00
. Select ( entities = > new SalesStatsItem
{
Date = entities . Key ,
Label = entities . Key . ToString ( "MMM dd" , CultureInfo . InvariantCulture ) ,
SalesCount = entities . Count ( )
} ) ;
// fill up the gaps
foreach ( var i in Enumerable . Range ( 0 , numberOfDays ) )
{
var date = ( DateTimeOffset . UtcNow - TimeSpan . FromDays ( i ) ) . Date ;
if ( ! series . Any ( e = > e . Date = = date ) )
{
series = series . Append ( new SalesStatsItem
{
Date = date ,
Label = date . ToString ( "MMM dd" , CultureInfo . InvariantCulture )
} ) ;
}
}
2023-01-06 14:18:07 +01:00
2023-03-17 03:56:32 +01:00
return Task . FromResult ( new SalesStats
2022-04-12 09:55:10 +02:00
{
2022-06-28 07:05:02 +02:00
SalesCount = series . Sum ( i = > i . SalesCount ) ,
2022-04-12 09:55:10 +02:00
Series = series . OrderBy ( i = > i . Label )
2023-03-17 03:56:32 +01:00
} ) ;
}
2023-04-10 11:07:03 +09:00
2023-03-17 03:56:32 +01:00
public async Task < SalesStats > GetSalesStats ( AppData app , int numberOfDays = 7 )
{
2023-03-20 10:39:26 +09:00
if ( GetAppType ( app . AppType ) is not IHasSaleStatsAppType salesType )
throw new InvalidOperationException ( "This app isn't a SalesAppBaseType" ) ;
2023-03-17 03:56:32 +01:00
var paidInvoices = await GetInvoicesForApp ( _InvoiceRepository , app , DateTimeOffset . UtcNow - TimeSpan . FromDays ( numberOfDays ) ,
2023-04-10 11:07:03 +09:00
new [ ]
2023-03-17 03:56:32 +01:00
{
InvoiceState . ToString ( InvoiceStatusLegacy . Paid ) ,
InvoiceState . ToString ( InvoiceStatusLegacy . Confirmed ) ,
InvoiceState . ToString ( InvoiceStatusLegacy . Complete )
} ) ;
2023-04-10 11:07:03 +09:00
2023-03-20 10:39:26 +09:00
return await salesType . GetSalesStats ( app , paidInvoices , numberOfDays ) ;
2022-04-12 09:55:10 +02:00
}
2023-03-17 03:56:32 +01:00
public class InvoiceStatsItem
2022-06-28 07:05:02 +02:00
{
2023-03-20 10:39:26 +09:00
public string ItemCode { get ; set ; } = string . Empty ;
2022-06-28 07:05:02 +02:00
public decimal FiatPrice { get ; set ; }
public DateTime Date { get ; set ; }
}
2023-01-06 14:18:07 +01:00
2023-03-17 03:56:32 +01:00
public static Func < List < InvoiceStatsItem > , InvoiceEntity , List < InvoiceStatsItem > > AggregateInvoiceEntitiesForStats ( ViewPointOfSaleViewModel . Item [ ] items )
2022-06-28 07:05:02 +02:00
{
return ( res , e ) = >
{
2023-09-14 02:26:47 +02:00
// flatten single items from POS data
var data = e . Metadata . PosData ? . ToObject < PosAppData > ( ) ;
if ( data is { Cart . Length : > 0 } )
2022-06-28 07:05:02 +02:00
{
foreach ( var lineItem in data . Cart )
{
var item = items . FirstOrDefault ( p = > p . Id = = lineItem . Id ) ;
2023-01-06 14:18:07 +01:00
if ( item = = null )
continue ;
2022-06-28 07:05:02 +02:00
for ( var i = 0 ; i < lineItem . Count ; i + + )
{
res . Add ( new InvoiceStatsItem
{
ItemCode = item . Id ,
2023-06-02 09:34:55 +02:00
FiatPrice = lineItem . Price ,
2022-06-28 07:05:02 +02:00
Date = e . InvoiceTime . Date
} ) ;
}
}
}
2022-07-06 05:40:16 +02:00
else
2023-09-14 02:26:47 +02:00
{
2022-07-06 05:40:16 +02:00
res . Add ( new InvoiceStatsItem
{
2023-09-14 02:26:47 +02:00
ItemCode = e . Metadata . ItemCode ? ? typeof ( PosViewType ) . DisplayName ( PosViewType . Light . ToString ( ) ) ,
2023-07-19 18:47:32 +09:00
FiatPrice = e . PaidAmount . Net ,
2022-07-06 05:40:16 +02:00
Date = e . InvoiceTime . Date
} ) ;
}
2022-06-28 07:05:02 +02:00
return res ;
} ;
}
2023-04-10 11:07:03 +09:00
2023-07-20 09:03:39 +02:00
public static string GetAppSearchTerm ( AppData app ) = > GetAppSearchTerm ( app . AppType , app . Id ) ;
public static string GetAppSearchTerm ( string appType , string appId ) = >
2023-03-17 03:56:32 +01:00
appType switch
2022-06-28 07:05:02 +02:00
{
2023-03-20 10:39:26 +09:00
CrowdfundAppType . AppType = > $"crowdfund-app_{appId}" ,
PointOfSaleAppType . AppType = > $"pos-app_{appId}" ,
2023-03-17 03:56:32 +01:00
_ = > $"{appType}_{appId}"
2022-06-28 07:05:02 +02:00
} ;
2019-02-19 13:04:58 +09:00
public static string GetAppInternalTag ( string appId ) = > $"APP#{appId}" ;
2019-02-25 16:15:45 +09:00
public static string [ ] GetAppInternalTags ( InvoiceEntity invoice )
2019-02-19 13:04:58 +09:00
{
2019-02-25 16:15:45 +09:00
return invoice . GetInternalTags ( "APP#" ) ;
2019-02-19 13:04:58 +09:00
}
2023-07-20 09:03:39 +02:00
public static string GetRandomOrderId ( int length = 16 )
{
return Encoders . Base58 . EncodeData ( RandomUtils . GetBytes ( length ) ) ;
}
2023-01-06 14:18:07 +01:00
2023-04-10 11:07:03 +09:00
public static async Task < InvoiceEntity [ ] > GetInvoicesForApp ( InvoiceRepository invoiceRepository , AppData appData , DateTimeOffset ? startDate = null , string [ ] ? status = null )
2019-02-19 13:04:58 +09:00
{
2023-03-17 03:56:32 +01:00
var invoices = await invoiceRepository . GetInvoices ( new InvoiceQuery
2019-02-19 13:04:58 +09:00
{
2023-03-17 03:56:32 +01:00
StoreId = new [ ] { appData . StoreDataId } ,
2023-07-20 09:03:39 +02:00
TextSearch = appData . TagAllInvoices ? null : GetAppSearchTerm ( appData ) ,
2023-04-10 11:07:03 +09:00
Status = status ? ? new [ ] {
2020-11-23 15:57:05 +09:00
InvoiceState . ToString ( InvoiceStatusLegacy . New ) ,
InvoiceState . ToString ( InvoiceStatusLegacy . Paid ) ,
InvoiceState . ToString ( InvoiceStatusLegacy . Confirmed ) ,
InvoiceState . ToString ( InvoiceStatusLegacy . Complete ) } ,
2019-02-19 13:04:58 +09:00
StartDate = startDate
} ) ;
// Old invoices may have invoices which were not tagged
2021-12-31 16:59:02 +09:00
invoices = invoices . Where ( inv = > appData . TagAllInvoices | | inv . Version < InvoiceEntity . InternalTagSupport_Version | |
2019-02-19 13:04:58 +09:00
inv . InternalTags . Contains ( GetAppInternalTag ( appData . Id ) ) ) . ToArray ( ) ;
return invoices ;
}
public async Task < bool > DeleteApp ( AppData appData )
{
2023-03-17 03:56:32 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2022-01-14 17:50:29 +09:00
ctx . Apps . Add ( appData ) ;
ctx . Entry ( appData ) . State = EntityState . Deleted ;
return await ctx . SaveChangesAsync ( ) = = 1 ;
2019-02-19 13:04:58 +09:00
}
2023-09-11 02:59:17 +02:00
public async Task < bool > SetArchived ( AppData appData , bool archived )
{
await using var ctx = _ContextFactory . CreateContext ( ) ;
appData . Archived = archived ;
ctx . Entry ( appData ) . State = EntityState . Modified ;
return await ctx . SaveChangesAsync ( ) = = 1 ;
}
public async Task < ListAppsViewModel . ListAppViewModel [ ] > GetAllApps ( string? userId , bool allowNoUser = false , string? storeId = null , bool includeArchived = false )
2019-02-19 13:04:58 +09:00
{
2023-03-17 03:56:32 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2023-11-20 02:48:56 +01:00
var types = GetAvailableAppTypes ( ) . Select ( at = > at . Key ) . ToHashSet ( ) ;
2023-05-26 16:49:32 +02:00
var listApps = ( await ctx . UserStore
2022-01-14 17:50:29 +09:00
. Where ( us = >
( allowNoUser & & string . IsNullOrEmpty ( userId ) | | us . ApplicationUserId = = userId ) & &
( storeId = = null | | us . StoreDataId = = storeId ) )
2023-05-26 16:49:32 +02:00
. Include ( store = > store . StoreRole )
. Include ( store = > store . StoreData )
. Join ( ctx . Apps , us = > us . StoreDataId , app = > app . StoreDataId , ( us , app ) = > new { us , app } )
2023-11-20 02:48:56 +01:00
. Where ( b = > types . Contains ( b . app . AppType ) & & ( ! b . app . Archived | | b . app . Archived = = includeArchived ) )
2023-05-26 16:49:32 +02:00
. OrderBy ( b = > b . app . Created )
. ToArrayAsync ( ) ) . Select ( arg = > new ListAppsViewModel . ListAppViewModel
{
Role = StoreRepository . ToStoreRole ( arg . us . StoreRole ) ,
StoreId = arg . us . StoreDataId ,
StoreName = arg . us . StoreData . StoreName ,
AppName = arg . app . Name ,
AppType = arg . app . AppType ,
Id = arg . app . Id ,
Created = arg . app . Created ,
2023-09-11 02:59:17 +02:00
Archived = arg . app . Archived ,
2023-05-26 16:49:32 +02:00
App = arg . app
} ) . ToArray ( ) ;
2023-04-10 11:07:03 +09:00
2023-02-02 12:53:42 +01:00
// allowNoUser can lead to apps being included twice, unify them with distinct
if ( allowNoUser )
{
listApps = listApps . DistinctBy ( a = > a . Id ) . ToArray ( ) ;
}
2022-01-14 17:50:29 +09:00
foreach ( ListAppsViewModel . ListAppViewModel app in listApps )
2019-02-19 13:04:58 +09:00
{
2023-03-17 03:56:32 +01:00
app . ViewStyle = GetAppViewStyle ( app . App , app . AppType ) ;
2019-02-19 13:04:58 +09:00
}
2022-01-14 17:50:29 +09:00
return listApps ;
2019-02-19 13:04:58 +09:00
}
2021-12-31 16:59:02 +09:00
2023-03-17 03:56:32 +01:00
public string GetAppViewStyle ( AppData app , string appType )
2021-11-02 14:55:31 -04:00
{
string style ;
2023-03-17 03:56:32 +01:00
switch ( appType )
2021-11-02 14:55:31 -04:00
{
2023-03-20 10:39:26 +09:00
case PointOfSaleAppType . AppType :
2023-03-17 03:56:32 +01:00
var settings = app . GetSettings < PointOfSaleSettings > ( ) ;
2021-11-02 14:55:31 -04:00
string posViewStyle = ( settings . EnableShoppingCart ? PosViewType . Cart : settings . DefaultView ) . ToString ( ) ;
style = typeof ( PosViewType ) . DisplayName ( posViewStyle ) ;
break ;
2023-04-10 11:07:03 +09:00
2021-11-02 14:55:31 -04:00
default :
style = string . Empty ;
break ;
}
return style ;
}
2020-06-28 17:55:27 +09:00
2023-09-11 02:59:17 +02:00
public async Task < List < AppData > > GetApps ( string [ ] appIds , bool includeStore = false , bool includeArchived = false )
2019-07-09 11:20:38 +02:00
{
2023-03-17 03:56:32 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2023-11-20 02:48:56 +01:00
var types = GetAvailableAppTypes ( ) . Select ( at = > at . Key ) ;
2022-01-14 17:50:29 +09:00
var query = ctx . Apps
2023-09-11 02:59:17 +02:00
. Where ( app = > appIds . Contains ( app . Id ) )
2023-11-20 02:48:56 +01:00
. Where ( app = > types . Contains ( app . AppType ) & & ( ! app . Archived | | app . Archived = = includeArchived ) ) ;
2022-01-14 17:50:29 +09:00
if ( includeStore )
{
query = query . Include ( data = > data . StoreData ) ;
2019-07-09 11:20:38 +02:00
}
2022-01-14 17:50:29 +09:00
return await query . ToListAsync ( ) ;
2019-07-09 11:20:38 +02:00
}
2019-02-19 13:04:58 +09:00
2023-03-17 03:56:32 +01:00
public async Task < List < AppData > > GetApps ( string appType )
2019-02-19 13:04:58 +09:00
{
2023-03-17 03:56:32 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
var query = ctx . Apps
. Where ( app = > app . AppType = = appType ) ;
return await query . ToListAsync ( ) ;
}
2023-09-11 02:59:17 +02:00
public async Task < AppData ? > GetApp ( string appId , string? appType , bool includeStore = false , bool includeArchived = false )
2023-03-17 03:56:32 +01:00
{
await using var ctx = _ContextFactory . CreateContext ( ) ;
2023-11-20 02:48:56 +01:00
var types = GetAvailableAppTypes ( ) . Select ( at = > at . Key ) ;
2022-01-14 17:50:29 +09:00
var query = ctx . Apps
2023-09-11 02:59:17 +02:00
. Where ( us = > us . Id = = appId & & ( appType = = null | | us . AppType = = appType ) )
2023-11-20 02:48:56 +01:00
. Where ( app = > types . Contains ( app . AppType ) & & ( ! app . Archived | | app . Archived = = includeArchived ) ) ;
2022-01-14 17:50:29 +09:00
if ( includeStore )
{
query = query . Include ( data = > data . StoreData ) ;
2019-02-19 13:04:58 +09:00
}
2022-01-14 17:50:29 +09:00
return await query . FirstOrDefaultAsync ( ) ;
2019-02-19 13:04:58 +09:00
}
2023-03-20 10:39:26 +09:00
public Task < StoreData ? > GetStore ( AppData app )
2019-02-19 13:04:58 +09:00
{
2019-05-30 07:02:52 +00:00
return _storeRepository . FindStore ( app . StoreDataId ) ;
2019-02-19 13:04:58 +09:00
}
2023-05-23 02:18:57 +02:00
public static string SerializeTemplate ( ViewPointOfSaleViewModel . Item [ ] items )
2023-03-17 03:56:32 +01:00
{
2023-05-23 02:18:57 +02:00
return JsonConvert . SerializeObject ( items , Formatting . Indented , _defaultSerializer ) ;
2023-03-17 03:56:32 +01:00
}
2023-05-23 02:18:57 +02:00
public static ViewPointOfSaleViewModel . Item [ ] Parse ( string template , bool includeDisabled = true )
2019-02-19 13:04:58 +09:00
{
if ( string . IsNullOrWhiteSpace ( template ) )
return Array . Empty < ViewPointOfSaleViewModel . Item > ( ) ;
2021-10-11 12:46:05 +02:00
2023-05-23 02:18:57 +02:00
return JsonConvert . DeserializeObject < ViewPointOfSaleViewModel . Item [ ] > ( template , _defaultSerializer ) ! . Where ( item = > includeDisabled | | ! item . Disabled ) . ToArray ( ) ;
2019-02-19 13:04:58 +09:00
}
2023-03-20 10:39:26 +09:00
#nullable restore
#nullable enable
public async Task < AppData ? > GetAppDataIfOwner ( string userId , string appId , string? type = null )
2019-02-19 13:04:58 +09:00
{
if ( userId = = null | | appId = = null )
return null ;
2023-03-17 03:56:32 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2022-01-14 17:50:29 +09:00
var app = await ctx . UserStore
2024-01-25 13:00:33 +01:00
. Include ( store = > store . StoreRole )
. Where ( us = > us . ApplicationUserId = = userId & & us . StoreRole . Permissions . Contains ( Policies . CanModifyStoreSettings ) )
. SelectMany ( us = > us . StoreData . Apps . Where ( a = > a . Id = = appId ) )
. FirstOrDefaultAsync ( ) ;
if ( app = = null )
return null ;
if ( type ! = null & & type ! = app . AppType )
return null ;
return app ;
}
public async Task < AppData ? > GetAppData ( string userId , string appId , string? type = null )
{
if ( userId = = null | | appId = = null )
return null ;
await using var ctx = _ContextFactory . CreateContext ( ) ;
var app = await ctx . UserStore
. Where ( us = > us . ApplicationUserId = = userId & & us . StoreData ! = null & & us . StoreData . UserStores . Any ( u = > u . ApplicationUserId = = userId ) )
. SelectMany ( us = > us . StoreData . Apps . Where ( a = > a . Id = = appId ) )
. FirstOrDefaultAsync ( ) ;
2022-01-14 17:50:29 +09:00
if ( app = = null )
return null ;
2023-03-17 03:56:32 +01:00
if ( type ! = null & & type ! = app . AppType )
2022-01-14 17:50:29 +09:00
return null ;
return app ;
2019-02-19 13:04:58 +09:00
}
2019-09-02 15:37:52 +02:00
2023-12-19 20:53:11 +09:00
record AppSettingsWithXmin ( string apptype , string settings , uint xmin ) ;
public record InventoryChange ( string ItemId , int Delta ) ;
public async Task UpdateInventory ( string appId , InventoryChange [ ] changes )
{
await using var ctx = _ContextFactory . CreateContext ( ) ;
// We use xmin to make sure we don't override changes made by another process
retry :
var connection = ctx . Database . GetDbConnection ( ) ;
var row = connection . QueryFirstOrDefault < AppSettingsWithXmin > (
"SELECT \"AppType\" AS apptype, \"Settings\" AS settings, xmin FROM \"Apps\" WHERE \"Id\"=@appId" , new { appId }
) ;
if ( row ? . settings is null )
return ;
var templatePath = row . apptype switch
{
CrowdfundAppType . AppType = > "PerksTemplate" ,
_ = > "Template"
} ;
var settings = JObject . Parse ( row . settings ) ;
2023-12-22 06:21:01 +01:00
if ( ! settings . TryGetValue ( templatePath , out var template ) )
return ;
var items = template . Type switch
{
JTokenType . String = > JArray . Parse ( template . Value < string > ( ) ! ) ,
JTokenType . Array = > ( JArray ) template ,
_ = > null
} ;
if ( items is null )
return ;
2023-12-19 21:48:11 +09:00
bool hasChange = false ;
2023-12-19 20:53:11 +09:00
foreach ( var change in changes )
{
2023-12-22 06:21:01 +01:00
var item = items . FirstOrDefault ( i = > i [ "id" ] ? . Value < string > ( ) = = change . ItemId & & i [ "inventory" ] ? . Type is JTokenType . Integer ) ;
2023-12-19 20:53:11 +09:00
if ( item is null )
continue ;
var inventory = item [ "inventory" ] ! . Value < int > ( ) ;
inventory + = change . Delta ;
item [ "inventory" ] = inventory ;
2023-12-19 21:48:11 +09:00
hasChange = true ;
2023-12-19 20:53:11 +09:00
}
2023-12-19 21:48:11 +09:00
if ( ! hasChange )
return ;
2023-12-19 20:53:11 +09:00
settings [ templatePath ] = items . ToString ( Formatting . None ) ;
var updated = await connection . ExecuteAsync ( "UPDATE \"Apps\" SET \"Settings\"=@v::JSONB WHERE \"Id\"=@appId AND xmin=@xmin" , new { appId , xmin = ( int ) row . xmin , v = settings . ToString ( Formatting . None ) } ) = = 1 ;
// If we can't update, it means someone else updated the row, so we need to retry
if ( ! updated )
goto retry ;
}
2019-09-02 15:37:52 +02:00
public async Task UpdateOrCreateApp ( AppData app )
{
2023-03-17 03:56:32 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2022-01-14 17:50:29 +09:00
if ( string . IsNullOrEmpty ( app . Id ) )
2019-09-02 15:37:52 +02:00
{
2022-01-14 17:50:29 +09:00
app . Id = Encoders . Base58 . EncodeData ( RandomUtils . GetBytes ( 20 ) ) ;
app . Created = DateTimeOffset . UtcNow ;
await ctx . Apps . AddAsync ( app ) ;
}
else
{
ctx . Apps . Update ( app ) ;
ctx . Entry ( app ) . Property ( data = > data . Created ) . IsModified = false ;
ctx . Entry ( app ) . Property ( data = > data . Id ) . IsModified = false ;
ctx . Entry ( app ) . Property ( data = > data . AppType ) . IsModified = false ;
2019-09-02 15:37:52 +02:00
}
2022-01-14 17:50:29 +09:00
await ctx . SaveChangesAsync ( ) ;
2019-09-02 15:37:52 +02:00
}
2020-06-28 17:55:27 +09:00
2023-03-20 10:39:26 +09:00
private static bool TryParseJson ( string json , [ MaybeNullWhen ( false ) ] out JObject result )
2019-09-02 15:37:52 +02:00
{
result = null ;
try
{
result = JObject . Parse ( json ) ;
return true ;
}
catch
{
return false ;
}
}
2023-02-25 23:34:49 +09:00
#nullable enable
2023-09-05 15:32:49 +09:00
public static bool TryParsePosCartItems ( JObject ? posData , [ MaybeNullWhen ( false ) ] out List < PosCartItem > cartItems )
2019-09-02 15:37:52 +02:00
{
cartItems = null ;
2023-02-25 23:34:49 +09:00
if ( posData is null )
return false ;
2023-09-05 15:32:49 +09:00
if ( ! posData . TryGetValue ( "cart" , out var cartObject ) | | cartObject is null )
2020-06-28 17:55:27 +09:00
return false ;
2023-09-05 15:32:49 +09:00
try
2023-02-25 23:34:49 +09:00
{
2023-09-05 15:32:49 +09:00
cartItems = new List < PosCartItem > ( ) ;
foreach ( var o in cartObject . OfType < JObject > ( ) )
2023-02-25 23:34:49 +09:00
{
2023-09-05 15:32:49 +09:00
var id = o . GetValue ( "id" , StringComparison . InvariantCulture ) ? . ToString ( ) ;
if ( id = = null )
continue ;
2023-02-25 23:34:49 +09:00
var countStr = o . GetValue ( "count" , StringComparison . InvariantCulture ) ? . ToString ( ) ? ? string . Empty ;
2023-09-05 15:32:49 +09:00
var price = o . GetValue ( "price" ) switch
{
JValue v = > v . Value < decimal > ( ) ,
// Don't crash on legacy format
JObject v2 = > v2 [ "value" ] ? . Value < decimal > ( ) ? ? 0 m ,
_ = > 0 m
} ;
2023-02-25 23:34:49 +09:00
if ( int . TryParse ( countStr , out var count ) )
{
2023-09-05 15:32:49 +09:00
cartItems . Add ( new PosCartItem { Id = id , Count = count , Price = price } ) ;
2023-02-25 23:34:49 +09:00
}
}
2023-09-05 15:32:49 +09:00
return true ;
2023-02-25 23:34:49 +09:00
}
2023-09-05 15:32:49 +09:00
catch ( FormatException )
2023-08-09 10:31:19 +03:00
{
2023-09-05 15:32:49 +09:00
return false ;
2023-08-09 10:31:19 +03:00
}
}
2023-03-17 03:56:32 +01:00
public async Task SetDefaultSettings ( AppData appData , string defaultCurrency )
{
2023-03-20 10:39:26 +09:00
var app = GetAppType ( appData . AppType ) ;
2023-03-17 03:56:32 +01:00
if ( app is null )
{
appData . SetSettings ( null ) ;
}
else
{
await app . SetDefaultSettings ( appData , defaultCurrency ) ;
}
}
public async Task < string? > ViewLink ( AppData app )
{
2023-03-20 10:39:26 +09:00
var appType = GetAppType ( app . AppType ) ;
2023-03-17 03:56:32 +01:00
return await appType ? . ViewLink ( app ) ! ;
}
2023-02-25 23:34:49 +09:00
#nullable restore
2019-02-19 13:04:58 +09:00
}
2022-04-12 09:55:10 +02:00
2023-08-09 10:31:19 +03:00
public class PosCartItem
{
public string Id { get ; set ; }
public int Count { get ; set ; }
public decimal Price { get ; set ; }
}
2022-04-12 09:55:10 +02:00
public class ItemStats
{
public string ItemCode { get ; set ; }
public string Title { get ; set ; }
public int SalesCount { get ; set ; }
public decimal Total { get ; set ; }
public string TotalFormatted { get ; set ; }
}
2023-01-06 14:18:07 +01:00
2022-04-12 09:55:10 +02:00
public class SalesStats
{
public int SalesCount { get ; set ; }
public IEnumerable < SalesStatsItem > Series { get ; set ; }
}
2023-01-06 14:18:07 +01:00
2022-04-12 09:55:10 +02:00
public class SalesStatsItem
{
public DateTime Date { get ; set ; }
public string Label { get ; set ; }
public int SalesCount { get ; set ; }
}
2019-02-19 13:04:58 +09:00
}