using Dfs.WayneChina.PosModelMini; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace VBaoProxyApp.Cloud { public enum PaymentID { Cash = 1, //室内现金 AliPay = 2, WechatPay = 3, IC = 4, MiniProgram = 5, ThirdPartyPay = 6, AutoCleared = 7, //油机大屏上的自动清除 BankCard = 8, OutdoorCash = 9, //油机大屏的室外现金 IndoorPay = 10, //油机大屏上的室内支付 AllInPay = 11, //通联支付, PumpTest = 12, //打油测试 AllInPayV2 = 13, //通联分账支付, WechatQrScan = 20, //微信扫码,统一下单,用户扫商户, AliPayQrScan = 21, //支付宝扫码,统一下单,用户扫商户, CarPlate = 100, /// /// outdoor driver drive off without real payment /// DriveOff = 400, /// /// indoor customer leave off without real payment /// LeaveOff = 401, /// /// membership depoist balance payment initiated by mini program, 余额支付 /// MembershipPay = 500, /// /// membership mobile payment initiated by mini program, 小程序微信支付 /// MembershipMobilePay = 501, PetroChinaIC = 600, } public class TrxCreationResponse { public Guid Id { get; set; } } public class ServerSideItem { public string itemId { get; set; } public string BarCode { get; set; } public string ItemName { get; set; } public decimal Price { get; set; } public bool IsFuelItem { get; set; } public Guid Id { get; set; } } public class ServerSideTrxItem { public int LineNum { get; set; } public Guid PosItemId { get; set; } public ServerSideItem Item { get; set; } public double Qty { get; set; } public int FuelItemSoldOnPumpId { get; set; } public int FuelItemSoldOnPumpNozzleId { get; set; } public string FuelItemFdcTransactionSeqNo { get; set; } public decimal FuelItemPumpTotalizerVolume { get; set; } } public enum ServerSideTrxStatus { Open, Finish } public enum ServerSideTrxType { Sale = 0, Refund = 1, Reconciliation = 2, Redemption = 3, LogOff = 4, LogOn = 5, EndOfShift = 6, EndOfDay = 7, Restore = 8, FuelDelivery = 9, //Manual input on POS Side TankDelivery = 10, //Auto Detect, in using TankReading = 11 //when POS EOS, POS Read ATG Data } public class ServerSideTrx { public Guid Id { get; set; } public ServerSideTrxType TransactionType { get; set; } public DateTime CreatedDateTime { get; set; } public Guid TargetBusinessUnitId { get; set; } public decimal NetAmount { get; set; } public decimal GrossAmount { get; set; } public List Items { get; set; } public ServerSideTrxStatus TransactionStatus { get; set; } } public class ClientFuelTrxInfo { public PosTrxSource Source { get; set; } public int Barcode { get; set; } public int PumpId { get; set; } public int NozzleId { get; set; } public int SiteNozzleNo { get; set; } /// /// should get it from cloud side. /// public Guid PosItemId { get; set; } //public ushort FPosSqNo { get; set; } //public int FdcSqNo { get; set; } //public DateTime TimeStamp { get; set; } public decimal Volume { get; set; } public decimal Amount { get; set; } public decimal PayAmount { get; set; } public decimal UnitPrice { get; set; } public int SeqNo { get; set; } //public string CardNo { get; set; } //public decimal CurrentCardBalance { get; set; } public DateTime FuelingStartTime { get; set; } public DateTime FuelingFinishedTime { get; set; } public decimal VolumeTotalizer { get; set; } //public string CardHolder { get; set; } //public string AccountName { get; set; } } public class AuthenticationParameter { public AuthenticationParameter(string grantType, string userName, string password) { Params.Clear(); Params.Add("grant_type", grantType); Params.Add("username", userName); Params.Add("password", password); } public Dictionary Params { get; } = new Dictionary(); } public class CloudCredential { public string UserName { get; set; } public string Password { get; set; } /// /// Sample: http://url:8698 /// DO NOT end with slash /// public string ApiGatewayEntryUrl { get; set; } //public string AuthServiceBaseUrl { get; set; } //public string TransactionServiceBaseUrl { get; set; } //public string DiscountServiceBaseUrl { get; set; } public string DeviceSN { get; set; } //public string CurrentBuId { get; set; } } //public class PosItem //{ // /// // /// Unique id. // /// // public Guid Id { get; set; } // /// // /// Item Id. // /// // public int ItemId { get; set; } // /// // /// Item barcode. // /// // public string BarCode { get; set; } // /// // /// Change set id. // /// // public Guid ChangesetId { get; set; } // /// // /// Name of the item. // /// // public string ItemName { get; set; } // /// // /// Unit id. // /// // public int UnitId { get; set; } // /// // /// Unit price. // /// // public decimal Price { get; set; } // /// // /// When the item is activated. // /// // public DateTime TimeToActivate { get; set; } // /// // /// When the time is deactivated. // /// // public DateTime TimeToDeactivate { get; set; } // /// // /// When the item is created. // /// // public DateTime CreatedDateTime { get; set; } // /// // /// Whether this item is a fuel item. // /// // public bool IsFuelItem { get; set; } // /// // /// Whether the item is deleted. // /// // public bool IsMarkedAsDeletion { get; set; } // /// // /// Target business unit id. // /// // public Guid TargetBusinessUnitId { get; set; } //} public class BusinessUnit { [JsonProperty("Id")] public Guid Id { get; set; } [JsonProperty("Name")] public string Name { get; set; } } public class AuthToken { #region Standard OAuth token format /// /// The access token string issued by the authorization server. /// [JsonProperty("access_token")] public string AccessToken { get; set; } /// /// Token type, simply `bearer`. /// [JsonProperty("token_type")] public string TokenType { get; set; } /// /// Duration of the time the access token is granted for, in total seconds. /// [JsonProperty("expires_in")] public int ExpiresIn { get; set; } /// /// Access token to be used to get a new token, optional. /// [JsonProperty("refresh_token")] public string RefreshToken { get; set; } /// /// The error information /// [JsonProperty("error")] public string Error { get; set; } [JsonProperty("userName")] public string UserName { get; set; } [JsonProperty("alias")] public string UserAlias { get; set; } [JsonProperty("roleNames")] public string RoleNames { get; set; } [JsonProperty("BusinessUnits")] public string BusinessUnitsJsonString { get; set; } public IList BusinessUnitList { get; set; } #endregion #region Custom extension /// /// When the token is retrieve from the auth server, to simplify, set the local received time. /// public DateTime TokenRetrievedTime { get; set; } /// /// Check if the token has expired or not, if not, then it's valid. /// /// valid or not public bool IsTokenValid() { //Current Time = Now + 5 minutes, Expiry Time = token retrieved time + expiry time span //If current time is not later than expiry time, then the token is valid if (DateTime.Now + new TimeSpan(0, 5, 0) < TokenRetrievedTime + new TimeSpan(0, 0, ExpiresIn)) return true; return false; } #endregion } public class CloudHelper { public CloudCredential Credential { get; set; } public ILogger Logger { get; set; } = NullLogger.Instance; public int Id { get; } private string grantType = "password"; private string tokenAuthPath = "token"; private string authScheme = "bearer"; private HttpClient _client = new HttpClient(); private AuthToken currentAuthToken = null; private static CloudHelper instance = new CloudHelper(); public static CloudHelper Default { get; } = instance; private CloudHelper() { } private async Task RefreshAuthTokenAsync(string userName, string password) { this.Logger.LogDebug("GetTokenAsync..."); _client.DefaultRequestHeaders.Clear(); string tokenUrl = string.Concat(this.Credential.ApiGatewayEntryUrl, "/", tokenAuthPath); var formParam = new AuthenticationParameter(grantType, userName, password); var response = await _client.PostAsync(tokenUrl, new FormUrlEncodedContent(formParam.Params)).ConfigureAwait(false); this.Logger.LogDebug($"Response for getting token, Response==null? {response == null}"); if (response != null && response.IsSuccessStatusCode) { var resultStr = await response.Content.ReadAsStringAsync(); this.currentAuthToken = JsonConvert.DeserializeObject(resultStr); if (!string.IsNullOrEmpty(this.currentAuthToken.BusinessUnitsJsonString)) { this.currentAuthToken.BusinessUnitList = JsonConvert.DeserializeObject>(currentAuthToken.BusinessUnitsJsonString); return; } } throw new InvalidOperationException($"GetAuthTokenAsync failed(serverUrl: {tokenUrl}, username/pwd: {userName ?? ""}/{password ?? ""}) due to response with code: {response?.StatusCode.ToString() ?? "-1"}, message: {response?.ReasonPhrase ?? ""}"); } /// /// query the last trx from server side, if its TransactionStatus == Open, then server will return it. otherwise, server will return null. /// /// /// public async Task GeLastTrxWithOpenStatusByUserNameAsync() { if (this.currentAuthToken == null) await this.RefreshAuthTokenAsync(this.Credential.UserName, this.Credential.Password); string targetUrl = string.Concat(this.Credential.ApiGatewayEntryUrl + "/api/transactions/?searchType=lastOpenSaleTrx&searchValue0=userName&searchValue1=", this.Credential.UserName); _client.DefaultRequestHeaders.Clear(); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, this.currentAuthToken.AccessToken); this.Logger.LogDebug($"{_client.DefaultRequestHeaders.Authorization.Scheme}, {_client.DefaultRequestHeaders.Authorization.Parameter}"); //BusinessUnitId = currentAuthToken.BusinessUnitList.Count > 0 ? currentAuthToken.BusinessUnitList.First().Id.ToString() : string.Empty; //Be aware, DeviceSN is `required` after 2019.04 for Smart Fuel v0.5 _client.DefaultRequestHeaders.Add("DeviceSN", this.Credential.DeviceSN); var response = await _client.GetAsync(targetUrl).ConfigureAwait(false); if (response != null && response.IsSuccessStatusCode) { var resultStr = await response.Content.ReadAsStringAsync(); if (string.IsNullOrEmpty(resultStr)) return default(T); var jd = JsonDocument.Parse(resultStr); return JsonConvert.DeserializeObject(resultStr); //return jd.RootElement.GetProperty("result"); } throw new InvalidOperationException($"GeLastOpenTrxByUserNameAsync failed(serverUrl: {targetUrl}, userName: {this.Credential.UserName ?? ""}, deviceSN: {this.Credential?.DeviceSN ?? ""}) due to response with code: {response?.StatusCode.ToString() ?? "-1"}, message: {response?.ReasonPhrase ?? ""}"); } public async Task GetPosItemAsync(string itemId) { if (this.currentAuthToken == null) await this.RefreshAuthTokenAsync(this.Credential.UserName, this.Credential.Password); string productItemUrl = string.Concat(this.Credential.ApiGatewayEntryUrl + "/api/Products/", "itemId/"); this.Logger.LogDebug("GetPosItemAsync..."); _client.DefaultRequestHeaders.Clear(); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, this.currentAuthToken.AccessToken); this.Logger.LogDebug($"{_client.DefaultRequestHeaders.Authorization.Scheme}, {_client.DefaultRequestHeaders.Authorization.Parameter}"); //BusinessUnitId = currentAuthToken.BusinessUnitList.Count > 0 ? currentAuthToken.BusinessUnitList.First().Id.ToString() : string.Empty; //Be aware, DeviceSN is `required` after 2019.04 for Smart Fuel v0.5 _client.DefaultRequestHeaders.Add("DeviceSN", this.Credential.DeviceSN); //_client.DefaultRequestHeaders.Add("CurrentBuId", BusinessUnitId); //this.logger.LogDebug($"DeviceSN: {DeviceSN}"); //this.logger.LogDebug($"BuId: {BusinessUnitId}"); var productUrl = string.Concat(productItemUrl, itemId); this.Logger.LogDebug($"item url: {productUrl}"); var response = await _client.GetAsync(productUrl).ConfigureAwait(false); if (response != null && response.IsSuccessStatusCode) { var resultStr = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(resultStr); } throw new InvalidOperationException($"GetPosItemAsync failed(serverUrl: {productItemUrl}, itemId: {itemId}, deviceSN: {this.Credential?.DeviceSN ?? ""}) due to response with code: {response?.StatusCode.ToString() ?? "-1"}, message: {response?.ReasonPhrase ?? ""}"); } public async Task GetAccountIdByVBaoBeaconIdAsync(string beaconId) { if (this.currentAuthToken == null) await this.RefreshAuthTokenAsync(this.Credential.UserName, this.Credential.Password); string targetUrl = string.Concat(this.Credential.ApiGatewayEntryUrl + "/userinfo/", "vBaoId?vBaoId=", beaconId); _client.DefaultRequestHeaders.Clear(); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, this.currentAuthToken.AccessToken); this.Logger.LogDebug($"{_client.DefaultRequestHeaders.Authorization.Scheme}, {_client.DefaultRequestHeaders.Authorization.Parameter}"); //BusinessUnitId = currentAuthToken.BusinessUnitList.Count > 0 ? currentAuthToken.BusinessUnitList.First().Id.ToString() : string.Empty; //Be aware, DeviceSN is `required` after 2019.04 for Smart Fuel v0.5 _client.DefaultRequestHeaders.Add("DeviceSN", this.Credential.DeviceSN); var response = await _client.GetAsync(targetUrl).ConfigureAwait(false); if (response != null && response.IsSuccessStatusCode) { var resultStr = await response.Content.ReadAsStringAsync(); var jd = JsonDocument.Parse(resultStr); return jd.RootElement.GetProperty("result"); } throw new InvalidOperationException($"GetAccountIdByVBaoBeaconIdAsync failed due to response with code: {response?.StatusCode.ToString() ?? "-1"}, message: {response?.ReasonPhrase ?? ""}"); } public async Task CreateTransactionAsync(ClientFuelTrxInfo clientFuelTrxInfo, string trxComment) { if (this.currentAuthToken == null) await this.RefreshAuthTokenAsync(this.Credential.UserName, this.Credential.Password); Guid trxId = Guid.Empty; var trxUrl = string.Concat(this.Credential.ApiGatewayEntryUrl + "/api/transactions/"); var clientPosTrx = new ClientPosTrx { RequestingCreationTimeInPosClient = DateTime.Now, Type = PosTrxType.Sale, Source = clientFuelTrxInfo.Source, Items = new List { new ClientRingUpPosItem { PosItemUniqueId = clientFuelTrxInfo.PosItemId, //GUID Qty = clientFuelTrxInfo.Volume, ClientRingUpTime = DateTime.Now, FuelItemSoldOnPumpId = clientFuelTrxInfo.PumpId, FuelItemSoldOnPumpNozzleId = clientFuelTrxInfo.SiteNozzleNo,//SiteNozzles[(clientTrxInfo.PumpId, clientTrxInfo.NozzleId)],//fdcTrx.Nozzle.LogicalId, FuelItemOriginalGrossAmount = clientFuelTrxInfo.Amount,//(decimal)trdInfo.Mon.Value / 100, FuelItemFdcTransactionSeqNo = Convert.ToString(clientFuelTrxInfo.SeqNo), fuelItemTransactionEndTime = clientFuelTrxInfo.FuelingFinishedTime, TransactionComment = trxComment, FuelItemPumpTotalizerVolume = clientFuelTrxInfo.VolumeTotalizer } } }; _client.DefaultRequestHeaders.Clear(); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, currentAuthToken.AccessToken); _client.DefaultRequestHeaders.Add("DeviceSN", this.Credential.DeviceSN); //_client.DefaultRequestHeaders.Add("CurrentBuId", BusinessUnitId); var response = await _client.PostAsJsonAsync(trxUrl, clientPosTrx).ConfigureAwait(false); if (response != null && response.IsSuccessStatusCode) { var resultStr = await response.Content.ReadAsStringAsync(); var trxCreationResponse = JsonConvert.DeserializeObject(resultStr); return trxCreationResponse.Id; } throw new InvalidOperationException($"CreateTransactionAsync failed(serverUrl: {trxUrl}, trx volume: {clientFuelTrxInfo?.Volume ?? -1 }, deviceSN: {this.Credential?.DeviceSN ?? ""}) due to response with code: {response?.StatusCode.ToString() ?? "-1"}, message: {response?.ReasonPhrase ?? ""}"); } public async Task CommitTransactionAsync(Guid trxId, PosTrxMop posTrxMop, string accountId) { if (this.currentAuthToken == null) await this.RefreshAuthTokenAsync(this.Credential.UserName, this.Credential.Password); string targetUrl = ""; if (string.IsNullOrEmpty(accountId)) targetUrl = string.Concat(this.Credential.ApiGatewayEntryUrl + "/api/transactions/", trxId, "/payment"); else targetUrl = string.Concat(this.Credential.ApiGatewayEntryUrl + "/api/transactions/", trxId, "/payment?accountId=", accountId); //Payment detail for receipt //string version = "1.0"; //var icPayDetail = new //{ // CardNo = clientTrxInfo.CardNo, // PayAmount = clientTrxInfo.PayAmount, // CardHolder = clientTrxInfo.CardHolder, // AccountName = clientTrxInfo.AccountName, // CardBalance = clientTrxInfo.CurrentCardBalance, // Version = version //}; //posTrxMop.Mop = new PosMop //{ // Id = Guid.Parse("ff64ab36-658d-40a4-94bf-bee7231ac788"), // Name = "IC", // PaymentId = 4, // CreatedDateTime = DateTime.Now, // ChangesetId = Guid.Parse("84f02b52-6950-4c50-a0b1-9827d6459edc"), // TargetBusinessUnitId = Guid.Parse("6c40e7f6-2b2d-40de-8c5c-f5693a05ab0d"), // DisplayName = "IC卡" //}; _client.DefaultRequestHeaders.Clear(); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, currentAuthToken.AccessToken); _client.DefaultRequestHeaders.Add("DeviceSN", this.Credential.DeviceSN); //_client.DefaultRequestHeaders.Add("CurrentBuId", BusinessUnitId); var response = await _client.PostAsJsonAsync(targetUrl, posTrxMop).ConfigureAwait(false); if (response != null && response.IsSuccessStatusCode) return true; throw new InvalidOperationException($"CommitTransactionAsync failed(serverUrl: {targetUrl}, deviceSN: {this.Credential?.DeviceSN ?? ""}) due to response with code: {response?.StatusCode.ToString() ?? "-1"}, message: {response?.ReasonPhrase ?? ""}"); } } }