using Edge.Core.Configuration; using Edge.Core.Database; using Edge.Core.Database.Models; using Edge.Core.IndustryStandardInterface.ATG; using Edge.Core.IndustryStandardInterface.Pump; using Edge.Core.Processor; using Edge.Core.Processor.Dispatcher.Attributes; using Edge.Core.UniversalApi; using FdcServerHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using Wayne.FDCPOSInterface; using Wayne.FDCPOSLibrary; namespace Applications.FDC { [UniversalApi(Name = "OnFdcControllerStateChange", EventDataType = typeof(FdcPumpControllerOnStateChangeEventArg), Description = "When pump state changed, the event will fired")] [UniversalApi(Name = "OnError", EventDataType = typeof(string))] [UniversalApi(Name = "OnCurrentFuellingStatusChange", EventDataType = typeof(FdcServerTransactionDoneEventArg), Description = "When pump in fueling state, the fueling progress will be reported via this event")] [UniversalApi(Name = "OnFdcFuelSaleTransactinStateChange", EventDataType = typeof(FdcFuelSaleTransactinStateChangeEventArg), Description = "When pump transaction state changed, the event will fired")] [MetaPartsDescriptor( "支持外部延迟授权模式的Fdc服务器App", "支持外部延迟授权模式的Fdc服务器App,并提供基于 IFSF-POS-FDC 协议定义的各类接口", new[] { "lang-zh-cn:延迟授权FdcServerlang-en-us:DelayAuthFdcServer" })] public class FdcServerHostAppDelayAuth : IAppProcessor { [UniversalApi(Description = "Get the overall pumps, nozzles info.")] public async Task> GetPumpsLayout(ApiData input) { //if (input == null || input.Parameters == null || !input.Parameters.Any()) // return this.fdcPumpControllers; //var targetPumpIds = input.Parameters.Where(v => v.Name != null && v.Name.ToLower() == "pumpid" && int.TryParse(v.Value, out _)) // .Select(v => int.Parse(v.Value)); ////if (!targetPumpIds.Any()) return this.fdcPumpControllers; return this.fdcPumpControllers.Select(p => new { p.Name, p.PumpId, Nozzles = p.Nozzles.Select(n => new { n.LogicalId, n.RealPriceOnPhysicalPump, SiteLevelNozzleId = this.nozzleExtraInfos.FirstOrDefault(c => c.PumpId == n.PumpId && c.NozzleLogicalId == n.LogicalId)?.SiteLevelNozzleId, ProductBarcode = this.nozzleExtraInfos.FirstOrDefault(c => c.PumpId == n.PumpId && c.NozzleLogicalId == n.LogicalId)?.ProductBarcode, ProductName = this.nozzleExtraInfos.FirstOrDefault(c => c.PumpId == n.PumpId && c.NozzleLogicalId == n.LogicalId)?.ProductName, }), p.AmountDecimalDigits, p.VolumeDecimalDigits, p.PriceDecimalDigits, p.VolumeTotalizerDecimalDigits }); } [UniversalApi(Description = "Get FuelSaleTrx Details info by searching conditions.
" + "Searching condition 1: by providing para.Name=='ReleaseToken' will ignore other conditions.
" + "Searching condition 2: para.Name=='PumpId' and para.Name=='LogicalNozzleId' must be provided, and para.Name=='TransactionNumber' is optional.")] public async Task> GetFuelSaleTrxDetailsAsync(ApiData input) { if (input == null || input.Parameters == null || !input.Parameters.Any()) throw new ArgumentException(nameof(input)); List transactions; if (int.TryParse(input.Parameters.FirstOrDefault(p => p.Name.ToLower() == "releasetoken")?.Value, out int targetReleaseToken)) { SqliteDbContext dbContext = new SqliteDbContext(); transactions = await dbContext.PumpTransactionModels.Where(t => t.ReleaseToken == targetReleaseToken).ToListAsync(); } else { if (!int.TryParse(input.Parameters.FirstOrDefault(p => p.Name.ToLower() == "pumpid")?.Value, out int targetPumpId) || !int.TryParse(input.Parameters.FirstOrDefault(p => p.Name.ToLower() == "logicalnozzleid")?.Value, out int targetLogicalNozzleId)) throw new ArgumentException("Must provide valid parameter value for PumpId and LogicalNozzleId"); var targetTransactionNumber = input.Parameters.FirstOrDefault(p => p.Name.ToLower() == "transactionnumber")?.Value; SqliteDbContext dbContext = new SqliteDbContext(); int maxReturnDataCount = 50; if (string.IsNullOrEmpty(targetTransactionNumber)) transactions = await dbContext.PumpTransactionModels.Where(t => t.PumpId == targetPumpId && t.LogicalNozzleId == targetLogicalNozzleId).OrderByDescending(t => t.SaleStartTime).Take(maxReturnDataCount).ToListAsync(); else transactions = await dbContext.PumpTransactionModels.Where(t => t.PumpId == targetPumpId && t.LogicalNozzleId == targetLogicalNozzleId && t.TransactionSeqNumberFromPhysicalPump == targetTransactionNumber).OrderByDescending(t => t.SaleStartTime).Take(maxReturnDataCount).ToListAsync(); } return transactions.Select(trx => { //NOTE, the queried trx may from old times, but the this.fdcPumpControllers and this.nozzleProductConfig //are always the latest, so they may mismatch and produce incorrect data. var pumpHandler = this.fdcPumpControllers.FirstOrDefault(fpc => fpc.PumpId == trx.PumpId); return new { trx.ReleaseToken, trx.PumpId, trx.LogicalNozzleId, SiteLevelNozzleId = this.nozzleExtraInfos.FirstOrDefault(c => c.PumpId == trx.PumpId && c.NozzleLogicalId == trx.LogicalNozzleId)?.SiteLevelNozzleId, trx.TransactionSeqNumberFromPhysicalPump, trx.ProductBarcode, ProductName = this.nozzleExtraInfos.FirstOrDefault(c => c.PumpId == trx.PumpId && c.NozzleLogicalId == trx.LogicalNozzleId)?.ProductName, RawAmount = trx.Amount, Amount = pumpHandler == null ? (double?)null : (trx.Amount / Math.Pow(10, pumpHandler.AmountDecimalDigits)), RawVolume = trx.Volumn, Volume = pumpHandler == null ? (double?)null : trx.Volumn / Math.Pow(10, pumpHandler.VolumeDecimalDigits), RawPrice = trx.UnitPrice, Price = pumpHandler == null ? (double?)null : trx.UnitPrice / Math.Pow(10, pumpHandler.PriceDecimalDigits), RawAmountTotalizer = trx.AmountTotalizer, AmountTotalizer = pumpHandler == null ? (double?)null : trx.AmountTotalizer / Math.Pow(10, pumpHandler.AmountDecimalDigits), RawVolumeTotalizer = trx.VolumeTotalizer, VolumeTotalizer = pumpHandler == null ? (double?)null : trx.VolumeTotalizer / Math.Pow(10, pumpHandler.VolumeDecimalDigits), trx.State, trx.SaleStartTime, trx.SaleEndTime, }; }); } [UniversalApi] public int PumpCount => this.fdcPumpControllers?.Count ?? 0; [UniversalApi] public async Task> GetDeviceState() { List resultList = new List(); foreach (var controller in this.FdcPumpControllers) { var state = await controller.QueryStatusAsync(); //var nozzles = controller.Nozzles.Select(n => new //{ // n.LogicalId, // VolumeTotalizer = GetVolumeTotalizer(controller.PumpId, n.LogicalId), // Amount = GetLatestTransactionAmount(controller.PumpId, n.LogicalId), // n.LogicalState //}); List nozzles = new List(); foreach (var n in controller.Nozzles) { if (n.VolumeTotalizer == null) { var result = await GetFuelPointTotalsAsync(controller.PumpId, n.LogicalId); n.VolumeTotalizer = (int?)(result.Item2 * Math.Pow(10, controller.VolumeDecimalDigits)); } var Amount = GetLatestTransactionAmount(controller.PumpId, n.LogicalId); nozzles.Add(new { n.LogicalId, n.VolumeTotalizer, Amount }); } resultList.Add(new { controller.PumpId, DeviceState = state, Nozzles = nozzles }); } return resultList; } private int GetLatestTransactionAmount(int pumpId, int nozzleId) { var dueDate = DateTime.Now.Subtract(new TimeSpan(30, 0, 0, 0)); SqliteDbContext dbContext = new SqliteDbContext(); var trans = dbContext.PumpTransactionModels.Where(t => t.State != FuelSaleTransactionState.Cleared && t.PumpId == pumpId && t.LogicalNozzleId == nozzleId && t.SaleStartTime >= dueDate) .OrderByDescending(r => r.SaleStartTime).Take(1); return trans.Any() ? trans.FirstOrDefault().Amount : 0; } [UniversalApi(Description = "Leave input null to retrieve alarm data for all nozzles" + "
input parameters are as follow, " + "
para.Name==\"NozzleId\" Site level nozzle Id, leave 0 for all nozzles" + "
\"PumpId\" the target pump id")] public async Task GetAvailableTransactions(ApiData input) { var nozzleId = -1; var pumpId = -1; if (input != null && !input.IsEmpty())//with no search condition specified, return latest 1000 records. { var temp = ""; foreach (var p in input.Parameters) { temp += p.Name + " " + p.Value + " "; } fdcLogger.LogDebug("input is not null, parameters:{0}", temp); nozzleId = input.Get("nozzleid", -1); pumpId = input.Get("pumpid", -1); } fdcLogger.LogDebug("GetAvailableTransactions for nozzle:{0},pump:{1}", nozzleId, pumpId); List resultList = new List(); var result = await GetAvailableFuelSaleTrxsWithDetailsAsync(pumpId, nozzleId, 1000); if (result != null && result.Any()) { var all = result.GroupBy(r => new { r.PumpId, r.LogicalNozzleId }); foreach (var a in all) { var trans = a.Where(_ => _.State == FuelSaleTransactionState.Payable || _.State == FuelSaleTransactionState.Locked); resultList.Add(new { a.Key.PumpId, a.Key.LogicalNozzleId, hasPayableTransactions = trans.Any(), trans.FirstOrDefault().VolumeTotalizer, trans.FirstOrDefault().Amount }); } return resultList; } resultList.Add(new { LogicalNozzleId = nozzleId, PumpId = pumpId, hasPayableTransactions = false }); return resultList; } [UniversalApi(Description = "Get Ifsf Fdc TLG Configuration, this data also could be get via Ifsf Fdc Protocol")] public async Task>>> GetIfsfFdcTLGConfiguration() { /* here based on local manual config to generate tanks and return to POS side, probly use the concrete AutoTankGaugeControl.Tanks * is a better choice but the known issue is the product code and label read from VR ATG console may not correct(may bug in VR console). * so keep this way of local manual config with high priority. */ var tankGroups = this.nozzleExtraInfos.Where(np => np.TankNumber.HasValue) .GroupBy(np => np.TankNumber) .Select(tg => { /* if real ATG exists, will use ATG Tank info, otherwise create a logical Tank object but fill with info from nozzleExtraInfo*/ var tank = this.autoTankGaugeControllers?.SelectMany(c => c.Tanks ?? new Tank[] { })?.FirstOrDefault(t => t.TankNumber == tg.Key) ?? new Tank() { TankNumber = (byte)tg.Key.Value, Label = "Logical tank", Product = new Product() { //using the info from nozzle config ProductCode = tg.First().ProductBarcode.ToString(), ProductLabel = tg.First().ProductName } }; //there're cases that the ATG side may has wrong product code and name, //so we perfer use the product code and name from nozzle config. var forceMapTankProductInfoFromConfigFile = true; if (forceMapTankProductInfoFromConfigFile) { tank.Product = new Product() { ProductCode = tg.First().ProductBarcode.ToString(), ProductLabel = tg.First().ProductName }; } var pumpGroups = tg.GroupBy(np => np.PumpId).Select(pg => new Grouping(pg.Key, pg.Select(x => (byte)x.NozzleLogicalId))); return new Grouping>(tank, pumpGroups); }); return tankGroups; } private bool config_AutoAuthCallingPumps; private int config_MaxStackUnpaidTrxPerPump; private int config_ListeningPort; private bool config_removeDelayAuthParameterAfterUseIt = true; private FDCPOSInterfaceServer fdcServer; public string MetaConfigName { get; set; } private IEnumerable nozzleExtraInfos; private List fdcPumpControllers; private List fdcCommunicableControllers; private List autoTankGaugeControllers = new List(); private IServiceProvider services; private Configurator configurator; private EventHandler onConfiguratorConfigFileChangedEventHandler; private System.Timers.Timer purgeDatabaseTimer; static ILogger fdcLogger;//= NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("FdcServer"); private object syncObject = new object(); enum FDCLogicalState { FDC_LOGICAL_ST_UNLOCKED = 0, FDC_LOGICAL_ST_LOCKED = 1, }; #region App configs /// /// when doing major schema change of the config class, the class should not be modify directly. /// instead, for better fallback support, create a new class with a version suffix, and also /// update the version string in appCtorParamsJsonSchema.json file. /// public class AppConfigV1 { public bool AutoAuthCallingPumps { get; set; } public int MaxStackUnpaidTrxPerPump { get; set; } public int FdcServerListeningPort { get; set; } public int? PurgePayableTrxOlderThanByMin { get; set; } public int? PurgeClearedTrxOlderThanByDay { get; set; } public List ProductConfigs { get; set; } public List PumpConfigs { get; set; } } public class AppPumpConfigV1 { public int PumpId { get; set; } public List NozzleConfigs { get; set; } } public class AppProductConfigV1 { public int ProductCode { get; set; } public string ProductName { get; set; } } public class AppNozzleConfigV1 { public int NozzleLogicalId { get; set; } public int? SiteLevelNozzleId { get; set; } public int ProductCode { get; set; } /// /// Gets or sets the tank number of this nozzle undergroud linked. /// public int? TankNumber { get; set; } public string Description { get; set; } } #endregion /// /// A callback function that will be called on DefaultDispatcher(a framework util for instantiate App and DeviceHandler) /// is trying to serialize param strings(most likely saved into a database) into params of this class's ctor, and got an error. /// /// should handle the json str that cause the serialize error, resolve and reformat it to a compatible content. /// /// sample: [{"UITemplateVersion":"1.1","AutoAuthCallingPumps":false,"MaxStackUnpaidTrxPerPump":3}, "2nd string parameter", 3] /// need follow format of json array, sample: [{firstObjectParameter},"2nd string parameter",3] private static List ResolveCtorMetaPartsConfigCompatibility(string incompatibleCtorParamsJsonStr) { var jsonParams = JsonDocument.Parse(incompatibleCtorParamsJsonStr).RootElement.EnumerateArray().ToArray(); //sample: "UITemplateVersion":"1.0" string uiTemplateVersionRegex = @"(?<=""UITemplateVersion""\:\"").+?(?="")"; var match = Regex.Match(jsonParams.First().GetRawText(), uiTemplateVersionRegex, RegexOptions.IgnoreCase | RegexOptions.Multiline); if (match.Success) { // the curVersion string has been found, can handle it with proper fallback. var curVersion = match.Value; if (curVersion == "1.0") { var existsAppConfigV1 = JsonSerializer.Deserialize(jsonParams.First().GetRawText(), typeof(AppConfigV1)); // AppConfigV2 appConfigV2 = new AppConfigV2(); // appConfigV2.aaa = existsAppConfigV1.aaaa; // appConfigV2.bbb = existsAppConfigV1.bbbb; // ... // return new List(){appConfigV2}; } //else if(curVersion == "2.0") //{ // var existsAppConfigV2 = JsonSerializer.Deserialize(jsonParams.First().GetRawText(), typeof(AppConfigV2)); // AppConfigV3 appConfigV3 = new AppConfigV3(); // appConfigV3.aaa = existsAppConfigV2.aaaa; // appConfigV3.bbb = existsAppConfigV2.bbbb; // ... // return new List(){appConfigV3}; //} } else { // try fallback as best as you can by reading the raw input string, // if you have no idea to do any fallback, then return a most common config for let the user can go through easier. return new List() { new AppConfigV1() { AutoAuthCallingPumps = false, FdcServerListeningPort = 4711, MaxStackUnpaidTrxPerPump = 3 } }; } return new List() { new AppConfigV1() { AutoAuthCallingPumps = false, FdcServerListeningPort = 4711, MaxStackUnpaidTrxPerPump = 3 } }; } public Task Test(params object[] parameters) { if ((!this.fdcServer?.IsStarted) ?? false) throw new InvalidOperationException("Fdc server failed to start on a port"); return Task.CompletedTask; } [ParamsJsonSchemas("appCtorParamsJsonSchema")] public FdcServerHostAppDelayAuth(AppConfigV1 appConfig, IServiceProvider services) { this.config_AutoAuthCallingPumps = appConfig.AutoAuthCallingPumps; this.config_MaxStackUnpaidTrxPerPump = appConfig.MaxStackUnpaidTrxPerPump; this.config_ListeningPort = appConfig.FdcServerListeningPort; if (appConfig.PurgeClearedTrxOlderThanByDay.HasValue || appConfig.PurgePayableTrxOlderThanByMin.HasValue) { this.purgeDatabaseTimer = new System.Timers.Timer(); this.purgeDatabaseTimer.Interval = 1000 * 60; this.purgeDatabaseTimer.Elapsed += (s, a) => { if (appConfig.PurgeClearedTrxOlderThanByDay.HasValue) { try { using (var dbContext = new SqliteDbContext()) { var due = DateTime.Now.Subtract(new TimeSpan(appConfig.PurgeClearedTrxOlderThanByDay.Value, 0, 0, 0)); var deleting = dbContext.PumpTransactionModels.Where(t => t.State == FuelSaleTransactionState.Cleared && t.PaidTime.HasValue && t.PaidTime <= due); dbContext.RemoveRange(deleting); var deletedRowCount = dbContext.SaveChanges(); if (deletedRowCount > 0) fdcLogger.LogDebug($"PurgeClearedTrxOlderThanByDay purged: {deletedRowCount} rows"); } } catch (Exception exxx) { fdcLogger.LogError("PurgeClearedTrxOlderThanByDay exceptioned: " + exxx.ToString()); } try { if (appConfig.PurgePayableTrxOlderThanByMin.HasValue) { using (var dbContext = new SqliteDbContext()) { var due = DateTime.Now.Subtract(new TimeSpan(0, appConfig.PurgePayableTrxOlderThanByMin.Value, 0)); var deleting = dbContext.PumpTransactionModels.Where(t => t.State == FuelSaleTransactionState.Payable && t.SaleStartTime.HasValue && t.SaleStartTime <= due); dbContext.RemoveRange(deleting); var deletedRowCount = dbContext.SaveChanges(); if (deletedRowCount > 0) fdcLogger.LogDebug($"PurgePayableTrxOlderThanByMin purged: {deletedRowCount} rows"); } } } catch (Exception exxx) { fdcLogger.LogError("PurgePayableTrxOlderThanByMin exceptioned: " + exxx.ToString()); } } }; this.purgeDatabaseTimer.Start(); } this.services = services; var loggerFactory = services.GetRequiredService(); fdcLogger = loggerFactory.CreateLogger("DynamicPrivate_FdcServer"); //this.configurator = services.GetService(); this.nozzleExtraInfos = appConfig.PumpConfigs ?.SelectMany(p => p.NozzleConfigs, (p, ns) => new NozzleExtraInfo() { PumpId = p.PumpId, NozzleLogicalId = ns.NozzleLogicalId, SiteLevelNozzleId = ns.SiteLevelNozzleId, ProductBarcode = ns.ProductCode, ProductName = appConfig.ProductConfigs?.FirstOrDefault(p => p.ProductCode == ns.ProductCode)?.ProductName, TankNumber = ns.TankNumber, Description = ns.Description, }); FdcResourceArbitrator.fdcLogger = fdcLogger; this.fdcServer = new FDCPOSInterfaceServer(services); } public void Init(IEnumerable processors) { this.fdcPumpControllers = new List(); this.fdcPumpControllers.AddRange( processors.WithHandlerOrApp().SelectHandlerOrAppThenCast()); foreach (var p in processors) { if (p.IsWithHandlerOrApp>()) { var pumpGroupHandler = p.SelectHandlerOrAppThenCast>(); this.fdcPumpControllers.AddRange(pumpGroupHandler); // dynamic check does `OnFdcServerInit` defined if (pumpGroupHandler.GetType().GetMethod("OnFdcServerInit")?.GetParameters() ?.FirstOrDefault()?.ParameterType?.IsAssignableFrom(typeof(Dictionary)) ?? false) { pumpGroupHandler.GetType().GetMethod("OnFdcServerInit")?.Invoke( pumpGroupHandler, new[]{ new Dictionary() { { "NozzleProductMapping", this.nozzleExtraInfos } } }); } } } if (this.fdcPumpControllers.GroupBy(p => p.PumpId).Any(g => g.Count() > 1)) throw new ArgumentException("Duplicate PumpId in Fdc Pump Controllers, PumpId must be unique"); this.fdcCommunicableControllers = new List(); this.fdcCommunicableControllers.AddRange(processors.WithHandlerOrApp().SelectHandlerOrAppThenCast()); this.autoTankGaugeControllers = new List(); this.autoTankGaugeControllers.AddRange(processors.WithHandlerOrApp().SelectHandlerOrAppThenCast()); } public class Grouping : List, IGrouping { public Grouping(TKey key) : base() => Key = key; public Grouping(TKey key, int capacity) : base(capacity) => Key = key; public Grouping(TKey key, IEnumerable collection) : base(collection) => Key = key; public TKey Key { get; private set; } public override string ToString() { if (this.Count > 0) return $"Key: {Key.ToString()} contains: {base.Count} items, they're: {this.Select(i => i.ToString()).Aggregate((acc, n) => acc + " | " + n)}"; else return $"Key: {Key.ToString()} contains: 0 items"; } } private class DelayAuthParameter { public int PumpId { get; set; } public IEnumerable NozzleLogicalIds { get; set; } public double? MaxTrxAmount { get; set; } public double? MaxTrxVolume { get; set; } public DateTime Timestamp { get; set; } public string WorkstationID { get; set; } public string ApplicationSender { get; set; } } /// /// key is the pumpid /// private ConcurrentDictionary DelayAuthParameters = new ConcurrentDictionary(); public Task Start() { //var fdcServer = new FDCPOSInterfaceServer(); #region OnGetCountrySettings, OnLogOnReq and OnLogOffReq fdcServer.OnGetCountrySettingsReq += (string workstationID, string applicationSender, int requestID) => { fdcLogger.LogDebug("OnGetCountrySettingsReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestID + ")"); fdcServer.GetCountrySettings(workstationID, applicationSender, requestID, "Litre", "CNY", "Litre", "C", "3", ".", "CHN", "zh-cn", (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); }; fdcServer.OnStartForecourtReq += (string workstationID, string applicationSender, int requestID) => { fdcLogger.LogDebug("OnStartForecourtReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestID + ")"); fdcServer.StartForecourt(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); }; fdcServer.OnLogOnReq += (string workstationID, string applicationSender, int requestId, int responsePort, int unsolicitedPort, int version) => { fdcLogger.LogInformation("OnLogOnReq(wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", ver: " + version + ")"); fdcServer.LogOn(workstationID, applicationSender, requestId, responsePort, unsolicitedPort, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); }; fdcServer.OnLogOffReq += (string workstationID, string applicationSender, int requestId) => { fdcLogger.LogInformation("OnLogOffReq(wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ")"); fdcServer.LogOff(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); }; #endregion #region reserve related fdcServer.OnLockNozzleReq += async (string workstationID, string applicationSender, int requestId, int deviceId, int NozzleNo) => { try { fdcLogger.LogDebug("OnLockNozzleReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ", NozzleNo: " + NozzleNo); var result = await this.LockNozzleAndNotifyAllFdcClientsAsync(int.Parse(workstationID), applicationSender, requestId, deviceId, NozzleNo); if (!result) fdcLogger.LogInformation(" LockNozzle failed"); fdcLogger.LogDebug(" OnLockNozzleReq done"); } catch (Exception exxx) { fdcLogger.LogError("OnLockNozzleReq exceptioned: " + exxx.ToString()); } }; fdcServer.OnUnlockNozzleReq += async (string workstationID, string applicationSender, int requestId, int deviceId, int NozzleNo) => { try { fdcLogger.LogDebug("OnUnlockNozzleReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ", NozzleNo: " + NozzleNo); var result = await this.UnlockNozzleAndNotifyAllFdcClientsAsync(int.Parse(workstationID), applicationSender, requestId, deviceId, NozzleNo); //if (!result) // fdcServer.UnlockNozzle(workstationID, applicationSender, requestId, deviceId, deviceId, NozzleNo, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); //else // fdcServer.UnlockNozzle(workstationID, applicationSender, requestId, deviceId, deviceId, NozzleNo, (int)ErrorCode.ERRCD_OK, OverallResult.Failure.ToString()); fdcLogger.LogDebug(" OnUnlockNozzleReq done"); } catch (Exception exxx) { fdcLogger.LogError("OnUnlockNozzleReq exceptioned: " + exxx.ToString()); } }; fdcServer.OnReserveFuelPointReq += async (string workstationID, string applicationSender, int requestId, int deviceId) => { try { fdcLogger.LogDebug("OnReserveFuelPointReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId); var result = await FdcResourceArbitrator.Default.TryReserveFuelPointAsync(int.Parse(workstationID), deviceId); if (result) fdcServer.ReserveFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); else { fdcLogger.LogInformation(" ReserveFuelPoint failed"); fdcServer.ReserveFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_FPLOCK, OverallResult.Failure.ToString()); } fdcLogger.LogDebug(" OnReserveFuelPointReq done"); } catch (Exception exxx) { fdcLogger.LogError("OnReserveFuelPointReq exceptioned: " + exxx.ToString()); fdcServer.ReserveFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_OK, OverallResult.Failure.ToString()); } }; fdcServer.OnFreeFuelPointReq += async (string workstationID, string applicationSender, int requestId, int deviceId) => { try { fdcLogger.LogDebug("OnFreeFuelPointReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId); var result = await FdcResourceArbitrator.Default.TryUnreserveFuelPointAsync(int.Parse(workstationID), deviceId); if (result) fdcServer.FreeFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); else fdcServer.FreeFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_OK, OverallResult.Failure.ToString()); fdcLogger.LogDebug(" OnFreeFuelPointReq done"); } catch (Exception exxx) { fdcLogger.LogError("OnFreeFuelPointReq exceptioned: " + exxx.ToString()); fdcServer.FreeFuelPoint(workstationID, applicationSender, requestId, deviceId, deviceId, (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.Failure.ToString()); } }; fdcServer.OnLockFuelSaleTrxReq += async (string workstationID, string applicationSender, int requestId, int deviceId, int transactionNo, string releaseToken) => { try { fdcLogger.LogDebug("OnLockFuelSaleTrxReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ", transactionNo: " + transactionNo + ", releaseToken: " + releaseToken + ")"); var result = await this.LockFuelSaleTrxAndNotifyAllFdcClientsAsync(int.Parse(workstationID), applicationSender, requestId, deviceId, transactionNo, int.Parse(releaseToken)); //var result = this.LockFuelSaleTrxAndNotifyAllFdcClients(int.Parse(workstationID), deviceId, transactionNo, int.Parse(releaseToken)); //if (result != null) //{ // var targetController = fdcPumpControllers // .First(c => c.PumpId == deviceId) as IFdcPumpController; // fdcServer.LockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId, // transactionNo, releaseToken, (int)ErrorCode.ERRCD_OK, 1, OverallResult.Success.ToString()); // fdcServer.FuelSaleTrx(deviceId, // result.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits), // result.Amount / Math.Pow(10, targetController.AmountDecimalDigits), // result.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits), // result.LogicalNozzleId, // int.Parse(result.ProductBarcode), // "refer cloud" + result.ProductBarcode, "", 1, // int.Parse(result.TransactionSeqNumberFromPhysicalPump), // (int)FuelSaleTransactionState.Locked, // 0, releaseToken, // result.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"), // result.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"), // workstationID, "", int.Parse(workstationID), 0); //} //else //{ // fdcServer.LockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId, // transactionNo, releaseToken, (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString()); //} if (result == null) fdcLogger.LogDebug(" OnLockFuelSaleTrxReq failed"); fdcLogger.LogDebug(" OnLockFuelSaleTrxReq done"); } catch (Exception exxx) { fdcLogger.LogError("OnLockFuelSaleTrxReq exceptioned: " + exxx.ToString()); fdcServer.LockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId, transactionNo, releaseToken, (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString()); } }; fdcServer.OnUnlockFuelSaleTrxReq += async (string workstationID, string applicationSender, int requestId, int deviceId, int transactionNo, string releaseToken) => { try { fdcLogger.LogDebug("OnUnlockFuelSaleTrxReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ", transactionNo: " + transactionNo + ", releaseToken: " + releaseToken); var result = await this.UnlockFuelSaleTrxAndNotifyAllFdcClientsAsync(int.Parse(workstationID), applicationSender, requestId, deviceId, transactionNo, int.Parse(releaseToken)); //var result = this.UnlockFuelSaleTrxAndNotifyAllFdcClients(int.Parse(workstationID), deviceId, transactionNo, int.Parse(releaseToken)); //if (result != null) //{ // var targetController = fdcPumpControllers // .First(c => c.PumpId == deviceId) as IFdcPumpController; // fdcServer.UnlockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId, // transactionNo, releaseToken, (int)ErrorCode.ERRCD_OK, 1, OverallResult.Success.ToString()); // fdcServer.FuelSaleTrx(deviceId, // result.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits), // result.Amount / Math.Pow(10, targetController.AmountDecimalDigits), // result.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits), // result.LogicalNozzleId, // int.Parse(result.ProductBarcode), // "refer cloud" + result.ProductBarcode, "", 1, // int.Parse(result.TransactionSeqNumberFromPhysicalPump), // (int)FuelSaleTransactionState.Payable, // 0, releaseToken, // result.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"), // result.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"), // "", "", int.Parse(workstationID), 0); //} //else //{ // fdcServer.UnlockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId, // transactionNo, releaseToken, (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString()); //} if (result == null) fdcLogger.LogDebug(" OnUnlockFuelSaleTrxReq failed"); fdcLogger.LogDebug(" OnUnlockFuelSaleTrxReq done"); } catch (Exception exxx) { fdcLogger.LogError("OnUnlockFuelSaleTrxReq exceptioned: " + exxx.ToString()); fdcServer.UnlockFuelSaleTrx(workstationID, applicationSender, requestId, deviceId, transactionNo, releaseToken, (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString()); } }; #endregion fdcServer.OnGetTankDataReq += async (string workstationID, string applicationSender, int requestID, int deviceId, int tankNo) => { try { fdcLogger.LogDebug("OnGetTankDataReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestID + ", deviceId: " + deviceId + ", tankNo: " + tankNo + ")"); if (this.autoTankGaugeControllers == null || !this.autoTankGaugeControllers.Any()) fdcServer.GetTankDataSend(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.DeviceUnavailable.ToString()); var deviceHandler = this.autoTankGaugeControllers.FirstOrDefault(); if (deviceHandler == null) fdcServer.GetTankDataSend(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.DeviceUnavailable.ToString()); var tankReading = await deviceHandler.GetTankReadingAsync(deviceId); fdcLogger.LogDebug($" GetTankProbeReadingsAsync for tankNumber: " + deviceId + " succeed " + Environment.NewLine + tankReading.ToString()); fdcServer.GetTankDataAdd(workstationID, applicationSender, requestID, deviceId, deviceId, 0, 0, tankReading.Height ?? 0, tankReading.Volume ?? 0, tankReading.TcVolume ?? 0, tankReading.Temperature ?? 0, tankReading.Water ?? 0, 0, (int)ErrorCode.ERRCD_OK, (int)LogicalDeviceState.FDC_READY); fdcServer.GetTankDataSend(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); } catch (Exception exxx) { fdcLogger.LogError("OnGetTankDataReq exceptioned: " + exxx.ToString()); fdcServer.GetTankDataSend(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_BADVAL, OverallResult.Failure.ToString()); } //try //{ // fdcLogger.LogDebug("OnGetTankDataReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestID + ", deviceId: " + deviceId + ", tankNo: " + tankNo + ")"); // fdcServer.GetTankDataAdd(workstationID, applicationSender, requestID, // deviceId, // tankNo, 0, (int)ErrorCode.ERRCD_OK, // new Random().Next(100, 9999), // new Random().Next(100, 9999), // new Random().Next(100, 9999), // new Random().Next(100, 9999), // new Random().Next(100, 9999), // 0, // (int)ErrorCode.ERRCD_OK, (int)LogicalDeviceState.FDC_READY); // fdcServer.GetTankDataSend(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); // return; // if (this.autoTankGaugeControllers == null || !this.autoTankGaugeControllers.Any()) // fdcServer.GetTankDataSend(workstationID, applicationSender, requestID, // (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.DeviceUnavailable.ToString()); // var deviceHandler = this.autoTankGaugeControllers.FirstOrDefault(); // if (deviceHandler == null) // fdcServer.GetTankDataSend(workstationID, applicationSender, requestID, // (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.DeviceUnavailable.ToString()); // var tankReading = await deviceHandler.GetTankReadingAsync(tankNo); // fdcLogger.LogDebug($" GetTankProbeReadingsAsync for tankNumber: " + tankNo + " succeed " + // Environment.NewLine + tankReading.ToString()); // fdcServer.GetTankDataAdd(workstationID, applicationSender, requestID, // deviceId, // tankNo, 0, 0, // tankReading.Height ?? 0, // tankReading.Volume ?? 0, // tankReading.TcVolume ?? 0, // tankReading.Temperature ?? 0, // tankReading.Water ?? 0, // 0, // (int)ErrorCode.ERRCD_OK, (int)LogicalDeviceState.FDC_READY); // fdcServer.GetTankDataSend(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); //} //catch (Exception exxx) //{ // fdcLogger.LogError("OnGetTankDataReq exceptioned: " + exxx.ToString()); // fdcServer.GetTankDataSend(workstationID, applicationSender, requestID, // (int)ErrorCode.ERRCD_BADVAL, OverallResult.Failure.ToString()); //} }; fdcServer.OnGetProductTableReq += (string workstationID, string applicationSender, int requestID) => { fdcLogger.LogDebug("OnGetProductTableReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestID + ")"); var allProductBarcodes = this.nozzleExtraInfos.GroupBy(p => p.ProductBarcode); foreach (var b in allProductBarcodes) fdcServer.GetProductTableAdd(workstationID, applicationSender, requestID, b.Key, (b.FirstOrDefault()?.ProductName ?? ("refer cloud" + b.Key))); fdcServer.GetProductTableSend(workstationID, applicationSender, requestID, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); }; fdcServer.OnGetConfigurationReq += async (string workstationID, string applicationSender, int requestId, string deviceType) => { try { fdcLogger.LogDebug("OnGetConfigurationReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceType: " + deviceType); if (deviceType == "TLG") { var tankGroups = await this.GetIfsfFdcTLGConfiguration(); if (!tankGroups.Any()) { fdcServer.GetConfigurationSend(workstationID, applicationSender, requestId, deviceType, OverallResult.WrongConfiguration.ToString()); fdcLogger.LogDebug(" OnGetConfigurationReq with TLG done with WrongConfiguration as no tanks info generated."); return; } fdcLogger.LogDebug(" OnGetConfigurationReq with TLG done with success, returned data-> " + tankGroups.Select(tg => $"Tank with number: {tg.Key.TankNumber}, " + $"label: {tg.Key.Label ?? ""}, productCode: {tg.Key.Product?.ProductCode ?? ""} linked to PumpIds: " + $"{tg.Select(p => $"{p.Key}(nzlLogiIds: {p.Select(n => n.ToString()).Aggregate("", (acc, n) => acc + ", " + n)})").Aggregate("", (acc, n) => acc + ", " + n)}") .Aggregate((acc, n) => acc + "; " + n)); fdcServer.GetConfigurationAddTLG(workstationID, applicationSender, requestId, tankGroups); } else { foreach (var fdcPumpController in fdcPumpControllers) { foreach (var nozzle in fdcPumpController.Nozzles) { var bindProduct = nozzleExtraInfos.FirstOrDefault(n => n.PumpId == fdcPumpController.PumpId && n.NozzleLogicalId == nozzle.LogicalId); var fuelPrice = nozzle.RealPriceOnPhysicalPump == null ? 0 : (nozzle.RealPriceOnPhysicalPump.Value / Math.Pow(10, fdcPumpController.PriceDecimalDigits)); if (string.IsNullOrEmpty(deviceType) || deviceType == "DSP") { int bindProductNo = (bindProduct == null ? 0 : bindProduct.ProductBarcode); if (bindProduct == null) fdcLogger.LogInformation("Could not find bind product for pumpId: " + fdcPumpController.PumpId + ", nozzleId: " + nozzle.LogicalId + ", will use 0 instead"); fdcLogger.LogDebug(" OnGetConfigurationReq pumpId: " + fdcPumpController.PumpId + ", nozzle logicalId: " + nozzle.LogicalId + ", nozzle phyId: " + nozzle.PhysicalId + ", productNo: " + bindProductNo + ", price: " + fuelPrice); fdcServer.GetConfigurationAddDSP(workstationID, applicationSender, requestId, fdcPumpController.PumpId, bindProductNo, bindProduct?.ProductName ?? ("refer cloud for name of " + bindProductNo), 1, fuelPrice); fdcServer.GetConfigurationAddFP(workstationID, applicationSender, requestId, fdcPumpController.PumpId, fdcPumpController.PumpId, nozzle.LogicalId, (bindProduct == null ? 0 : bindProduct.ProductBarcode), 0, 100, 0); } //if (string.IsNullOrEmpty(deviceType) || deviceType == "PP") // fdcServer.GetConfigurationAddPP(workstationID, applicationSender, requestId, fdcPumpController.PumpId); } } } fdcServer.GetConfigurationSend(workstationID, applicationSender, requestId, deviceType, OverallResult.Success.ToString()); fdcLogger.LogDebug(" OnGetConfigurationReq done"); } catch (Exception exxx) { fdcLogger.LogError("OnGetConfigurationReq exceptioned: " + exxx.ToString()); fdcServer.GetConfigurationSend(workstationID, applicationSender, requestId, deviceType, OverallResult.Failure.ToString()); } }; fdcServer.OnGetDeviceStateReq += async (string workstationID, string applicationSender, int requestId, string deviceType, int deviceId) => { try { fdcLogger.LogDebug("OnGetDeviceStateReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ")"); // -1 indicates query all pumps if (deviceId == -1) { var controllers = fdcPumpControllers; foreach (var c in controllers) { var s = await c.QueryStatusAsync(); fdcLogger.LogDebug(" Pump with pumpId: " + c.PumpId + " is in state: " + s.ToString() + ", nozzles states are(LogicalId-State): " + (c.Nozzles != null ? (c.Nozzles.Any() ? c.Nozzles.Select(n => n.LogicalId.ToString() + "-" + (n.LogicalState?.ToString() ?? "")) .Aggregate((n, acc) => n + ", " + acc) : "") : "")); byte nozzleLockedOrUnlockedBitMap = 0; foreach (var nozzle in c.Nozzles) { if (nozzle.LogicalState.HasValue && nozzle.LogicalState == LogicalDeviceState.FDC_LOCKED) nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 1); else nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 0); } fdcServer.GetDeviceStateAdd(workstationID, applicationSender, requestId, deviceType, c.PumpId, (int)s, -1, "", "", (int)FDCLogicalState.FDC_LOGICAL_ST_UNLOCKED, "", c.Nozzles.Count(), 0, nozzleLockedOrUnlockedBitMap, 0); } } else { var targetController = fdcPumpControllers.First(c => c.PumpId == deviceId) as IFdcPumpController; byte nozzleLockedOrUnlockedBitMap = 0; foreach (var nozzle in targetController.Nozzles) { if (nozzle.LogicalState.HasValue && nozzle.LogicalState == LogicalDeviceState.FDC_LOCKED) nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 1); else nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 0); } fdcServer.GetDeviceStateAdd(workstationID, applicationSender, requestId, deviceType, deviceId, (int)targetController.QueryStatusAsync().Result, -1, "", "", (int)FDCLogicalState.FDC_LOGICAL_ST_UNLOCKED, "", targetController.Nozzles.Count(), 0, nozzleLockedOrUnlockedBitMap, 0); } fdcServer.GetDeviceStateSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); fdcLogger.LogDebug(" OnGetDeviceStateReq done"); } catch (Exception exxx) { fdcLogger.LogError("OnGetDeviceStateReq exceptioned: " + exxx.ToString()); fdcServer.GetDeviceStateSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_BADCONF, OverallResult.Failure.ToString()); } }; fdcServer.OnAuthoriseFuelPointReq += async (string workstationID, string applicationSender, int requestId, string releaseToken, int fuellingType, int deviceId, int reservingDeviceId, double maxTrxAmount, double maxTrxVolume, string products, int mode, bool lockFuelSaleTrx, string payType) => { //enum DeviceStatus //{ // FDC_ST_NOTHING = -1, // FDC_ST_CONFIGURE = 0, // FDC_ST_DISABLED = 1, // FDC_ST_ERRORSTATE = 2, // FDC_ST_FUELLING = 3, // FDC_ST_INVALIDSTATE = 4, // FDC_ST_LOCKED = 5, // FDC_ST_OFFLINE = 6, // FDC_ST_OUTOFORDER = 7, // FDC_ST_READY = 8, // FDC_ST_REQUESTED = 9, // FDC_ST_STARTED = 10, // FDC_ST_SUSPENDED = 11, // FDC_ST_CALLING = 12, // FDC_ST_TEST = 13, // FDC_ST_SUSPENDED_STARTING = 14, // Added in FDC version 00.05 // FDC_ST_SUSPENDED_FUELLING = 15, // FDC_ST_CLOSED = 16, // FDC_ST_AUTHORISED = 17, // Added in FDC version 00.07 //}; try { fdcLogger.LogDebug("OnAuthoriseFuelPointReq(wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", releaseToken: " + releaseToken + ", fuellingType: " + fuellingType + ", deviceId: " + deviceId + ", reservingDeviceId: " + reservingDeviceId + ", maxTrxAmount: " + maxTrxAmount + ", maxTrxVolume: " + maxTrxVolume + ", products: " + products + ", lockFuelSaleTrx: " + lockFuelSaleTrx + ", payType: " + payType + ")"); //if (targetController.QueryStatus() == LogicalDeviceState.FDC_FUELLING) //{ //fdcLogger.LogDebug(" Pump: " + targetController.PumpId + " is in fueling so re-authorising request will be interpreted as RoundUpByAmount request"); //var result = targetController.FuelingRoundUpByAmount(); //fdcLogger.LogDebug(" Pump: " + targetController.PumpId + " RoundUpByAmount returned with: " + result); //fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId, // (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); //return; //} //if (!string.IsNullOrEmpty(maxStackUnpaidTrxConfig)) //{ // SqliteDbContext dbContext = new SqliteDbContext(); // var unpaidTrxCount = dbContext.PumpTransactionModels.Count(t => t.PumpId == targetController.PumpId && t.State == FuelSaleTransactionState.Payable); // if (unpaidTrxCount >= int.Parse(maxStackUnpaidTrxConfig)) // { // fdcLogger.LogInformation(" Authorizing failed due to pump: " + targetController.PumpId + " have: " + unpaidTrxCount + " unpaid trx"); // fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId, // (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_MAXSTACKLIMIT, OverallResult.Failure.ToString()); // return; // } //} if (fuellingType == 8) { /*indicates the request purpose is to set(update or add) a delayAuthParameter*/ var targetController = fdcPumpControllers.FirstOrDefault(c => c.PumpId == deviceId); if (targetController == null) { fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId, (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_BADDEVID, OverallResult.WrongDeviceNo.ToString()); } else { this.DelayAuthParameters.AddOrUpdate(deviceId, newPumpId => new DelayAuthParameter() { PumpId = newPumpId, NozzleLogicalIds = null, ApplicationSender = applicationSender, WorkstationID = workstationID, MaxTrxAmount = (maxTrxAmount == 0 ? (double?)null : maxTrxAmount), MaxTrxVolume = (maxTrxVolume == 0 ? (double?)null : maxTrxVolume), Timestamp = DateTime.Now, }, (existedPumpId, existed) => { existed.ApplicationSender = applicationSender; existed.WorkstationID = workstationID; existed.MaxTrxAmount = (maxTrxAmount == 0 ? (double?)null : maxTrxAmount); existed.MaxTrxVolume = (maxTrxVolume == 0 ? (double?)null : maxTrxVolume); existed.Timestamp = DateTime.Now; return existed; }); //if (this.DelayAuthParameters.TryGetValue(deviceId, out DelayAuthParameter found)) //{ // fdcLogger.LogDebug(" Updating a DelayAuthParameter..."); // found.ApplicationSender = applicationSender; // found.WorkstationID = workstationID; // found.MaxTrxAmount = (maxTrxAmount == 0 ? (double?)null : maxTrxAmount); // found.MaxTrxVolume = (maxTrxVolume == 0 ? (double?)null : maxTrxVolume); // found.Timestamp = DateTime.Now; //} //else //{ // fdcLogger.LogDebug(" Adding a DelayAuthParameter..."); // this.DelayAuthParameters.(deviceId, ); //} fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -8, releaseToken, deviceId, (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); } } else if (fuellingType == 9) { /*indicates the request purpose is to remove a delayAuthParameter*/ if (this.DelayAuthParameters.TryRemove(deviceId, out DelayAuthParameter _)) fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -9, releaseToken, deviceId, (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); else fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -9, releaseToken, deviceId, (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_INOP, OverallResult.Failure.ToString()); } else { var succeed = await this.AuthorizePumpAsync(deviceId, maxTrxAmount, maxTrxVolume); if (succeed) fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId, (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); else { fdcLogger.LogError("Authorising FP(requestId: " + requestId + ", releaseToken: " + releaseToken + ", deviceId: " + deviceId + ", reservingDeviceId: " + reservingDeviceId + ", maxTrxAmount: " + maxTrxAmount + ", maxTrxVolume: " + maxTrxVolume + ", products: " + products + ", lockFuelSaleTrx: " + lockFuelSaleTrx + ", payType: " + payType + ") failed"); fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId, (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_NOPERM, OverallResult.Failure.ToString()); } } fdcLogger.LogDebug(" Authorising FP done"); } catch (Exception exxx) { fdcLogger.LogError("OnAuthoriseFuelPointReq exceptioned: " + exxx.ToString()); fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId, (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_NOPERM, OverallResult.Failure.ToString()); } }; fdcServer.OnTerminateFuellingReq += async (string workstationID, string applicationSender, int requestId, int deviceId) => { fdcLogger.LogInformation("OnTerminateFuellingReq(wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ")"); try { /* interpreted as Remove delay auth parameter has no special reason but for a rush request from customer */ fdcLogger.LogInformation(" OnTerminateFuellingReq will be interpreted as Remove delay auth parameter"); if (this.DelayAuthParameters.TryRemove(deviceId, out DelayAuthParameter _)) fdcServer.TerminateFuellingSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); else { fdcLogger.LogInformation(" OnTerminateFuelling for deviceId: " + deviceId + " has not found DelayAuth set, however still give a Success response."); fdcServer.TerminateFuellingSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); } } catch (Exception exx) { fdcLogger.LogError("OnTerminateFuellingReq exceptioned: " + exx); fdcServer.TerminateFuellingSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.Failure.ToString()); } }; fdcServer.OnGetAvailableFuelSaleTrxsReq += (string workstationID, string applicationSender, int requestId, int deviceId) => { // for limit data size, only show latest N days' data. int maxReturnDays = 365; // for further limit data size, only show latest N count of data. int maxReturnDataCount = 1000; lock (this.syncObject) { try { fdcLogger.LogDebug("OnGetAvailableFuelSaleTrxsReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId); var dueDate = DateTime.Now.Subtract(new TimeSpan(maxReturnDays, 0, 0, 0)); int totalReturnTrxCount = 0; SqliteDbContext dbContext = new SqliteDbContext(); if (deviceId == -1) { var all = dbContext.PumpTransactionModels.Where(t => t.State != FuelSaleTransactionState.Paid && t.State != FuelSaleTransactionState.Cleared && t.SaleStartTime >= dueDate) .Select(t => new { t.PumpId, t.TransactionSeqNumberFromPhysicalPump, t.State, t.SaleStartTime, t.ReleaseToken }) .OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount); foreach (var unpaidTrx in all) { // only show the trx in active pump list. if (!fdcPumpControllers.Select(p => p.PumpId).Any(p => p == unpaidTrx.PumpId)) continue; totalReturnTrxCount++; fdcServer.GetAvailableFuelSaleTrxsAdd(workstationID, applicationSender, requestId, unpaidTrx.PumpId, int.Parse(unpaidTrx.TransactionSeqNumberFromPhysicalPump), unpaidTrx.ReleaseToken.ToString(), (int)unpaidTrx.State); } } else { var all = dbContext.PumpTransactionModels.Where(t => t.PumpId == deviceId && t.State != FuelSaleTransactionState.Paid && t.State != FuelSaleTransactionState.Cleared && t.SaleStartTime >= dueDate) .Select(t => new { t.PumpId, t.TransactionSeqNumberFromPhysicalPump, t.State, t.SaleStartTime, t.ReleaseToken }) .OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount); foreach (var unpaidTrx in all) { // only show the trx in active pump list. if (!fdcPumpControllers.Select(p => p.PumpId).Any(p => p == unpaidTrx.PumpId)) continue; totalReturnTrxCount++; fdcServer.GetAvailableFuelSaleTrxsAdd(workstationID, applicationSender, requestId, unpaidTrx.PumpId, int.Parse(unpaidTrx.TransactionSeqNumberFromPhysicalPump), unpaidTrx.ReleaseToken.ToString(), (int)unpaidTrx.State); } } fdcServer.GetAvailableFuelSaleTrxsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, (totalReturnTrxCount > 0 ? OverallResult.Success.ToString() : OverallResult.NoData.ToString())); fdcLogger.LogDebug(" OnGetAvailableFuelSaleTrxsReq done (total count: " + totalReturnTrxCount + ")"); } catch (Exception exxx) { fdcLogger.LogError("OnGetAvailableFuelSaleTrxsReq exceptioned: " + exxx.ToString()); fdcServer.GetAvailableFuelSaleTrxsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_BADCONF, OverallResult.Failure.ToString()); } } }; fdcServer.OnGetFuelSaleTrxDetailsReq += (string workstationID, string applicationSender, int requestId, int deviceId, int transactionNo, string releaseToken) => { lock (this.syncObject) { try { /* Fdc client may send wildchar `*` in transactionNo which will be interpreted as -1 here, that means query all trx */ fdcLogger.LogDebug("OnGetFuelSaleTrxDetailsReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ", transactionNo: " + transactionNo + ", releaseToken: " + releaseToken); int databaseId = int.Parse(releaseToken); SqliteDbContext dbContext = new SqliteDbContext(); List target; if (transactionNo == -1) target = dbContext.PumpTransactionModels.Where(t => t.PumpId == deviceId).ToList(); else target = dbContext.PumpTransactionModels.Where(t => t.ReleaseToken == databaseId && t.PumpId == deviceId && t.TransactionSeqNumberFromPhysicalPump == transactionNo.ToString()).ToList(); if (target.Any()) { var targetController = fdcPumpControllers .First(c => c.PumpId == deviceId) as IFdcPumpController; foreach (var trx in target) { var reservedBy = string.IsNullOrEmpty(trx.LockedByFdcClientId) ? -1 : int.Parse(trx.LockedByFdcClientId); fdcServer.GetFuelSaleTrxDetailsAdd(workstationID, applicationSender, requestId, deviceId, trx.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits), trx.Amount / Math.Pow(10, targetController.AmountDecimalDigits), trx.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits), trx.LogicalNozzleId, int.Parse(trx.ProductBarcode), "", "", 1, int.Parse(trx.TransactionSeqNumberFromPhysicalPump), releaseToken, (int)trx.State, trx.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"), trx.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"), trx.LockedByFdcClientId, "9999", reservedBy, 1); } fdcServer.GetFuelSaleTrxDetailsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); } else { fdcLogger.LogInformation(" Trx with releaseToken: " + releaseToken + " is not found"); fdcServer.GetFuelSaleTrxDetailsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.NoData.ToString()); } fdcLogger.LogDebug(" OnGetFuelSaleTrxDetailsReq done"); } catch (Exception exxx) { fdcLogger.LogError("OnGetFuelSaleTrxDetailsReq exceptioned: " + exxx.ToString()); fdcServer.GetFuelSaleTrxDetailsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_BADVAL, OverallResult.Failure.ToString()); } } }; fdcServer.OnClearFuelSaleTrxReq += async (string workstationID, string applicationSender, int requestId, int deviceId, int transactionNo, string releaseToken) => { try { fdcLogger.LogDebug("OnClearFuelSaleTrxReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ", transactionNo: " + transactionNo + ", releaseToken: " + releaseToken + ")"); var target = await this.ClearFuelSaleTrxAndNotifyAllFdcClientsAsync(deviceId, transactionNo.ToString(), int.Parse(releaseToken), workstationID, applicationSender, requestId); if (target == null) fdcLogger.LogError("OnClearFuelSaleTrxReq failed(wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ", transactionNo: " + transactionNo + ", releaseToken: " + releaseToken + ")"); } catch (Exception exxx) { fdcLogger.LogError("OnClearFuelSaleTrxReq exceptioned: " + exxx.ToString()); fdcServer.ClearFuelSaleTrx(workstationID, applicationSender, requestId, deviceId, transactionNo, releaseToken, (int)ErrorCode.ERRCD_BADVAL, 1, OverallResult.Failure.ToString()); } }; // FdcClient send ServiceRequestChangeFuelPrice will be routed into here fdcServer.OnChangeFuelPriceInStringReq += (string workstationID, string applicationSender, int requestId, string formattedValues) => { try { fdcLogger.LogInformation("OnChangeFuelPriceInStringReq (requestId:" + requestId + ") with data-> (barcode; PriceNew; ModeNo; PriceOld; EffectiveDatetime;!): " + formattedValues); //formattedValues is: gradeId(barCode);PriceNew;ModeNo;EffectiveDatetime! e.g.: 5;1980;1;! //this function should return: gradeId(barCode);PriceNew;ModeNo;PriceOld;EffectiveDatetime;! e.g.: 2;1998;1;1981;! var targetProductBarcode = int.Parse(formattedValues.Split(';')[0]); // raw means the price is send from POS, and typically it contains decimal points, // need re-caculate with money digits since pump does not recognize decimal points. var rawNewPriceWithHumanReadableFormat = double.Parse(formattedValues.Split(';')[1]); var succeedNozzles = this.ChangeFuelPriceAsync(targetProductBarcode, rawNewPriceWithHumanReadableFormat).Result; fdcLogger.LogInformation(" OnChangeFuelPriceInStringReq (requestId:" + requestId + ") is done"); // even only one succeed, return a success. need refine? if (succeedNozzles != null && succeedNozzles.Any()) { // notify all tcp FdcClients that price changed. fdcServer.FuelPriceChange(targetProductBarcode, 1, rawNewPriceWithHumanReadableFormat, 0, // always set older time since the new price is already activated on pump. DateTime.Now.Subtract(new TimeSpan(0, 5, 0)).ToString("yyyy-MM-dd HH:mm:ss")); return targetProductBarcode.ToString() + ";" + rawNewPriceWithHumanReadableFormat.ToString() + ";1;" + 0 + ";!"; } else return null; } catch (Exception exxx) { fdcLogger.LogError("OnChangeFuelPriceInStringReq exceptioned: " + exxx); return null; } }; //fdcServer.OnChangeFuelPriceAddReq += (string workstationID, string applicationSender, // int requestId, int product, double price, int mode, string effectiveDateTime) => // { // }; //fdcServer.OnChangeFuelPriceEndReq += (string workstationID, string applicationSender, int requestId) => // { // fdcServer.ChangeFuelPriceEndReq(workstationID, applicationSender, requestId); // return ""; // }; fdcServer.OnGetCurrentFuellingStatusReq += (string workstationID, string applicationSender, int requestId, int deviceId) => { /* do nothing for now, the fuelling status will notify by unsolicited event. */ var targetController = fdcPumpControllers.First(c => c.PumpId == deviceId) as IFdcPumpController; //if (targetController.QueryStatus() == LogicalDeviceState.FDC_FUELLING) //{ // fdcServer.GetCurrentFuellingStatusAdd(workstationID, applicationSender, requestId, deviceId) //} }; fdcServer.OnGetFuelPointTotalsReq += async (string workstationID, string applicationSender, int requestId, int deviceId, int nozzleId) => { try { fdcLogger.LogDebug("OnGetFuelPointTotalsReq (wid: " + workstationID + ", appSender: " + applicationSender + ", requestId: " + requestId + ", deviceId: " + deviceId + ", nozzleId: " + nozzleId); if (deviceId == -1) { foreach (var pumpController in fdcPumpControllers) { foreach (var nozzle in pumpController.Nozzles) { var result = await this.GetFuelPointTotalsAsync(pumpController.PumpId, (byte)nozzle.LogicalId); fdcLogger.LogInformation(" OnGetFuelPointTotalsReq for pump: " + pumpController.PumpId + ", nozzle: " + nozzle.LogicalId + " result(with decimal points) is: " + result.Item1 + " <-> " + result.Item2); fdcServer.GetFuelPointTotalsAdd(workstationID, applicationSender, requestId, pumpController.PumpId, nozzle.LogicalId, nozzleExtraInfos.FirstOrDefault(n => n.PumpId == pumpController.PumpId && n.NozzleLogicalId == nozzle.LogicalId).ProductBarcode, result.Item2, result.Item1, (pumpController.Nozzles.FirstOrDefault(n => n.LogicalId == nozzleId)?.RealPriceOnPhysicalPump ?? 0) / Math.Pow(10, pumpController.PriceDecimalDigits)); } } fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); } else if (nozzleId == -1) { var pumpController = fdcPumpControllers .First(c => c.PumpId == deviceId) as IFdcPumpController; foreach (var nozzle in pumpController.Nozzles) { var result = await this.GetFuelPointTotalsAsync(pumpController.PumpId, (byte)nozzle.LogicalId); fdcLogger.LogInformation(" OnGetFuelPointTotalsReq for pump: " + pumpController.PumpId + ", nozzle: " + nozzle.LogicalId + " result(with decimal points) is: " + result.Item1 + " <-> " + result.Item2); fdcServer.GetFuelPointTotalsAdd(workstationID, applicationSender, requestId, pumpController.PumpId, nozzle.LogicalId, nozzleExtraInfos.FirstOrDefault(n => n.PumpId == pumpController.PumpId && n.NozzleLogicalId == nozzle.LogicalId).ProductBarcode, result.Item2, result.Item1, (pumpController.Nozzles.FirstOrDefault(n => n.LogicalId == nozzleId)?.RealPriceOnPhysicalPump ?? 0) / Math.Pow(10, pumpController.PriceDecimalDigits)); } fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); } else { var targetController = fdcPumpControllers .First(c => c.PumpId == deviceId) as IFdcPumpController; var result = await this.GetFuelPointTotalsAsync(targetController.PumpId, (byte)nozzleId); fdcLogger.LogInformation(" OnGetFuelPointTotalsReq for pump: " + targetController.PumpId + ", nozzle: " + nozzleId + " result(with decimal points) is: " + result.Item1 + " <-> " + result.Item2); fdcServer.GetFuelPointTotalsAdd(workstationID, applicationSender, requestId, deviceId, nozzleId, nozzleExtraInfos.FirstOrDefault(n => n.PumpId == targetController.PumpId && n.NozzleLogicalId == nozzleId).ProductBarcode, result.Item2, result.Item1, (targetController.Nozzles.FirstOrDefault(n => n.LogicalId == nozzleId)?.RealPriceOnPhysicalPump ?? 0) / Math.Pow(10, targetController.PriceDecimalDigits)); fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); } fdcLogger.LogDebug(" OnGetFuelPointTotals done"); } catch (Exception exxx) { fdcLogger.LogError("OnGetFuelPointTotalsReq exceptioned: " + exxx.ToString()); fdcServer.GetFuelPointTotalsSend(workstationID, applicationSender, requestId, (int)ErrorCode.ERRCD_BADCONF, OverallResult.Failure.ToString()); } }; #region setup Fdc unsolicited event foreach (var fdcPumpController in fdcPumpControllers) { fdcPumpController.OnStateChange += async (s, stateChangeArg) => { var pump = s as IFdcPumpController; try { fdcLogger.LogDebug("Pump " + pump.PumpId + " StateChanged to: " + stateChangeArg.NewPumpState.ToString() + ", nozzles states are(LogicalId-State): " + (pump.Nozzles != null ? (pump.Nozzles.Any() ? pump.Nozzles.Select(n => n.LogicalId.ToString() + "-" + (n.LogicalState?.ToString() ?? "")) .Aggregate((n, acc) => n + ", " + acc) : "") : "") + ", StateChangedNozzles are: " + (stateChangeArg.StateChangedNozzles != null ? (stateChangeArg.StateChangedNozzles.Any() ? stateChangeArg.StateChangedNozzles.Select(n => n.LogicalId.ToString()) .Aggregate((n, acc) => n + ", " + acc) : "") : "")); this.OnStateChange?.Invoke(s, stateChangeArg); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FireEvent(this, "OnFdcControllerStateChange", new { pump.PumpId, logicalId = (stateChangeArg.StateChangedNozzles != null && stateChangeArg.StateChangedNozzles.Any()) ? stateChangeArg.StateChangedNozzles.FirstOrDefault().LogicalId.ToString() : "", logicalState = stateChangeArg.NewPumpState }); fdcLogger.LogTrace("Pump " + pump.PumpId + " StateChanged event fired and back"); // this is used for send to fdc client. byte nozzleUpOrDownBitMap = 0; if (stateChangeArg.StateChangedNozzles != null) foreach (var upNozzle in stateChangeArg.StateChangedNozzles) { nozzleUpOrDownBitMap = SetBit(nozzleUpOrDownBitMap, upNozzle.LogicalId - 1, upNozzle.LogicalId, 1); } byte nozzleLockedOrUnlockedBitMap = 0; foreach (var nozzle in pump.Nozzles) { if (nozzle.LogicalState.HasValue && nozzle.LogicalState == LogicalDeviceState.FDC_LOCKED) nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 1); else nozzleLockedOrUnlockedBitMap = SetBit(nozzleLockedOrUnlockedBitMap, nozzle.LogicalId - 1, nozzle.LogicalId, 0); } //fdcLogger.LogDebug("@@@@@@@@@@@Pump " + pump.PumpId // + " StateChanged bitmap is: " + nozzleUpOrDownBitMap); fdcServer.DeviceStateChange(Wayne.FDCPOSLibrary.DeviceType.DT_FuellingPoint, pump.PumpId, (int)(stateChangeArg.NewPumpState), (int)LogicalDeviceState.FDC_UNDEFINED, "", "", "", pump.Nozzles.Count(), nozzleUpOrDownBitMap, nozzleLockedOrUnlockedBitMap, 0); if (stateChangeArg.NewPumpState == LogicalDeviceState.FDC_CALLING) { //CloudRestClient.Default.UploadDataAsync( // new PumpDeviceUploadData_V1( // this.pumpControllersWithDeviceSerialNumbers.First(f => f.Item2 == pump).Item1, // DataPriorityLevel.Info) // { // PumpId = pump.PumpId, // LogicalNozzleId = (a.StateChangedNozzles != null ? // (a.StateChangedNozzles.Any() ? a.StateChangedNozzles.First().LogicalId : 1) : 1), // PumpState = "Calling", // }, null); if (this.config_AutoAuthCallingPumps) { if (this.config_MaxStackUnpaidTrxPerPump > 0) { var targetController = fdcPumpControllers .First(c => c.PumpId == pump.PumpId) as IFdcPumpController; SqliteDbContext dbContext = new SqliteDbContext(); var unpaidTrxCount = dbContext.PumpTransactionModels.Count(t => t.PumpId == targetController.PumpId && t.State == FuelSaleTransactionState.Payable); if (unpaidTrxCount >= this.config_MaxStackUnpaidTrxPerPump) { fdcLogger.LogInformation(" Auto authorizing is not permit since pump " + targetController.PumpId + " have: " + unpaidTrxCount + " unpaid trx"); return; } } //must use another thread for release the com port thread, otherwise the thread which from COM port I/O threadpool will get stuck in Authorize(autoresetEvent.waitone), and the next //Hanlder.Process() would never get called. //ThreadPool.QueueUserWorkItem(async o => //{ fdcLogger.LogDebug("Auto authorizing Pump: " + fdcPumpController.PumpId); //var result = fdcPumpController.Authorize(1); int autoAuthDefaultAmount = 4567; var result = await fdcPumpController.AuthorizeWithAmountAsync( (int)(autoAuthDefaultAmount * Math.Pow(10, fdcPumpController.AmountDecimalDigits)), 1); if (!result) fdcLogger.LogError("Auto auth Pump: " + fdcPumpController.PumpId + " FAILED!"); //} //); } else { var targetController = fdcPumpControllers .First(c => c.PumpId == pump.PumpId) as IFdcPumpController; if (this.config_MaxStackUnpaidTrxPerPump > 0) { SqliteDbContext dbContext = new SqliteDbContext(); var unpaidTrxCount = dbContext.PumpTransactionModels.Count(t => t.PumpId == targetController.PumpId && t.State == FuelSaleTransactionState.Payable); if (unpaidTrxCount >= this.config_MaxStackUnpaidTrxPerPump) { fdcLogger.LogInformation(" Delay authorizing is not permit since pump " + targetController.PumpId + " has: " + unpaidTrxCount + " unpaid trx"); return; } } if (this.DelayAuthParameters.TryGetValue(pump.PumpId, out DelayAuthParameter delayAuthParameter)) { fdcLogger.LogDebug("Delay authorizing Pump: " + targetController.PumpId); if (delayAuthParameter.MaxTrxAmount == null && delayAuthParameter.MaxTrxVolume == null) { int autoAuthDefaultAmount = 9999; var result = await targetController.AuthorizeWithAmountAsync( (int)(autoAuthDefaultAmount * Math.Pow(10, targetController.AmountDecimalDigits)), 1); if (!result) fdcLogger.LogError("Delay auth with defaultAmount to Pump: " + fdcPumpController.PumpId + " FAILED!"); } else if (delayAuthParameter.MaxTrxVolume == null) { var result = await targetController.AuthorizeWithAmountAsync( (int)(delayAuthParameter.MaxTrxAmount.Value * Math.Pow(10, targetController.AmountDecimalDigits)), 1); if (!result) fdcLogger.LogError("Delay auth with para.MaxTrxAmount(" + (delayAuthParameter.MaxTrxAmount ?? -1) + ") to Pump: " + fdcPumpController.PumpId + " FAILED!"); } else if (delayAuthParameter.MaxTrxAmount == null) { var result = await targetController.AuthorizeWithVolumeAsync( (int)(delayAuthParameter.MaxTrxVolume.Value * Math.Pow(10, targetController.VolumeDecimalDigits)), 1); if (!result) fdcLogger.LogError("Delay auth with para.MaxTrxVolume(" + (delayAuthParameter.MaxTrxVolume ?? -1) + ") Pump: " + fdcPumpController.PumpId + " FAILED!"); } if (this.config_removeDelayAuthParameterAfterUseIt) this.DelayAuthParameters.TryRemove(pump.PumpId, out DelayAuthParameter _); } else { fdcLogger.LogDebug("Bypass Delay authorizing Pump: " + targetController.PumpId + " as no DelayAuthParameter was set for it."); } } } } catch (Exception exxx) { fdcLogger.LogError("fdcPumpController.OnStateChange exceptioned: " + exxx.ToString()); } }; fdcPumpController.OnCurrentFuellingStatusChange += async (s, a) => { var pump = s as IFdcPumpController; try { fdcLogger.LogDebug($"Pump {pump.PumpId }, Nozzle: {a?.Transaction?.Nozzle?.LogicalId.ToString() ?? ""}, OnCurrentFuellingStatusChange"); var product = nozzleExtraInfos.FirstOrDefault(c => c.PumpId == pump.PumpId && c.NozzleLogicalId == a.Transaction.Nozzle.LogicalId); if (product != null) a.Transaction.Barcode = product.ProductBarcode; else a.Transaction.Barcode = 1; if (a.Transaction.Finished) { fdcLogger.LogInformation("Pump " + pump.PumpId + ", transaction is finished, vol: " + a.Transaction.Volumn + ", amount: " + a.Transaction.Amount + ", price: " + a.Transaction.Price + ", nozzleNo: " + a.Transaction.Nozzle.LogicalId + ", volumeTotalizer: " + (a.Transaction.VolumeTotalizer ?? -1) + ", amountTotalizer: " + (a.Transaction.AmountTotalizer ?? -1) + ", seqNumber: " + a.Transaction.SequenceNumberGeneratedOnPhysicalPump + ", productBarcode: " + product?.ProductBarcode + ", productName: " + (product?.ProductName ?? "")); //enum SaleTrxStatus //{ // SALE_TRX_UNDEFINED = 0, // SALE_TRX_PAYABLE = 1, // SALE_TRX_LOCKED = 2, // SALE_TRX_PAID = 3, // SALE_TRX_CLEARED = 4, //}; // will do a duplication search in a short time range(by days) since physical pump will reuse seq id. var duplicationDectectTimeRange = 3; var range = DateTime.Now.Subtract(new TimeSpan(duplicationDectectTimeRange, 0, 0, 0)); SqliteDbContext dbContext = new SqliteDbContext(); var existed = dbContext.PumpTransactionModels.Where(f => f.PumpId == pump.PumpId && f.TransactionSeqNumberFromPhysicalPump == a.Transaction.SequenceNumberGeneratedOnPhysicalPump.ToString() && f.UnitPrice == a.Transaction.Price && f.Amount == a.Transaction.Amount && f.Volumn == a.Transaction.Volumn && f.SaleEndTime > range).ToList(); if (existed.Any()) { fdcLogger.LogWarning("A new trx duplicated with an existed trx in db which done in recent " + duplicationDectectTimeRange + " days, it was with " + "releaseToken:" + existed.First().ReleaseToken + ", pumpId: " + pump.PumpId + " and seqNo: " + a.Transaction.SequenceNumberGeneratedOnPhysicalPump.ToString() + ", will do nothing and NOT notify any POS, while the existed one in database detail is-> " + "Vol: " + existed.First().Volumn + ", Amount: " + existed.First().Amount + ", State: " + existed.First().State.ToString() + ", SaleStartTime: " + (existed.First().SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "") + ", EndStartTime: " + (existed.First().SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "") + ", ProductBarcode: " + existed.First().ProductBarcode.ToString() + ""); } else { var trx = new Edge.Core.Database.Models.FuelSaleTransaction() { TransactionSeqNumberFromPhysicalPump = a.Transaction.SequenceNumberGeneratedOnPhysicalPump.ToString(), PumpId = pump.PumpId, LogicalNozzleId = a.Transaction.Nozzle.LogicalId, Amount = a.Transaction.Amount, Volumn = a.Transaction.Volumn, UnitPrice = a.Transaction.Price, VolumeTotalizer = a.Transaction.VolumeTotalizer ?? -1, AmountTotalizer = a.Transaction.AmountTotalizer ?? -1, ProductBarcode = product?.ProductBarcode.ToString() ?? "9999", //ProductName = "refer cloud", State = FuelSaleTransactionState.Payable, LockedByFdcClientId = "", // hard code for now, need do it in PumpHandler SaleStartTime = DateTime.Now.Subtract(new TimeSpan(0, 3, 0)), SaleEndTime = DateTime.Now, }; dbContext.PumpTransactionModels.Add(trx); dbContext.SaveChanges(); fdcLogger.LogDebug(" ######transaction is done saving to db with ReleaseToken(Id): " + trx.ReleaseToken); var safe = this.OnFdcFuelSaleTransactinStateChange; safe?.Invoke(this, new FdcFuelSaleTransactinStateChangeEventArg(trx, FuelSaleTransactionState.Payable)); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FireEvent(this, "OnFdcFuelSaleTransactinStateChange", new FdcFuelSaleTransactinStateChangeEventArg(trx, FuelSaleTransactionState.Payable)); var startingTime = DateTime.Now; fdcServer.FuelSaleTrx(pump.PumpId, trx.Volumn / Math.Pow(10, pump.VolumeDecimalDigits), trx.Amount / Math.Pow(10, pump.AmountDecimalDigits), trx.UnitPrice / Math.Pow(10, pump.PriceDecimalDigits), trx.LogicalNozzleId, product?.ProductBarcode ?? 0, product?.ProductName ?? ("refer cloud for name of " + (product?.ProductBarcode ?? -1)), "", 1, int.Parse(trx.TransactionSeqNumberFromPhysicalPump), (int)trx.State, 0, trx.ReleaseToken.ToString(), trx.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"), trx.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"), "", "", 0, 0); fdcLogger.LogDebug($" ######transaction(releaseToken: {trx.ReleaseToken}) is done broadcasting to POSes, used: {DateTime.Now.Subtract(startingTime).TotalMilliseconds}"); this.OnCurrentFuellingStatusChange?.Invoke(s, new FdcServerTransactionDoneEventArg(a.Transaction) { ReleaseToken = trx.ReleaseToken }); fdcLogger.LogTrace("Pump " + pump.PumpId + " OnCurrentFuellingStatusChange event fired and back"); if (a.Transaction.VolumeTotalizer == null) { var result = await GetFuelPointTotalsAsync(pump.PumpId, a.Transaction.Nozzle.LogicalId); a.Transaction.AmountTotalizer = (int?)(result.Item1 * Math.Pow(10, pump.AmountDecimalDigits)); a.Transaction.VolumeTotalizer = (int?)(result.Item2 * Math.Pow(10, pump.VolumeDecimalDigits)); } var targetNozzle = pump.Nozzles.First(n => n.LogicalId == a.Transaction.Nozzle.LogicalId); if (targetNozzle != null) targetNozzle.VolumeTotalizer = a.Transaction.VolumeTotalizer; await universalApiHub.FireEvent(this, "OnCurrentFuellingStatusChange", new FdcServerTransactionDoneEventArg(a.Transaction) { FuelingEndTime = trx.SaleEndTime }); } } else { fdcLogger.LogDebug(" transaction is ongoing, vol: " + a.Transaction.Volumn + ", amount: " + a.Transaction.Amount + ", price: " + a.Transaction.Price + ", nozzleNo: " + a.Transaction.Nozzle.LogicalId + ", seqNumber: " + a.Transaction.SequenceNumberGeneratedOnPhysicalPump ?? "" + ", productBarcode: " + product.ProductBarcode + ", productName: " + (product.ProductName ?? "")); fdcServer.CurrentFuellingStatus(pump.PumpId, a.Transaction.Volumn / Math.Pow(10, pump.VolumeDecimalDigits), a.Transaction.Amount / Math.Pow(10, pump.AmountDecimalDigits), a.Transaction.Price / Math.Pow(10, pump.PriceDecimalDigits), a.Transaction.Nozzle.LogicalId, a.Transaction.SequenceNumberGeneratedOnPhysicalPump, "9999", 1, a.Transaction.Nozzle.LogicalId); this.OnCurrentFuellingStatusChange?.Invoke(s, new FdcServerTransactionDoneEventArg(a.Transaction)); fdcLogger.LogTrace("Pump " + pump.PumpId + " OnCurrentFuellingStatusChange event fired and back"); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FireEvent(this, "OnCurrentFuellingStatusChange", new FdcServerTransactionDoneEventArg(a.Transaction)); } } catch (Exception exxx) { fdcLogger.LogError($"PumpId: { pump?.PumpId ?? -1}, fdcPumpController.OnCurrentFuellingStatusChange exceptioned: " + exxx); } }; } #endregion #region handle BroadcastGenericTypelessMessage this.fdcCommunicableControllers.ToList().ForEach(c => { c.BroadcastMessageViaFdc += (msg) => { fdcServer.SendGenericTypelessMessageToFdcClient(null, null, msg); return true; }; }); this.fdcCommunicableControllers.ToList().ForEach(c => { c.SendMessageViaFdc += (workstationID, applicationSender, msg) => { fdcServer.SendGenericTypelessMessageToFdcClient(workstationID, applicationSender, msg); return true; }; }); fdcServer.OnGetGenericTypelessMessageReq += (string workstationID, string applicationSender, int requestId, string message) => { this.fdcCommunicableControllers.ToList().ForEach(c => { if (c.OnMessageReceivedViaFdc != null) { var returnResult = c.OnMessageReceivedViaFdc(message); fdcServer.GenericTypelessMessageSend(workstationID, applicationSender, requestId, returnResult.Item1, returnResult.Item2.ToString()); } }); }; #endregion fdcLogger.LogDebug("Start all FdcPumpController initing...(total: " + fdcPumpControllers.Count() + ", pump Ids are: " + (fdcPumpControllers.Any() ? (fdcPumpControllers.Select(c => c.PumpId.ToString()).Aggregate((acc, n) => acc + ", " + n) + ")") : ")")); foreach (var fdcPumpController in fdcPumpControllers) { var onFdcServerInitParams = new Dictionary(); try { #region LastPriceChange for each nozzles // nozzle logical id:rawPrice var innerParams0 = new Dictionary(); foreach (var nozzle in fdcPumpController.Nozzles) { var dbContext = new SqliteDbContext(); var lastPriceChange = dbContext.FuelPriceChanges .Where(t => t.PumpId == fdcPumpController.PumpId && t.LogicalNozzleId == nozzle.LogicalId) .OrderByDescending(b => b.Id).FirstOrDefault(); if (lastPriceChange != null) innerParams0.Add(nozzle.LogicalId, lastPriceChange.NewPriceWithoutDecimal); } onFdcServerInitParams.Add("LastPriceChange", innerParams0); #endregion #region LastFuelSaleTrx for each nozzles // nozzle logical id:LastSaleTrx var innerParams1 = new Dictionary(); foreach (var nozzle in fdcPumpController.Nozzles) { var dbContext = new SqliteDbContext(); var lastPaidTrx = dbContext.PumpTransactionModels .Where(t => t.PumpId == fdcPumpController.PumpId && t.LogicalNozzleId == nozzle.LogicalId) .OrderByDescending(b => b.SaleStartTime).FirstOrDefault(); if (lastPaidTrx != null) innerParams1.Add(nozzle.LogicalId, lastPaidTrx); } onFdcServerInitParams.Add("LastFuelSaleTrx", innerParams1); #endregion } catch (Exception exxx) { fdcLogger.LogError("Retrieve lastPriceChange for OnFdcServerInit from db for Pump with pumpid: " + fdcPumpController.PumpId + " exceptioned: " + exxx.ToString() + System.Environment.NewLine + "Will skip this pump and continue for next pump."); continue; } try { fdcPumpController.OnFdcServerInit(onFdcServerInitParams); } catch (Exception exxx) { fdcLogger.LogError("FdcPumpController with pumpid: " + fdcPumpController.PumpId + " exceptioned in OnFdcServerInit, detail: " + exxx.ToString() + System.Environment.NewLine + "Will skip and continue for next FdcPumpController."); continue; } } fdcLogger.LogDebug("Done all FdcPumpController init, start FdcServer tcp listening..."); var _ = fdcServer.Start(this.config_ListeningPort, true, "WINCOR", 2, true, "NONE"); fdcLogger.LogDebug("Done FdcServer tcp listening..."); return Task.FromResult(_); } public Task Stop() { if (this.configurator != null && this.onConfiguratorConfigFileChangedEventHandler != null) this.configurator.OnConfigFileChanged -= this.onConfiguratorConfigFileChangedEventHandler; this.purgeDatabaseTimer?.Stop(); // set fdcPumpControllers to an empty list, as it needs to be stopped from firing any event to any substriber. this.fdcPumpControllers = new List(); var stopFdcServerResult = fdcServer.Stop(); return Task.FromResult(stopFdcServerResult); } private static byte SetBit(byte target, int bitStartIndex, int bitEndIndex, int replacedValue) { if (bitStartIndex < 0 || bitEndIndex > 7 || bitEndIndex < bitStartIndex) { throw new ArgumentException("bitStartIndex or bitEndIndex value is not valid"); } byte mask = 0; for (int i = 0; i < bitEndIndex - bitStartIndex + 1; i++) { mask += (byte)Math.Pow(2, i); } if (replacedValue > mask) { throw new ArgumentOutOfRangeException("Replaced value: " + replacedValue + " cannot fit the bits range"); } byte maskedValue = (byte)(target & (255 - (mask << bitStartIndex))); return (byte)(maskedValue + (replacedValue << bitStartIndex)); } public void Dispose() { } #region pump control interface opened for local call, used in other fc app. /// /// fired once PumpController state changed. /// public event EventHandler OnStateChange; /// /// fired once the fuelling trx from PumpController state changed. /// used to watch fuelling progress for a PumpController. /// public event EventHandler OnCurrentFuellingStatusChange; /// /// fired once the Fdc fuel sale trx state changed, like it's turn into Payable, Locked, Unlocked state etc. /// used for local app processors to watch the trx state, the state change mostly triggered by Fdc client request. /// public event EventHandler OnFdcFuelSaleTransactinStateChange; public IEnumerable FdcPumpControllers => this.fdcPumpControllers; /// /// maxTrxAmount has the high priority. /// /// the target pump, which for authorizing /// human readable number, with decimal point, like 5 RMB, should input 5. leave 0 if set with unlimited amount. /// human readable number, with decimal point, like 6.5L, should input 6.5. leave 0 if set with unlimited vol /// public async Task AuthorizePumpAsync(int pumpId, double maxTrxAmount, double maxTrxVolume) { fdcLogger.LogDebug("Authorizing Pump: " + pumpId + ", maxTrxAmount: " + maxTrxAmount + ", maxTrxVolume: " + maxTrxVolume); var targetController = fdcPumpControllers .First(c => c.PumpId == pumpId) as IFdcPumpController; if (this.config_MaxStackUnpaidTrxPerPump > 0) { SqliteDbContext dbContext = new SqliteDbContext(); var unpaidTrxCount = dbContext.PumpTransactionModels.Count(t => t.PumpId == targetController.PumpId && t.State == FuelSaleTransactionState.Payable); if (unpaidTrxCount >= this.config_MaxStackUnpaidTrxPerPump) { fdcLogger.LogInformation(" Authorizing from FdcClient is not permit since pump " + targetController.PumpId + " has: " + unpaidTrxCount + " unpaid trx"); //fdcServer.AuthoriseFuelPoint(workstationID, applicationSender, requestId, -99, releaseToken, deviceId, // (int)LogicalDeviceState.FDC_READY, (int)ErrorCode.ERRCD_MAXSTACKLIMIT, OverallResult.Failure.ToString()); return false; } } bool succeed = false; if (maxTrxAmount == 0 && maxTrxVolume == 0) // fuel with unlimited succeed = await targetController.AuthorizeAsync(1); else if (maxTrxAmount == 0 && maxTrxVolume != 0) succeed = await targetController.AuthorizeWithVolumeAsync((int)(maxTrxVolume * Math.Pow(10, targetController.VolumeDecimalDigits)), 1); else succeed = await targetController.AuthorizeWithAmountAsync((int)(maxTrxAmount * Math.Pow(10, targetController.AmountDecimalDigits)), 1); fdcLogger.LogDebug(" AuthorizePump: " + pumpId + " finished with: " + succeed.ToString()); return succeed; } /// /// Clear an unpaid fuel sale trx from db and notify all tcp FdcClients. /// This function is used for non-tcp-FdcClient caller, which are most likely the Fcc Apps, /// so it's a local in-process call. /// /// pump id of the target trx belongs /// trx Number of the target trx that is on clearing /// database row unique id of the target trx that is on clearing /// the caller identity, the trx will be marked as paid by it, make sure it unique in process /// , otherwise and the only impact, you lose the chance to know who really mark this trx with paid in db. /// the trx successfully cleared(marked as state `Paid`) from db, otherwise failed to clear and return null value public async Task ClearFuelSaleTrxAndNotifyAllFdcClientsAsync(int pumpId, string transactionNo, int trxDbUniqueId, string clientId) { return await this.ClearFuelSaleTrxAndNotifyAllFdcClientsAsync(pumpId, transactionNo, trxDbUniqueId, clientId, null, -1); } /// /// Clear an unpaid fuel sale trx from db and notify all tcp FdcClients. /// This function is used for Tcp FdcClient caller, the caller most likely a POS which connected in via TCP. /// Will return a clearFuelSaleTrx response to tcp Fdc client caller. /// /// /// /// /// fdc client's workstation id /// must specify the correct value that used for send respone back via tcp /// must specify the correct value that used for send respone back via tcp, for pair with request /// public async Task ClearFuelSaleTrxAndNotifyAllFdcClientsAsync(int pumpId, string transactionNo, int trxDbUniqueId, string workstationID, string appSenderId, int fdcClientRequestId) { try { fdcLogger.LogDebug("ClearFuelSaleTrxAndNotifyAllFdcClients (wid: " + workstationID + ", appSenderId: " + (appSenderId ?? "") + ", pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", releaseToken: " + trxDbUniqueId + ")"); SqliteDbContext dbContext = new SqliteDbContext(); int databaseId = trxDbUniqueId; var target = await dbContext.PumpTransactionModels.FirstOrDefaultAsync(t => t.ReleaseToken == databaseId && t.PumpId == pumpId && t.TransactionSeqNumberFromPhysicalPump == transactionNo); if (target != null) { if (target.State == FuelSaleTransactionState.Paid) { fdcLogger.LogInformation("ClearFuelSaleTrx for workstationId: " + workstationID + " on pump with pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", releaseToken: " + trxDbUniqueId + " failed due to target trx is already a Paid trx"); // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(appSenderId)) { int.TryParse(transactionNo, out int p_trxNo); fdcServer.ClearFuelSaleTrx(workstationID, appSenderId, fdcClientRequestId, pumpId, p_trxNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_NOTPOSSIBLE, 1, OverallResult.Failure.ToString()); } return null; } else { target.State = FuelSaleTransactionState.Paid; target.PaidByFdcClientId = workstationID; target.PaidTime = DateTime.Now; await dbContext.SaveChangesAsync(); var targetController = fdcPumpControllers .First(c => c.PumpId == pumpId) as IFdcPumpController; int.TryParse(transactionNo, out int p_trxNo); // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(appSenderId)) fdcServer.ClearFuelSaleTrx(workstationID, appSenderId, fdcClientRequestId, pumpId, p_trxNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_OK, 1, OverallResult.Success.ToString()); var safe = this.OnFdcFuelSaleTransactinStateChange; safe?.Invoke(this, new FdcFuelSaleTransactinStateChangeEventArg(target, FuelSaleTransactionState.Paid)); var universalApiHub = this.services.GetRequiredService(); await universalApiHub.FireEvent(this, "OnFdcFuelSaleTransactinStateChange", new FdcFuelSaleTransactinStateChangeEventArg(target, FuelSaleTransactionState.Paid)); fdcServer.FuelSaleTrx(target.PumpId, target.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits), target.Amount / Math.Pow(10, targetController.AmountDecimalDigits), target.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits), target.LogicalNozzleId, int.Parse(target.ProductBarcode), "", "", 1, int.Parse(target.TransactionSeqNumberFromPhysicalPump), // looks like should return 'Paid', but from old code, we put 'Cleared' (int)FuelSaleTransactionState.Cleared, 0, trxDbUniqueId.ToString(), target.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"), target.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"), target.LockedByFdcClientId ?? "", "", -1, 0); return target; } } else { fdcLogger.LogInformation("ClearFuelSaleTrx for workstationId: " + workstationID + " on pump with pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", releaseToken: " + trxDbUniqueId + " failed due to target trx could not found"); // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(appSenderId)) { int.TryParse(transactionNo, out int p_trxNo); fdcServer.ClearFuelSaleTrx(workstationID, appSenderId, fdcClientRequestId, pumpId, p_trxNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_BADVAL, 1, OverallResult.Failure.ToString()); } return null; } } catch (Exception exxx) { fdcLogger.LogInformation("ClearFuelSaleTrx for workstationId: " + workstationID + " on pump with pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", releaseToken: " + trxDbUniqueId + " failed due to exception: " + exxx); return null; } } /// /// Lock an unlocked fuel sale trx from db and notify all tcp FdcClients. /// This function is used for non-tcp-FdcClient caller, which are most likely the Fcc Apps, /// so it's a local in-process call. /// /// the caller identity, the trx will be marked as locked by it, make sure it unique in process /// , otherwise, the locked trx may get unlocked unexpectly by other caller with same id. /// /// /// /// public async Task LockFuelSaleTrxAndNotifyAllFdcClientsAsync(int lockingClientId, int pumpId, int transactionNo, int trxDbUniqueId) { return await this.LockFuelSaleTrxAndNotifyAllFdcClientsAsync(lockingClientId, "", -1, pumpId, transactionNo, trxDbUniqueId); } /// /// Lock an unlocked fuel sale trx from db and notify all tcp FdcClients. /// This function is used for tcp FdcClient caller, the caller most likely a POS which connected in via TCP. /// Will return a response to tcp Fdc client caller. /// /// the caller identity, the trx will be marked as locked by it, make sure it unique in process /// , otherwise, the locked trx may get unlocked unexpectly by other caller with same id. /// /// /// /// /// /// public async Task LockFuelSaleTrxAndNotifyAllFdcClientsAsync(int lockingClientId, string fdcClientAppSender, int fdcClientRequestId, int pumpId, int transactionNo, int trxDbUniqueId) { try { fdcLogger.LogDebug("LockFuelSaleTrxAndNotifyAllFdcClients from lockingClientId: " + lockingClientId + ", fdcClientAppSender: " + (fdcClientAppSender ?? "") + ", fdcClientRequestId: " + fdcClientRequestId + ", pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", trxDbUniqueId: " + trxDbUniqueId); var result = await FdcResourceArbitrator.Default.TryLockFuelSaleTrxAsync(lockingClientId, pumpId, transactionNo, trxDbUniqueId); fdcLogger.LogDebug("@@@@@TryLockFuelSaleTrx with: " + !(result == null)); //this.LockFuelSaleTrx(int.Parse(workstationID), deviceId, transactionNo, int.Parse(releaseToken)); if (result != null) { // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(fdcClientAppSender)) fdcServer.LockFuelSaleTrx(lockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId, transactionNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_OK, 1, OverallResult.Success.ToString()); var safe = this.OnFdcFuelSaleTransactinStateChange; safe?.Invoke(this, new FdcFuelSaleTransactinStateChangeEventArg(result, FuelSaleTransactionState.Locked)); var targetController = fdcPumpControllers .First(c => c.PumpId == pumpId) as IFdcPumpController; fdcServer.FuelSaleTrx(pumpId, result.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits), result.Amount / Math.Pow(10, targetController.AmountDecimalDigits), result.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits), result.LogicalNozzleId, int.Parse(result.ProductBarcode), "refer cloud" + result.ProductBarcode, "", 1, int.Parse(result.TransactionSeqNumberFromPhysicalPump), (int)FuelSaleTransactionState.Locked, 0, trxDbUniqueId.ToString(), result.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"), result.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"), lockingClientId.ToString(), "", lockingClientId, 0); } else { // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(fdcClientAppSender)) fdcServer.LockFuelSaleTrx(lockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId, transactionNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString()); } fdcLogger.LogDebug("@@@@@TryLockFuelSaleTrx returned! "); return result; } catch (Exception exx) { fdcLogger.LogError("LockFuelSaleTrxAndNotifyAllFdcClients exceptioned for lockingClientId: " + lockingClientId + ", pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", trxDbUniqueId: " + trxDbUniqueId + ", exception detail: " + exx); throw; } } /// /// Unlock a locked fuel sale trx from db and notify all tcp FdcClients. /// This function is used for non-tcp-FdcClient caller, which are most likely the Fcc Apps, /// so it's a local in-process call. /// /// the id of the caller when locking the trx, unlocking must be performed by the client who locked the trx /// /// /// /// public async Task UnlockFuelSaleTrxAndNotifyAllFdcClientsAsync(int lockingClientId, int pumpId, int transactionNo, int trxDbUniqueId) { return await this.UnlockFuelSaleTrxAndNotifyAllFdcClientsAsync(lockingClientId, "", -1, pumpId, transactionNo, trxDbUniqueId); } /// /// Unlock a locked fuel sale trx from db and notify all tcp FdcClients. /// This function is used for tcp FdcClient caller, the caller most likely a POS which connected in via TCP. /// Will return a response to tcp Fdc client caller. /// /// the id of the caller when locking the trx, unlocking must be performed by the client who locked the trx /// /// /// /// /// /// public async Task UnlockFuelSaleTrxAndNotifyAllFdcClientsAsync(int unlockingClientId, string fdcClientAppSender, int fdcClientRequestId, int pumpId, int transactionNo, int trxDbUniqueId) { try { fdcLogger.LogDebug("UnlockFuelSaleTrxAndNotifyAllFdcClients from lockingClientId: " + unlockingClientId + ", fdcClientAppSender: " + (fdcClientAppSender ?? "") + ", fdcClientRequestId: " + fdcClientRequestId + ", pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", trxDbUniqueId: " + trxDbUniqueId); var result = await FdcResourceArbitrator.Default.TryUnlockFuelSaleTrxAsync(unlockingClientId, pumpId, transactionNo, trxDbUniqueId); if (result != null) { var targetController = fdcPumpControllers .First(c => c.PumpId == pumpId) as IFdcPumpController; // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(fdcClientAppSender)) fdcServer.UnlockFuelSaleTrx(unlockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId, transactionNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_OK, 1, OverallResult.Success.ToString()); var safe = this.OnFdcFuelSaleTransactinStateChange; safe?.Invoke(this, new FdcFuelSaleTransactinStateChangeEventArg(result, FuelSaleTransactionState.Payable)); fdcServer.FuelSaleTrx(pumpId, result.Volumn / Math.Pow(10, targetController.VolumeDecimalDigits), result.Amount / Math.Pow(10, targetController.AmountDecimalDigits), result.UnitPrice / Math.Pow(10, targetController.PriceDecimalDigits), result.LogicalNozzleId, int.Parse(result.ProductBarcode), "refer cloud" + result.ProductBarcode, "", 1, int.Parse(result.TransactionSeqNumberFromPhysicalPump), (int)FuelSaleTransactionState.Payable, 0, trxDbUniqueId.ToString(), result.SaleStartTime?.ToString("yyyy-MM-dd HH:mm:ss"), result.SaleEndTime?.ToString("yyyy-MM-dd HH:mm:ss"), "", "", unlockingClientId, 0); } else { // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(fdcClientAppSender)) fdcServer.UnlockFuelSaleTrx(unlockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId, transactionNo, trxDbUniqueId.ToString(), (int)ErrorCode.ERRCD_TRANSLOCKED, 1, OverallResult.Failure.ToString()); } return result; } catch (Exception exxx) { fdcLogger.LogError("UnlockFuelSaleTrxAndNotifyAllFdcClients exceptioned for unlockingClientId: " + unlockingClientId + ", pumpId: " + pumpId + ", transactionNo: " + transactionNo + ", trxDbUniqueId: " + trxDbUniqueId + ", exception detail: " + exxx); throw; } } /// /// Lock a nozzle by call into a PumpHandler and then notify all tcp FdcClients. /// This function is used for non-tcp-FdcClient caller, which are most likely the Fcc Apps, /// so it's a local in-process call. /// /// the caller identity, the trx will be marked as locked by it, make sure it unique in process /// , otherwise, the locked trx may get unlocked unexpectly by other caller with same id. /// /// logical nozzle in the pump for locking /// public async Task LockNozzleAndNotifyAllFdcClientsAsync(int lockingClientId, int pumpId, int logicalNozzleId) { return await this.LockNozzleAndNotifyAllFdcClientsAsync(lockingClientId, "", -1, pumpId, logicalNozzleId); } /// /// Lock a nozzle by call into a PumpHandler and then notify all tcp FdcClients. /// This function is used for tcp FdcClient caller, the caller most likely a POS which connected in via TCP. /// Will return a response to tcp Fdc client caller. /// /// the caller identity, the trx will be marked as locked by it, make sure it unique in process /// , otherwise, the locked trx may get unlocked unexpectly by other caller with same id. /// /// /// /// /// public async Task LockNozzleAndNotifyAllFdcClientsAsync(int lockingClientId, string fdcClientAppSender, int fdcClientRequestId, int pumpId, int logicalNozzleId) { try { fdcLogger.LogDebug("LockNozzleAndNotifyAllFdcClientsAsync from lockingClientId: " + lockingClientId + ", fdcClientAppSender: " + (fdcClientAppSender ?? "") + ", fdcClientRequestId: " + fdcClientRequestId + ", pumpId: " + pumpId + ", logicalNozzleId: " + logicalNozzleId); var targetController = fdcPumpControllers .First(c => c.PumpId == pumpId) as IFdcPumpController; var result = await targetController.LockNozzleAsync((byte)logicalNozzleId); if (result) { // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(fdcClientAppSender)) fdcServer.LockNozzle(lockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId, pumpId, logicalNozzleId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); } else { fdcLogger.LogDebug("LockNozzleAndNotifyAllFdcClientsAsync failed"); // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(fdcClientAppSender)) fdcServer.LockNozzle(lockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId, pumpId, logicalNozzleId, (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.Failure.ToString()); } return result; } catch (Exception exx) { fdcLogger.LogError("LockNozzleAndNotifyAllFdcClientsAsync exceptioned for lockingClientId: " + lockingClientId + ", pumpId: " + pumpId + ", logicalNozzleId: " + logicalNozzleId + ", exception detail: " + exx); fdcServer.LockNozzle(lockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId, pumpId, logicalNozzleId, (int)ErrorCode.ERRCD_NOTALLOWED, OverallResult.Failure.ToString()); return false; } } /// /// Unlock a nozzle by call into a PumpHandler and then notify all tcp FdcClients /// This function is used for non-tcp-FdcClient caller, which are most likely the Fcc Apps, /// so it's a local in-process call. /// /// the id of the caller when locking the trx, unlocking must be performed by the client who locked the trx /// /// /// public async Task UnlockNozzleAndNotifyAllFdcClientsAsync(int lockingClientId, int pumpId, int logicalNozzleId) { return await this.UnlockNozzleAndNotifyAllFdcClientsAsync(lockingClientId, "", -1, pumpId, logicalNozzleId); } /// /// Unlock a nozzle by call into a PumpHandler and then notify all tcp FdcClients /// This function is used for tcp FdcClient caller, the caller most likely a POS which connected in via TCP. /// Will return a response to tcp Fdc client caller. /// /// the id of the caller when locking the trx, unlocking must be performed by the client who locked the trx /// /// /// /// /// public async Task UnlockNozzleAndNotifyAllFdcClientsAsync(int unlockingClientId, string fdcClientAppSender, int fdcClientRequestId, int pumpId, int logicalNozzleId) { try { fdcLogger.LogDebug("UnlockNozzleAndNotifyAllFdcClientsAsync from lockingClientId: " + unlockingClientId + ", fdcClientAppSender: " + (fdcClientAppSender ?? "") + ", fdcClientRequestId: " + fdcClientRequestId + ", pumpId: " + pumpId + ", logicalNozzleId: " + logicalNozzleId); var targetController = fdcPumpControllers .First(c => c.PumpId == pumpId) as IFdcPumpController; var result = await targetController.UnlockNozzleAsync((byte)logicalNozzleId); if (result) { // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(fdcClientAppSender)) fdcServer.UnlockNozzle(unlockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId, pumpId, logicalNozzleId, (int)ErrorCode.ERRCD_OK, OverallResult.Success.ToString()); } else { fdcLogger.LogDebug("UnlockNozzleAndNotifyAllFdcClientsAsync failed"); // null or empty appSenderId indicates the clear request is not send from logged in FdcClient, but from some other source, like // litefccCore internal app, then do not reply a response. if (!string.IsNullOrEmpty(fdcClientAppSender)) fdcServer.UnlockNozzle(unlockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId, pumpId, logicalNozzleId, (int)ErrorCode.ERRCD_NOTPOSSIBLE, OverallResult.Failure.ToString()); } return result; } catch (Exception exx) { fdcLogger.LogError("UnlockNozzleAndNotifyAllFdcClientsAsync exceptioned for lockingClientId: " + unlockingClientId + ", pumpId: " + pumpId + ", logicalNozzleId: " + logicalNozzleId + ", exception detail: " + exx); fdcServer.UnlockNozzle(unlockingClientId.ToString(), fdcClientAppSender, fdcClientRequestId, pumpId, pumpId, logicalNozzleId, (int)ErrorCode.ERRCD_NOTALLOWED, OverallResult.Failure.ToString()); return false; } } /// /// Query the totalizer value (money and volume) with decimal point, with default timeout 10 seconds. /// /// /// from 1 to N /// With decimal points value, Money:Volume public async Task> GetFuelPointTotalsAsync(int pumpId, byte nozzleLogicalId) { if (fdcLogger.IsEnabled(LogLevel.Debug)) fdcLogger.LogDebug("GetFuelPointTotals for pumpId: " + pumpId + ", nozzleLogicalId: " + nozzleLogicalId); var targetController = fdcPumpControllers .First(c => c.PumpId == pumpId) as IFdcPumpController; var totalizerResultTask = targetController.QueryTotalizerAsync(nozzleLogicalId); await Task.WhenAny(totalizerResultTask, Task.Delay(10000)); if (totalizerResultTask.IsCompleted) { if (fdcLogger.IsEnabled(LogLevel.Debug)) fdcLogger.LogDebug(" GetFuelPointTotals for pumpId: " + pumpId + ", nozzleLogicalId: " + nozzleLogicalId + ", no decimal point Money<->Volume pair is: " + totalizerResultTask.Result.Item1 + "<->" + totalizerResultTask.Result.Item2); return new Tuple( totalizerResultTask.Result.Item1 / Math.Pow(10, targetController.AmountDecimalDigits), totalizerResultTask.Result.Item2 / Math.Pow(10, targetController.VolumeDecimalDigits)); } else { fdcLogger.LogInformation(" GetFuelPointTotals for pumpId: " + pumpId + ", nozzleLogicalId: " + nozzleLogicalId + " timed out."); return new Tuple(-1, -1); } } /// /// Query the IFdcPumpController /// /// /// IFdcPumpController public IFdcPumpController GetFdcPumpController(int pumpId) { if (fdcLogger.IsEnabled(LogLevel.Debug)) fdcLogger.LogDebug("GetFdcPumpController for pumpId: " + pumpId); var targetController = fdcPumpControllers .First(c => c.PumpId == pumpId) as IFdcPumpController; return targetController; } /// /// Get the fuel sale trxs with details which are in state: Undefined, Payable, or Locked for target Pump. /// NOTE, for performance purpose, internal throttle is enabled to only return latest 365 days /// or latest 1000 rows of data. /// /// the target pump id, or -1 which means the target is all pumps /// list of FuelSaleTransaction, the most recent trx is in more front position public async Task> GetAvailableFuelSaleTrxsWithDetailsAsync(int pumpId) { return await this.GetAvailableFuelSaleTrxsWithDetailsAsync(pumpId, -1, 1000); } /// /// Get the fuel sale trxs with details which are in state: Undefined, Payable, or Locked for target Pump. /// /// the target pump id, or -1 which means the target is all pumps /// further specify the nozzle logical id, or -1 which means all nozzles in the pump. /// limit the returned row count /// list of FuelSaleTransaction, the most recent trx is in more front position public async Task> GetAvailableFuelSaleTrxsWithDetailsAsync(int pumpId, int nozzleLogicalId, int rowCount) { // for limit data size, only show latest N days' data. int maxReturnDays = 365; // for further limit data size, only show latest N count of data. int maxReturnDataCount = rowCount; lock (this.syncObject) { try { var dueDate = DateTime.Now.Subtract(new TimeSpan(maxReturnDays, 0, 0, 0)); SqliteDbContext dbContext = new SqliteDbContext(); if (pumpId == -1) { if (nozzleLogicalId == -1) { var all = dbContext.PumpTransactionModels.Where(t => t.State != FuelSaleTransactionState.Paid && t.State != FuelSaleTransactionState.Cleared && t.SaleStartTime >= dueDate) .OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount); return all; } else { var all = dbContext.PumpTransactionModels.Where(t => t.LogicalNozzleId == nozzleLogicalId && t.State != FuelSaleTransactionState.Paid && t.State != FuelSaleTransactionState.Cleared && t.SaleStartTime >= dueDate) .OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount); return all; } } if (nozzleLogicalId == -1) { var some = dbContext.PumpTransactionModels.Where(t => t.PumpId == pumpId && t.State != FuelSaleTransactionState.Paid && t.State != FuelSaleTransactionState.Cleared && t.SaleStartTime >= dueDate) .OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount); return some; } else { var some = dbContext.PumpTransactionModels.Where(t => t.PumpId == pumpId && t.LogicalNozzleId == nozzleLogicalId && t.State != FuelSaleTransactionState.Paid && t.State != FuelSaleTransactionState.Cleared && t.SaleStartTime >= dueDate) .OrderByDescending(r => r.SaleStartTime).Take(maxReturnDataCount); return some; } } catch (Exception exxx) { fdcLogger.LogError("GetAvailableFuelSaleTrxsWithDetails for pumpId: " + pumpId + ", nozzleLogicalId: " + nozzleLogicalId + ", rowCount: " + rowCount + " exceptioned: " + exxx.ToString()); return null; } } } /// /// Change a price for pumps that have the target barcode product configurated for their nozzles. /// /// fuel product barcode /// new price with decimal points /// the successfully price changed nozzle will be returned [UniversalApi] public async Task> ChangeFuelPriceAsync(int barcode, double newPriceWithDecimalPoints) { try { fdcLogger.LogInformation("ChangeFuelPrice for product with barcode: " + barcode + " to new price(with decimal points): " + newPriceWithDecimalPoints); var targetNozzles = fdcPumpControllers.SelectMany(p => p.Nozzles).Join( nozzleExtraInfos.Where(c => c.ProductBarcode == barcode), n => n.PumpId + "," + n.LogicalId, c => c.PumpId + "," + c.NozzleLogicalId, (n, c) => n); if (!targetNozzles.Any()) { fdcLogger.LogError("ChangeFuelPrice, have NOT seen product with barcode: " + barcode + " bound to any nozzles from all pumps, will quit."); return null; } //var oldPriceWithNoHumanReadableFormat = targetNozzles.First().RealPriceOnPhysicalPump; var succeedNozzles = new List(); foreach (var nozzle in targetNozzles) { bool succeed = false; var targetController = fdcPumpControllers.First(p => p.PumpId == nozzle.PumpId); var pumpRawFormatPrice = (int)( Math.Round(newPriceWithDecimalPoints * Math.Pow(10, targetController.PriceDecimalDigits), MidpointRounding.AwayFromZero)); var previousPrice = targetController.Nozzles .First(n => n.LogicalId == nozzle.LogicalId)?.RealPriceOnPhysicalPump; //if (previousPrice.HasValue // && previousPrice.Value == pumpRawFormatPrice) //{ // succeed = true; // fdcLogger.LogInformation(" price change, Pump " + targetController.PumpId + ", NozzleLogicalId " + nozzle.LogicalId + ", new price EQUALS the previous price, will NOT perform ChangeFuelPrice request"); //} //else //{ FuelPriceChange dbFuelPriceChange = null; try { dbFuelPriceChange = new FuelPriceChange() { PumpId = nozzle.PumpId, LogicalNozzleId = nozzle.LogicalId, NewPriceWithoutDecimal = pumpRawFormatPrice, StartTime = DateTime.Now, FinishTime = null }; var dbContext = new SqliteDbContext(); dbContext.FuelPriceChanges.Add(dbFuelPriceChange); await dbContext.SaveChangesAsync(); //fdcLogger.LogDebug(" ChangeFuelPrice, new price for pump: " + targetController.PumpId + " pre-save into database succeed"); } catch (Exception exxx) { fdcLogger.LogError(" ChangeFuelPrice, new price pre-save into database for pump: " + targetController.PumpId + ", logical Nozzle: " + nozzle.LogicalId + " exceptioned: " + exxx + Environment.NewLine + "Will ignore this nozzle and continue to next..."); continue; } try { succeed = await targetController.ChangeFuelPriceAsync(pumpRawFormatPrice, nozzle.LogicalId); fdcLogger.LogInformation(" ChangeFuelPrice succeed in IFdcController side for pump with PumpId: " + targetController.PumpId + ", LogicalNozzleId: " + nozzle.LogicalId); } catch (Exception exxx) { fdcLogger.LogError(" ChangeFuelPrice, pump " + targetController.PumpId + ", logical nozzle " + nozzle.LogicalId + " exceptioned in pump side: " + exxx + Environment.NewLine + "Will continue to next nozzle."); continue; } if (succeed) { succeedNozzles.Add(nozzle); //fdcLogger.LogDebug("ChangeFuelPrice for pump: " + targetController.PumpId // + ", logicalNozzle: " + nozzle.LogicalId + " succeed on pump side, will post-save to db"); try { dbFuelPriceChange.FinishTime = DateTime.Now; var dbContext = new SqliteDbContext(); await dbContext.SaveChangesAsync(); } catch (Exception exxx) { fdcLogger.LogError(" ChangeFuelPrice, new price post-save into database for pump: " + targetController.PumpId + ", logicalNozzle: " + nozzle.LogicalId + " exceptioned: " + exxx + Environment.NewLine + "Will ignore and continue to next nozzle"); } } else { fdcLogger.LogError(" ChangeFuelPrice, Pump " + targetController.PumpId + ", NozzleLogicalId " + nozzle.LogicalId + " apply new price failed on pump side"); } } return succeedNozzles; } catch (Exception exxx) { fdcLogger.LogError("ChangeFuelPrice, generic exception: " + exxx); return null; } } #endregion } }