using Dfs.WayneChina.HengshanPayTerminal; using Dfs.WayneChina.HengshanPayTerminal.MessageEntity; using Dfs.WayneChina.HengshanPayTerminal.MessageEntity.Incoming; using Dfs.WayneChina.HengshanPayTerminal.MessageEntity.Outgoing; using Dfs.WayneChina.SpsDbManager; using Dfs.WayneChina.SpsDbManager.ResultSet; using System; using System.Collections.Generic; using System.Text; using System.Linq; using Wayne.FDCPOSLibrary; using Edge.Core.Processor;using Edge.Core.IndustryStandardInterface.Pump; using Dfs.WayneChina.HengshanPayTerminal.Support; using System.Timers; using Dfs.WayneChina.IPosPlus.ServiceClient; using System.Threading.Tasks; using System.Collections.Concurrent; namespace Dfs.WayneChina.IPosPlus { /// /// An entity that manages the IC card terminal. /// public class TerminalManager { #region Fields private readonly byte STX = 0xFA; /// /// Sps_db database manager. /// private readonly SpsManager _dbManager; /// /// The terminal handler. /// private readonly HengshanPayTermHandler _terminal; /// /// IPosPlusApp instance. /// private readonly IPosPlusApp _posApp; /// /// Internally maintained pump state. /// private HengshanPumpState _pumpState; private HengshanPumpState _lastPumpState; /// /// All listed cards (Newly-added blacklist, base blacklist, newly-deleted blacklist, whitelist) /// public VersionedListedCard VersionedListedCard; /// /// Connection detection timer. /// private Timer timer; private int interval; private HengshanPayTerminal.Parser parser = new HengshanPayTerminal.Parser(); private CardAppType _cardAppType = CardAppType.CpuCard; private WorkMode currentWorkMode = WorkMode.Disabled; private IList currentDataVersions = null; #endregion #region Filling control state data private FillingInfo currentFilling; private FillingInfo lastFilling; #endregion #region Logger private static NLog.Logger logger = NLog.LogManager.LoadConfiguration("NLog.config").GetLogger("IPosPlusApp"); #endregion #region Queue private Queue activeSendQueue = new Queue(); private object sendQueueSyncObj = new object(); #endregion #region Payment queue private System.Threading.Thread thread; private ConcurrentQueue paymentRequestQueue = new ConcurrentQueue(); private System.Threading.ManualResetEvent mre = new System.Threading.ManualResetEvent(false); #endregion #region Reserved balance private readonly int reservedBalance = 0; #endregion #region Constructor public TerminalManager(IPosPlusApp posApp, int pumpId, int subAddress, SpsManager spsManager, HengshanPayTermHandler terminal, int interval, int cardAppType) { _posApp = posApp; PumpId = pumpId; SubAddress = subAddress; _dbManager = spsManager; _terminal = terminal; _pumpState = HengshanPumpState.Idle; _lastPumpState = HengshanPumpState.Idle; this.interval = interval; if (cardAppType == 1) _cardAppType = CardAppType.RfCard; else if (cardAppType == 2) _cardAppType = CardAppType.CpuCard; reservedBalance = posApp.ReservedBalance; RegisterEncodingProvider(); InitializeTimer(); thread = new System.Threading.Thread(() => HandleCardPayment()); thread.Start(); } private void RegisterEncodingProvider() { EncodingProvider provider = CodePagesEncodingProvider.Instance; Encoding.RegisterProvider(provider); } #endregion #region Properties public string TerminalId { get; private set; } public string Identifier => _terminal.PumpIdList; public int PumpId { get; private set; } public int SubAddress { get; } public DiscountServiceClient DiscountClient => _posApp.DiscountServiceClient; #endregion #region Timer private void InitializeTimer() { timer = new Timer(); timer.Interval = interval * 1000; timer.Elapsed += Timer_Elapsed; timer.Start(); } private void Timer_Elapsed(object sender, ElapsedEventArgs e) { logger.Error($"*** Terminal {PumpId}, no incoming command from terminal/pump for {interval} seconds! Probably communication lost."); _pumpState = HengshanPumpState.Offline; if (_pumpState != _lastPumpState) { _terminal.UpdatePumpState(PumpId, 1, LogicalDeviceState.FDC_OFFLINE); _lastPumpState = _pumpState; } // When pump is offline, it's offline. if (_lastPumpState == HengshanPumpState.Offline) { timer.Stop(); } } private void ResetTimer() { timer.Stop(); logger.Debug($"terminal {PumpId.ToString()} timer stopped"); timer.Start(); logger.Debug($"terminal {PumpId.ToString()} timer started"); } #endregion #region Methods #region Register 油机上电注册 public void HandleRegiser(RegisterRequest request) { logger.Info($"Terminal registration from pump {PumpId}, Terminal Id: {request.TerminalId}"); CheckTerminalId(request.TerminalId); var registerResults = _dbManager.RegisterTerminal(request.TerminalId); RegisterTerminalResult currentResult = null; if (registerResults.Count > 0) { currentResult = registerResults[0]; } SendRegisterResult(request, currentResult); } private void SendRegisterResult(RegisterRequest request, RegisterTerminalResult registerResult) { if (registerResult == null) { } RegisterResult result = new RegisterResult { Prefix = STX, SourceAddress = request.SourceAddress, DestinationAddress = request.DestinationAddress, FrameSqNoByte = request.FrameSqNoByte, Handle = Convert.ToByte(Command.RegisterResult), Result = Convert.ToByte(registerResult.RegisterResultCode), TerminalId = request.TerminalId, SystemKey = registerResult.SystemKey }; if (!string.IsNullOrEmpty(registerResult.StationName)) { result.StationNameLength = Convert.ToInt32(registerResult.StationNameLength); result.StationName = Encoding.GetEncoding("GB2312").GetBytes(registerResult.StationName).ToList(); } _terminal.Write(result); } #endregion #region REGURLAR CHECK 油机普通查询命令处理 /// /// Handles the Check_cmd request. /// 处理油机普通查询命令。 /// /// The Check_cmd request. public void HandleCheckCmdRequest(CheckCmdRequest request) { CheckTerminalId(request.TerminalId); ResetTimer(); if (request.WorkMode != (byte)currentWorkMode) { currentWorkMode = (WorkMode)_dbManager.GetPumpWorkMode(Convert.ToByte(PumpId)); if (request.WorkMode != 0x05) { ChangeAuthMode(request, currentWorkMode); } if (IsMessagePendingForSend()) { var message = PickAndSendCardMessage(); logger.Info($"Active send queue, pending message exists, send it, MessageHandle={message.Handle}"); message.FrameSqNoByte = request.FrameSqNoByte; _terminal.Write(message); return; } } //Reset the `Versioned listed cards` VersionedListedCard = null; logger.Debug($"Pump {PumpId}, Amount: {request.Amount}, Volume: {request.Volume}, Price: {request.Price}"); _terminal.StoreLatestFrameSqNo(PumpId, request.FrameSqNoByte); if (_terminal.TrySendNextMessage()) { //Really sent something, return! logger.Debug("Will not send the CheckCmdResponse"); return; } var versions = _dbManager.GetDataVersions(); if (currentDataVersions == null) { currentDataVersions = versions; } if (currentDataVersions != null && versions != null) { var storedGeneralInfo = versions.FirstOrDefault(v => v.VersionType == VersionType.GeneralInfoVersion); var currentGeneralInfo = currentDataVersions.FirstOrDefault(v => v.VersionType == VersionType.GeneralInfoVersion); if (storedGeneralInfo != null && currentGeneralInfo != null) { if (currentGeneralInfo.VersionNo != storedGeneralInfo.VersionNo) { byte currentMode = _dbManager.GetPumpWorkMode(Convert.ToByte(PumpId)); if (currentMode != (byte)currentWorkMode) { ChangeAuthMode(request, (WorkMode)currentMode); logger.Warn($"Auth mode changed!, {currentWorkMode} -> {currentMode}"); } if (IsMessagePendingForSend()) { var message = PickAndSendCardMessage(); logger.Info($"Active send queue, pending message exists, send it, MessageHandle={message.Handle}"); message.FrameSqNoByte = request.FrameSqNoByte; _terminal.Write(message); return; } } } } SendCheckCmdResult(versions, request); var currentNozzleNo = request.FuelingPoint.NozzleNo; int currentLogicId; bool getLogicIdResult = _terminal.NozzleLogicIdDict.TryGetValue(currentNozzleNo, out currentLogicId); if (getLogicIdResult == false) currentLogicId = 1; //Get dispenser station from check cmd if (request.DispenserState == 0x08) { _pumpState = HengshanPumpState.Fueling; if (request.WorkMode == 0x05 && _lastPumpState != _pumpState) { EmulatePumpStateSequentialChange(); } if (_lastPumpState != _pumpState) { logger.Info($"Pump {PumpId} aleady in fuelling, but last state was not, trigger a state change"); _terminal.UpdatePumpState(PumpId, currentLogicId, LogicalDeviceState.FDC_FUELLING); } _lastPumpState = _pumpState; } else if (request.DispenserState == 0x03) { _pumpState = HengshanPumpState.Idle; if (_lastPumpState == HengshanPumpState.Fueling && _lastPumpState != _pumpState) { logger.Info($"Pump {PumpId}, Nozzle {currentNozzleNo}, fueling => ready"); _terminal.UpdatePumpState(PumpId, currentLogicId, LogicalDeviceState.FDC_READY); } else if (_lastPumpState == HengshanPumpState.Offline && _pumpState == HengshanPumpState.Idle) { logger.Info($"Pump {PumpId}, Nozzle {currentNozzleNo}, offline => ready"); _terminal.UpdatePumpState(PumpId, currentLogicId, LogicalDeviceState.FDC_READY); } } if (_pumpState == HengshanPumpState.Idle) { //If the pump is idle, it's idle. _lastPumpState = _pumpState; } else if (_pumpState == HengshanPumpState.Offline) { _pumpState = HengshanPumpState.Idle; _terminal.UpdatePumpState(PumpId, currentLogicId, LogicalDeviceState.FDC_READY); _lastPumpState = _pumpState; } else if (_pumpState == HengshanPumpState.Authorized) { if (request.Amount >= 0 || request.Volume >= 0) { logger.Info($"Pump {PumpId}, Nozzle {currentNozzleNo}, authorized => fuelling"); _pumpState = HengshanPumpState.Fueling; _terminal.UpdatePumpState(currentNozzleNo, currentLogicId, LogicalDeviceState.FDC_FUELLING); _lastPumpState = _pumpState; } } else if (_pumpState == HengshanPumpState.Fueling) { logger.Info($"Pump {PumpId}, Nozzle {currentNozzleNo}, fueling in progress, Amount: {request.Amount}, Volume: {request.Volume}, Price: {request.Price}"); _terminal.UpdateFuelingStatus(PumpId, new FdcTransaction { Nozzle = new LogicalNozzle(PumpId, 1, Convert.ToByte(currentLogicId), null), Amount = request.Amount, Volumn = request.Volume, Price = request.Price, Finished = false }); _lastPumpState = _pumpState; } } private void EmulatePumpStateSequentialChange() { logger.Info("Emulating pump state sequential changes..."); _terminal.UpdatePumpState(PumpId, 1, LogicalDeviceState.FDC_CALLING); System.Threading.Thread.Sleep(50); _terminal.UpdatePumpState(PumpId, 1, LogicalDeviceState.FDC_AUTHORISED); System.Threading.Thread.Sleep(50); _terminal.UpdatePumpState(PumpId, 1, LogicalDeviceState.FDC_STARTED); System.Threading.Thread.Sleep(50); _terminal.UpdatePumpState(PumpId, 1, LogicalDeviceState.FDC_FUELLING); System.Threading.Thread.Sleep(50); } /// /// Sends result of the Check_cmd. /// 返回油机普通查询命令结果。 /// private void SendCheckCmdResult(IList dataVersions, CheckCmdRequest request) { if(dataVersions == null) { logger.Error("Retrieved empty version information"); return; } InquiryResult result = new InquiryResult { Prefix = STX, SourceAddress = request.SourceAddress, DestinationAddress = request.DestinationAddress, FrameSqNoByte = request.FrameSqNoByte, Handle = Convert.ToByte(Command.Ver_info), BaseBlacklistVersion = dataVersions.First(d => d.VersionType == VersionType.BaseBlacklistVersion).VersionNo, NewlyAddedBlacklistVersion = dataVersions.First(d => d.VersionType == VersionType.NewlyAddedBlacklistVersion).VersionNo, NewlyDeletedBlacklistVersion = dataVersions.First(d => d.VersionType == VersionType.NewlyDeletedBlacklistVersion).VersionNo, WhitelistVersion = dataVersions.First(d => d.VersionType == VersionType.WhitelistVersion).VersionNo, FuelPriceVersion = dataVersions.First(d => d.VersionType == VersionType.FuelPriceChangeVersion).VersionNo, StationGeneralInfoVersion = dataVersions.First(d => d.VersionType == VersionType.GeneralInfoVersion).VersionNo, PrinterReceiptInfoVersion = 0x00, SoftwareDownloadFlag = 0x00, UnspecifiedField1 = 0x00, }; result.SetTime(DateTime.Now); _terminal.Write(result); } #endregion #region Validate card 验卡 public void HandleCardValidation(ValidateCardRequest request) { ResetTimer(); var results = _dbManager.GetCheckCardResult(GetCardNo(request.Asn), long.Parse(request.PhysicalCardNo, System.Globalization.NumberStyles.HexNumber), request.FuelingPoint.PumpNo); var blacklistedCard = _dbManager.IsCardBlackListed(GetCardNo(request.Asn)); if (results!= null && results.Count == 1) { var currentResult = results[0]; var cardInfo = _dbManager.GetCardInfoByCardNo(GetCardNo(request.Asn)); if (cardInfo != null) { byte cardClass = cardInfo.CardClass.Value; byte cardType = cardInfo.CardType.Value; SendValidateCardResult(currentResult, request, blacklistedCard, cardClass, cardType, cardInfo.Holder, cardInfo.Carno); } else { SendValidateCardResult(currentResult, request, blacklistedCard, 0, 0, string.Empty, string.Empty); } } } private void SendValidateCardResult(CardResult checkResult, ValidateCardRequest request, bool blacklistedCard, byte cardClass, byte cardType, string holder, string carNo) { var cardState = checkResult.CardStatus; if (blacklistedCard) cardState = SpsDbManager.ResultSet.CardState.AccountFrosen; ValidateCardResult result = new ValidateCardResult(); result.Prefix = STX; result.SourceAddress = request.SourceAddress; result.DestinationAddress = request.DestinationAddress; result.Handle = Convert.ToByte(Command.ValidateCardResult); result.FrameSqNoByte = request.FrameSqNoByte; result.Asn = request.Asn; result.DiscountNo = Convert.ToUInt16(checkResult.DiscountNo); result.CardState = (HengshanPayTerminal.Support.CardState)Convert.ToByte(cardState); result.AdditionalInfo = GenerateAdditionalInfo(cardState); result.AdditionalInfoLength = Convert.ToByte(result.AdditionalInfo.Count); if (result.AdditionalInfoLength == 0) { List addInfo = new List(); if (cardClass == 1 || cardClass == 2) { addInfo.Add(1); if (!string.IsNullOrEmpty(carNo)) addInfo.AddRange(Encoding.GetEncoding("GB2312").GetBytes(carNo)); else addInfo.AddRange(Encoding.GetEncoding("GB2312").GetBytes(holder)); } if (_cardAppType == CardAppType.CpuCard) { if (cardClass == 3 && cardType == 1) { addInfo.Add(1); addInfo.AddRange(Encoding.GetEncoding("GB2312").GetBytes(holder)); } } else if (_cardAppType == CardAppType.RfCard) { if (cardClass == 3 && cardType == 3) { addInfo.Add(1); addInfo.AddRange(Encoding.GetEncoding("GB2312").GetBytes(holder)); } } result.AdditionalInfo = addInfo; result.AdditionalInfoLength = Convert.ToByte(addInfo.Count); } int reserveAmount = reservedBalance * 100; result.CardBalance = request.Balance - reserveAmount; int maxAllowedAmount = Convert.ToInt32(checkResult.MaxAllowedAmount); result.MaxAllowedAmount = request.Balance - reserveAmount > maxAllowedAmount ? maxAllowedAmount : request.Balance - reserveAmount; result.PhysicalCardNo = long.Parse( ByteArrayToString( StringToByteArray(request.PhysicalCardNo).Reverse().ToArray()), System.Globalization.NumberStyles.HexNumber); _terminal.Write(result); } private List GenerateAdditionalInfo(SpsDbManager.ResultSet.CardState cardState) { if (cardState == SpsDbManager.ResultSet.CardState.NoInfo) { return Encoding.GetEncoding("GB2312").GetBytes("验卡失败,无此卡").ToList(); } else if (cardState == SpsDbManager.ResultSet.CardState.Lost) { return Encoding.GetEncoding("GB2312").GetBytes("卡已锁定!").ToList(); } return new List(); } #endregion #region Authorization 授权处理 public void HandlAuthorization(AuthRequest request) { ResetTimer(); var currentNozzleNo = request.FuelingPoint.NozzleNo; int logicId; bool getResult = _terminal.NozzleLogicIdDict.TryGetValue(currentNozzleNo, out logicId); if (!getResult) logicId = 1; //var logicId = _terminal.NozzleLogicIdDict[currentNozzleNo]; _pumpState = HengshanPumpState.Calling; _terminal.UpdatePumpState(PumpId, logicId, LogicalDeviceState.FDC_CALLING); _lastPumpState = _pumpState; SendAuthResult(request); _pumpState = HengshanPumpState.Authorized; _terminal.UpdatePumpState(PumpId, logicId, LogicalDeviceState.FDC_AUTHORISED); _lastPumpState = _pumpState; } private void SendAuthResult(AuthRequest request) { var result = new AuthResult { Prefix = STX, SourceAddress = request.SourceAddress, DestinationAddress = request.DestinationAddress, FrameSqNoByte = request.FrameSqNoByte, Handle = Convert.ToByte(Command.AuthResult), Asn = request.Asn, PosTtc = request.PosTtc, SeqNo = request.SeqNo, FuelCode = request.FPCode.ToUInt16(), ResultCode = HengshanPayTerminal.Support.AuthResultCode.Passed, AdditionalInfoLength = 0 }; _terminal.Write(result); } #endregion #region Fueling Data 加油数据 public void HandleFuelingData(FuelingDataRequest request) { ResetTimer(); var currentNozzleNo = request.FuelingPoint.NozzleNo; int logicId; bool getResult = _terminal.NozzleLogicIdDict.TryGetValue(currentNozzleNo, out logicId); if (!getResult) logicId = 1; //var logicId = _terminal.NozzleLogicIdDict[currentNozzleNo]; var cardNo = GetCardNo(request.Asn); var cardAccount = _dbManager.GetCardAccountInfo(cardNo); var correctFuelPrice = GetCorrectFuelPrice(Convert.ToString(request.ProductCode), request.Price, request.Volume, request.Amount); if (request.CurrentCardInfo == null) logger.Error("Card info is null!!!"); if (cardAccount == null) { logger.Error("card account is null"); _terminal.UpdateFuelingStatus(PumpId, new FdcTransaction { Nozzle = new LogicalNozzle(PumpId, 1, Convert.ToByte(logicId), null), Amount = request.Amount, Volumn = request.Volume, Price = correctFuelPrice,//request.Price, VolumeTotalizer = request.VolumeTotal, SequenceNumberGeneratedOnPhysicalPump = request.PosTtc//, //I would like to use SeqNo, but TQC pumps always use 0 for it. //Finished = true //Set FDC transaction as FINISHED }); } currentFilling = new FillingInfo { PumpId = this.PumpId, NozzleId = request.FuelingPoint.NozzleNo,//1, SequenceNo = request.PosTtc, CardNo = cardNo, CardType = request.CurrentCardInfo.CardType, CardBalance = cardAccount == null ? 0 : Convert.ToInt32(cardAccount.Money), FillingAmount = request.Amount, Volume = request.Volume, UnitPrice = correctFuelPrice, //UnitPrice = request.Price, FuelProductCode = request.ProductCode, StartTime =GetDateTime(request.DispenserTime), EndTime = GetDateTime(request.TransactionEndTime) }; if (request.CurrentCardInfo.CardType != 0) { bool result = _dbManager.InsertAuthInfo(_posApp.PosId, 0, cardNo, request.Amount, request.Volume, request.DispenserTime, request.TransactionEndTime, request.PosTtc, request.SeqNo, 0x50, request.ProductCode, correctFuelPrice, request.CurrentCardInfo.CardCtc, 00, Convert.ToByte(PumpId), Convert.ToByte(PumpId), Convert.ToInt32(cardAccount.Money), request.CurrentCardInfo.CardType, cardAccount.DiscountNo, 1, request.VolumeTotal); logger.Debug($"Insert auth info, success? {result}"); } SendFuelingDataAck(request); logger.Info($"Filling done, Amount: {request.Amount}, Volume: {request.Volume}, " + $"corrected price: {correctFuelPrice}, pump price: {request.Price}, ready for payment"); } private void SendFuelingDataAck(FuelingDataRequest request) { var result = new FuelingDataProcessingResult { Prefix = STX, SourceAddress = request.SourceAddress, DestinationAddress = request.DestinationAddress, FrameSqNoByte = request.FrameSqNoByte, Handle = Convert.ToByte(Command.FuelingDataResult), PosTtc = request.PosTtc, SeqNo = request.SeqNo, FPCode = request.FPCode.ToUInt16() }; _terminal.Write(result); } #endregion #region Payment 支付 private void HandleCardPayment() { while (true) { logger.Info($"Terminal {PumpId}, in the loop"); if (paymentRequestQueue.Count > 0) { logger.Info($"Terminal {PumpId}, Payment request exists, send it! Queue item count: {paymentRequestQueue.Count}"); PaymentRequest request; if (paymentRequestQueue.TryDequeue(out request)) { SendPaymentData(request); } logger.Info($"Terminal {PumpId}, Payment request queue, item count: {paymentRequestQueue.Count}"); } else { logger.Info($"Terminal {PumpId}, nothing in payment queue, put thread on hold"); mre.Reset(); mre.WaitOne(); } } } public void HandlePaymentRequest(PaymentRequest request) { ResetTimer(); if (paymentRequestQueue.Count > 0) { if (paymentRequestQueue.Any(r => r.Asn == request.Asn && r.PosTtc == request.PosTtc)) { logger.Info("Payment request already in the queue"); return; } } else { paymentRequestQueue.Enqueue(request); } logger.Info($"Terminal {PumpId}, releasing payment request processing thread"); mre.Set(); //SendPaymentData(request); } private void SendPaymentData(PaymentRequest request) { int paymentAmount = 0; int fillingAmount = 0; int fillingPrice = 0; int productCode = 0; int volume = 0; if (DiscountClient != null && currentFilling != null && (_posApp.CardAppType == 1 && currentFilling.CardType == 3 || _posApp.CardAppType == 2 && currentFilling.CardType == 1)) { fillingAmount = currentFilling.FillingAmount; fillingPrice = currentFilling.UnitPrice; productCode = currentFilling.FuelProductCode; volume = currentFilling.Volume; logger.Info($"Applying fuel discount against customer card, amount: {fillingAmount}, price: {fillingPrice}, code: {productCode}"); DiscountRequest discountRequest = new DiscountRequest(); discountRequest.Barcode = _posApp.GetBarcode(currentFilling.FuelProductCode).ToString(); discountRequest.FillingAmount = Convert.ToDecimal(currentFilling.FillingAmount) / 100; discountRequest.UnitPrice = currentFilling.UnitPrice == 151 ? Convert.ToDecimal( _posApp.CurrentFuelPrices[currentFilling.FuelProductCode.ToString()]) / 100 : Convert.ToDecimal(currentFilling.UnitPrice) / 100; discountRequest.Volume = Convert.ToDecimal(currentFilling.Volume) / 100; discountRequest.TimeStamp = DateTime.Now; discountRequest.CardNo = GetCardNo(request.Asn); var discountResponse = DiscountClient.CalculatDiscount(discountRequest).Result; if (discountResponse != null) { paymentAmount = Convert.ToInt32(discountResponse.PayAmount * 100); } } else { logger.Info($"Possible crash 1"); //var amountAfterDiscount = _dbManager.GetDiscountedAmount(request.Asn, fillingAmount, volume, fillingPrice, productCode); if (currentFilling != null) paymentAmount = currentFilling.FillingAmount;//amountAfterDiscount.Real_Mon; } if (currentFilling == null) { logger.Info($"Current Filling missing, could be reset"); return; } if (paymentAmount > currentFilling.FillingAmount) { logger.Warn($"This is absurd, payment amount {paymentAmount.ToString()} larger than filling amount"); paymentAmount = fillingAmount; } logger.Info($"payment amount is: {paymentAmount}"); PaymentData data = new PaymentData { Prefix = STX, SourceAddress = request.SourceAddress, DestinationAddress = request.DestinationAddress, FrameSqNoByte = request.FrameSqNoByte, Handle = Convert.ToByte(Command.PaymentData), Asn = request.Asn, PosTtc = request.PosTtc, FillingAmount = fillingAmount, PayAmount = paymentAmount == 0? fillingAmount : paymentAmount, FillingVolume = volume, Price = Convert.ToUInt16(fillingPrice), ProductCode = Convert.ToUInt16(productCode), C_Name = "00000000000000000000000000000000", FPCode = request.FPCode.ToUInt16(), Result = HengshanPayTerminal.Support.PaymentProcessingResult.Ok, AdditionalInfoLength = 0 }; _terminal.Write(data); mre.WaitOne(); } #endregion #region Transaction Data 交易数据 public async Task HandleTransactionData(TransactionDataRequest request) { ResetTimer(); var currentNozzleNo = request.FuelingPoint.NozzleNo; int logicId; bool getResult = _terminal.NozzleLogicIdDict.TryGetValue(currentNozzleNo, out logicId); if (!getResult) logicId = 1; //var logicId = _terminal.NozzleLogicIdDict[currentNozzleNo]; logger.Info($"*** Handle TransactionData {request.Handle}, Pump {PumpId} ***"); byte trxType = request.TrxType; var cardNo = GetCardNo(request.Asn); var cardAccount = _dbManager.GetCardAccountInfo(cardNo); if (cardAccount == null) { logger.Info($"Terminal {PumpId}, card account queried for card no: {cardNo}"); } var creditResult = _dbManager.GetCredits(request.ProductCode, cardNo, request.FillingAmount, request.Volume); logger.Debug("Credit queried"); bool trxExists = _dbManager.CheckIfTransactionExists(request.TrxTime, request.DitTTC, request.FuelingPoint.PumpNo, trxType); logger.Debug($"Transaction Existence checked, Exists? {trxExists}"); var operatorCardNo = GetCardNo(request.PsamAsn); bool ackSent = false; var correctedFuelPrice = GetCorrectFuelPrice(Convert.ToString(request.ProductCode), request.Price, request.Volume, request.FillingAmount); //Make sure there is not an existing payment trx record. if (!trxExists) { logger.Info("No existing record"); int payModeId = request.PaymentMethodLocation; //100; string payModeNo = string.Empty; int stationNo = 1; if (_posApp.StationInfo != null) stationNo = _posApp.StationInfo.StationNo; uint volumeTotal = request.VolumeTotal < 0 ? 0 : Convert.ToUInt32(request.VolumeTotal); var newGid = _dbManager.AddTrade( stationNo, cardNo, operatorCardNo, payModeId, payModeNo, trxType, Convert.ToString(request.ProductCode), correctedFuelPrice,//request.Price, request.Volume, request.FillingAmount, request.PaymentAmount, request.CardBalance, request.CurrentCardInfo.CardType, request.CurrentCardInfo.CardCtc, GetDateTime(request.TrxTime), GetDateTime(request.TrxEndTime), request.DitTTC, request.SeqNo, _posApp.GetNextBillNo(), //billNo request.FuelingPoint.NozzleNo, request.FuelingPoint.PumpNo, Convert.ToInt32(request.TerminalId.TrimStart('0')), volumeTotal, 0, //discountNo 1001, 1, request.PsamAsn, //PsamAsn uint.Parse(request.PsamTac, System.Globalization.NumberStyles.HexNumber),//PsamTac request.PsamTid,//psam tid uint.Parse(request.PsamTtc, System.Globalization.NumberStyles.HexNumber),//psam ttc uint.Parse(request.Tac, System.Globalization.NumberStyles.HexNumber),//tac uint.Parse(request.Gmac, System.Globalization.NumberStyles.HexNumber),//gmac uint.Parse(request.Tmac, System.Globalization.NumberStyles.HexNumber),//tmac 0, //Integral Convert.ToInt32(_posApp.CurrentShiftNo), "000000", "000000", "", 0); logger.Info($"Trdinfo added, new Gid: {newGid}, TrdType: {trxType}, CardNo: {request.Asn}, CTC: {request.CurrentCardInfo.CardCtc}"); if (trxType == 0x01) { _dbManager.InsertGrayInfo( cardNo, payModeId, trxType, Convert.ToString(request.ProductCode), correctedFuelPrice,//request.Price request.Volume, request.FillingAmount, request.PaymentAmount, request.CardBalance, request.CurrentCardInfo.CardCtc, request.TrxTime, request.TrxEndTime, Convert.ToUInt32(request.DitTTC), request.SeqNo, request.FuelingPoint.NozzleNo, request.FuelingPoint.PumpNo, request.TerminalId, Convert.ToUInt64(request.VolumeTotal), 0, request.PsamAsn, uint.Parse(request.PsamTac, System.Globalization.NumberStyles.HexNumber),//PsamTac//request.PsamTac, request.PsamTid, uint.Parse(request.PsamTtc, System.Globalization.NumberStyles.HexNumber),//psam ttc uint.Parse(request.Tac, System.Globalization.NumberStyles.HexNumber),//tac uint.Parse(request.Gmac, System.Globalization.NumberStyles.HexNumber),//gmac uint.Parse(request.Tmac, System.Globalization.NumberStyles.HexNumber),//tmac 0); logger.Info("Gray info inserted"); } else if (trxType == 0x02) { var releaseResult = _dbManager.ReleaseGrayCard(cardNo, request.CurrentCardInfo.CardCtc, request.TrxTime); logger.Info($"Gray info deleted, card released success? {releaseResult}"); } _dbManager.UpdateCardInfo(cardNo, request.FillingAmount, request.CardBalance, request.CurrentCardInfo.CardType, creditResult.IntegralResult, 1); logger.Debug("Card Info updated"); _dbManager.DeleteAuthInfo(request.FuelingPoint.PumpNo, request.DitTTC, request.TrxTime); logger.Debug("Auth Info deleted"); if (trxType == 2) { logger.Info($"Trying to find the gray trade, ASN: {request.Asn}, CTC: {request.CurrentCardInfo.CardCtc}"); var grayTrd = _dbManager.GetGrayTrdInfo(request.Asn, request.CurrentCardInfo.CardCtc, request.FuelingPoint.PumpNo); if (grayTrd != null) { logger.Info($"Found gray trade for card: {request.Asn}"); currentFilling = new FillingInfo { PumpId = this.PumpId, NozzleId = request.FuelingPoint.NozzleNo,//1, SequenceNo = request.DitTTC, CardNo = cardNo, CardBalance = request.CardBalance,//Convert.ToInt32(cardAccount.Money), FillingAmount = grayTrd.Mon.HasValue ? grayTrd.Mon.Value : request.FillingAmount, PayAmount = request.PaymentAmount, Volume = request.Volume, UnitPrice = correctedFuelPrice,//request.Price, FuelProductCode = int.Parse(grayTrd.CommId), //request.ProductCode, StartTime = GetDateTime(request.TrxTime), EndTime = GetDateTime(request.TrxEndTime), VolumeTotal = Convert.ToInt32(grayTrd.EndPumpId)//request.VolumeTotal }; } else { currentFilling = new FillingInfo { PumpId = this.PumpId, NozzleId = request.FuelingPoint.NozzleNo,//1, SequenceNo = request.DitTTC, CardNo = cardNo, CardBalance = request.CardBalance,//Convert.ToInt32(cardAccount.Money), FillingAmount = request.FillingAmount, PayAmount = request.PaymentAmount, Volume = request.Volume, UnitPrice = correctedFuelPrice,//request.Price, FuelProductCode = request.ProductCode, StartTime = GetDateTime(request.TrxTime), EndTime = GetDateTime(request.TrxEndTime), VolumeTotal = request.VolumeTotal }; } } else { currentFilling = new FillingInfo { PumpId = this.PumpId, NozzleId = request.FuelingPoint.NozzleNo,//1, SequenceNo = request.DitTTC, CardNo = cardNo, CardBalance = request.CardBalance,//Convert.ToInt32(cardAccount.Money), FillingAmount = request.FillingAmount, PayAmount = request.PaymentAmount, Volume = request.Volume, UnitPrice = correctedFuelPrice,//request.Price, FuelProductCode = request.ProductCode, StartTime = GetDateTime(request.TrxTime), EndTime = GetDateTime(request.TrxEndTime), VolumeTotal = request.VolumeTotal }; } //Customer card, submit it. if ((_posApp.CardAppType == 1 && request.CurrentCardInfo.CardType == 3 || _posApp.CardAppType == 2 && request.CurrentCardInfo.CardType == 1) && request.TrxType != 2) { logger.Info($"Pump {PumpId}, there is a customer card transaction pending for upload, ttc: {currentFilling.SequenceNo}"); if (request.FillingAmount != 0) _posApp.AddCustomerCardFilling(currentFilling); } if (request.FillingAmount != 0 && request.TrxType != 2) { if (_posApp != null) { //2023-08-22 SendTrasactionDataAck(request); ackSent = true; logger.Info("TransactionData ack sent"); //Should first upload the record to cloud... logger.Info("Start force upload"); //_ = _posApp?.SpsDataCourier.TriggerTransacationLookup(newGid); await _posApp?.SpsDataCourier.ForceTransactionUploadAsync(newGid).ContinueWith(t => { logger.Info("Push transaction to FDC Server"); _terminal.UpdateFuelingStatus(PumpId, new FdcTransaction { Nozzle = new LogicalNozzle(PumpId, 1, Convert.ToByte(logicId), null), Amount = request.FillingAmount, Volumn = request.Volume, Price = correctedFuelPrice,//request.Price, VolumeTotalizer = request.VolumeTotal, SequenceNumberGeneratedOnPhysicalPump = request.DitTTC, //I would like to use SeqNo, but TQC pumps always use 0 for it. Finished = true //Set FDC transaction as FINISHED }); _pumpState = HengshanPumpState.Idle; _terminal.UpdatePumpState(PumpId, logicId, LogicalDeviceState.FDC_READY); _lastPumpState = _pumpState; }); } //logger.Info("Push transaction to FDC Server"); //_terminal.UpdateFuelingStatus(PumpId, new FdcTransaction //{ // Nozzle = new LogicalNozzle(PumpId, 1, 1, null), // Amount = request.FillingAmount, // Volumn = request.Volume, // Price = request.Price, // VolumeTotalizer = request.VolumeTotal, // SequenceNumberGeneratedOnPhysicalPump = request.DitTTC, //I would like to use SeqNo, but TQC pumps always use 0 for it. // Finished = true //Set FDC transaction as FINISHED //}); } //_pumpState = HengshanPumpState.Idle; //_terminal.UpdatePumpState(PumpId, LogicalDeviceState.FDC_READY); //_lastPumpState = _pumpState; } if (!ackSent) { logger.Info($"Pump {PumpId}, TransactionData ack not sent, send it now"); SendTrasactionDataAck(request); } if (currentFilling != null) { lastFilling = new FillingInfo { FillingAmount = currentFilling.FillingAmount, Volume = currentFilling.Volume, UnitPrice = currentFilling.UnitPrice, FuelProductCode = currentFilling.FuelProductCode }; } else { logger.Info($"Pump {PumpId}, currentFilling is null"); } if (currentFilling != null) { //重置当前的Filling if (currentFilling.SequenceNo == request.DitTTC) { logger.Info($"Pump {PumpId}, reset current filling, ttc: {currentFilling.SequenceNo}"); currentFilling = null; } } } private void CheckTransactionType(byte trxType) { if (trxType == 0x00) { logger.Info("Normal card transaction"); } else if (trxType == 0x01) { logger.Info("Gray card transaction"); } else if (trxType == 0x02) { logger.Info("Ungray card transaction"); } else if (trxType == 0x03) { logger.Info("Indoor card payment transaction"); } else if (trxType == 0x04) { logger.Info("POS auth transaction (cash)"); } else if (trxType == 0x05) { logger.Info("Cancel auth card transaction"); } else if (trxType == 0x06) { logger.Info("Non card transaction"); } else if (trxType == 0x07) { logger.Info("Cancel auth non card transaction"); } else if (trxType == 0x08) { logger.Info("Fuel price download transaction"); } else if (trxType == 0x09) { logger.Info("Other card release transaction"); } else if (trxType == 0x0A) { logger.Info("Change fuel type transaction"); } else if (trxType == 0x0B) { logger.Info("POS bank card transaction"); } else { logger.Info("Unknown transaction type"); } } private void SendTrasactionDataAck(TransactionDataRequest request) { TransactionDataAck ack = new TransactionDataAck { Prefix = STX, SourceAddress = request.SourceAddress, DestinationAddress = request.DestinationAddress, FrameSqNoByte = request.FrameSqNoByte, Handle = Convert.ToByte(Command.TransactionDataAck), TerminalId = request.TerminalId, Result = 0, UnspecifiedField1 = 0 }; _terminal.Write(ack); } private ushort GetCorrectFuelPrice(string fuelNo, int price, int volume, int amount) { try { var recordedPrice = Convert.ToInt32(_posApp.CurrentFuelPrices[fuelNo]); if (recordedPrice != price) { logger.Info($"Price: {price} on pump: {PumpId}, is different from system recorded price: {recordedPrice}"); int calculatedAmount = price * volume; int candidateAmount = recordedPrice * volume; double calculatedDiff = Math.Abs(calculatedAmount - amount * 100); double candidateDiff = Math.Abs(candidateAmount - amount * 100); if (candidateDiff < calculatedDiff) { logger.Info($"Returning system recorded price: {recordedPrice}"); return Convert.ToUInt16(recordedPrice); } return Convert.ToUInt16(price); } } catch (Exception ex) { logger.Error("Error in getting correct fuel price ", ex.ToString()); } return Convert.ToUInt16(price); } #endregion #region Lock/Unlock Pump 锁定/解锁油机 public void LockUnlockPump(LockUnlockOperation operation) { var request = new LockOrUnlockPumpRequest { Prefix = STX, Handle = Convert.ToByte(Command.LockOrUnlockPump), FPCode = EncodeFPCodeString(PumpId, PumpId), OperationType = operation }; PrepareSendMessage(request); } public void HandleLockUnlockPumpResult(LockOrUnlockPumpAck lockUnlockPumpResult) { if (lockUnlockPumpResult.DispenserState == DispenserState.Closed) { logger.Info($"Current pump {PumpId} is locked!"); //... should notify POS then. } else if (lockUnlockPumpResult.DispenserState == DispenserState.Idle) { logger.Info($"Current pump {PumpId} is idle (unlocked)!"); //... should notify POS then. } } #endregion #region Request Data 加油机请求下载数据 public void HandleDataDownloadRequest(DataDownloadRequest request) { ResetTimer(); //Reset VersionedListedCard = null; if (request.DataContentType == DataContentType.FuelPriceList) { logger.Info($"Terminal {PumpId} initiates fuel price list download"); } else if (request.DataContentType == DataContentType.StationGeneralInfo) { logger.Info($"Terminal {PumpId} initiates station general info download"); } else if (request.DataContentType == DataContentType.Whitelist) { logger.Info($"Terminal {PumpId} initiates whitelist download"); } else if (request.DataContentType == DataContentType.NewlyAddedBlacklist) { logger.Info($"Terminal {PumpId} initiates newly added blacklist download"); } else if (request.DataContentType == DataContentType.NewlyDeletedBlacklist) { logger.Info($"Terminal {PumpId} initiates newly deleted blacklist download"); } else if (request.DataContentType == DataContentType.BaseBlacklist) { logger.Info($"Terminal {PumpId} initiates base blacklist download"); } else if (request.DataContentType == DataContentType.StationGeneralInfo) { logger.Info($"Terminal {PumpId} initiates station general info download"); } SendDataLength(request); } public void SendDataLength(DataDownloadRequest request) { DataBytesLength response = new DataBytesLength(); response.Prefix = STX; response.SourceAddress = request.SourceAddress; response.DestinationAddress = request.DestinationAddress; response.Handle = (byte)Command.DataBytesLength; response.FrameSqNoByte = request.FrameSqNoByte; if (request.DataContentType == DataContentType.FuelPriceList) { response.DataContentType = request.DataContentType; response.DataLength = 0x32; logger.Info($"Fuel Price Update, Data Length: {response.DataLength}"); } else if (request.DataContentType == DataContentType.Whitelist) { response.DataContentType = DataContentType.Whitelist; var result = _dbManager.GetWhitelistedCards(_cardAppType); if (result != null) { response.DataLength = 16 + 10 * result.Count; logger.Info($"WhiteList, Data Length: {response.DataLength}"); } } else if (request.DataContentType == DataContentType.NewlyAddedBlacklist) { response.DataContentType = DataContentType.NewlyAddedBlacklist; var result = _dbManager.GetNewlyAddedBlacklistedCards(_cardAppType); if (result != null) { response.DataLength = 16 + 10 * result.Count; logger.Info($"Incremental Black List, Data Length: {response.DataLength}"); } } else if (request.DataContentType == DataContentType.NewlyDeletedBlacklist) { response.DataContentType = DataContentType.NewlyDeletedBlacklist; var result = _dbManager.GetNewlyDeletedBlacklistedCards(_cardAppType); if (result != null) { response.DataLength = 16 + 10 * result.Count; logger.Info($"Decremental Black List, Data Length: {response.DataLength}"); } } else if (request.DataContentType == DataContentType.BaseBlacklist) { response.DataContentType = DataContentType.BaseBlacklist; var result = _dbManager.GetBaseBlacklistedCards(_cardAppType); if (result != null) { response.DataLength = 16 + 10 * result.Count; logger.Info($"Base Black List, Data Length: {response.DataLength}"); } } else if (request.DataContentType == DataContentType.StationGeneralInfo) { response.DataContentType = DataContentType.StationGeneralInfo; var result = _dbManager.GetPumpInfo(Convert.ToByte(PumpId)); if (result != null && result.Count > 0) { response.DataLength = 6; } logger.Info($"Station General Info, Data Length: {response.DataLength}"); } _terminal.Write(response); } #endregion #region Request Data Content 加油机申请下载数据的内容 public void HandleDataContentRequest(DataContentRequest request) { ResetTimer(); if (request.DataContentType == DataContentType.FuelPriceList) { logger.Info($"Terminal {PumpId} downloads fuel price list content"); } SendDataContent(request); } public void SendDataContent(DataContentRequest request) { DataContent response = new DataContent(); response.Prefix = STX; response.SourceAddress = request.SourceAddress; response.DestinationAddress = request.DestinationAddress; response.FrameSqNoByte = request.FrameSqNoByte; response.Handle = (byte)Command.DataContent; //Fuel price, 油品油价列表 if (request.DataContentType == DataContentType.FuelPriceList) { logger.Info($"Terminal downloads fuel price, Source: {request.SourceAddress}"); response.DataContentType = DataContentType.FuelPriceList; response.SegmentOffset = request.SegmentOffset; var result = _dbManager.GetFuelPriceChangeConfig(PumpId); if (result.Count > 0) { var priceChangeRecordCount = result.Count; if (priceChangeRecordCount <= 1) { var priceChangeRecord = result.First(); FuelPriceRecord record = new FuelPriceRecord(); record.Version = Convert.ToByte(priceChangeRecord.Ver); record.EffectiveTime = priceChangeRecord.EffeTime; record.RecordCount = Convert.ToByte(priceChangeRecordCount); record.NozzleNo = Convert.ToByte(priceChangeRecord.LogicId); record.FuelProductCode = Convert.ToUInt16(priceChangeRecord.OilId); record.FuelProductName = ByteArrayToString(Encoding.GetEncoding("GB2312").GetBytes(priceChangeRecord.OilName)).PadRight(64, '0'); record.Density = priceChangeRecord.Density == "" ? 0 : Convert.ToInt32(priceChangeRecord.Density); record.ValidPriceCount = 1; record.Price1 = priceChangeRecord.Price > UInt16.MaxValue ? UInt16.MaxValue : Convert.ToUInt16(priceChangeRecord.Price); _terminal.SetRealPrice(PumpId, Convert.ToInt32(priceChangeRecord.Price)); response.Content = parser.SerializeInnerElement(record).ToList(); if (response.Content.Count % 16 == 0) { response.SegmentCount = Convert.ToByte(response.Content.Count / 16); } else { response.SegmentCount = Convert.ToByte(response.Content.Count / 16 + 1); } } else { logger.Info("There are multiple price change records, seriously? ..."); var first = result.First(); MultiFuelPriceRecord record = new MultiFuelPriceRecord(); record.Version = Convert.ToByte(first.Ver); record.EffectiveTime = first.EffeTime; record.RecordCount = Convert.ToByte(result.Count); record.FirstNozzleNo = Convert.ToByte(result[0].LogicId); record.FirstFuelProductCode = Convert.ToUInt16(result[0].OilId); record.FirstFuelProductName = ByteArrayToString(Encoding.GetEncoding("GB2312").GetBytes(result[0].OilName)).PadRight(64, '0'); record.FirstDensity = result[0].Density == "" ? 0 : Convert.ToInt32(result[0].Density); record.FirstValidPriceCount = 1; record.FirstPrice1 = Convert.ToUInt16(result[0].Price); record.SecondNozzleNo = Convert.ToByte(result[1].LogicId); record.SecondFuelProductCode = Convert.ToUInt16(result[1].OilId); record.SecondFuelProductName = ByteArrayToString(Encoding.GetEncoding("GB2312").GetBytes(result[1].OilName)).PadRight(64, '0'); record.SecondDensity = result[1].Density == "" ? 0 : Convert.ToInt32(result[1].Density); record.SecondValidPriceCount = 1; record.SecondPrice1 = Convert.ToUInt16(result[1].Price); response.Content = new List(); response.Content.AddRange(parser.SerializeInnerElement(record).ToList()); if (response.Content.Count % 16 == 0) { response.SegmentCount = Convert.ToByte(response.Content.Count / 16); } else { response.SegmentCount = Convert.ToByte(response.Content.Count / 16 + 1); } } } } //Incremental blacklist, 新增(增量)黑名单 else if (request.DataContentType == DataContentType.NewlyAddedBlacklist) { logger.Info($"Terminal downloading newly added (incremental) blacklist, Source: {request.SourceAddress}, " + $"Segment offset: {request.SegmentOffset}, Segment count: {request.SegmentCount}"); response.DataContentType = DataContentType.NewlyAddedBlacklist; response.SegmentOffset = request.SegmentOffset; response.SegmentCount = request.SegmentCount; var cards = _dbManager.GetNewlyAddedBlacklistedCards(_cardAppType); var versions = _dbManager.GetDataVersions(); if (versions.Count > 0 && cards.Count > 0) { var versionInfo = versions.First(v => v.VersionType == VersionType.NewlyAddedBlacklistVersion); ListedCardRecord listedCardRecord = null; if (versionInfo != null) { listedCardRecord = new ListedCardRecord(); listedCardRecord.Version = versionInfo.VersionNo; listedCardRecord.ValidStartDate = versionInfo.Effectivetime.ToString("yyyyMMdd"); listedCardRecord.ExpiryDate = versionInfo.ExpiredTime.ToString("yyyyMMdd"); listedCardRecord.Region = Convert.ToString(ushort.MaxValue); listedCardRecord.CardCount = cards.Count; } if (VersionedListedCard == null) { logger.Info("Formatting card list (newly added)"); VersionedListedCard = new VersionedListedCard(versionInfo.VersionNo, versionInfo.Effectivetime.ToString("yyyyMMdd"), versionInfo.ExpiredTime.ToString("yyyyMMdd"), cards.Count, cards.ToList()); } if (request.SegmentOffset == 0) { if (request.SegmentCount == 1) { logger.Info("Terminal requesting just one segment of newly added blacklist data"); response.Content = parser.SerializeInnerElement(listedCardRecord).ToList(); } else { logger.Info($"Terminal requests Newly-added blacklist, Segment offset: 0000, Segment count: {request.SegmentCount}"); var versionInfoBytes = parser.SerializeInnerElement(listedCardRecord); var cardBytes = StringToByteArray(VersionedListedCard.AllCards); if (versionInfoBytes.Length + cardBytes.Length >= 160) { var bytes = new byte[160]; Buffer.BlockCopy(versionInfoBytes, 0, bytes, 0, versionInfoBytes.Length); Buffer.BlockCopy(cardBytes, 0, bytes, versionInfoBytes.Length, 160 - versionInfoBytes.Length); response.Content = bytes.ToList(); } else { var bytes = new byte[160]; Buffer.BlockCopy(versionInfoBytes, 0, bytes, 0, versionInfoBytes.Length); Buffer.BlockCopy(cardBytes, 0, bytes, versionInfoBytes.Length, cardBytes.Length); response.Content = bytes.ToList(); } } } else { var bytes = new byte[160]; int cardsToBeSkipped = request.SegmentOffset > 10 ? request.SegmentOffset / 10 : 0; logger.Info($"Newly-added blacklist, Segment Offset: {request.SegmentOffset}, Cards to be skipped: {cardsToBeSkipped * 16}"); if (VersionedListedCard.CardCount <= 16) { var cardBytes = StringToByteArray(VersionedListedCard.AllCards); Buffer.BlockCopy(cardBytes, 0, bytes, 0, cardBytes.Length); response.Content = bytes.ToList(); } else { IEnumerable currentCards = VersionedListedCard.ListedCards.Skip(cardsToBeSkipped * 16).Take(16); StringBuilder sb = new StringBuilder(); foreach (var card in currentCards) { sb.Append(card.CardNo.PadLeft(20, '0')); logger.Info(card.CardNo); } var cardBytes = StringToByteArray(sb.ToString()); Buffer.BlockCopy(cardBytes, 0, bytes, 0, cardBytes.Length); response.Content = bytes.ToList(); } } } } //Decremental blacklist, 新删(减量)黑名单 else if (request.DataContentType == DataContentType.NewlyDeletedBlacklist) { logger.Info($"Terminal downloading newly deleted blacklist, Source: {request.SourceAddress}"); response.DataContentType = DataContentType.NewlyDeletedBlacklist; response.SegmentOffset = request.SegmentOffset; response.SegmentCount = request.SegmentCount; var cards = _dbManager.GetNewlyDeletedBlacklistedCards(_cardAppType); var versions = _dbManager.GetDataVersions(); if (versions.Count > 0 && cards.Count > 0) { var versionInfo = versions.First(v => v.VersionType == VersionType.NewlyDeletedBlacklistVersion); ListedCardRecord record = null; if (versionInfo != null) { record = new ListedCardRecord(); record.Version = versionInfo.VersionNo; record.ValidStartDate = versionInfo.Effectivetime.ToString("yyyyMMdd"); record.ExpiryDate = versionInfo.ExpiredTime.ToString("yyyyMMdd"); record.Region = Convert.ToString(ushort.MaxValue); record.CardCount = cards.Count; } if (VersionedListedCard == null) { logger.Info("Formatting card list (newly deleted)"); VersionedListedCard = new VersionedListedCard(versionInfo.VersionNo, versionInfo.Effectivetime.ToString("yyyyMMdd"), versionInfo.ExpiredTime.ToString("yyyyMMdd"), cards.Count, cards.ToList()); } if (request.SegmentOffset == 0) { if (request.SegmentCount == 1) { response.Content = parser.SerializeInnerElement(record).ToList(); } else { var versionInfoBytes = parser.SerializeInnerElement(record); var cardBytes = StringToByteArray(VersionedListedCard.AllCards); if (versionInfoBytes.Length + cardBytes.Length >= 160) { var bytes = new byte[160]; Buffer.BlockCopy(versionInfoBytes, 0, bytes, 0, versionInfoBytes.Length); Buffer.BlockCopy(cardBytes, 0, bytes, versionInfoBytes.Length, 160 - versionInfoBytes.Length); response.Content = bytes.ToList(); } else { var bytes = new byte[160]; Buffer.BlockCopy(versionInfoBytes, 0, bytes, 0, versionInfoBytes.Length); Buffer.BlockCopy(cardBytes, 0, bytes, versionInfoBytes.Length, cardBytes.Length); response.Content = bytes.ToList(); } } } else { var bytes = new byte[160]; int segmentsToBeSkipped = request.SegmentOffset > 10 ? request.SegmentOffset / 10 : 0; logger.Info($"Newly-deleted blacklist, Segment Offset: {request.SegmentOffset.ToString("X")}, " + $"Cards to be skipped: {segmentsToBeSkipped * 16}"); if (VersionedListedCard.CardCount <= 16) { var cardBytes = StringToByteArray(VersionedListedCard.AllCards); Buffer.BlockCopy(cardBytes, 0, bytes, 0, cardBytes.Length); response.Content = bytes.ToList(); } else { var currentCards = VersionedListedCard.ListedCards.Skip(segmentsToBeSkipped * 16).Take(16); StringBuilder sb = new StringBuilder(); foreach (var card in currentCards) { sb.Append(card.CardNo.PadLeft(20, '0')); logger.Info(card.CardNo); } var cardBytes = StringToByteArray(sb.ToString()); Buffer.BlockCopy(cardBytes, 0, bytes, 0, cardBytes.Length); response.Content = bytes.ToList(); } } } } //White list, 白名单数据 else if (request.DataContentType == DataContentType.Whitelist) { logger.Info($"Terminal downloads whitelist, Source: {request.SourceAddress}"); response.DataContentType = DataContentType.Whitelist; response.SegmentOffset = request.SegmentOffset; response.SegmentCount = request.SegmentCount; var cards = _dbManager.GetWhitelistedCards(_cardAppType); var versions = _dbManager.GetDataVersions(); if (versions.Count > 0 && cards.Count > 0) { logger.Info($"Whitelist, card count: {cards.Count}"); var versionInfo = versions.First(v => v.VersionType == VersionType.WhitelistVersion); ListedCardRecord record = null; if (versionInfo != null) { logger.Info($"Whitelist, version: {versionInfo.VersionNo}"); record = new ListedCardRecord(); record.Version = versionInfo.VersionNo; record.ValidStartDate = versionInfo.Effectivetime.ToString("yyyyMMdd"); record.ExpiryDate = versionInfo.ExpiredTime.ToString("yyyyMMdd"); record.Region = Convert.ToString(ushort.MaxValue); record.CardCount = cards.Count; } if (VersionedListedCard == null) { logger.Info("Formatting card list (whitelist)"); VersionedListedCard = new VersionedListedCard(versionInfo.VersionNo, versionInfo.Effectivetime.ToString("yyyyMMdd"), versionInfo.ExpiredTime.ToString("yyyyMMdd"), cards.Count, cards.ToList()); } if (request.SegmentOffset == 0) { logger.Info("Segment offset is: 0"); if (request.SegmentCount == 1) { logger.Info("Segment count is: 1"); response.Content = parser.SerializeInnerElement(record).ToList(); } else { logger.Info($"Segment count is: {request.SegmentCount}"); var versionInfoBytes = parser.SerializeInnerElement(record); var cardBytes = StringToByteArray(VersionedListedCard.AllCards); if (versionInfoBytes.Length + cardBytes.Length >= 160) { logger.Info($"Version Info serialized: {ByteArrayToString(versionInfoBytes)}, Length: {versionInfoBytes.Length}, "); logger.Info($"More than 160 bytes, Cards bytes Length: {cardBytes.Length}"); var bytes = new byte[160]; try { Buffer.BlockCopy(versionInfoBytes, 0, bytes, 0, versionInfoBytes.Length); Buffer.BlockCopy(cardBytes, 0, bytes, versionInfoBytes.Length, 160 - versionInfoBytes.Length); } catch (Exception ex) { logger.Info($"{ex.ToString()}"); } response.Content = bytes.ToList(); } else { logger.Info("Less than 160 bytes"); var bytes = new byte[160]; Buffer.BlockCopy(versionInfoBytes, 0, bytes, 0, versionInfoBytes.Length); Buffer.BlockCopy(cardBytes, 0, bytes, versionInfoBytes.Length - 1, cardBytes.Length); response.Content = bytes.ToList(); } } } else { logger.Info($"Segment offset is: {request.SegmentOffset}"); var bytes = new byte[160]; int segmentsToBeSkipped = request.SegmentOffset > 10 ? request.SegmentOffset / 10 : 0; logger.Info($"Whitelist, Segment Offset: {request.SegmentOffset.ToString("X")}, " + $"Cards to be skipped: {segmentsToBeSkipped * 16}"); if (VersionedListedCard.CardCount <= 16) { var currentCards = VersionedListedCard.ListedCards.Skip((request.SegmentOffset - 1) * 15).Take(15); StringBuilder sb = new StringBuilder(); foreach (var card in currentCards) { sb.Append(card.CardNo.PadLeft(20, '0')); logger.Info(card.CardNo); } var cardBytes = StringToByteArray(sb.ToString()); Buffer.BlockCopy(cardBytes, 0, bytes, 0, cardBytes.Length); response.Content = bytes.ToList(); } else { var currentCards = VersionedListedCard.ListedCards.Skip(segmentsToBeSkipped * 16).Take(16); StringBuilder sb = new StringBuilder(); foreach (var card in currentCards) { sb.Append(card.CardNo.PadLeft(20, '0')); logger.Info(card.CardNo); } var cardBytes = StringToByteArray(sb.ToString()); Buffer.BlockCopy(cardBytes, 0, bytes, 0, cardBytes.Length); response.Content = bytes.ToList(); //var cardBytes = StringToByteArray(VersionedListedCard.AllCards); //Buffer.BlockCopy(cardBytes, 0, bytes, 0, cardBytes.Length); //response.Content = bytes.ToList(); } } } else if (cards.Count == 0) { logger.Info("No cards for whitelist"); var versionInfo = versions.First(v => v.VersionType == VersionType.WhitelistVersion); ListedCardRecord record = null; if (versionInfo != null) { record = new ListedCardRecord(); record.Version = versionInfo.VersionNo; record.ValidStartDate = versionInfo.Effectivetime.ToString("yyyyMMdd"); record.ExpiryDate = versionInfo.ExpiredTime.ToString("yyyyMMdd"); record.Region = Convert.ToString(ushort.MaxValue); record.CardCount = 0; } if (VersionedListedCard == null) { logger.Info("Formatting card list (whitelist)"); VersionedListedCard = new VersionedListedCard(versionInfo.VersionNo, versionInfo.Effectivetime.ToString("yyyyMMdd"), versionInfo.ExpiredTime.ToString("yyyyMMdd"), 0, null); } response.Content = parser.SerializeInnerElement(record).ToList(); } } //Base blacklist, 基础黑名单 else if (request.DataContentType == DataContentType.BaseBlacklist) { logger.Info($"Terminal downloads base blacklist, Source: {request.SourceAddress}"); response.DataContentType = DataContentType.BaseBlacklist; response.SegmentOffset = request.SegmentOffset; response.SegmentCount = request.SegmentCount; var cards = _dbManager.GetBaseBlacklistedCards(_cardAppType); var versions = _dbManager.GetDataVersions(); if (versions.Count > 0 && cards.Count > 0) { var versionInfo = versions.First(v => v.VersionType == VersionType.BaseBlacklistVersion); ListedCardRecord record = null; if (versionInfo != null) { record = new ListedCardRecord(); record.Version = versionInfo.VersionNo; record.ValidStartDate = versionInfo.Effectivetime.ToString("yyyyMMdd"); record.ExpiryDate = versionInfo.ExpiredTime.ToString("yyyyMMdd"); record.Region = Convert.ToString(ushort.MaxValue); record.CardCount = cards.Count; } if (VersionedListedCard == null) { logger.Info("Formatting card list (base blacklist)"); VersionedListedCard = new VersionedListedCard(versionInfo.VersionNo, versionInfo.Effectivetime.ToString("yyyyMMdd"), versionInfo.ExpiredTime.ToString("yyyyMMdd"), cards.Count, cards.ToList()); } if (request.SegmentOffset == 0) { if (request.SegmentCount == 1) { response.Content = parser.SerializeInnerElement(record).ToList(); } else { var versionInfoBytes = parser.SerializeInnerElement(record); var cardBytes = StringToByteArray(VersionedListedCard.AllCards); if (versionInfoBytes.Length + cardBytes.Length >= 160) { var bytes = new byte[160]; Buffer.BlockCopy(versionInfoBytes, 0, bytes, 0, versionInfoBytes.Length); Buffer.BlockCopy(cardBytes, 0, bytes, versionInfoBytes.Length, 160 - versionInfoBytes.Length); response.Content = bytes.ToList(); } else { var bytes = new byte[160]; Buffer.BlockCopy(versionInfoBytes, 0, bytes, 0, versionInfoBytes.Length); Buffer.BlockCopy(cardBytes, 0, bytes, versionInfoBytes.Length - 1, cardBytes.Length); response.Content = bytes.ToList(); } } } else { var bytes = new byte[160]; int cardsToBeSkipped = request.SegmentOffset > 10 ? request.SegmentOffset / 10 : 0; logger.Info($"Base blacklist, Segment Offset: {request.SegmentOffset}, Cards to be skipped: {cardsToBeSkipped * 16}"); if (VersionedListedCard.CardCount <= 16) { var cardBytes = StringToByteArray(VersionedListedCard.AllCards); Buffer.BlockCopy(cardBytes, 0, bytes, 0, cardBytes.Length); response.Content = bytes.ToList(); } else { IEnumerable currentCards = VersionedListedCard.ListedCards.Skip(cardsToBeSkipped * 16).Take(16); StringBuilder sb = new StringBuilder(); foreach (var card in currentCards) { sb.Append(card.CardNo.PadLeft(20, '0')); logger.Info(card.CardNo); } var cardBytes = StringToByteArray(sb.ToString()); Buffer.BlockCopy(cardBytes, 0, bytes, 0, cardBytes.Length); response.Content = bytes.ToList(); } } } } //Station general info, 油站通用信息 else if (request.DataContentType == DataContentType.StationGeneralInfo) { logger.Info($"Terminal downloads Station general info (油站通用信息内容), Source: {request.SourceAddress}"); response.DataContentType = DataContentType.StationGeneralInfo; response.SegmentOffset = request.SegmentOffset; var result = _dbManager.GetPumpInfo(Convert.ToByte(PumpId)); List data = new List(); var pumpInfo = result.First(); data.Add(Convert.ToByte(pumpInfo.VersionId)); data.Add(Convert.ToByte(pumpInfo.PosId)); data.Add(Convert.ToByte(PumpId)); if (result != null && result.Count > 0) { data.Add(Convert.ToByte(result.Count())); foreach (var item in result) data.Add(Convert.ToByte(item.LogicId)); data.AddRange(new byte[10]); } response.SegmentCount = 1; response.Content = data; } if (response.Content != null && response.Content.Count > 0) logger.Info($"ContentType: {response.DataContentType}, Content: {ByteArrayToString(response.Content.ToArray())}"); else logger.Info($"ContentType: {response.DataContentType}, Content emtpy"); _terminal.Write(response); } #endregion #region Change Auth Mode 修改授权模式 private void ChangeAuthMode(CheckCmdRequest checkCmd, WorkMode workMode) { var request = new ChangeAuthMode { Prefix = STX, SourceAddress = checkCmd.SourceAddress, DestinationAddress = checkCmd.DestinationAddress, FrameSqNoByte = checkCmd.FrameSqNoByte, Handle = Convert.ToByte(Command.ChangeAuthMode), FPCode = EncodeFPCodeString(PumpId, PumpId), WorkMode = workMode }; PrepareSendMessage(request); } public void HandleModeChangeResult(ChangeAuthModeAck response) { logger.Info($"Mode change result: {response.ModeChangeResult}, current mode: {response.WorkMode}"); } #endregion #region Cancel Auth request 撤销授权 public void HandleCancelAuth(CancelAuthRequest cancelAuthRequest) { logger.Info("Handling CancelAuth (0x19)"); //There is no special handling for cancel auth. SendCancelAuthResult(cancelAuthRequest); } private void SendCancelAuthResult(CancelAuthRequest request) { var result = new CancelAuthResult(); result.Prefix = STX; result.DestinationAddress = request.DestinationAddress; result.SourceAddress = request.SourceAddress; result.FrameSqNoByte = request.FrameSqNoByte; result.Handle = Convert.ToByte(Command.CancelAuthResult); result.Asn = request.Asn; result.PosTtc = request.PosTtc; result.SeqNo = request.SeqNo; result.FPCode = request.FPCode.ToUInt16(); result.Result = 0; result.AdditionalInfoLength = 0; _terminal.Write(result); } #endregion #region Query gray record 查询灰记录 public void HandleQueryGrayRecord(QueryGrayRecordRequest request) { logger.Info($"Query gray record, info, ASN: {request.Asn}, CTC: {request.Ctc}, Time: {request.Time}"); SendGrayRecord(request); } private void SendGrayRecord(QueryGrayRecordRequest request) { var record = _dbManager.SelectGrayInfo(request.Asn, request.Ctc, request.Time); QueryGrayRecordResult result = new QueryGrayRecordResult(); result.Prefix = STX; result.SourceAddress = request.SourceAddress; result.DestinationAddress = request.DestinationAddress; result.FrameSqNoByte = request.FrameSqNoByte; result.Handle = Convert.ToByte(Command.GrayRecord); if (record != null) { result.Match = 0; result.Asn = request.Asn; result.Balance = Convert.ToInt32(record.CardBal); //result.Amount = Convert.ToInt32(record.Mon); result.Amount1 = Convert.ToInt32(record.RealMon); result.Ctc = request.Ctc; result.Time = record.Ttctime; result.Gmac = record.Gmac.ToString("X"); result.PsamTid = record.Psamtid; result.PsamTtc = Convert.ToInt32(record.Psamttc); result.Volume = Convert.ToInt32(record.Vol); } else { result.Match = 1; } _terminal.Write(result); } #endregion public void HandleFakeNullMessage() { logger.Info($"Terminal {PumpId} Handling Fake null message"); ResetTimer(); } #region Message sent to terminal from System private bool IsMessagePendingForSend() { lock (sendQueueSyncObj) { logger.Debug($"Current send queue count: {activeSendQueue.Count}"); return activeSendQueue.Count > 0; } } private void PrepareSendMessage(CardMessageBase cardMessage) { lock (sendQueueSyncObj) { activeSendQueue.Enqueue(cardMessage); } } private CardMessageBase PickAndSendCardMessage() { lock (sendQueueSyncObj) { if (activeSendQueue.Count > 0) { logger.Info("Dequeued message from SendQueue"); return activeSendQueue.Dequeue(); } } return null; } #endregion #region Check terminal id private void CheckTerminalId(string terminalId) { int tid; if (int.TryParse(terminalId, out tid)) { if (tid <= 0 || tid >= 99) { logger.Error($"Invalid terminal id: {tid}"); } } else { logger.Error("Invalid terminal id, unrecognized"); } } #endregion #endregion #region Helper methods public byte[] StringToByteArray(string hex) { int numberChars = hex.Length; byte[] bytes = new byte[numberChars / 2]; for (int i = 0; i < numberChars; i += 2) bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); return bytes; } public static string ByteArrayToString(byte[] ba) { return BitConverter.ToString(ba).Replace("-", ""); } private DateTime GetDateTime(string time) { if (time == "00000000000000") return new DateTime(0001, 01, 01, 00, 00, 00); var timeFormat = "yyyyMMddHHmmss"; return DateTime.ParseExact(time, timeFormat, null); } private string EncodeFPCodeString(int nozzleId, int pumpId) { return nozzleId.ToString("X").PadLeft(2, '0') + pumpId.ToString("X").PadLeft(2, '0'); } #endregion #region Save non card trade private long AddTradeFromFuelingData(FuelingDataRequest request) { int payModeId = 102; string payModeNo = string.Empty; byte trxType = 6; string asn = string.Empty; if (request.Asn.Any(c => c > 48)) asn = request.Asn; int stationNo = 1; if (_posApp.StationInfo != null) stationNo = _posApp.StationInfo.StationNo; var newGid = _dbManager.AddTrade( stationNo, asn, asn, payModeId, payModeNo, trxType, Convert.ToString(request.ProductCode), request.Price, request.Volume, request.Amount, request.Amount, 0, //Card Balance, 0, //card type, use 0 for non-card transaction 0, //card ctc use 0 for non-card transaction GetDateTime(request.DispenserTime), GetDateTime(request.TransactionEndTime), request.PosTtc, request.SeqNo, _posApp.GetNextBillNo(), //billNo request.FuelingPoint.NozzleNo, request.FuelingPoint.PumpNo, Convert.ToInt32(request.TerminalId.TrimStart('0')), request.VolumeTotal, 0, //discountNo 1001, 1, string.Empty,//request.PsamAsn, //PsamAsn 0,//uint.Parse(request.PsamTac, System.Globalization.NumberStyles.HexNumber),//PsamTac string.Empty,//request.PsamTid,//psam tid 0,//uint.Parse(request.PsamTtc, System.Globalization.NumberStyles.HexNumber),//psam ttc 0,//uint.Parse(request.Tac, System.Globalization.NumberStyles.HexNumber),//tac 0,//uint.Parse(request.Gmac, System.Globalization.NumberStyles.HexNumber),//gmac 0,//uint.Parse(request.Tmac, System.Globalization.NumberStyles.HexNumber),//tmac 0, //Integral Convert.ToInt32(_posApp.CurrentShiftNo), string.Empty, string.Empty, string.Empty, 0); return newGid; } #endregion private string GetCardNo(string asn) { if (_cardAppType == CardAppType.CpuCard) { string leftOver = asn.TrimStart('0'); return leftOver.PadLeft(20, '0'); } else if (_cardAppType == CardAppType.RfCard) { string leftOver = asn.TrimStart('0'); return leftOver.PadLeft(8, '0'); } return asn; } } }