/// <summary> /// Suggests other potential options based on the entities passed /// </summary> /// <param name="rows">The rows being imported</param> /// <returns>Entites with suggestions</returns> public Suggestions SuggestEntites(ImportRow[] rows) { var rowSuggestionsConcurrentDictionary = new ConcurrentDictionary<int, RowSuggestions>(); var suggestionsToReturn = new Suggestions(); var clients = new ConcurrentDictionary<Guid, FoundOps.Api.Models.Client>(); var locations = new ConcurrentDictionary<Guid, FoundOps.Api.Models.Location>(); var contactInfoSets = new ConcurrentDictionary<Guid, FoundOps.Api.Models.ContactInfo>(); Parallel.For((long)0, rows.Count(), rowIndex => { var row = rows[rowIndex]; var rowSuggestions = new RowSuggestions(); #region Location if (row.Location != null) { //Find all the Locations to be suggested by finding all Locations for the Client of the row var locationSuggestions = row.Client != null ? _locations.Where(l => l.Value.ClientId == row.Client.Id).ToArray() : null; if (locationSuggestions != null) { //Add any of the suggestions to the rows suggestions rowSuggestions.LocationSuggestions.AddRange(locationSuggestions.Select(l => l.Key)); var convertedLocationSuggestions = locationSuggestions.Select(l => l.Value).Select(FoundOps.Api.Models.Location.ConvertModel); //Add all suggested Locations to the list of Locations to be returned foreach (var location in convertedLocationSuggestions) locations.GetOrAdd(location.Id, location); } //Add the matched/new location as the first suggestion rowSuggestions.LocationSuggestions.Add(row.Location.Id); //Add the location passed to the list of location entites locations.GetOrAdd(row.Location.Id, row.Location); } #endregion #region Client if (row.Client != null) { //Find all the Clients to be suggested by finding all Clients for the Location of the row var clientSuggestions = row.Location != null ? _clients.Where(c => c.Key == row.Location.ClientId).ToArray() : null; if (clientSuggestions != null) { //Add any of the suggestions to the rows suggestions rowSuggestions.ClientSuggestions.AddRange(clientSuggestions.Select(c => c.Key)); var convertedClientSuggestions = clientSuggestions.Select(c => c.Value).Select(FoundOps.Api.Models.Client.ConvertModel); //Add all suggested Clients to the list of Clients to be returned foreach (var client in convertedClientSuggestions) clients.GetOrAdd(client.Id, client); } //Add the matched/new client as the first suggestion rowSuggestions.ClientSuggestions.Add(row.Client.Id); //Add the Client passed to the list of client entites clients.GetOrAdd(row.Client.Id, row.Client); } #endregion //Repeat if (row.Repeat != null) rowSuggestions.Repeats.Add(row.Repeat); //Contact Info if (row.ContactInfoSet.Count != 0) { rowSuggestions.ContactInfoSuggestions.AddRange(row.ContactInfoSet.Select(ci => ci.Id)); foreach (var contactInfoSet in row.ContactInfoSet) contactInfoSets.GetOrAdd(contactInfoSet.Id, contactInfoSet); } //Add this row's suggestions to the list to be returned rowSuggestionsConcurrentDictionary.GetOrAdd((int)rowIndex, rowSuggestions); }); //Order the row suggestions by rowIndex suggestionsToReturn.RowSuggestions.AddRange(rowSuggestionsConcurrentDictionary.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value)); //Only add distinct Clients var distinctClients = clients.Distinct(); suggestionsToReturn.Clients.AddRange(distinctClients.Select(dc => dc.Value)); //Only add distinct Locations var distinctLocations = locations.Distinct(); suggestionsToReturn.Locations.AddRange(distinctLocations.Select(dl => dl.Value)); //Only add distinct ContactInfo var distinctContactInfo = contactInfoSets.Select(ci => ci.Value).Distinct(); suggestionsToReturn.ContactInfoSet.AddRange(distinctContactInfo); return suggestionsToReturn; }
/// <summary> /// Setup a fake import using random existing Clients, Locations and Repeats /// </summary> /// <param name="importClients">Whether or not to import Clients</param> /// <param name="newClient">Whether or not to create a new Client</param> /// <param name="importLocations">Whether or not to import Locations</param> /// <param name="newLocation">Whether or not to create a new Location</param> /// <param name="importContactInfo">Whether or not to import ContactInfo</param> /// <param name="newContactInfo">Whether or not to create a new ContactInfoSet</param> /// <param name="importRepeats">Whether or not to import Repeats</param> /// <returns></returns> private ImportRow[] SetupRows(bool importClients, bool newClient, bool importLocations, bool newLocation, bool importContactInfo, bool newContactInfo, bool importRepeats) { var random = new Random(); var rows = new List<ImportRow>(); var newRow = new ImportRow(); if (importClients) { var client = newClient ? new Client { Name = "Test Client" } : CoreEntitiesContainer.Clients.ToArray().ElementAt(random.Next(48)); newRow.Client = Api.Models.Client.ConvertModel(client); } if (importLocations) { var location = newLocation ? new Location { AddressLineOne = "2827 Floral Drive", AddressLineTwo = "Linen Closet", AdminDistrictTwo = "Northbrook", AdminDistrictOne = "IL", PostalCode = "60062", CountryCode = "US" } : CoreEntitiesContainer.Locations.Where(l => l.BusinessAccountIdIfDepot == null).Include(l => l.Region).ToArray().ElementAt(random.Next(51)); newRow.Location = Api.Models.Location.ConvertModel(location); } if (importContactInfo) { var phoneContactInfo = newContactInfo && (newLocation || newClient) ? new Core.Models.CoreEntities.ContactInfo { Id = Guid.NewGuid(), Type = "Phone", Label = "New Phone", Data = "(123) 345-6789" } : CoreEntitiesContainer.ContactInfoSet.Where(c => c.Type == "Phone").ToArray().ElementAt(random.Next(70)); var emailContactInfo = newContactInfo && (newLocation || newClient) ? new Core.Models.CoreEntities.ContactInfo { Id = Guid.NewGuid(), Type = "Email", Label = "New Email", Data = "*****@*****.**" } : CoreEntitiesContainer.ContactInfoSet.Where(c => c.Type == "Email").ToArray().ElementAt(random.Next(70)); var websiteContactInfo = newContactInfo && (newLocation || newClient) ? new Core.Models.CoreEntities.ContactInfo { Id = Guid.NewGuid(), Type = "Website", Label = "New Website", Data = "foundops.com" } : CoreEntitiesContainer.ContactInfoSet.Where(c => c.Type == "Website").ToArray().ElementAt(random.Next(70)); newRow.ContactInfoSet.AddRange(new List<ContactInfo> { ContactInfo.Convert(phoneContactInfo), ContactInfo.Convert(emailContactInfo), ContactInfo.Convert(websiteContactInfo) }); } if (importRepeats) { var repeat = CoreEntitiesContainer.Repeats.Where(r => r.FrequencyInt == 3).ToArray().ElementAt(random.Next(144)); newRow.Repeat = Api.Models.Repeat.ConvertModel(repeat); } rows.Add(newRow); return rows.ToArray(); }
/// <summary> /// Manipulates the input strings to existing or new entities. /// Then it passes those entities to SuggestEntities to generate suggestions. /// <note>This method is only used for the initial step of the Import. Once entites are generated; use SuggestEntities</note> /// </summary> /// <param name="rowsWithHeaders">List of string[]. The first string[] is the headers, the rest are data to be imported</param> /// <returns>Entites with suggestions</returns> public Suggestions ValidateThenSuggestEntities(List<string[]> rowsWithHeaders) { var headers = rowsWithHeaders[0]; rowsWithHeaders.RemoveAt(0); var rows = rowsWithHeaders; #region Assign Column placement //Client var clientNameCol = Array.IndexOf(headers, "Client Name"); //Location var addressLineOneCol = Array.IndexOf(headers, "Address Line One"); var addressLineTwoCol = Array.IndexOf(headers, "Address Line Two"); var cityCol = Array.IndexOf(headers, "AdminDistrictTwo"); var stateCol = Array.IndexOf(headers, "AdminDistrictOne"); var zipCodeCol = Array.IndexOf(headers, "PostalCode"); var countryCodeCol = Array.IndexOf(headers, "Country Code"); var regionNameCol = Array.IndexOf(headers, "Region Name"); var latitudeCol = Array.IndexOf(headers, "Latitude"); var longitudeCol = Array.IndexOf(headers, "Longitude"); //Repeat var frequencyCol = Array.IndexOf(headers, "Frequency"); var repeatEveryCol = Array.IndexOf(headers, "Repeat Every"); var startDateCol = Array.IndexOf(headers, "Start Date"); var endDateCol = Array.IndexOf(headers, "End Date"); var endAfterCol = Array.IndexOf(headers, "End After Times"); var frequencyDetailCol = Array.IndexOf(headers, "Frequency Detail"); //Contact Info var phoneNumberValueCols = headers.Where(h => h.Contains("Phone Value")).ToArray(); var phoneNumberLabelCols = headers.Where(h => h.Contains("Phone Label")).ToArray(); var emailValueCols = headers.Where(h => h.Contains("Email Value")).ToArray(); var emailLabelCols = headers.Where(h => h.Contains("Email Label")).ToArray(); var websiteValueCols = headers.Where(h => h.Contains("Website Value")).ToArray(); var websiteLabelCols = headers.Where(h => h.Contains("Website Label")).ToArray(); var otherValueCols = headers.Where(h => h.Contains("Other Value")).ToArray(); var otherLabelCols = headers.Where(h => h.Contains("Other Label")).ToArray(); #endregion var importRowsConcurrentDictionary = new ConcurrentDictionary<int, ImportRow>(); var rowCount = rows.Count(); Parallel.For((long)0, rowCount, rowIndex => { var row = rows[(int)rowIndex]; var importRow = new ImportRow(); #region Location var importedLocation = new Api.Models.Location(); string clientName; //Checks if at least one location column is passed if (new[] { addressLineOneCol, addressLineTwoCol, cityCol, stateCol, countryCodeCol, zipCodeCol, latitudeCol, longitudeCol }.Any(col => col != -1)) { var latitude = latitudeCol != -1 ? row[latitudeCol] : null; var longitude = longitudeCol != -1 ? row[longitudeCol] : null; var addressLineOne = addressLineOneCol != -1 ? row[addressLineOneCol] : null; var addressLineTwo = addressLineTwoCol != -1 ? row[addressLineTwoCol] : null; var adminDistrictTwo = cityCol != -1 ? row[cityCol] : null; var adminDistrictOne = stateCol != -1 ? row[stateCol] : null; var postalCode = zipCodeCol != -1 ? row[zipCodeCol] : null; var regionName = regionNameCol != -1 ? row[regionNameCol] : null; //If lat/lon dont have values, try to Geocode if (latitude == null && longitude == null) { var address = new Address { AddressLineOne = addressLineOne, City = adminDistrictTwo, State = adminDistrictOne, ZipCode = postalCode }; //Attempt to Geocode the address var geocodeResult = BingLocationServices.TryGeocode(address).FirstOrDefault(gc => gc != null); latitude = geocodeResult != null ? geocodeResult.Latitude : null; longitude = geocodeResult != null ? geocodeResult.Longitude : null; //If they still do not have values, throw error on the location if (latitude == null && longitude == null) importedLocation.StatusInt = (int)ImportStatus.Error; //Matched the entire address to a location (AddressLineOne, AddressLineTwo, City, State and ZipCode) var matchedLocation = _locations.FirstOrDefault(l => l.Value.AddressLineOne == addressLineOne && l.Value.AddressLineTwo == addressLineTwo && l.Value.AdminDistrictTwo == adminDistrictTwo && l.Value.AdminDistrictOne == adminDistrictOne && l.Value.PostalCode == postalCode); if (matchedLocation.Key != Guid.Empty) importedLocation = ConvertLocationSetRegionAndStatus(matchedLocation.Value, regionName, ImportStatus.Linked); else { clientName = clientNameCol != -1 ? row[clientNameCol] : null; //Matched the address entered and client name to a location matchedLocation = _locations.FirstOrDefault(l => clientName != null && l.Value.ClientId != null && (addressLineTwo != null && (l.Value.AddressLineOne == addressLineOne && l.Value.AddressLineTwo == addressLineTwo && _clients.First(c => c.Key == l.Value.ClientId).Value.Name == clientName))); if (matchedLocation.Key != Guid.Empty) importedLocation = ConvertLocationSetRegionAndStatus(matchedLocation.Value, regionName, ImportStatus.Linked); } } //If lat/lon exist, try and match based on that if (latitude != null && longitude != null && importedLocation.StatusInt != (int)ImportStatus.Error) { var setError = false; //check for invalid deciamls Decimal convertedLatitude = 0; Decimal convertedLongitude = 0; Decimal tempdeciaml; if (Decimal.TryParse(latitude, out tempdeciaml)) convertedLatitude = tempdeciaml; else setError = true; if (Decimal.TryParse(longitude, out tempdeciaml)) convertedLongitude = tempdeciaml; else setError = true; if (!setError) { var roundedLatitude = Math.Round(convertedLatitude, 6); var roundedLongitude = Math.Round(convertedLongitude, 6); //Try and match a location to one in the FoundOPS system var matchedLocation = addressLineTwo != null //Use this statement if Address Line Two is not null ? _locations.FirstOrDefault(l => l.Value.AddressLineTwo == addressLineTwo && (l.Value.Latitude != null && l.Value.Longitude != null) && (decimal.Round(l.Value.Latitude.Value, 6) == roundedLatitude && decimal.Round(l.Value.Longitude.Value, 6) == roundedLongitude)) //Use this statement if Address Line Two is null : _locations.FirstOrDefault(l => (l.Value.Latitude != null && l.Value.Longitude != null) && (decimal.Round(l.Value.Latitude.Value, 6) == roundedLatitude && decimal.Round(l.Value.Longitude.Value, 6) == roundedLongitude)); //If a match is found, assign Linked status to all cells if (matchedLocation.Key != Guid.Empty) importedLocation = ConvertLocationSetRegionAndStatus(matchedLocation.Value, regionName, ImportStatus.Linked); } else importedLocation.StatusInt = (int) ImportStatus.Error; } //If no linked location is found, it means that the location is new if (importedLocation.StatusInt != (int)ImportStatus.Linked) { var status = (int) ImportStatus.New; if (importedLocation.StatusInt == (int) ImportStatus.Error) status = (int) ImportStatus.Error; importedLocation = new Api.Models.Location { Id = Guid.NewGuid(), AddressLineOne = addressLineOne, AddressLineTwo = addressLineTwo ?? null, AdminDistrictTwo = adminDistrictTwo, AdminDistrictOne = adminDistrictOne, PostalCode = postalCode, CountryCode = "US", ContactInfoSet = new List<ContactInfo>(), Latitude = latitude, Longitude = longitude, StatusInt = status, Region = SetRegion(regionName) }; //Since the location is new, we add it to the list of locations to be searched next time we try and match _locations.GetOrAdd(importedLocation.Id, FoundOps.Api.Models.Location.ConvertBack(importedLocation)); } importRow.Location = importedLocation; } #endregion #region Client clientName = clientNameCol != -1 ? row[clientNameCol] : null; //Make sure a Client is being Imported if (clientName != null) { //Find an existing Client based on the Name var existingClient = _clients.FirstOrDefault(c => c.Value.Name == clientName); Models.Client importedClient; //If a Client was found, set it to be imported with a Linked Status if (existingClient.Key != Guid.Empty) { importedClient = FoundOps.Api.Models.Client.ConvertModel(existingClient.Value); importedClient.StatusInt = (int)ImportStatus.Linked; } //If not, create a new Client and add it to _clients for matching later else { importedClient = new FoundOps.Api.Models.Client { Id = Guid.NewGuid(), Name = clientName, ContactInfoSet = new List<ContactInfo>(), StatusInt = (int)ImportStatus.New }; //Since the client is new, we add it to the list of clients to be searched next time we try and match _clients.GetOrAdd(importedClient.Id, FoundOps.Api.Models.Client.ConvertBack(importedClient)); } importRow.Client = importedClient; } #endregion #region Repeat if (new[] { startDateCol, endDateCol, endAfterCol, repeatEveryCol, frequencyCol, frequencyDetailCol }.Any(col => col != -1)) { var setError = false; //check for invalid DateTime's DateTime startDate = DateTime.UtcNow.Date; DateTime? endDate = null; DateTime tempDate; if (startDateCol != -1 && !String.IsNullOrEmpty(row[startDateCol])) { if (DateTime.TryParse(row[startDateCol], out tempDate)) startDate = tempDate; else { startDate = DateTime.UtcNow.Date; setError = true; } } if (endDateCol != -1 && !String.IsNullOrEmpty(row[endDateCol])) { if (DateTime.TryParse(row[endDateCol], out tempDate)) endDate = tempDate; else { endDate = null; setError = true; } } //Check for invalid Int's int? endAfterTimes = null; int? repeatEveryTimes = null; int tempInt; if (endAfterCol != -1 && !String.IsNullOrEmpty(row[endAfterCol])) { if (Int32.TryParse(row[endAfterCol], out tempInt)) endAfterTimes = tempInt; else { endAfterTimes = null; setError = true; } } if (repeatEveryCol != -1 && !String.IsNullOrEmpty(row[repeatEveryCol])) { if (Int32.TryParse(row[repeatEveryCol], out tempInt)) repeatEveryTimes = tempInt; else { repeatEveryTimes = null; setError = true; } } //Create the Repeat object var repeat = new Repeat { Id = Guid.NewGuid(), StartDate = startDate, EndDate = endDate, EndAfterTimes = endAfterTimes, RepeatEveryTimes = repeatEveryTimes }; #region Frequency var val = frequencyCol != -1 ? row[frequencyCol].ToLower() : String.Empty; switch (val) { case "o": case "once": repeat.FrequencyInt = (int)Frequency.Once; break; case "d": case "daily": repeat.FrequencyInt = (int)Frequency.Daily; break; case "w": case "weekly": repeat.FrequencyInt = (int)Frequency.Weekly; break; case "m": case "monthly": repeat.FrequencyInt = (int)Frequency.Monthly; break; case "y": case "yearly": repeat.FrequencyInt = (int)Frequency.Yearly; break; default: repeat.FrequencyInt = null; break; } #endregion #region Frequency Detail val = frequencyDetailCol != -1 ? row[frequencyDetailCol].ToLower() : String.Empty; val = val.Replace(" ", ""); if (repeat.Frequency == Frequency.Weekly) { var startDayOfWeek = repeat.StartDate.DayOfWeek; //If it is empty assume the Start Date if (string.IsNullOrEmpty(val)) repeat.FrequencyDetailAsWeeklyFrequencyDetail = new[] { startDayOfWeek }; else { var dayStrings = val.Split(','); var daysOfWeek = new List<DayOfWeek>(); if (dayStrings.Any(s => s == "s" || s == "su" || s == "sun" || s == "sunday")) daysOfWeek.Add(DayOfWeek.Sunday); if (dayStrings.Any(s => s == "m" || s == "mo" || s == "mon" || s == "monday")) daysOfWeek.Add(DayOfWeek.Monday); if (dayStrings.Any(s => s == "t" || s == "tu" || s == "tue" || s == "tues" || s == "tuesday")) daysOfWeek.Add(DayOfWeek.Tuesday); if (dayStrings.Any(s => s == "w" || s == "we" || s == "wed" || s == "wednesday")) daysOfWeek.Add(DayOfWeek.Wednesday); if (dayStrings.Any(s => s == "r" || s == "th" || s == "tr" || s == "thur" || s == "thurs" || s == "thursday")) daysOfWeek.Add(DayOfWeek.Thursday); if (dayStrings.Any(s => s == "f" || s == "fr" || s == "fri" || s == "friday")) daysOfWeek.Add(DayOfWeek.Friday); if (dayStrings.Any(s => s == "s" || s == "sa" || s == "sat" || s == "saturday")) daysOfWeek.Add(DayOfWeek.Saturday); //Make sure the days include the startdate if (!daysOfWeek.Contains(startDayOfWeek)) daysOfWeek.Add(startDayOfWeek); repeat.FrequencyDetailAsWeeklyFrequencyDetail = daysOfWeek.OrderBy(e => (int)e).ToArray(); } } if (repeat.Frequency == Frequency.Monthly) { if (string.IsNullOrEmpty(val) || val == "date") { repeat.FrequencyDetailAsMonthlyFrequencyDetail = MonthlyFrequencyDetail.OnDayInMonth; } else if (val == "day") { var detailsAvailable = repeat.AvailableMonthlyFrequencyDetailTypes.ToList(); if (detailsAvailable.Count() > 1) detailsAvailable.Remove(MonthlyFrequencyDetail.OnDayInMonth); repeat.FrequencyDetailAsMonthlyFrequencyDetail = detailsAvailable.First(); } } #endregion repeat.StatusInt = repeat.RepeatEveryTimes == null || repeat.FrequencyInt == null || setError ? (int)ImportStatus.Error : (int)ImportStatus.New; importRow.Repeat = repeat; } #endregion #region Contact Info //Create label and value dictionaries for Phone Number contact information var phoneValueDictionary = phoneNumberValueCols.ToDictionary(p => Convert.ToInt32(p.Split(' ').ElementAt(2)), p => row[Array.IndexOf(headers, p)]); var phoneLabelDictionary = phoneNumberLabelCols.ToDictionary(p => Convert.ToInt32(p.Split(' ').ElementAt(2)), p => row[Array.IndexOf(headers, p)]); //Create label and value dictionaries for Email Address contact information var emailValueDictionary = emailValueCols.ToDictionary(e => Convert.ToInt32(e.Split(' ').ElementAt(2)), e => row[Array.IndexOf(headers, e)]); var emailLabelDictionary = emailLabelCols.ToDictionary(e => Convert.ToInt32(e.Split(' ').ElementAt(2)), e => row[Array.IndexOf(headers, e)]); //Create label and value dictionaries for Website contact information var websiteValueDictionary = websiteValueCols.ToDictionary(w => Convert.ToInt32(w.Split(' ').ElementAt(2)), w => row[Array.IndexOf(headers, w)]); var websiteLabelDictionary = websiteLabelCols.ToDictionary(w => Convert.ToInt32(w.Split(' ').ElementAt(2)), w => row[Array.IndexOf(headers, w)]); //Create label and value dictionaries for any other types of contact information var otherValueDictionary = otherValueCols.ToDictionary(o => Convert.ToInt32(o.Split(' ').ElementAt(2)), o => row[Array.IndexOf(headers, o)]); var otherLabelDictionary = otherLabelCols.ToDictionary(o => Convert.ToInt32(o.Split(' ').ElementAt(2)), o => row[Array.IndexOf(headers, o)]); //Find which type of contact info is being imported the most //This way we only have one loop var maxLabel = Math.Max(Math.Max(phoneLabelDictionary.Count, emailLabelDictionary.Count), Math.Max(websiteLabelDictionary.Count, otherLabelDictionary.Count)); var maxValue = Math.Max(Math.Max(phoneValueDictionary.Count, emailValueDictionary.Count), Math.Max(websiteValueDictionary.Count, otherValueDictionary.Count)); var max = Math.Max(maxLabel, maxValue); //Dictionary of Cantact Information to be imported for the row var concurrentContactInfoDictionary = new ConcurrentDictionary<Guid, ContactInfo>(); Parallel.For((long)1, max + 1, contactIndex => { //Phone if (phoneLabelDictionary.Count >= contactIndex || phoneValueDictionary.Count >= contactIndex) MatchContactInfo(phoneLabelDictionary, phoneValueDictionary, contactIndex, concurrentContactInfoDictionary, "Phone Number"); //Email if (emailLabelDictionary.Count >= contactIndex || emailValueDictionary.Count >= contactIndex) MatchContactInfo(emailLabelDictionary, emailValueDictionary, contactIndex, concurrentContactInfoDictionary, "Email Address"); //Website if (websiteLabelDictionary.Count >= contactIndex || websiteValueDictionary.Count >= contactIndex) MatchContactInfo(websiteLabelDictionary, websiteValueDictionary, contactIndex, concurrentContactInfoDictionary, "Website"); //Other if (otherLabelDictionary.Count >= contactIndex || otherValueDictionary.Count >= contactIndex) MatchContactInfo(otherLabelDictionary, otherValueDictionary, contactIndex, concurrentContactInfoDictionary, "Other"); }); //Once all the contact info sets are made or matched, add them to the ImportRow importRow.ContactInfoSet.AddRange(concurrentContactInfoDictionary.Select(ci => ci.Value)); #endregion //Add the ImportRow to the concurrent dictionary of ImportRows importRowsConcurrentDictionary.GetOrAdd((int)rowIndex, importRow); }); //Order the ImportRows by rowIndex var importRows = importRowsConcurrentDictionary.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value).ToArray(); //Send the validated ImportRows to get suggestions and return return SuggestEntites(importRows); }