From a42342456185ed230962bc98ff492cf1778d8e3e Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 13 Mar 2024 13:15:14 -0700 Subject: [PATCH] adds better error handling on part searches --- Binner/Binner.Web/ClientApp/src/custom.css | 3 +- .../ClientApp/src/pages/Inventory.js | 32 +- .../Binner.Web/Controllers/PartController.cs | 35 +- .../Binner.Common/Integrations/DigikeyApi.cs | 1700 ++++++++--------- .../Binner.Common/Integrations/MouserApi.cs | 2 +- .../Binner.Common/Services/PartService.cs | 184 +- .../Integrations/ApiResponseState.cs | 16 + 7 files changed, 1053 insertions(+), 919 deletions(-) create mode 100644 Binner/Library/Binner.Model/Integrations/ApiResponseState.cs diff --git a/Binner/Binner.Web/ClientApp/src/custom.css b/Binner/Binner.Web/ClientApp/src/custom.css index 7175979e..6e28b6c3 100644 --- a/Binner/Binner.Web/ClientApp/src/custom.css +++ b/Binner/Binner.Web/ClientApp/src/custom.css @@ -605,7 +605,7 @@ section.formHeader { } .page-banner { - height: 30px; + min-height: 30px; } .page-notice { @@ -648,6 +648,7 @@ section.formHeader { cursor: pointer; width: 100%; max-height: 28px; + margin-bottom: 1px; } .page-error div { diff --git a/Binner/Binner.Web/ClientApp/src/pages/Inventory.js b/Binner/Binner.Web/ClientApp/src/pages/Inventory.js index fb6e666c..a555348a 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Inventory.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Inventory.js @@ -112,7 +112,7 @@ export function Inventory(props) { const [loadingPartTypes, setLoadingPartTypes] = useState(true); const [loadingRecent, setLoadingRecent] = useState(true); const [partMetadataIsSubscribed, setPartMetadataIsSubscribed] = useState(false); - const [partMetadataError, setPartMetadataError] = useState(null); + const [partMetadataErrors, setPartMetadataErrors] = useState([]); const [saveMessage, setSaveMessage] = useState(""); const [bulkScanIsOpen, setBulkScanIsOpen] = useState(false); const [partExistsInInventory, setPartExistsInInventory] = useState(false); @@ -139,7 +139,7 @@ export function Inventory(props) { setIsEditing(newIsEditing); const fetchData = async () => { setPartMetadataIsSubscribed(false); - setPartMetadataError(null); + setPartMetadataErrors([]); await fetchPartTypes(); await fetchRecentRows(); if (partNumberStr) { @@ -179,7 +179,7 @@ export function Inventory(props) { Inventory.infoAbortController = new AbortController(); setLoadingPartMetadata(true); setPartMetadataIsSubscribed(false); - setPartMetadataError(null); + setPartMetadataErrors([]); try { const includeInventorySearch = !pageHasParameters; const { data, existsInInventory } = await doFetchPartMetadata(input, localPart, includeInventorySearch); @@ -328,11 +328,7 @@ export function Inventory(props) { if (!data) return; if (data.errors && data.errors.length > 0) { - setPartMetadataError(`Error: [${data.apiName}] ${data.errors.join()}`); - setMetadataParts([]); - setInfoResponse({}); - setLoadingPartMetadata(false); - return; + setPartMetadataErrors(data.errors); } let metadataParts = []; @@ -489,7 +485,7 @@ export function Inventory(props) { // barcode found if (cleanPartNumber) { setPartMetadataIsSubscribed(false); - setPartMetadataError(null); + setPartMetadataErrors([]); if (!isEditing) setPartFromMetadata(metadataParts, { ...partInfo, quantity: partInfo.quantityAvailable }); if (viewPreferences.rememberLast) updateViewPreferences({ lastQuantity: partInfo.quantityAvailable }); @@ -501,7 +497,7 @@ export function Inventory(props) { // no barcode info found, try searching the part number if (cleanPartNumber) { setPartMetadataIsSubscribed(false); - setPartMetadataError(null); + setPartMetadataErrors([]); let newQuantity = parseInt(input.value?.quantity) || DefaultQuantity; if (isNaN(newQuantity)) newQuantity = 1; const newPart = { @@ -780,7 +776,7 @@ export function Inventory(props) { //e.preventDefault(); //e.stopPropagation(); setPartMetadataIsSubscribed(false); - setPartMetadataError(null); + setPartMetadataErrors([]); let searchPartNumber = control.value; if (searchPartNumber && searchPartNumber.length >= MinSearchKeywordLength) { @@ -806,7 +802,7 @@ export function Inventory(props) { e.preventDefault(); e.stopPropagation(); setPartMetadataIsSubscribed(false); - setPartMetadataError(null); + setPartMetadataErrors([]); const updatedPart = { ...part }; updatedPart[control.name] = control.value; @@ -985,10 +981,12 @@ export function Inventory(props) { )} - {partMetadataError && ( -
setPartMetadataError(null)}> - {partMetadataError} -
+ {partMetadataErrors?.length > 0 && ( + partMetadataErrors.map((error, key) => ( +
setPartMetadataErrors(_.filter(partMetadataErrors, i => i !== error))}> + {error} +
+ )) )} @@ -1421,7 +1419,7 @@ export function Inventory(props) { ); - }, [inputPartNumber, part, viewPreferences.rememberLast, loadingPart, loadingPartMetadata, partMetadataError, isEditing, allPartTypes, isDirty, handlePartTypeChange]); + }, [inputPartNumber, part, viewPreferences.rememberLast, loadingPart, loadingPartMetadata, partMetadataErrors, isEditing, allPartTypes, isDirty, handlePartTypeChange]); return (
diff --git a/Binner/Binner.Web/Controllers/PartController.cs b/Binner/Binner.Web/Controllers/PartController.cs index 2307f522..3b5be88b 100644 --- a/Binner/Binner.Web/Controllers/PartController.cs +++ b/Binner/Binner.Web/Controllers/PartController.cs @@ -450,24 +450,31 @@ public async Task GetLowStockAsync([FromQuery] PaginatedRequest r [HttpGet("info")] public async Task GetPartInfoAsync([FromQuery] string partNumber, [FromQuery] string partTypeId = "", [FromQuery] string mountingTypeId = "", [FromQuery] string supplierPartNumbers = "") { - var partType = partTypeId; - var mountingType = mountingTypeId; - if (int.TryParse(partTypeId, out var parsedPartTypeId)) - { - var partTypeWithName = await _partTypeService.GetPartTypeAsync(parsedPartTypeId); - if (partTypeWithName != null) partType = partTypeWithName.Name; - } - if (int.TryParse(mountingTypeId, out var parsedMountingTypeId)) + try { - if (Enum.IsDefined(typeof(MountingType), parsedMountingTypeId)) + var partType = partTypeId; + var mountingType = mountingTypeId; + if (int.TryParse(partTypeId, out var parsedPartTypeId)) { - var mountingTypeEnum = (MountingType)parsedMountingTypeId; - mountingType = mountingTypeEnum.ToString(); + var partTypeWithName = await _partTypeService.GetPartTypeAsync(parsedPartTypeId); + if (partTypeWithName != null) partType = partTypeWithName.Name; + } + if (int.TryParse(mountingTypeId, out var parsedMountingTypeId)) + { + if (Enum.IsDefined(typeof(MountingType), parsedMountingTypeId)) + { + var mountingTypeEnum = (MountingType)parsedMountingTypeId; + mountingType = mountingTypeEnum.ToString(); + } } - } - var metadata = await _partService.GetPartInformationAsync(partNumber, partType ?? string.Empty, mountingType, supplierPartNumbers); - return Ok(metadata); + var metadata = await _partService.GetPartInformationAsync(partNumber, partType ?? string.Empty, mountingType, supplierPartNumbers); + return Ok(metadata); + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, new ExceptionResponse("Failed to fetch part information! ", ex)); + } } /// diff --git a/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs b/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs index a6cb1947..a1387be6 100644 --- a/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs +++ b/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs @@ -1,854 +1,854 @@ -using ApiClient.OAuth2; -using Binner.Common.Extensions; -using Binner.Common.Integrations.Models; -using Binner.Common.Services; -using Binner.Global.Common; -using Binner.Model; -using Binner.Model.Configuration; -using Binner.Model.Configuration.Integrations; -using Binner.Model.Integrations.DigiKey; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Net.Http; -using System.Security.Authentication; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using TypeSupport.Extensions; - -namespace Binner.Common.Integrations -{ - public class DigikeyApi : IIntegrationApi - { - public static readonly TimeSpan MaxAuthorizationWaitTime = TimeSpan.FromSeconds(30); - - #region Regex Matching - private readonly Regex PercentageRegex = new Regex("^\\d{0,4}(\\.\\d{0,4})? *%?$", RegexOptions.Compiled); - private readonly Regex PowerRegex = new Regex("^(\\d+[\\/\\d. ]*[Ww]$|\\d*[Ww]$)", RegexOptions.Compiled); - private readonly Regex ResistanceRegex = new Regex("^(\\d+[\\d. ]*[KkMm]$|\\d*[KkMm]$|\\d*(?i)ohm(?-i)$)", RegexOptions.Compiled); - private readonly Regex CapacitanceRegex = new Regex("^\\d+\\.?\\d*(uf|pf|mf|f)$", RegexOptions.Compiled); - private readonly Regex VoltageRegex = new Regex("^\\d+\\.?\\d*(v|mv)$", RegexOptions.Compiled); - private readonly Regex CurrentRegex = new Regex("^\\d+\\.?\\d*(a|ma)$", RegexOptions.Compiled); - private readonly Regex InductanceRegex = new Regex("^\\d+\\.?\\d*(nh|uh|h)$", RegexOptions.Compiled); - #endregion - - // the full url to the Api - private readonly DigikeyConfiguration _configuration; - private readonly LocaleConfiguration _localeConfiguration; - private readonly OAuth2Service _oAuth2Service; - private readonly ICredentialService _credentialService; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly RequestContextAccessor _requestContext; - private readonly HttpClient _client; - private readonly ManualResetEvent _manualResetEvent = new ManualResetEvent(false); - private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings - { - Formatting = Formatting.Indented, - // ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() }, - Converters = new List { new StringEnumConverter() } - }; - - /// - /// Get the OAuth2 service associated with this api - /// - public OAuth2Service OAuth2Service => _oAuth2Service; - - public bool IsEnabled => _configuration.Enabled; - - public IApiConfiguration Configuration => _configuration; - - public DigikeyApi(DigikeyConfiguration configuration, LocaleConfiguration localeConfiguration, ICredentialService credentialService, IHttpContextAccessor httpContextAccessor, RequestContextAccessor requestContext) - { - _configuration = configuration; - _localeConfiguration = localeConfiguration; - _oAuth2Service = new OAuth2Service(configuration); - _credentialService = credentialService; - _httpContextAccessor = httpContextAccessor; - _client = new HttpClient(); - _requestContext = requestContext; - } - - public enum MountingTypes - { - None = 0, - SurfaceMount = 3, - ThroughHole = 80 - } - - public async Task GetOrderAsync(string orderId, Dictionary? additionalOptions = null) - { - var authResponse = await AuthorizeAsync(); - if (!authResponse.IsAuthorized) - return ApiResponse.Create(true, authResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); - return await WrapApiRequestAsync(authResponse, async (authenticationResponse) => - { - try - { - // set what fields we want from the API - var uri = Url.Combine(_configuration.ApiUrl, "OrderDetails/v3/Status/", orderId); - var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Get, uri); - // perform a keywords API search - var response = await _client.SendAsync(requestMessage); - if (TryHandleResponse(response, authenticationResponse, out var apiResponse)) - { - return apiResponse; - } - - // 200 OK - var resultString = response.Content.ReadAsStringAsync().Result; - var results = JsonConvert.DeserializeObject(resultString, _serializerSettings) ?? new(); - return new ApiResponse(results, nameof(DigikeyApi)); - } - catch (Exception) - { - throw; - } - }); - } - - public async Task GetProductDetailsAsync(string partNumber, Dictionary? additionalOptions = null) - { - var authResponse = await AuthorizeAsync(); - if (!authResponse.IsAuthorized) - return ApiResponse.Create(true, authResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); - return await WrapApiRequestAsync(authResponse, async (authenticationResponse) => - { - try +using ApiClient.OAuth2; +using Binner.Common.Extensions; +using Binner.Common.Integrations.Models; +using Binner.Common.Services; +using Binner.Global.Common; +using Binner.Model; +using Binner.Model.Configuration; +using Binner.Model.Configuration.Integrations; +using Binner.Model.Integrations.DigiKey; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net.Http; +using System.Security.Authentication; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using TypeSupport.Extensions; + +namespace Binner.Common.Integrations +{ + public class DigikeyApi : IIntegrationApi + { + public static readonly TimeSpan MaxAuthorizationWaitTime = TimeSpan.FromSeconds(30); + + #region Regex Matching + private readonly Regex PercentageRegex = new Regex("^\\d{0,4}(\\.\\d{0,4})? *%?$", RegexOptions.Compiled); + private readonly Regex PowerRegex = new Regex("^(\\d+[\\/\\d. ]*[Ww]$|\\d*[Ww]$)", RegexOptions.Compiled); + private readonly Regex ResistanceRegex = new Regex("^(\\d+[\\d. ]*[KkMm]$|\\d*[KkMm]$|\\d*(?i)ohm(?-i)$)", RegexOptions.Compiled); + private readonly Regex CapacitanceRegex = new Regex("^\\d+\\.?\\d*(uf|pf|mf|f)$", RegexOptions.Compiled); + private readonly Regex VoltageRegex = new Regex("^\\d+\\.?\\d*(v|mv)$", RegexOptions.Compiled); + private readonly Regex CurrentRegex = new Regex("^\\d+\\.?\\d*(a|ma)$", RegexOptions.Compiled); + private readonly Regex InductanceRegex = new Regex("^\\d+\\.?\\d*(nh|uh|h)$", RegexOptions.Compiled); + #endregion + + // the full url to the Api + private readonly DigikeyConfiguration _configuration; + private readonly LocaleConfiguration _localeConfiguration; + private readonly OAuth2Service _oAuth2Service; + private readonly ICredentialService _credentialService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly RequestContextAccessor _requestContext; + private readonly HttpClient _client; + private readonly ManualResetEvent _manualResetEvent = new ManualResetEvent(false); + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + // ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() }, + Converters = new List { new StringEnumConverter() } + }; + + /// + /// Get the OAuth2 service associated with this api + /// + public OAuth2Service OAuth2Service => _oAuth2Service; + + public bool IsEnabled => _configuration.Enabled; + + public IApiConfiguration Configuration => _configuration; + + public DigikeyApi(DigikeyConfiguration configuration, LocaleConfiguration localeConfiguration, ICredentialService credentialService, IHttpContextAccessor httpContextAccessor, RequestContextAccessor requestContext) + { + _configuration = configuration; + _localeConfiguration = localeConfiguration; + _oAuth2Service = new OAuth2Service(configuration); + _credentialService = credentialService; + _httpContextAccessor = httpContextAccessor; + _client = new HttpClient(); + _requestContext = requestContext; + } + + public enum MountingTypes + { + None = 0, + SurfaceMount = 3, + ThroughHole = 80 + } + + public async Task GetOrderAsync(string orderId, Dictionary? additionalOptions = null) + { + var authResponse = await AuthorizeAsync(); + if (!authResponse.IsAuthorized) + return ApiResponse.Create(true, authResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); + return await WrapApiRequestAsync(authResponse, async (authenticationResponse) => + { + try + { + // set what fields we want from the API + var uri = Url.Combine(_configuration.ApiUrl, "OrderDetails/v3/Status/", orderId); + var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Get, uri); + // perform a keywords API search + var response = await _client.SendAsync(requestMessage); + if (TryHandleResponse(response, authenticationResponse, out var apiResponse)) + { + return apiResponse; + } + + // 200 OK + var resultString = response.Content.ReadAsStringAsync().Result; + var results = JsonConvert.DeserializeObject(resultString, _serializerSettings) ?? new(); + return new ApiResponse(results, nameof(DigikeyApi)); + } + catch (Exception) + { + throw; + } + }); + } + + public async Task GetProductDetailsAsync(string partNumber, Dictionary? additionalOptions = null) + { + var authResponse = await AuthorizeAsync(); + if (!authResponse.IsAuthorized) + return ApiResponse.Create(true, authResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); + return await WrapApiRequestAsync(authResponse, async (authenticationResponse) => + { + try { // set what fields we want from the API var uri = Url.Combine(_configuration.ApiUrl, "Search/v3/Products/", HttpUtility.UrlEncode(partNumber)); - var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Get, uri); - // perform a keywords API search - var response = await _client.SendAsync(requestMessage); - if (TryHandleResponse(response, authenticationResponse, out var apiResponse)) - { - return apiResponse; - } - - // 200 OK - var resultString = response.Content.ReadAsStringAsync().Result; - var results = JsonConvert.DeserializeObject(resultString, _serializerSettings) ?? new(); - return new ApiResponse(results, nameof(DigikeyApi)); - } - catch (Exception) - { - throw; - } - }); - } - - /// - /// Get information about a DigiKey product via a barcode value - /// - /// - /// - /// - public async Task GetBarcodeDetailsAsync(string barcode, ScannedBarcodeType barcodeType) - { - var authResponse = await AuthorizeAsync(); - if (!authResponse.IsAuthorized) - return ApiResponse.Create(true, authResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); - return await WrapApiRequestAsync(authResponse, async (authenticationResponse) => - { - try - { - // set what fields we want from the API - // https://developer.digikey.com/products/barcode/barcoding/productbarcode - - var is2dBarcode = barcode.StartsWith("[)>"); - var endpoint = "Barcoding/v3/ProductBarcodes/"; - switch (barcodeType) - { - case ScannedBarcodeType.Product: - default: - endpoint = "Barcoding/v3/ProductBarcodes/"; - if (is2dBarcode) - endpoint = "Barcoding/v3/Product2DBarcodes/"; - break; - case ScannedBarcodeType.Packlist: - endpoint = "Barcoding/v3/PackListBarcodes/"; - if (is2dBarcode) - endpoint = "Barcoding/v3/PackList2DBarcodes/"; - break; - } - - var barcodeFormatted = barcode.ToString(); - if (is2dBarcode) - { - // DigiKey requires the GS (Group separator) to be \u241D, and the RS (Record separator) to be \u241E - // GS - var gsReplacement = "\u241D"; - barcodeFormatted = barcodeFormatted.Replace("\u001d", gsReplacement); - barcodeFormatted = barcodeFormatted.Replace("\u005d", gsReplacement); - // RS - var rsReplacement = "\u241E"; - barcodeFormatted = barcodeFormatted.Replace("\u001e", rsReplacement); - barcodeFormatted = barcodeFormatted.Replace("\u005e", rsReplacement); - - } - var barcodeFormattedPath = HttpUtility.UrlEncode(barcodeFormatted); - - //var uri = Url.Combine(_configuration.ApiUrl, endpoint, barcodeFormatted); - //var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Get, uri); - var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Get, string.Join("/", _configuration.ApiUrl, endpoint) + barcodeFormattedPath); - // perform a keywords API search - var response = await _client.SendAsync(requestMessage); - if (TryHandleResponse(response, authenticationResponse, out var apiResponse)) - { - var contentString = response.Content.ReadAsStringAsync().Result; - apiResponse.Errors.Add(contentString); - return apiResponse; - } - - // 200 OK - var resultString = response.Content.ReadAsStringAsync().Result; - var results = JsonConvert.DeserializeObject(resultString, _serializerSettings) ?? new(); - return new ApiResponse(results, nameof(DigikeyApi)); - } - catch (Exception) - { - throw; - } - }); - } - - public async Task GetCategoriesAsync() - { - var authResponse = await AuthorizeAsync(); - if (!authResponse.IsAuthorized) - return ApiResponse.Create(true, authResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); - return await WrapApiRequestAsync(authResponse, async (authenticationResponse) => - { - try - { - // set what fields we want from the API - var uri = Url.Combine(_configuration.ApiUrl, "Search/v3/Categories"); - var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Get, uri); - // perform a keywords API search - var response = await _client.SendAsync(requestMessage); - if (TryHandleResponse(response, authenticationResponse, out var apiResponse)) - { - return apiResponse; - } - - // 200 OK - var resultString = response.Content.ReadAsStringAsync().Result; - var results = JsonConvert.DeserializeObject(resultString, _serializerSettings) ?? new(); - return new ApiResponse(results, nameof(DigikeyApi)); - } - catch (Exception) - { - throw; - } - }); - } - - public Task SearchAsync(string partNumber, int recordCount = 25, Dictionary? additionalOptions = null) => SearchAsync(partNumber, string.Empty, string.Empty, recordCount, additionalOptions); - - public Task SearchAsync(string partNumber, string partType, int recordCount = 25, Dictionary? additionalOptions = null) => SearchAsync(partNumber, partType, string.Empty, recordCount, additionalOptions); - - public async Task SearchAsync(string partNumber, string partType, string mountingType, int recordCount = 25, Dictionary? additionalOptions = null) - { - if (!(recordCount > 0)) throw new ArgumentOutOfRangeException(nameof(recordCount)); - var authResponse = await AuthorizeAsync(); - if (!authResponse.IsAuthorized) - return ApiResponse.Create(true, authResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); - - var keywords = new List(); - if (!string.IsNullOrEmpty(partNumber)) - keywords = partNumber.ToLower().Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries).ToList(); - var packageTypeEnum = MountingTypes.None; - if (!string.IsNullOrEmpty(mountingType)) - { - switch (mountingType.ToLower()) - { - case "surface mount": - packageTypeEnum = MountingTypes.SurfaceMount; - break; - case "through hole": - packageTypeEnum = MountingTypes.ThroughHole; - break; - } - } - - return await WrapApiRequestAsync(authResponse, async (authenticationResponse) => - { - try - { - // set what fields we want from the API - var includes = new List { - "DigiKeyPartNumber", - "QuantityAvailable", - "Manufacturer", - "ManufacturerPartNumber", - "PrimaryDatasheet", - "ProductDescription", - "DetailedDescription", - "MinimumOrderQuantity", - "NonStock", - "UnitPrice", - "ProductStatus", - "ProductUrl", - "PrimaryPhoto", - "PrimaryVideo", - "Packaging", - "AlternatePackaging", - "Family", - "Category", - "Parameters" - }; - var values = new Dictionary - { - { "Includes", $"Products({string.Join(",", includes)})" }, - }; - var uri = Url.Combine(_configuration.ApiUrl, "/Search/v3/Products", $"/Keyword?" + string.Join("&", values.Select(x => $"{x.Key}={x.Value}"))); - var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Post, uri); - var taxonomies = MapTaxonomies(partType, packageTypeEnum); - var parametricFilters = MapParametricFilters(keywords, packageTypeEnum, taxonomies); - var request = new KeywordSearchRequest - { - Keywords = string.Join(" ", keywords), - RecordCount = recordCount, - Filters = new Filters - { - TaxonomyIds = taxonomies.Select(x => (int)x).ToList(), - ParametricFilters = parametricFilters - }, - SearchOptions = new List { } - }; - var json = JsonConvert.SerializeObject(request, _serializerSettings); - requestMessage.Content = new StringContent(json, Encoding.UTF8, "application/json"); - // perform a keywords API search - var response = await _client.SendAsync(requestMessage); - if (TryHandleResponse(response, authenticationResponse, out var apiResponse)) - { - return apiResponse; - } - - // 200 OK - var resultString = response.Content.ReadAsStringAsync().Result; - var results = JsonConvert.DeserializeObject(resultString, _serializerSettings) ?? new(); - return new ApiResponse(results, nameof(DigikeyApi)); - } - catch (UnauthorizedAccessException) - { - // refresh token likely expired, need to re-authenticate - throw new DigikeyUnauthorizedException(authenticationResponse); - } - catch (Exception) - { - throw; - } - }); - } - - private ICollection MapParametricFilters(ICollection keywords, MountingTypes packageType, ICollection taxonomies) - { - var filters = new List(); - var percent = ""; - var power = ""; - var resistance = ""; - var capacitance = ""; - var voltageRating = ""; - var currentRating = ""; - var inductance = ""; - foreach (var keyword in keywords) - { - if (PercentageRegex.IsMatch(keyword)) - percent = keyword; - if (PowerRegex.IsMatch(keyword)) - power = keyword; - if (ResistanceRegex.IsMatch(keyword)) - resistance = keyword; - if (CapacitanceRegex.IsMatch(keyword)) - capacitance = keyword; - if (VoltageRegex.IsMatch(keyword)) - voltageRating = keyword; - if (CurrentRegex.IsMatch(keyword)) - currentRating = keyword; - if (InductanceRegex.IsMatch(keyword)) - inductance = keyword; - } - // add tolerance parameter - if (keywords.Contains("precision") || !string.IsNullOrEmpty(percent)) - { - if (keywords.Contains("precision")) - keywords.Remove("precision"); - if (keywords.Contains(percent)) - keywords.Remove(percent); - else - percent = "1%"; - var filter = new ParametricFilter - { - ParameterId = (int)Parametrics.Tolerance, - ValueId = ((int)GetTolerance(percent)).ToString() - }; - filters.Add(filter); - } - if (!string.IsNullOrEmpty(power)) - { - keywords.Remove(power); - var filter = new ParametricFilter - { - ParameterId = (int)Parametrics.Power, - ValueId = GetPower(power) - }; - filters.Add(filter); - } - if (!string.IsNullOrEmpty(resistance)) - { - keywords.Remove(resistance); - var filter = new ParametricFilter - { - ParameterId = (int)Parametrics.Resistance, - ValueId = GetResistance(resistance) - }; - filters.Add(filter); - } - if (!string.IsNullOrEmpty(capacitance)) - { - keywords.Remove(capacitance); - var filter = new ParametricFilter - { - ParameterId = (int)Parametrics.Capacitance, - ValueId = GetCapacitance(capacitance) - }; - filters.Add(filter); - } - if (!string.IsNullOrEmpty(voltageRating)) - { - keywords.Remove(voltageRating); - var filter = new ParametricFilter - { - ParameterId = (int)Parametrics.VoltageRating, - ValueId = GetVoltageRating(voltageRating) - }; - filters.Add(filter); - } - if (!string.IsNullOrEmpty(currentRating)) - { - keywords.Remove(currentRating); - var filter = new ParametricFilter - { - ParameterId = (int)Parametrics.CurrentRating, - ValueId = GetVoltageRating(currentRating) - }; - filters.Add(filter); - } - if (!string.IsNullOrEmpty(inductance)) - { - keywords.Remove(inductance); - var filter = new ParametricFilter - { - ParameterId = (int)Parametrics.Inductance, - ValueId = GetInductance(inductance) - }; - filters.Add(filter); - } - // dont add mounting type to resistors, they dont seem to be mapped - if (!taxonomies.ContainsAny(new List { Taxonomies.Resistor, Taxonomies.SurfaceMountResistor, Taxonomies.ThroughHoleResistor })) - { - if (packageType != MountingTypes.None) - filters.Add(new ParametricFilter - { - ParameterId = (int)Parametrics.MountingType, - ValueId = ((int)packageType).ToString() - }); - } - return filters; - } - - private ICollection MapTaxonomies(string partType, MountingTypes packageType) - { - var taxonomies = new List(); - var taxonomy = Taxonomies.None; - if (!string.IsNullOrEmpty(partType) && partType != "-1") - { - if (Enum.TryParse(partType, true, out taxonomy)) - { - var addBaseType = true; - // also map all the alternates - var memberInfos = typeof(Taxonomies).GetMember(taxonomy.ToString()); - var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == typeof(Taxonomies)); - if (enumValueMemberInfo != null) - { - var valueAttributes = enumValueMemberInfo.GetCustomAttributes(typeof(AlternatesAttribute), false); - if (valueAttributes.Any()) - { - var alternateIds = ((AlternatesAttribute)valueAttributes[0]).Ids; - // taxonomies.AddRange(alternateIds); - } - } - - switch (taxonomy) - { - case Taxonomies.Resistor: - if (packageType == MountingTypes.ThroughHole) - { - taxonomies.Add(Taxonomies.ThroughHoleResistor); - addBaseType = false; - } - if (packageType == MountingTypes.SurfaceMount) - { - taxonomies.Add(Taxonomies.SurfaceMountResistor); - addBaseType = false; - } - break; - } - if (addBaseType) - taxonomies.Add(taxonomy); - } - } - - return taxonomies; - } - - /// - /// Handle known error conditions first, if response is OK false will be returned - /// - /// - /// - /// - /// - /// - private bool TryHandleResponse(HttpResponseMessage response, OAuthAuthorization authenticationResponse, out IApiResponse apiResponse) - { - apiResponse = ApiResponse.Create($"Api returned error status code {response.StatusCode}: {response.ReasonPhrase}", nameof(DigikeyApi)); - if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - throw new DigikeyUnauthorizedException(authenticationResponse); - else if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable) - { - if (response.Headers.Contains("X-RateLimit-Limit")) - { - // throttled - var remainingTime = TimeSpan.Zero; - if (response.Headers.Contains("X-RateLimit-Remaining")) - remainingTime = TimeSpan.FromSeconds(int.Parse(response.Headers.GetValues("X-RateLimit-Remaining").First())); - apiResponse = ApiResponse.Create($"{nameof(DigikeyApi)} request throttled. Try again in {remainingTime}", nameof(DigikeyApi)); - return true; - } - - // return generic error - return true; - } - else if (response.IsSuccessStatusCode) - { - // allow processing of response - return false; - } - else if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) - { - apiResponse = ApiResponse.Create($"Api returned error status code {response.StatusCode}: {response.ReasonPhrase}", nameof(DigikeyApi)); - var resultString = response.Content.ReadAsStringAsync().Result; - if (!string.IsNullOrEmpty(resultString)) - apiResponse.Errors.Add(resultString); - else - apiResponse.Errors.Add($"Api returned error status code {response.StatusCode}: {response.ReasonPhrase}"); - return true; - } - - // return generic error - return true; - } - - /// - /// Wraps an API request - if the request is unauthorized it will refresh the Auth token and re-issue the request - /// - /// - /// - /// - private async Task WrapApiRequestAsync(OAuthAuthorization authResponse, Func> func) - { - try - { - return await func(authResponse); - } - catch (DigikeyUnauthorizedException ex) - { - // get refresh token, retry - _oAuth2Service.AccessTokens.RefreshToken = ex.Authorization.RefreshToken; - var token = await _oAuth2Service.RefreshTokenAsync(); - if (_httpContextAccessor.HttpContext == null) - throw new Exception($"HttpContext cannot be null!"); - var referer = _httpContextAccessor.HttpContext.Request.Headers["Referer"].ToString(); - var refreshTokenResponse = new OAuthAuthorization(nameof(DigikeyApi), _configuration.ClientId ?? string.Empty, referer) - { - AccessToken = token.AccessToken ?? string.Empty, - RefreshToken = token.RefreshToken ?? string.Empty, - CreatedUtc = DateTime.UtcNow, - ExpiresUtc = DateTime.UtcNow.Add(TimeSpan.FromSeconds(token.ExpiresIn)), - AuthorizationReceived = true, - UserId = _requestContext.GetUserContext()?.UserId, - }; - if (refreshTokenResponse.IsAuthorized) - { - // save the credential - await _credentialService.SaveOAuthCredentialAsync(new OAuthCredential - { - Provider = nameof(DigikeyApi), - AccessToken = refreshTokenResponse.AccessToken, - RefreshToken = refreshTokenResponse.RefreshToken, - DateCreatedUtc = refreshTokenResponse.CreatedUtc, - DateExpiresUtc = refreshTokenResponse.ExpiresUtc, - }); - try - { - // call the API again using the refresh token - return await func(refreshTokenResponse); - } - catch (DigikeyUnauthorizedException) - { - // refresh token failed, restart access token retrieval process - await ForgetAuthenticationTokens(); - var freshResponse = await AuthorizeAsync(); - if (freshResponse.MustAuthorize) - return ApiResponse.Create(true, freshResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); - // call the API again - return await func(freshResponse); - } - } - // user must authorize - // request a token if we don't already have one - var authRequest = await CreateOAuthAuthorizationRequestAsync(_requestContext.GetUserContext()?.UserId); - return ApiResponse.Create(true, authRequest.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); - } - } - - private async Task ForgetAuthenticationTokens() - { - var user = _requestContext.GetUserContext(); - await _credentialService.RemoveOAuthCredentialAsync(nameof(DigikeyApi)); - } - - private async Task AuthorizeAsync() - { - var user = _requestContext.GetUserContext(); - if (user != null && user.UserId <= 0) - throw new AuthenticationException("User is not authenticated!"); - - // check if we have saved an existing auth credential in the database - var credential = await _credentialService.GetOAuthCredentialAsync(nameof(DigikeyApi)); - if (credential != null) - { - // reuse a saved oAuth credential - var referer = _httpContextAccessor.HttpContext.Request.Headers["Referer"].ToString(); - var authRequest = new OAuthAuthorization(nameof(DigikeyApi), _configuration.ClientId ?? string.Empty, referer) - { - AccessToken = credential.AccessToken ?? string.Empty, - RefreshToken = credential.RefreshToken ?? string.Empty, - CreatedUtc = credential.DateCreatedUtc, - ExpiresUtc = credential.DateExpiresUtc, - AuthorizationReceived = true, - UserId = user?.UserId - }; - - return authRequest; - } - - // user must authorize - // request a token if we don't already have one - return await CreateOAuthAuthorizationRequestAsync(user?.UserId); - } - - private async Task CreateOAuthAuthorizationRequestAsync(int? userId) - { - var referer = _httpContextAccessor.HttpContext.Request.Headers["Referer"].ToString(); - var uriBuilder = new UriBuilder(referer); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query["api-authenticate"] = "true"; - query["api"] = "DigiKey"; - uriBuilder.Query = query.ToString(); - var authRequest = new OAuthAuthorization(nameof(DigikeyApi), _configuration.ClientId ?? string.Empty, uriBuilder.ToString()) - { - UserId = userId - }; - authRequest = await _credentialService.CreateOAuthRequestAsync(authRequest); - // no scopes necessary - var scopes = ""; - // state will be send as the RequestId - var state = authRequest.Id.ToString(); - var authUrl = _oAuth2Service.GenerateAuthUrl(scopes, state); - - return new OAuthAuthorization(nameof(DigikeyApi), true, authUrl); - } - - private HttpRequestMessage CreateRequest(OAuthAuthorization authResponse, HttpMethod method, string url) - { - var message = new HttpRequestMessage(method, url); - message.Headers.Add("X-DIGIKEY-Client-Id", authResponse.ClientId); - message.Headers.Add("Authorization", $"Bearer {authResponse.AccessToken}"); - message.Headers.Add("X-DIGIKEY-Locale-Site", _configuration.Site.ToString()); - message.Headers.Add("X-DIGIKEY-Locale-Language", _localeConfiguration.Language.ToString().ToLower()); - message.Headers.Add("X-DIGIKEY-Locale-Currency", _localeConfiguration.Currency.ToString().ToUpper()); - return message; - } - - private HttpRequestMessage CreateRequest(OAuthAuthorization authResponse, HttpMethod method, Uri uri) - { - var message = new HttpRequestMessage(method, uri); - message.Headers.Add("X-DIGIKEY-Client-Id", authResponse.ClientId); - message.Headers.Add("Authorization", $"Bearer {authResponse.AccessToken}"); - message.Headers.Add("X-DIGIKEY-Locale-Site", _configuration.Site.ToString()); - message.Headers.Add("X-DIGIKEY-Locale-Language", _localeConfiguration.Language.ToString().ToLower()); - message.Headers.Add("X-DIGIKEY-Locale-Currency", _localeConfiguration.Currency.ToString().ToUpper()); - return message; - } - - private Tolerances GetTolerance(string perc) - { - return GetEnumByDescription(perc); - } - - private string GetPower(string power) - { - power = new Regex("[Ww]|").Replace(power, ""); - // convert decimal percentages to fractions - if (power.Contains(".")) - { - var fraction = MathExtensions.RealToFraction(double.Parse(power), 0.01); - return ((int)GetEnumByDescription($"{fraction.Numerator}/{fraction.Denominator}")).ToString(); - } - return ((int)GetEnumByDescription(power)).ToString(); - } - - private string GetResistance(string resistance) - { - var val = new String(resistance.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); - if (string.IsNullOrEmpty(val)) - val = "0"; - var unitsParsed = resistance.Replace(val, "").ToLower(); - var units = "ohms"; - switch (unitsParsed) - { - case "k": - case "kohms": - units = "kOhms"; - break; - case "m": - case "mohms": - units = "mOhms"; - break; - } - var result = $"u{val} {units}"; - return result; - } - - private string GetCapacitance(string capacitance) - { - var val = new String(capacitance.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); - var unitsParsed = capacitance.Replace(val, "").ToLower(); - var units = "µF"; - switch (unitsParsed) - { - case "uf": - units = "µF"; - break; - case "nf": - // convert to uf, api doesn't seem to handle it? - val = (decimal.Parse(val) * 0.001M).ToString(); - units = "µF"; - break; - case "pf": - units = "pF"; - break; - case "f": - units = "F"; - break; - } - var result = $"u{val}{units}"; - return result; - } - - private string GetVoltageRating(string voltage) - { - var val = new String(voltage.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); - var unitsParsed = voltage.Replace(val, "").ToLower(); - var units = "V"; - switch (unitsParsed) - { - case "v": - units = "V"; - break; - } - var result = $"u{val}{units}"; - return result; - } - - private string GetCurrentRating(string current) - { - var val = new String(current.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); - var unitsParsed = current.Replace(val, "").ToLower(); - var units = "A"; - switch (unitsParsed) - { - case "a": - units = "A"; - break; - case "ma": - units = "mA"; - break; - } - var result = $"u{val}{units}"; - return result; - } - - private string GetInductance(string inductance) - { - var val = new String(inductance.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); - var unitsParsed = inductance.Replace(val, "").ToLower(); - var units = "µH"; - switch (unitsParsed) - { - case "uh": - units = "µH"; - break; - case "nh": - units = "nH"; - break; - case "mh": - units = "mH"; - break; - case "h": - units = "H"; - break; - } - var result = $"u{val}{units}"; - return result; - } - - private T? GetEnumByDescription(string description) - { - var type = typeof(T).GetExtendedType(); - foreach (var val in type.EnumValues) - { - var memberInfos = type.Type.GetMember(val.Value); - var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == type.Type); - var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(DescriptionAttribute), false); - if (valueAttributes != null) - { - var descriptionVal = ((DescriptionAttribute)valueAttributes[0]).Description; - if (descriptionVal.Equals(description)) - return (T)val.Key; - } - } - return default(T); - } - - } - - public class DigikeyUnauthorizedException : Exception - { - public OAuthAuthorization Authorization { get; } - public DigikeyUnauthorizedException(OAuthAuthorization authorization) - { - Authorization = authorization; - } - } + var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Get, uri); + // perform a keywords API search + var response = await _client.SendAsync(requestMessage); + if (TryHandleResponse(response, authenticationResponse, out var apiResponse)) + { + return apiResponse; + } + + // 200 OK + var resultString = response.Content.ReadAsStringAsync().Result; + var results = JsonConvert.DeserializeObject(resultString, _serializerSettings) ?? new(); + return new ApiResponse(results, nameof(DigikeyApi)); + } + catch (Exception) + { + throw; + } + }); + } + + /// + /// Get information about a DigiKey product via a barcode value + /// + /// + /// + /// + public async Task GetBarcodeDetailsAsync(string barcode, ScannedBarcodeType barcodeType) + { + var authResponse = await AuthorizeAsync(); + if (!authResponse.IsAuthorized) + return ApiResponse.Create(true, authResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); + return await WrapApiRequestAsync(authResponse, async (authenticationResponse) => + { + try + { + // set what fields we want from the API + // https://developer.digikey.com/products/barcode/barcoding/productbarcode + + var is2dBarcode = barcode.StartsWith("[)>"); + var endpoint = "Barcoding/v3/ProductBarcodes/"; + switch (barcodeType) + { + case ScannedBarcodeType.Product: + default: + endpoint = "Barcoding/v3/ProductBarcodes/"; + if (is2dBarcode) + endpoint = "Barcoding/v3/Product2DBarcodes/"; + break; + case ScannedBarcodeType.Packlist: + endpoint = "Barcoding/v3/PackListBarcodes/"; + if (is2dBarcode) + endpoint = "Barcoding/v3/PackList2DBarcodes/"; + break; + } + + var barcodeFormatted = barcode.ToString(); + if (is2dBarcode) + { + // DigiKey requires the GS (Group separator) to be \u241D, and the RS (Record separator) to be \u241E + // GS + var gsReplacement = "\u241D"; + barcodeFormatted = barcodeFormatted.Replace("\u001d", gsReplacement); + barcodeFormatted = barcodeFormatted.Replace("\u005d", gsReplacement); + // RS + var rsReplacement = "\u241E"; + barcodeFormatted = barcodeFormatted.Replace("\u001e", rsReplacement); + barcodeFormatted = barcodeFormatted.Replace("\u005e", rsReplacement); + + } + var barcodeFormattedPath = HttpUtility.UrlEncode(barcodeFormatted); + + //var uri = Url.Combine(_configuration.ApiUrl, endpoint, barcodeFormatted); + //var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Get, uri); + var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Get, string.Join("/", _configuration.ApiUrl, endpoint) + barcodeFormattedPath); + // perform a keywords API search + var response = await _client.SendAsync(requestMessage); + if (TryHandleResponse(response, authenticationResponse, out var apiResponse)) + { + var contentString = response.Content.ReadAsStringAsync().Result; + apiResponse.Errors.Add(contentString); + return apiResponse; + } + + // 200 OK + var resultString = response.Content.ReadAsStringAsync().Result; + var results = JsonConvert.DeserializeObject(resultString, _serializerSettings) ?? new(); + return new ApiResponse(results, nameof(DigikeyApi)); + } + catch (Exception) + { + throw; + } + }); + } + + public async Task GetCategoriesAsync() + { + var authResponse = await AuthorizeAsync(); + if (!authResponse.IsAuthorized) + return ApiResponse.Create(true, authResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); + return await WrapApiRequestAsync(authResponse, async (authenticationResponse) => + { + try + { + // set what fields we want from the API + var uri = Url.Combine(_configuration.ApiUrl, "Search/v3/Categories"); + var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Get, uri); + // perform a keywords API search + var response = await _client.SendAsync(requestMessage); + if (TryHandleResponse(response, authenticationResponse, out var apiResponse)) + { + return apiResponse; + } + + // 200 OK + var resultString = response.Content.ReadAsStringAsync().Result; + var results = JsonConvert.DeserializeObject(resultString, _serializerSettings) ?? new(); + return new ApiResponse(results, nameof(DigikeyApi)); + } + catch (Exception) + { + throw; + } + }); + } + + public Task SearchAsync(string partNumber, int recordCount = 25, Dictionary? additionalOptions = null) => SearchAsync(partNumber, string.Empty, string.Empty, recordCount, additionalOptions); + + public Task SearchAsync(string partNumber, string partType, int recordCount = 25, Dictionary? additionalOptions = null) => SearchAsync(partNumber, partType, string.Empty, recordCount, additionalOptions); + + public async Task SearchAsync(string partNumber, string partType, string mountingType, int recordCount = 25, Dictionary? additionalOptions = null) + { + if (!(recordCount > 0)) throw new ArgumentOutOfRangeException(nameof(recordCount)); + var authResponse = await AuthorizeAsync(); + if (!authResponse.IsAuthorized) + return ApiResponse.Create(true, authResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); + + var keywords = new List(); + if (!string.IsNullOrEmpty(partNumber)) + keywords = partNumber.ToLower().Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries).ToList(); + var packageTypeEnum = MountingTypes.None; + if (!string.IsNullOrEmpty(mountingType)) + { + switch (mountingType.ToLower()) + { + case "surface mount": + packageTypeEnum = MountingTypes.SurfaceMount; + break; + case "through hole": + packageTypeEnum = MountingTypes.ThroughHole; + break; + } + } + + return await WrapApiRequestAsync(authResponse, async (authenticationResponse) => + { + try + { + // set what fields we want from the API + var includes = new List { + "DigiKeyPartNumber", + "QuantityAvailable", + "Manufacturer", + "ManufacturerPartNumber", + "PrimaryDatasheet", + "ProductDescription", + "DetailedDescription", + "MinimumOrderQuantity", + "NonStock", + "UnitPrice", + "ProductStatus", + "ProductUrl", + "PrimaryPhoto", + "PrimaryVideo", + "Packaging", + "AlternatePackaging", + "Family", + "Category", + "Parameters" + }; + var values = new Dictionary + { + { "Includes", $"Products({string.Join(",", includes)})" }, + }; + var uri = Url.Combine(_configuration.ApiUrl, "/Search/v3/Products", $"/Keyword?" + string.Join("&", values.Select(x => $"{x.Key}={x.Value}"))); + var requestMessage = CreateRequest(authenticationResponse, HttpMethod.Post, uri); + var taxonomies = MapTaxonomies(partType, packageTypeEnum); + var parametricFilters = MapParametricFilters(keywords, packageTypeEnum, taxonomies); + var request = new KeywordSearchRequest + { + Keywords = string.Join(" ", keywords), + RecordCount = recordCount, + Filters = new Filters + { + TaxonomyIds = taxonomies.Select(x => (int)x).ToList(), + ParametricFilters = parametricFilters + }, + SearchOptions = new List { } + }; + var json = JsonConvert.SerializeObject(request, _serializerSettings); + requestMessage.Content = new StringContent(json, Encoding.UTF8, "application/json"); + // perform a keywords API search + var response = await _client.SendAsync(requestMessage); + if (TryHandleResponse(response, authenticationResponse, out var apiResponse)) + { + return apiResponse; + } + + // 200 OK + var resultString = response.Content.ReadAsStringAsync().Result; + var results = JsonConvert.DeserializeObject(resultString, _serializerSettings) ?? new(); + return new ApiResponse(results, nameof(DigikeyApi)); + } + catch (UnauthorizedAccessException) + { + // refresh token likely expired, need to re-authenticate + throw new DigikeyUnauthorizedException(authenticationResponse); + } + catch (Exception) + { + throw; + } + }); + } + + private ICollection MapParametricFilters(ICollection keywords, MountingTypes packageType, ICollection taxonomies) + { + var filters = new List(); + var percent = ""; + var power = ""; + var resistance = ""; + var capacitance = ""; + var voltageRating = ""; + var currentRating = ""; + var inductance = ""; + foreach (var keyword in keywords) + { + if (PercentageRegex.IsMatch(keyword)) + percent = keyword; + if (PowerRegex.IsMatch(keyword)) + power = keyword; + if (ResistanceRegex.IsMatch(keyword)) + resistance = keyword; + if (CapacitanceRegex.IsMatch(keyword)) + capacitance = keyword; + if (VoltageRegex.IsMatch(keyword)) + voltageRating = keyword; + if (CurrentRegex.IsMatch(keyword)) + currentRating = keyword; + if (InductanceRegex.IsMatch(keyword)) + inductance = keyword; + } + // add tolerance parameter + if (keywords.Contains("precision") || !string.IsNullOrEmpty(percent)) + { + if (keywords.Contains("precision")) + keywords.Remove("precision"); + if (keywords.Contains(percent)) + keywords.Remove(percent); + else + percent = "1%"; + var filter = new ParametricFilter + { + ParameterId = (int)Parametrics.Tolerance, + ValueId = ((int)GetTolerance(percent)).ToString() + }; + filters.Add(filter); + } + if (!string.IsNullOrEmpty(power)) + { + keywords.Remove(power); + var filter = new ParametricFilter + { + ParameterId = (int)Parametrics.Power, + ValueId = GetPower(power) + }; + filters.Add(filter); + } + if (!string.IsNullOrEmpty(resistance)) + { + keywords.Remove(resistance); + var filter = new ParametricFilter + { + ParameterId = (int)Parametrics.Resistance, + ValueId = GetResistance(resistance) + }; + filters.Add(filter); + } + if (!string.IsNullOrEmpty(capacitance)) + { + keywords.Remove(capacitance); + var filter = new ParametricFilter + { + ParameterId = (int)Parametrics.Capacitance, + ValueId = GetCapacitance(capacitance) + }; + filters.Add(filter); + } + if (!string.IsNullOrEmpty(voltageRating)) + { + keywords.Remove(voltageRating); + var filter = new ParametricFilter + { + ParameterId = (int)Parametrics.VoltageRating, + ValueId = GetVoltageRating(voltageRating) + }; + filters.Add(filter); + } + if (!string.IsNullOrEmpty(currentRating)) + { + keywords.Remove(currentRating); + var filter = new ParametricFilter + { + ParameterId = (int)Parametrics.CurrentRating, + ValueId = GetVoltageRating(currentRating) + }; + filters.Add(filter); + } + if (!string.IsNullOrEmpty(inductance)) + { + keywords.Remove(inductance); + var filter = new ParametricFilter + { + ParameterId = (int)Parametrics.Inductance, + ValueId = GetInductance(inductance) + }; + filters.Add(filter); + } + // dont add mounting type to resistors, they dont seem to be mapped + if (!taxonomies.ContainsAny(new List { Taxonomies.Resistor, Taxonomies.SurfaceMountResistor, Taxonomies.ThroughHoleResistor })) + { + if (packageType != MountingTypes.None) + filters.Add(new ParametricFilter + { + ParameterId = (int)Parametrics.MountingType, + ValueId = ((int)packageType).ToString() + }); + } + return filters; + } + + private ICollection MapTaxonomies(string partType, MountingTypes packageType) + { + var taxonomies = new List(); + var taxonomy = Taxonomies.None; + if (!string.IsNullOrEmpty(partType) && partType != "-1") + { + if (Enum.TryParse(partType, true, out taxonomy)) + { + var addBaseType = true; + // also map all the alternates + var memberInfos = typeof(Taxonomies).GetMember(taxonomy.ToString()); + var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == typeof(Taxonomies)); + if (enumValueMemberInfo != null) + { + var valueAttributes = enumValueMemberInfo.GetCustomAttributes(typeof(AlternatesAttribute), false); + if (valueAttributes.Any()) + { + var alternateIds = ((AlternatesAttribute)valueAttributes[0]).Ids; + // taxonomies.AddRange(alternateIds); + } + } + + switch (taxonomy) + { + case Taxonomies.Resistor: + if (packageType == MountingTypes.ThroughHole) + { + taxonomies.Add(Taxonomies.ThroughHoleResistor); + addBaseType = false; + } + if (packageType == MountingTypes.SurfaceMount) + { + taxonomies.Add(Taxonomies.SurfaceMountResistor); + addBaseType = false; + } + break; + } + if (addBaseType) + taxonomies.Add(taxonomy); + } + } + + return taxonomies; + } + + /// + /// Handle known error conditions first, if response is OK false will be returned + /// + /// + /// + /// + /// + /// + private bool TryHandleResponse(HttpResponseMessage response, OAuthAuthorization authenticationResponse, out IApiResponse apiResponse) + { + apiResponse = ApiResponse.Create($"Api returned error status code {response.StatusCode}: {response.ReasonPhrase}", nameof(DigikeyApi)); + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + throw new DigikeyUnauthorizedException(authenticationResponse); + else if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable) + { + if (response.Headers.Contains("X-RateLimit-Limit")) + { + // throttled + var remainingTime = TimeSpan.Zero; + if (response.Headers.Contains("X-RateLimit-Remaining")) + remainingTime = TimeSpan.FromSeconds(int.Parse(response.Headers.GetValues("X-RateLimit-Remaining").First())); + apiResponse = ApiResponse.Create($"{nameof(DigikeyApi)} request throttled. Try again in {remainingTime}", nameof(DigikeyApi)); + return true; + } + + // return generic error + return true; + } + else if (response.IsSuccessStatusCode) + { + // allow processing of response + return false; + } + else if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + apiResponse = ApiResponse.Create($"Api returned error status code {response.StatusCode}: {response.ReasonPhrase}", nameof(DigikeyApi)); + var resultString = response.Content.ReadAsStringAsync().Result; + if (!string.IsNullOrEmpty(resultString)) + apiResponse.Errors.Add(resultString); + else + apiResponse.Errors.Add($"Api returned error status code {response.StatusCode}: {response.ReasonPhrase}"); + return true; + } + + // return generic error + return true; + } + + /// + /// Wraps an API request - if the request is unauthorized it will refresh the Auth token and re-issue the request + /// + /// + /// + /// + private async Task WrapApiRequestAsync(OAuthAuthorization authResponse, Func> func) + { + try + { + return await func(authResponse); + } + catch (DigikeyUnauthorizedException ex) + { + // get refresh token, retry + _oAuth2Service.AccessTokens.RefreshToken = ex.Authorization.RefreshToken; + var token = await _oAuth2Service.RefreshTokenAsync(); + if (_httpContextAccessor.HttpContext == null) + throw new Exception($"HttpContext cannot be null!"); + var referer = _httpContextAccessor.HttpContext.Request.Headers["Referer"].ToString(); + var refreshTokenResponse = new OAuthAuthorization(nameof(DigikeyApi), _configuration.ClientId ?? string.Empty, referer) + { + AccessToken = token.AccessToken ?? string.Empty, + RefreshToken = token.RefreshToken ?? string.Empty, + CreatedUtc = DateTime.UtcNow, + ExpiresUtc = DateTime.UtcNow.Add(TimeSpan.FromSeconds(token.ExpiresIn)), + AuthorizationReceived = true, + UserId = _requestContext.GetUserContext()?.UserId, + }; + if (refreshTokenResponse.IsAuthorized) + { + // save the credential + await _credentialService.SaveOAuthCredentialAsync(new OAuthCredential + { + Provider = nameof(DigikeyApi), + AccessToken = refreshTokenResponse.AccessToken, + RefreshToken = refreshTokenResponse.RefreshToken, + DateCreatedUtc = refreshTokenResponse.CreatedUtc, + DateExpiresUtc = refreshTokenResponse.ExpiresUtc, + }); + try + { + // call the API again using the refresh token + return await func(refreshTokenResponse); + } + catch (DigikeyUnauthorizedException) + { + // refresh token failed, restart access token retrieval process + await ForgetAuthenticationTokens(); + var freshResponse = await AuthorizeAsync(); + if (freshResponse.MustAuthorize) + return ApiResponse.Create(true, freshResponse.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); + // call the API again + return await func(freshResponse); + } + } + // user must authorize + // request a token if we don't already have one + var authRequest = await CreateOAuthAuthorizationRequestAsync(_requestContext.GetUserContext()?.UserId); + return ApiResponse.Create(true, authRequest.AuthorizationUrl, $"User must authorize", nameof(DigikeyApi)); + } + } + + private async Task ForgetAuthenticationTokens() + { + var user = _requestContext.GetUserContext(); + await _credentialService.RemoveOAuthCredentialAsync(nameof(DigikeyApi)); + } + + private async Task AuthorizeAsync() + { + var user = _requestContext.GetUserContext(); + if (user != null && user.UserId <= 0) + throw new AuthenticationException("User is not authenticated!"); + + // check if we have saved an existing auth credential in the database + var credential = await _credentialService.GetOAuthCredentialAsync(nameof(DigikeyApi)); + if (credential != null) + { + // reuse a saved oAuth credential + var referer = _httpContextAccessor.HttpContext.Request.Headers["Referer"].ToString(); + var authRequest = new OAuthAuthorization(nameof(DigikeyApi), _configuration.ClientId ?? string.Empty, referer) + { + AccessToken = credential.AccessToken ?? string.Empty, + RefreshToken = credential.RefreshToken ?? string.Empty, + CreatedUtc = credential.DateCreatedUtc, + ExpiresUtc = credential.DateExpiresUtc, + AuthorizationReceived = true, + UserId = user?.UserId + }; + + return authRequest; + } + + // user must authorize + // request a token if we don't already have one + return await CreateOAuthAuthorizationRequestAsync(user?.UserId); + } + + private async Task CreateOAuthAuthorizationRequestAsync(int? userId) + { + var referer = _httpContextAccessor.HttpContext.Request.Headers["Referer"].ToString(); + var uriBuilder = new UriBuilder(referer); + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + query["api-authenticate"] = "true"; + query["api"] = "DigiKey"; + uriBuilder.Query = query.ToString(); + var authRequest = new OAuthAuthorization(nameof(DigikeyApi), _configuration.ClientId ?? string.Empty, uriBuilder.ToString()) + { + UserId = userId + }; + authRequest = await _credentialService.CreateOAuthRequestAsync(authRequest); + // no scopes necessary + var scopes = ""; + // state will be send as the RequestId + var state = authRequest.Id.ToString(); + var authUrl = _oAuth2Service.GenerateAuthUrl(scopes, state); + + return new OAuthAuthorization(nameof(DigikeyApi), true, authUrl); + } + + private HttpRequestMessage CreateRequest(OAuthAuthorization authResponse, HttpMethod method, string url) + { + var message = new HttpRequestMessage(method, url); + message.Headers.Add("X-DIGIKEY-Client-Id", authResponse.ClientId); + message.Headers.Add("Authorization", $"Bearer {authResponse.AccessToken}"); + message.Headers.Add("X-DIGIKEY-Locale-Site", _configuration.Site.ToString()); + message.Headers.Add("X-DIGIKEY-Locale-Language", _localeConfiguration.Language.ToString().ToLower()); + message.Headers.Add("X-DIGIKEY-Locale-Currency", _localeConfiguration.Currency.ToString().ToUpper()); + return message; + } + + private HttpRequestMessage CreateRequest(OAuthAuthorization authResponse, HttpMethod method, Uri uri) + { + var message = new HttpRequestMessage(method, uri); + message.Headers.Add("X-DIGIKEY-Client-Id", authResponse.ClientId); + message.Headers.Add("Authorization", $"Bearer {authResponse.AccessToken}"); + message.Headers.Add("X-DIGIKEY-Locale-Site", _configuration.Site.ToString()); + message.Headers.Add("X-DIGIKEY-Locale-Language", _localeConfiguration.Language.ToString().ToLower()); + message.Headers.Add("X-DIGIKEY-Locale-Currency", _localeConfiguration.Currency.ToString().ToUpper()); + return message; + } + + private Tolerances GetTolerance(string perc) + { + return GetEnumByDescription(perc); + } + + private string GetPower(string power) + { + power = new Regex("[Ww]|").Replace(power, ""); + // convert decimal percentages to fractions + if (power.Contains(".")) + { + var fraction = MathExtensions.RealToFraction(double.Parse(power), 0.01); + return ((int)GetEnumByDescription($"{fraction.Numerator}/{fraction.Denominator}")).ToString(); + } + return ((int)GetEnumByDescription(power)).ToString(); + } + + private string GetResistance(string resistance) + { + var val = new String(resistance.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); + if (string.IsNullOrEmpty(val)) + val = "0"; + var unitsParsed = resistance.Replace(val, "").ToLower(); + var units = "ohms"; + switch (unitsParsed) + { + case "k": + case "kohms": + units = "kOhms"; + break; + case "m": + case "mohms": + units = "mOhms"; + break; + } + var result = $"u{val} {units}"; + return result; + } + + private string GetCapacitance(string capacitance) + { + var val = new String(capacitance.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); + var unitsParsed = capacitance.Replace(val, "").ToLower(); + var units = "µF"; + switch (unitsParsed) + { + case "uf": + units = "µF"; + break; + case "nf": + // convert to uf, api doesn't seem to handle it? + val = (decimal.Parse(val) * 0.001M).ToString(); + units = "µF"; + break; + case "pf": + units = "pF"; + break; + case "f": + units = "F"; + break; + } + var result = $"u{val}{units}"; + return result; + } + + private string GetVoltageRating(string voltage) + { + var val = new String(voltage.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); + var unitsParsed = voltage.Replace(val, "").ToLower(); + var units = "V"; + switch (unitsParsed) + { + case "v": + units = "V"; + break; + } + var result = $"u{val}{units}"; + return result; + } + + private string GetCurrentRating(string current) + { + var val = new String(current.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); + var unitsParsed = current.Replace(val, "").ToLower(); + var units = "A"; + switch (unitsParsed) + { + case "a": + units = "A"; + break; + case "ma": + units = "mA"; + break; + } + var result = $"u{val}{units}"; + return result; + } + + private string GetInductance(string inductance) + { + var val = new String(inductance.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); + var unitsParsed = inductance.Replace(val, "").ToLower(); + var units = "µH"; + switch (unitsParsed) + { + case "uh": + units = "µH"; + break; + case "nh": + units = "nH"; + break; + case "mh": + units = "mH"; + break; + case "h": + units = "H"; + break; + } + var result = $"u{val}{units}"; + return result; + } + + private T? GetEnumByDescription(string description) + { + var type = typeof(T).GetExtendedType(); + foreach (var val in type.EnumValues) + { + var memberInfos = type.Type.GetMember(val.Value); + var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == type.Type); + var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(DescriptionAttribute), false); + if (valueAttributes != null) + { + var descriptionVal = ((DescriptionAttribute)valueAttributes[0]).Description; + if (descriptionVal.Equals(description)) + return (T)val.Key; + } + } + return default(T); + } + + } + + public class DigikeyUnauthorizedException : Exception + { + public OAuthAuthorization Authorization { get; } + public DigikeyUnauthorizedException(OAuthAuthorization authorization) : base("User must authorize") + { + Authorization = authorization; + } + } } \ No newline at end of file diff --git a/Binner/Library/Binner.Common/Integrations/MouserApi.cs b/Binner/Library/Binner.Common/Integrations/MouserApi.cs index ba1d8e39..00d10e27 100644 --- a/Binner/Library/Binner.Common/Integrations/MouserApi.cs +++ b/Binner/Library/Binner.Common/Integrations/MouserApi.cs @@ -193,7 +193,7 @@ private HttpRequestMessage CreateRequest(HttpMethod method, Uri uri) public class MouserErrorsException : Exception { public ICollection Errors { get; set; } - public MouserErrorsException(ICollection errors) + public MouserErrorsException(ICollection errors) : base(errors.FirstOrDefault()?.Message) { Errors = errors; } diff --git a/Binner/Library/Binner.Common/Services/PartService.cs b/Binner/Library/Binner.Common/Services/PartService.cs index aa8c167c..708ba5a7 100644 --- a/Binner/Library/Binner.Common/Services/PartService.cs +++ b/Binner/Library/Binner.Common/Services/PartService.cs @@ -22,6 +22,7 @@ using System.Data; using System.Linq; using System.Linq.Expressions; +using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; using Part = Binner.Model.Part; @@ -195,28 +196,27 @@ public async Task DeletePartSupplierAsync(PartSupplier partSupplier) var user = _requestContext.GetUserContext(); var digikeyApi = await _integrationApiFactory.CreateAsync(user?.UserId ?? 0); if (!digikeyApi.IsEnabled) - return ServiceResult.Create("Api is not enabled.", nameof(Integrations.DigikeyApi)); + return ServiceResult.Create("Api is not enabled.", nameof(Integrations.DigikeyApi)); var apiResponse = await digikeyApi.GetCategoriesAsync(); if (apiResponse.RequiresAuthentication) - return ServiceResult.Create(true, apiResponse.RedirectUrl ?? string.Empty, apiResponse.Errors, apiResponse.ApiName); + return ServiceResult.Create(true, apiResponse.RedirectUrl ?? string.Empty, apiResponse.Errors, apiResponse.ApiName); else if (apiResponse.Errors?.Any() == true) - return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); + return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); if (apiResponse.Response != null) { var digikeyResponse = (CategoriesResponse)apiResponse.Response; - return ServiceResult.Create(digikeyResponse); + return ServiceResult.Create(digikeyResponse); } - return ServiceResult.Create("Invalid response received", apiResponse.ApiName); + return ServiceResult.Create("Invalid response received", apiResponse.ApiName); } /// /// Get an external order /// - /// - /// + /// /// public async Task> GetExternalOrderAsync(OrderImportRequest request) { @@ -247,7 +247,7 @@ public async Task DeletePartSupplierAsync(PartSupplier partSupplier) return ServiceResult.Create(true, apiResponse.RedirectUrl ?? string.Empty, apiResponse.Errors, apiResponse.ApiName); else if (apiResponse.Errors?.Any() == true) return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); - + var messages = new List(); var digikeyResponse = (OrderSearchResponse?)apiResponse.Response ?? new OrderSearchResponse(); @@ -505,7 +505,7 @@ public async Task DeletePartSupplierAsync(PartSupplier partSupplier) OrderDate = mouserOrderResponse.OrderDate, Currency = mouserOrderResponse.CurrencyCode, CustomerId = mouserOrderResponse.BuyerName, - Amount = double.Parse(mouserOrderResponse.SummaryDetail?.OrderTotal.Replace("$","") ?? "0"), + Amount = double.Parse(mouserOrderResponse.SummaryDetail?.OrderTotal.Replace("$", "") ?? "0"), TrackingNumber = mouserOrderResponse.DeliveryDetail?.ShippingMethodName, Messages = messages, Parts = commonParts @@ -819,6 +819,7 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri return ServiceResult.Create(response); } + var apiResponses = new Dictionary(); var datasheets = new List(); var swarmResponse = new SwarmApi.Response.SearchPartResponse(); var digikeyResponse = new KeywordSearchResponse(); @@ -829,7 +830,18 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri if (digikeyApi.Configuration.IsConfigured) { - var apiResponse = await digikeyApi.SearchAsync(searchKeywords, partType, mountingType); + IApiResponse? apiResponse = null; + try + { + apiResponse = await digikeyApi.SearchAsync(searchKeywords, partType, mountingType); + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{nameof(DigikeyApi)}]: {ex.GetBaseException().Message}"); + apiResponse = new ApiResponse(new List { ex.GetBaseException().Message }, nameof(DigikeyApi)); + } + apiResponses.Add(nameof(DigikeyApi), new Model.Integrations.ApiResponseState(false, apiResponse)); + if (apiResponse.RequiresAuthentication) return ServiceResult.Create(true, apiResponse.RedirectUrl ?? string.Empty, apiResponse.Errors, apiResponse.ApiName); if (apiResponse.Warnings?.Any() == true) @@ -839,7 +851,7 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri if (apiResponse.Errors?.Any() == true) { _logger.LogError($"[{apiResponse.ApiName}]: {string.Join(". ", apiResponse.Errors)}"); - return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); + // return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); } digikeyResponse = (KeywordSearchResponse?)apiResponse.Response; @@ -852,10 +864,19 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri if (isNumber) { var barcode = searchKeywords; - var barcodeResult = await GetBarcodeInfoProductAsync(barcode, ScannedBarcodeType.Product); - digikeyResponse = new KeywordSearchResponse(); - if (barcodeResult.Response != null) - digikeyResponse.Products.Add(barcodeResult.Response); + IServiceResult barcodeResult = null; + try + { + barcodeResult = await GetBarcodeInfoProductAsync(barcode, ScannedBarcodeType.Product); + digikeyResponse = new KeywordSearchResponse(); + if (barcodeResult.Response != null) + digikeyResponse.Products.Add(barcodeResult.Response); + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{nameof(DigikeyApi)}]: {ex.GetBaseException().Message}"); + apiResponse.Errors.Add($"Error fetching barcode info on '{WebUtility.HtmlEncode(barcode)}': {ex.GetBaseException().Message}"); + } } } @@ -874,12 +895,21 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri if (!string.IsNullOrEmpty(supplierPartNumber)) { // try looking it up via the digikey part number - var partResponse = await digikeyApi.GetProductDetailsAsync(supplierPartNumber); - if (!partResponse.RequiresAuthentication && partResponse?.Errors.Any() == false) + IApiResponse? partResponse = null; + try { - var part = (Product?)partResponse.Response; - if (part != null) - digikeyResponse.Products.Add(part); + partResponse = await digikeyApi.GetProductDetailsAsync(supplierPartNumber); + if (!partResponse.RequiresAuthentication && partResponse?.Errors.Any() == false) + { + var part = (Product?)partResponse.Response; + if (part != null) + digikeyResponse.Products.Add(part); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{nameof(DigikeyApi)}]: {ex.GetBaseException().Message}"); + apiResponse.Errors.Add($"Error fetching product details on '{WebUtility.HtmlEncode(supplierPartNumber)}': {ex.GetBaseException().Message}"); } } } @@ -891,22 +921,48 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri { var supplierPartNumber = searchKeywords; // try looking it up via the digikey part number - var partResponse = await digikeyApi.GetProductDetailsAsync(supplierPartNumber); - if (!partResponse.RequiresAuthentication && partResponse?.Errors.Any() == false) + try { - var part = (Product?)partResponse.Response; - if (part != null) - digikeyResponse.Products.Add(part); + var partResponse = await digikeyApi.GetProductDetailsAsync(supplierPartNumber); + if (!partResponse.RequiresAuthentication && partResponse?.Errors.Any() == false) + { + var part = (Product?)partResponse.Response; + if (part != null) + digikeyResponse.Products.Add(part); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{nameof(DigikeyApi)}]: {ex.GetBaseException().Message}"); + apiResponse.Errors.Add($"Error fetching product details on supplier part number '{WebUtility.HtmlEncode(supplierPartNumbers)}': {ex.GetBaseException().Message}"); + } } + + apiResponses[nameof(DigikeyApi)].IsSuccess = digikeyResponse.Products.Any(); } } if (mouserApi.Configuration.IsConfigured) { - var apiResponse = await mouserApi.SearchAsync(searchKeywords, partType, mountingType); + IApiResponse? apiResponse = null; + try + { + apiResponse = await mouserApi.SearchAsync(searchKeywords, partType, mountingType); + } + catch (MouserErrorsException ex) + { + _logger.LogError(ex, $"[{nameof(MouserApi)}]: {string.Join(", ", ex.Errors)}"); + apiResponse = new ApiResponse(ex.Errors.Select(x => x.Message).ToList(), nameof(MouserApi)); + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{nameof(MouserApi)}]: {ex.GetBaseException().Message}"); + apiResponse = new ApiResponse(new List { ex.GetBaseException().Message }, nameof(MouserApi)); + } + apiResponses.Add(nameof(MouserApi), new Model.Integrations.ApiResponseState(false, apiResponse)); if (apiResponse.RequiresAuthentication) - return ServiceResult.Create(true, apiResponse.RedirectUrl ?? string.Empty, apiResponse.Errors, apiResponse.ApiName); + return ServiceResult.Create(true, apiResponse.RedirectUrl ?? string.Empty, apiResponse.Errors, apiResponse.ApiName); if (apiResponse.Warnings?.Any() == true) { _logger.LogWarning($"[{apiResponse.ApiName}]: {string.Join(". ", apiResponse.Warnings)}"); @@ -914,15 +970,26 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri if (apiResponse.Errors?.Any() == true) { _logger.LogError($"[{apiResponse.ApiName}]: {string.Join(". ", apiResponse.Errors)}"); - return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); + //return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); } mouserResponse = (SearchResultsResponse?)apiResponse.Response; + apiResponses[nameof(MouserApi)].IsSuccess = mouserResponse?.SearchResults?.Parts?.Any() == true; } if (nexarApi.Configuration.IsConfigured) { - var apiResponse = await nexarApi.SearchAsync(searchKeywords, partType, mountingType); + IApiResponse? apiResponse = null; + try + { + apiResponse = await nexarApi.SearchAsync(searchKeywords, partType, mountingType); + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{nameof(NexarApi)}]: {ex.GetBaseException().Message}"); + apiResponse = new ApiResponse(new List { ex.GetBaseException().Message }, nameof(NexarApi)); + } + apiResponses.Add(nameof(NexarApi), new Model.Integrations.ApiResponseState(false, apiResponse)); if (apiResponse.RequiresAuthentication) return ServiceResult.Create(true, apiResponse.RedirectUrl ?? string.Empty, apiResponse.Errors, apiResponse.ApiName); if (apiResponse.Warnings?.Any() == true) @@ -932,15 +999,28 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri if (apiResponse.Errors?.Any() == true) { _logger.LogError($"[{apiResponse.ApiName}]: {string.Join(". ", apiResponse.Errors)}"); - return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); + //return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); } nexarResponse = (NexarPartResults?)apiResponse.Response; + apiResponses[nameof(NexarApi)].IsSuccess = nexarResponse?.Parts?.Any() == true; } if (swarmApi.Configuration.IsConfigured) { - //var apiResponse = await swarmApi.SearchAsync(partNumber, partType, mountingType); - var apiResponse = await swarmApi.SearchAsync(partNumber); + IApiResponse? apiResponse = null; + //apiResponse = await swarmApi.SearchAsync(partNumber, partType, mountingType); + try + { + apiResponse = await swarmApi.SearchAsync(partNumber); + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{nameof(SwarmApi)}]: {ex.GetBaseException().Message}"); + apiResponse = new ApiResponse(new List { ex.GetBaseException().Message }, nameof(SwarmApi)); + + } + apiResponses.Add(nameof(SwarmApi), new Model.Integrations.ApiResponseState(false, apiResponse)); + if (apiResponse.Warnings?.Any() == true) { _logger.LogWarning($"[{apiResponse.ApiName}]: {string.Join(". ", apiResponse.Warnings)}"); @@ -948,15 +1028,26 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri if (apiResponse.Errors?.Any() == true) { _logger.LogError($"[{apiResponse.ApiName}]: {string.Join(". ", apiResponse.Errors)}"); - return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); + //return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); } swarmResponse = (SwarmApi.Response.SearchPartResponse?)apiResponse.Response ?? new SwarmApi.Response.SearchPartResponse(); + apiResponses[nameof(SwarmApi)].IsSuccess = swarmResponse.Parts?.Any() == true; } if (arrowApi.Configuration.IsConfigured) { - var apiResponse = await arrowApi.SearchAsync(searchKeywords, partType, mountingType); + IApiResponse? apiResponse = null; + try + { + apiResponse = await arrowApi.SearchAsync(searchKeywords, partType, mountingType); + } + catch (Exception ex) + { + _logger.LogError(ex, $"[{nameof(ArrowApi)}]: {ex.GetBaseException().Message}"); + apiResponse = new ApiResponse(new List { ex.GetBaseException().Message }, nameof(ArrowApi)); + } + apiResponses.Add(nameof(ArrowApi), new Model.Integrations.ApiResponseState(false, apiResponse)); if (apiResponse.Warnings?.Any() == true) { _logger.LogWarning($"[{apiResponse.ApiName}]: {string.Join(". ", apiResponse.Warnings)}"); @@ -964,10 +1055,26 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri if (apiResponse.Errors?.Any() == true) { _logger.LogError($"[{apiResponse.ApiName}]: {string.Join(". ", apiResponse.Errors)}"); - return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); + //return ServiceResult.Create(apiResponse.Errors, apiResponse.ApiName); } arrowResponse = (ArrowResponse?)apiResponse.Response; + apiResponses[nameof(ArrowApi)].IsSuccess = arrowResponse?.ItemServiceResult?.Data?.Any() == true; + } + + if (!apiResponses.Any(x => x.Value.IsSuccess)) + { + if (apiResponses.Any(x => x.Value.Response?.Errors.Any() == true)) + { + // there are errors, and no successful responses + var errors = apiResponses + .Where(x => x.Value.Response != null && x.Value.Response.Errors.Any()) + .SelectMany(x => x.Value.Response!.Errors.Select(errorMessage => $"[{x.Value.Response.ApiName}] {errorMessage}")).ToList(); + var apiNames = apiResponses.Where(x => x.Value.Response?.Errors.Any() == true).GroupBy(x => x.Key); + var apiName = "Multiple"; + if (apiNames.Count() == 1) apiName = apiNames.First().Key; + return ServiceResult.Create(errors, apiName); + } } var partTypes = await _storageProvider.GetPartTypesAsync(_requestContext.GetUserContext()); @@ -1032,7 +1139,12 @@ private CommonPart MouserOrderLineToCommonPart(OrderHistoryLine? orderLine, stri response.ProductImages = productImageUrls.DistinctBy(x => x.Value).ToList(); response.Datasheets = datasheetUrls.DistinctBy(x => x.Value).ToList(); - return ServiceResult.Create(response); + var serviceResult = ServiceResult.Create(response); + if (apiResponses.Any(x => x.Value.Response != null && x.Value.Response.Errors.Any())) + serviceResult.Errors = apiResponses + .Where(x => x.Value.Response != null && x.Value.Response.Errors.Any()) + .SelectMany(x => x.Value.Response!.Errors.Select(errorMessage => $"[{x.Value.Response.ApiName}] {errorMessage}")); + return serviceResult; void ProcessSwarmResponse(string partNumber, PartResults response, SwarmApi.Response.SearchPartResponse swarmResponse, ICollection partTypes, List> productImageUrls, List> datasheetUrls) { diff --git a/Binner/Library/Binner.Model/Integrations/ApiResponseState.cs b/Binner/Library/Binner.Model/Integrations/ApiResponseState.cs new file mode 100644 index 00000000..878e442f --- /dev/null +++ b/Binner/Library/Binner.Model/Integrations/ApiResponseState.cs @@ -0,0 +1,16 @@ +using Binner.Common.Integrations.Models; + +namespace Binner.Model.Integrations +{ + public class ApiResponseState + { + public bool IsSuccess { get; set; } + public IApiResponse? Response { get; init; } + + public ApiResponseState(bool isSuccess, IApiResponse? response) + { + IsSuccess = isSuccess; + Response = response; + } + } +}