using Infiniti.Interfaces.Ebay; using Infiniti.Models.Configuration; using Infiniti.Models.Ebay; using Infiniti.Models.Logging; using Infiniti.Models.Tokens; using Infiniti.Models.Transactions; using Infiniti.Models.Users; using Infiniti.Standard; 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.Threading; using System.Threading.Tasks; using System.Web; namespace Infiniti.Services.eBay { public class EbaySalesService : ServiceBase, IEbaySalesService { private IHttpClientFactory httpClientFactory; public EbaySalesService(IEventPublisher eventPublisher, IHttpClientFactory httpClientFactory) : base(eventPublisher) => this.httpClientFactory = httpClientFactory; public async Task FetchTransactionsAsync(EbayFetchTransRequest request) { try { var queryString = ""; var eBaySales = request.eBaySales; if (eBaySales.limit.HasValue) queryString += $"&limit={eBaySales.limit.Value}"; if (eBaySales.offset.HasValue) queryString += $"&offset={eBaySales.offset.Value}"; foreach (var item in eBaySales.filter) queryString += $"&filter={HttpUtility.UrlEncode(item)}"; var fragmentURI = $"?{queryString.TrimStart('&')}"; var content = await SalesCommonAsync(fragmentURI, request.User, request.ClientId, EbaySalesType.Transaction).ConfigureAwait(false); var response = JsonConvert.DeserializeObject(content); return new EbayFetchTransactionsResponse(request, response); } catch (Exception ex) { return new EbayFetchTransactionsResponse(request, null, ex); } } public async Task FetchOrdersAsync(EbayFetchOrdersRequest request) { try { var queryString = ""; var eBaySales = request.eBaySales; if (eBaySales.limit.HasValue) queryString += $"&limit={eBaySales.limit.Value}"; if (eBaySales.offset.HasValue) queryString += $"&offset={eBaySales.offset.Value}"; foreach (var item in eBaySales.filter) queryString += $"&filter={HttpUtility.UrlEncode(item)}"; var fragmentURI = $"?{queryString.TrimStart('&')}"; var content = await SalesCommonAsync(fragmentURI, request.User, request.ClientId, EbaySalesType.Order).ConfigureAwait(false); var response = JsonConvert.DeserializeObject(content); return new EbayFetchOrdersResponse(request, response); } catch (Exception ex) { return new EbayFetchOrdersResponse(request, null, ex); } } public async Task GetTransactionsAsync(EbayGetTransRequest request) { var dates = eBayGetDates(request.Datespan); var dateRange = $"transactiondate:[{dates.dtsStart:s}Z..{dates.dtsFinish:s}Z]"; var queryString = $"&filter={HttpUtility.UrlEncode(dateRange)}"; queryString += $"&filter={HttpUtility.UrlEncode("transactionType:{SALE}")}"; queryString += $"&filter={HttpUtility.UrlEncode("transactionStatus:{PAYOUT}")}"; var pageSize = 20; var pageStart = 0; var pageCount = 0; var totalTrans = 0; var finished = false; var transactionList = new List(); try { var fragmentURI = ""; var content = await SalesCommonAsync(fragmentURI, request.User, request.ClientId, EbaySalesType.Identity).ConfigureAwait(false); var eBaySeller = JsonConvert.DeserializeObject(content); do { //# get block of eBay transaction objects, and map to Infiniti-Transaction records fragmentURI = $"?limit={pageSize}&offset={pageStart}{queryString}"; content = await SalesCommonAsync(fragmentURI, request.User, request.ClientId, EbaySalesType.Transaction).ConfigureAwait(false); var eBayTransactions = JsonConvert.DeserializeObject(content); if (eBayTransactions == null) throw new ApplicationException("No transaction records"); if (pageCount == 0) { totalTrans = eBayTransactions.total; if (totalTrans > 9999) throw new ApplicationException("Too many total transaction records"); } foreach (var vTransaction in eBayTransactions.transactions) { var transRecord = BuildTransactionRecord(vTransaction, eBaySeller); transactionList.Add(transRecord); } pageStart += pageSize; pageCount++; if (pageCount * pageSize > totalTrans) finished = true; if (pageCount > 999) throw new ApplicationException("Too many transaction record pages"); } while (!finished); return new EbayGetTransResponse(request, transactionList); } catch (ApplicationException ex) { return new EbayGetTransResponse(request, transactionList, ex); } catch (Exception ex) { return new EbayGetTransResponse(request, null, ex); } } public async Task GetOrdersAsync(EbayGetOrdersRequest request) { var dates = eBayGetDates(request.Datespan); var dateRange = $"creationdate:[{dates.dtsStart:s}Z..{dates.dtsFinish:s}Z]"; var queryString = $"&fieldGroups=TAX_BREAKDOWN&filter={HttpUtility.UrlEncode(dateRange)}&filter=orderfulfillmentstatus%7Bfulfilled%7Cin_progress%7D"; var pageSize = 20; var pageStart = 0; var pageCount = 0; var totalTrans = 0; var finished = false; var transactionList = new List(); try { var fragmentURI = ""; var content = await SalesCommonAsync(fragmentURI, request.User, request.ClientId, EbaySalesType.Identity).ConfigureAwait(false); var eBaySeller = JsonConvert.DeserializeObject(content); do { //# get chunk of eBay order objects, and map into Infinity-Transaction records fragmentURI = $"?limit={pageSize}&offset={pageStart}{queryString}"; content = await SalesCommonAsync(fragmentURI, request.User, request.ClientId, EbaySalesType.Order).ConfigureAwait(false); var eBayOrders = JsonConvert.DeserializeObject(content); if (eBayOrders == null) throw new ApplicationException("No Order records"); if (pageCount == 0) { totalTrans = eBayOrders.total; //.total; if (totalTrans > 9999) throw new ApplicationException("Too many total order records"); } foreach (var vOrder in eBayOrders.orders.Where(o => o.orderFulfillmentStatus.ToLower() == "fulfilled")) { foreach (var orderItem in vOrder.lineItems) { var transRecord = BuildTransactionRecord(vOrder, orderItem, eBaySeller);//transaction, transactionList.Add(transRecord); } } pageStart += pageSize; pageCount++; if (pageCount * pageSize > totalTrans) finished = true; if (pageCount > 999) throw new ApplicationException("Too many order record pages"); } while (!finished); return new EbayGetOrdersResponse(request, transactionList); } catch (ApplicationException ex) { return new EbayGetOrdersResponse(request, transactionList, ex); } catch (Exception ex) { return new EbayGetOrdersResponse(request, null, ex); } } private async Task SalesCommonAsync(string fragmentURI, User user, string avaskClientId, EbaySalesType salesType) { var funcName = nameof(SalesCommonAsync); var domainSetting = (salesType == EbaySalesType.Order) ? "EBAYSalesOrderBase" : "EBAYSalesTransBase"; var pathSetting = (salesType == EbaySalesType.Order) ? "EBAYSalesOrderPath" : (salesType == EbaySalesType.Transaction) ? "EBAYSalesTransPath" : "EBAYSalesIdentPath"; var salesDomain = await eventPublisher.GetApplicationSettingAsync(domainSetting).ConfigureAwait(false); var salesPath = await eventPublisher.GetApplicationSettingAsync(pathSetting).ConfigureAwait(false); var authDomain = await eventPublisher.GetApplicationSettingAsync("EBAYAuthBaseURL").ConfigureAwait(false); var tokenPath = await eventPublisher.GetApplicationSettingAsync("EBAYAuthTokenPath").ConfigureAwait(false); var clientSecret = await eventPublisher.GetApplicationSettingAsync("EBAYAuthClientSecret").ConfigureAwait(false); var client_id = await eventPublisher.GetApplicationSettingAsync("EBAYAuthClientId").ConfigureAwait(false); var encodedKey = Convert.ToBase64String(ASCIIEncoding.ASCII.GetBytes($"{client_id}:{clientSecret}")); var scope = HttpUtility.UrlEncode(await eventPublisher.GetApplicationSettingAsync("EBAYAuthClientScope").ConfigureAwait(false)); var clientTimeout = await eventPublisher.GetApplicationSettingIntAsync("DefaultHttpTimeout").ConfigureAwait(false); var clientRetries = await eventPublisher.GetApplicationSettingIntAsync("DefaultHttpRetries").ConfigureAwait(false); var content = ""; var client = httpClientFactory.CreateClient(); var loop = 0; var reason = 0; do { try { var request = new HttpRequestMessage(HttpMethod.Get, $"{salesDomain}{salesPath}{fragmentURI}"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var accessToken = await eventPublisher.GetTokenAsync(user, "EBAYAuthAccessToken", avaskClientId).ConfigureAwait(false); // task concurrency problem fetch request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken ?? "KensTestUnauth"); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(clientTimeout)); var response = await client.SendAsync(request, cts.Token).ConfigureAwait(false); content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (response.IsSuccessStatusCode) { return content; } if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { await eventPublisher.LogInformationAsync(user, $"{funcName}: User Bad Path '{content}'.").ConfigureAwait(false); throw new NotSupportedException("Error: User Invalid Path"); } if (response.StatusCode == System.Net.HttpStatusCode.Forbidden) { await eventPublisher.LogInformationAsync(user, $"{funcName}: User Access Forbiden '{content}'.").ConfigureAwait(false); throw new UnauthorizedAccessException("Error: User Access Forbidden "); } if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { if (content.Contains("Invalid Order Id")) return ""; await eventPublisher.LogInformationAsync(user, $"{funcName}: User Bad Request '{content}'.").ConfigureAwait(false); throw new ArgumentException("Fault: User Invalid Request"); } if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { //# 401, try to re-authenticate with stored refresh-token, task concurrency problem fetch await eventPublisher.LogInformationAsync(user, $"{funcName}: User Access Token expired '{content}'.").ConfigureAwait(false); request = new HttpRequestMessage(HttpMethod.Post, $"{authDomain}{tokenPath}"); var refreshToken = await eventPublisher.GetTokenAsync(user, "EBAYAuthRefreshToken", avaskClientId).ConfigureAwait(false); var authContent = $"grant_type=refresh_token&refresh_token={refreshToken}&scope={scope}"; request.Content = new StringContent(authContent, Encoding.UTF8, "application/x-www-form-urlencoded"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); request.Headers.Authorization = new AuthenticationHeaderValue("Basic", encodedKey); cts = new CancellationTokenSource(TimeSpan.FromSeconds(clientTimeout)); response = await client.SendAsync(request, cts.Token).ConfigureAwait(false); content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { await eventPublisher.LogInformationAsync(user, $"{funcName}: Refresh Token Failure '{content}'.").ConfigureAwait(false); throw new UnauthorizedAccessException($"Error: Authentication Refresh Token Failure: '{content}'"); } if (!response.IsSuccessStatusCode) { await eventPublisher.LogInformationAsync(user, $"{funcName}: Refresh Token Error '{content}'.").ConfigureAwait(false); throw new Exception($"Error: Authentication Refresh Token Error: '{content}'"); } var AuthTokens = JsonConvert.DeserializeObject(content); await eventPublisher.SaveTokenAsync(user, "EBAYAuthAccessToken", AuthTokens.access_token, avaskClientId).ConfigureAwait(false); if (reason != 2) loop--; reason = 2; throw new ApplicationException($"Authentication Access Token Refreshed: '{content}'"); } reason = 1; throw new ApplicationException($"Bad Response fault: '{content}'"); } catch (ApplicationException ex) { await eventPublisher.LogInformationAsync(user, $"{funcName}: Application Exception '{ex.Message}'.").ConfigureAwait(false); if ((reason != 2) && (loop < clientRetries)) await Task.Delay(client.Timeout).ConfigureAwait(false); } catch (HttpRequestException ex) { await eventPublisher.LogInformationAsync(user, $"{funcName}: Communication Exception '{ex.Message}'.").ConfigureAwait(false); if (loop < clientRetries) await Task.Delay(client.Timeout).ConfigureAwait(false); } catch (TaskCanceledException ex) { await eventPublisher.LogInformationAsync(user, $"{funcName}: Timeout Exception '{ex.Message}'.").ConfigureAwait(false); } catch (Exception ex) { await eventPublisher.LogInformationAsync(user, $"{funcName}: General Exception '{ex.Message}'.").ConfigureAwait(false); throw; } ++loop; } while (loop <= clientRetries); await eventPublisher.LogInformationAsync(user, $"{funcName}: No Response from Target: '{content}'").ConfigureAwait(false); throw new ApplicationException("Fault: No Response from Target."); } private (DateTime dtsStart, DateTime dtsFinish) eBayGetDates(EbaySalesDatespan datespan) { return GetDates(datespan.Year, datespan.Month, datespan.Day); } // overload private (DateTime dtsStart, DateTime dtsFinish) GetDates(int year, int months = 0, int days = 0) { var month = Math.Abs(months); var day = Math.Abs(days); //# determine date span, annual DateTime dtsStart = new DateTime(year, 1, 1); DateTime dtsFinish = dtsStart.AddYears(1); if (month > 0) { // monthly dtsStart = new DateTime(year, month, 1); dtsFinish = dtsStart.AddMonths(1); if (day > 0) { // daily dtsStart = new DateTime(year, month, day); dtsFinish = dtsStart.AddDays(1); } } else if (day > 0) { // week of year, built on annual start dts var DoW = dtsStart.DayOfWeek; int addDays = day * 7 - (int)DoW + (((DoW <= DayOfWeek.Thursday) || (DoW == DayOfWeek.Sunday)) ? -6 : 1); dtsStart = dtsStart.AddDays(addDays); dtsFinish = dtsStart.AddDays(7); } if (months < 0) dtsFinish = DateTime.Now; return (dtsStart, dtsFinish.AddSeconds(-10)); // ten seconds to midnight } private Transaction BuildTransactionRecord(EbayTransactionOrder transOrder, EbayTransactionOrderLineItem orderLineItem, EbaySellerIdentity seller) //### prototype { // ignore VAT, duties and shipping-costs double dTemp; var grossAmount = double.TryParse(orderLineItem.total.value, out dTemp) ? dTemp : 0.0; var arrGrossAmount = grossAmount; var currencyCode = orderLineItem.total.currency; var arrCurrencyCode = currencyCode; if (orderLineItem.total.convertedFromCurrency != "") { // converted grossAmount = double.TryParse(orderLineItem.total.convertedFromValue, out dTemp) ? dTemp : 0.0; currencyCode = orderLineItem.total.convertedFromCurrency; } var contactAddr = transOrder.fulfillmentStartInstructions.FirstOrDefault().shippingStep.shipTo.contactAddress; var sellerAddr = seller.businessAccount.address; if (seller.accountType == "INDIVIDUAL") sellerAddr = seller.individualAccount.registrationAddress; //# create infin-trans object (24 fields) var transRecord = new Transaction( DateTime.Parse(transOrder.creationDate), // date orderLineItem.lineItemId, // "id", "", // "key", //### orderLineItem.purchaseMarketplaceId, // "salesChannelId", orderLineItem.soldFormat, // "type", "", // "productCode", //### double.TryParse(orderLineItem.quantity, out dTemp) ? dTemp : 0, // quantity grossAmount, // gross amount currencyCode, // "currencyCode", "", // "salesChannel", //### sellerAddr.country, // "depCountry", contactAddr.countryCode, // "arrCountry", orderLineItem.purchaseMarketplaceId, // "marketPlace", orderLineItem.title, // "desc", 0, // vatRate 0, // salesChannelVatRate "", // "vatCountry", "", // "buyerVatNo", "", // "buyerVatNoCountry", transOrder.buyer.username, // "BuyerName", "", // "invoiceURL", sellerAddr.city, // "depCity", contactAddr.city, // "arrCity", contactAddr.postalCode, // "arrPostCode", orderLineItem?.sku ?? "", taxCollectedByMarketplace: false); //# add non-constructor fields(15) transRecord.ReportCountry = ""; //### transRecord.MappedDepartureCountry = ""; //### transRecord.MappedArrivalCountry = ""; //### transRecord.DepartureCountryName = sellerAddr.country; // country code not name transRecord.ArrivalCountryName = contactAddr.county; transRecord.CostDeparture = 0; transRecord.CostArrival = 0; transRecord.NetAmount = grossAmount; transRecord.ArrivalGrossAmount = arrGrossAmount; transRecord.ArrivalNetAmount = arrGrossAmount; transRecord.ArrivalCurrency = arrCurrencyCode; transRecord.BuyerVatNumberIsValid = false; transRecord.BuyerAddress = string.Empty; transRecord.TransactionType = string.Empty; return transRecord; } private Transaction BuildTransactionRecord(EbayTransaction eBayTransaction, EbaySellerIdentity seller) //### prototype { //ignore VAT, duties and shipping-costs double dTemp; var grossAmount = double.TryParse(eBayTransaction.amount.value, out dTemp) ? dTemp : 0.0; var arrGrossAmount = grossAmount; var currencyCode = eBayTransaction.amount.currency; var arrCurrencyCode = currencyCode; if (eBayTransaction.amount.convertedFromCurrency != "") { // converted grossAmount = double.TryParse(eBayTransaction.amount.convertedFromValue, out dTemp) ? dTemp : 0.0; currencyCode = eBayTransaction.amount.convertedFromCurrency; } //var contactAddr = transOrder.fulfillmentStartInstructions.FirstOrDefault().shippingStep.shipTo.contactAddress;//### var sellerAddr = seller.businessAccount.address; if (seller.accountType == "INDIVIDUAL") sellerAddr = seller.individualAccount.registrationAddress; //# create infin-trans object (24 fields) var transRecord = new Transaction( DateTime.Parse(eBayTransaction.transactionDate), // date eBayTransaction.transactionId, // "id", eBayTransaction.orderId, // "key", "eBay", // "salesChannelId", //### eBayTransaction.transactionType, // "type", "", // "productCode", //### eBayTransaction.orderLineItems.Count() , // quantity //### grossAmount, // gross amount currencyCode, // "currencyCode", "eBay", // "salesChannel", //### sellerAddr.country, // "depCountry", "", // "arrCountry", //###contactAddr.countryCode "eBay", // "marketPlace", //### $"{eBayTransaction.orderLineItems.Count()} line items", // "desc", //### 0, // vatRate 0, // salesChannelVatRate "", // "vatCountry", "", // "buyerVatNo", "", // "buyerVatNoCountry", eBayTransaction.buyer.username, // "BuyerName", "", // "invoiceURL", sellerAddr.city, // "depCity", "", // "arrCity", //###contactAddr.city "", // "arrPostCode", //###contactAddr.postalCode sku: "", taxCollectedByMarketplace: false); //# add non-constructor fields(15) transRecord.ReportCountry = string.Empty; transRecord.MappedDepartureCountry = string.Empty; transRecord.MappedArrivalCountry = string.Empty; transRecord.DepartureCountryName = sellerAddr.country; // country code not name transRecord.ArrivalCountryName = string.Empty; transRecord.CostDeparture = 0; transRecord.CostArrival = 0; transRecord.NetAmount = grossAmount; transRecord.ArrivalGrossAmount = arrGrossAmount; transRecord.ArrivalNetAmount = arrGrossAmount; transRecord.ArrivalCurrency = arrCurrencyCode; transRecord.BuyerVatNumberIsValid = false; transRecord.BuyerAddress = string.Empty; transRecord.TransactionType = eBayTransaction.transactionType; return transRecord; } } }