21 Commits

Author SHA1 Message Date
dcd1a9ccec Pricing updates 2025-10-30 09:11:43 -04:00
aee37eb8f7 NOT null or empty 2025-06-03 10:48:17 -04:00
0ce0dc35e1 Updated timestamp string format 2025-06-03 09:39:46 -04:00
1ddf18a400 Merge pull request 'Add support for inventory timestamps' (#2) from InventoryTimestamps into master
Reviewed-on: #2
2025-05-29 13:55:40 +00:00
41a7f57988 Add support for inventory timestamps 2025-05-29 09:55:07 -04:00
bd6682e861 Merge conflicts 2025-05-28 10:35:06 -04:00
bbeb96dbda Minor changes 2025-05-28 10:34:35 -04:00
ca45a77a0f Tweaks for stability 2025-04-09 11:51:59 -04:00
8b6892df60 Some things broke 2025-04-03 21:19:33 -04:00
f1ca48c1d0 FTP configuration 2025-04-03 20:49:56 -04:00
57f42a0e47 Finalize hourly inventory configuration 2025-04-03 20:48:03 -04:00
eb928a7c56 Merge pull request 'HourlyInventory' (#1) from HourlyInventory into master
Reviewed-on: #1
2025-04-04 00:35:03 +00:00
36b05af60e Merge from master 2025-04-03 20:34:29 -04:00
a3a08d9cff WIP 2025-04-03 20:28:46 -04:00
cc2cbd09e1 Whatever this is 2025-02-12 18:12:19 -05:00
aed30707be WIP 2023-12-19 14:49:30 -05:00
b8406a7f71 WIP 2023-11-14 11:58:54 -05:00
469fb0ff5f DBContext changes 2023-10-11 13:52:53 -04:00
9a2d57f975 WIP 2023-09-13 09:44:04 -04:00
6a81fe4f87 WIP 2023-09-13 09:41:33 -04:00
547c5c935c WIP 2023-09-13 09:33:14 -04:00
38 changed files with 883 additions and 526 deletions

View File

@@ -1,8 +1,7 @@
using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using PartSource.Data.Models; using PartSource.Data.Models;
using PartSource.Services; using PartSource.Services;
using System.Net;
using System.Threading.Tasks;
namespace PartSource.Api.Controllers namespace PartSource.Api.Controllers
{ {
@@ -23,7 +22,7 @@ namespace PartSource.Api.Controllers
[Route("sku/{sku}/storeNumber/{storeNumber}")] [Route("sku/{sku}/storeNumber/{storeNumber}")]
public async Task<ActionResult> GetInventory(int sku, int storeNumber) public async Task<ActionResult> GetInventory(int sku, int storeNumber)
{ {
PartsAvailability inventory = await _inventoryService.GetInventory(sku, storeNumber); PartAvailability inventory = await _inventoryService.GetInventory(sku, storeNumber);
if (inventory == null) if (inventory == null)
{ {
@@ -36,7 +35,8 @@ namespace PartSource.Api.Controllers
{ {
StoreNumber = inventory.Store, StoreNumber = inventory.Store,
Sku = sku, Sku = sku,
Quantity = inventory.QTY Quantity = inventory.QTY,
Updated = inventory.Updated
} }
}); });
} }

View File

@@ -4,6 +4,7 @@ using PartSource.Data.Dtos;
using PartSource.Data.Models; using PartSource.Data.Models;
using PartSource.Data.Nexpart; using PartSource.Data.Nexpart;
using PartSource.Services; using PartSource.Services;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -41,6 +42,77 @@ namespace PartSource.Api.Controllers
return NotFound(); return NotFound();
} }
try
{
string[] segments = vehicleFitment.NoteText.Split(']');
vehicleFitment.PartDescription = segments[0].TrimStart('[');
vehicleFitment.DriveTypes = GetDriveTypesFromNote(vehicleFitment.NoteText);
vehicleFitment.Notes = segments[1].Split(';')
.Select(n => n.Trim())
.ToList();
}
catch
{
throw new InvalidOperationException($"The note_text field provided by WHI for {vehicleFitment.LineCode} {vehicleFitment.PartNumber} was in an invalid format.");
}
SmartPageDataSearch smartPageDataSearch = new SmartPageDataSearch
{
Items = new[]
{
new Item { PartNumber = vehicleFitment.PartNumber, MfrCode = vehicleFitment.LineCode }
}
};
SmartPageDataSearchResponse smartPageResponse = await _nexpartService.SendRequest<SmartPageDataSearch, SmartPageDataSearchResponse>(smartPageDataSearch);
if (smartPageResponse.ResponseBody?.Item != null)
{
PartType[] partTypes = smartPageResponse.ResponseBody.Item.Select(i => new PartType
{
Id = i.Part.PartType.Id
})
.ToArray();
ApplicationSearch applicationSearch = new ApplicationSearch
{
VehicleIdentifier = new VehicleIdentifier
{
BaseVehicleId = vehicleFitment.BaseVehicleId,
EngineConfigId = vehicleFitment.EngineConfigId
},
MfrCode = new[] { vehicleFitment.LineCode },
PartType = partTypes,
GroupBy = "MFR",
QuestionOption = "QUESTION_OTHERWISE_APP"
};
ApplicationSearchResponse response = await _nexpartService.SendRequest<ApplicationSearch, ApplicationSearchResponse>(applicationSearch);
if (response.ResponseBody != null && response.ResponseBody is Questions)
{
Question driveTypeQuestion = ((Questions)response.ResponseBody).Question
.Where(q => q.Attribute == "DRIVE_TYPE")
.FirstOrDefault();
if (driveTypeQuestion != null)
{
foreach (Answer answer in driveTypeQuestion.Answer)
{
applicationSearch.Criterion = new[]
{
new Criterion { Attribute = "DRIVE_TYPE", Id = answer.Id}
};
ApplicationSearchResponse driveTypeResponse = await _nexpartService.SendRequest<ApplicationSearch, ApplicationSearchResponse>(applicationSearch);
if (driveTypeResponse.ResponseBody != null && ((Apps)driveTypeResponse.ResponseBody).App.Where(a => a.Part == vehicleFitment.PartNumber).Any())
{
vehicleFitment.DriveTypes.Add(answer.Value);
}
}
}
}
}
return Ok(vehicleFitment); return Ok(vehicleFitment);
} }
@@ -51,15 +123,24 @@ namespace PartSource.Api.Controllers
Part part = await _partService.GetPartBySku(sku); Part part = await _partService.GetPartBySku(sku);
Vehicle vehicle = await _vehicleService.GetVehicleById(vehicleId); Vehicle vehicle = await _vehicleService.GetVehicleById(vehicleId);
if (part == null || vehicle == null) if (part == null)
{ {
return BadRequest(new return BadRequest(new
{ {
Message = $"No data is available for SKU {sku}. Confirm it is available in the database maintained by Sound Press.", Message = $"No part data is available for SKU {sku}. Confirm it is available in the database maintained by Sound Press.",
Reason = $"{nameof(_partService.GetPartBySku)} returned null" Reason = $"{nameof(_partService.GetPartBySku)} returned null"
}); });
} }
if (vehicle == null)
{
return BadRequest(new
{
Message = $"No vehicle data is available for vehicle ID {vehicleId}. Confirm it is available in the database maintained by Sound Press.",
Reason = $"{nameof(_vehicleService.GetVehicleById)} returned null"
});
}
IList<DcfMapping> mappings = await _partService.GetDcfMapping(part.LineCode); IList<DcfMapping> mappings = await _partService.GetDcfMapping(part.LineCode);
Item[] items = mappings.Select(m => new Item Item[] items = mappings.Select(m => new Item
{ {
@@ -87,7 +168,7 @@ namespace PartSource.Api.Controllers
{ {
Id = i.Part.PartType.Id Id = i.Part.PartType.Id
}) })
.ToArray(); .ToArray();
ApplicationSearch applicationSearch = new ApplicationSearch ApplicationSearch applicationSearch = new ApplicationSearch
{ {
@@ -98,13 +179,13 @@ namespace PartSource.Api.Controllers
MfrCode = mappings.Select(m => m.WhiCode).ToArray(), MfrCode = mappings.Select(m => m.WhiCode).ToArray(),
PartType = new[] { new PartType { Id = smartPageResponse.ResponseBody.Item[0].Part.PartType.Id } }, PartType = new[] { new PartType { Id = smartPageResponse.ResponseBody.Item[0].Part.PartType.Id } },
Criterion = new[] Criterion = new[]
{
new Criterion
{ {
new Criterion Attribute = "REGION",
{ Id = 2
Attribute = "REGION", }
Id = 2 },
}
},
GroupBy = "PARTTYPE" GroupBy = "PARTTYPE"
}; };
@@ -120,7 +201,7 @@ namespace PartSource.Api.Controllers
} }
IList<string> positions = new List<string>(); IList<string> positions = new List<string>();
foreach (App app in response.ResponseBody?.App) foreach (App app in ((Apps)response.ResponseBody)?.App)
{ {
if (!string.IsNullOrEmpty(app.Position) && app.Part == part.PartNumber) if (!string.IsNullOrEmpty(app.Position) && app.Part == part.PartNumber)
{ {
@@ -136,5 +217,33 @@ namespace PartSource.Api.Controllers
}); });
} }
private IList<string> GetDriveTypesFromNote(string fitmentNote)
{
fitmentNote = fitmentNote.ToUpperInvariant();
IList<string> driveTypes = new List<string>();
if (fitmentNote.Contains("FWD"))
{
driveTypes.Add("FWD");
}
if (fitmentNote.Contains("RWD"))
{
driveTypes.Add("RWD");
}
if (fitmentNote.Contains("AWD"))
{
driveTypes.Add("AWD");
}
if (fitmentNote.Contains("4WD"))
{
driveTypes.Add("4WD");
}
return driveTypes;
}
} }
} }

View File

@@ -51,7 +51,7 @@ namespace PartSource.Api.Controllers
if (response.ResponseBody != null) if (response.ResponseBody != null)
{ {
return NexpartResponse<ApplicationSearchResponse, Apps>(response); return NexpartResponse<ApplicationSearchResponse, object>(response);
} }
else else

View File

@@ -4,6 +4,7 @@
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<UserSecretsId>f9e2fd37-0f2d-4e3a-955a-8e49a16fce1c</UserSecretsId> <UserSecretsId>f9e2fd37-0f2d-4e3a-955a-8e49a16fce1c</UserSecretsId>
<Configurations>Debug;Release;Also Debug</Configurations> <Configurations>Debug;Release;Also Debug</Configurations>
<SatelliteResourceLanguages>en-us;en</SatelliteResourceLanguages>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -34,6 +35,7 @@
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.5" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Ratermania.Shopify" Version="6.16.11" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" />
</ItemGroup> </ItemGroup>

View File

@@ -1,13 +1,4 @@
{ {
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:31337",
"sslPort": 0
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": { "profiles": {
"IIS Express": { "IIS Express": {
"commandName": "IISExpress", "commandName": "IISExpress",
@@ -18,12 +9,20 @@
}, },
"PartSource.Api": { "PartSource.Api": {
"commandName": "Project", "commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values", "launchUrl": "api/values",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"applicationUrl": "https://localhost:5001;http://localhost:5000" "applicationUrl": "https://localhost:5001;http://localhost:5000"
} }
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:31337",
"sslPort": 0
}
} }
} }

View File

@@ -1,8 +1,8 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"PartSourceDatabase": "Server=tcp:ps-whi.database.windows.net,1433;Initial Catalog=ps-whi-stage;Persist Security Info=False;User ID=ps-whi;Password=9-^*N5dw!6:|.5Q;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;", "PartSourceDatabase": "Server=tcp:ps-whi.database.windows.net,1433;Initial Catalog=ps-whi-stage;Persist Security Info=False;User ID=ps-whi;Password=9-^*N5dw!6:|.5Q;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;",
"FitmentDatabase": "Server=tcp:ps-automation.eastus2.cloudapp.azure.com,1433;Initial Catalog=WhiFitment;User ID=automation;Password=)6L)XP%m(x-UU#M;Encrypt=True;TrustServerCertificate=True;Connection Timeout=300"
//"FitmentDatabase": "Data Source=localhost;Initial Catalog=WhiFitment;Integrated Security=true" //"FitmentDatabase": "Data Source=localhost;Initial Catalog=WhiFitment;Integrated Security=true"
"FitmentDatabase": "Server=tcp:ps-automation.eastus2.cloudapp.azure.com,1433;Initial Catalog=WhiFitment;User ID=sa;Password=GZ0`-ekd~[2u;Encrypt=True;TrustServerCertificate=True;Connection Timeout=300"
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {

View File

@@ -14,19 +14,22 @@ using PartSource.Automation.Models.Ftp;
using PartSource.Automation.Services; using PartSource.Automation.Services;
using Ratermania.Automation.Interfaces; using Ratermania.Automation.Interfaces;
namespace PartSource.Automation.Jobs.POC namespace PartSource.Automation.Jobs
{ {
public class BulkUpdateInventory : IAutomationJob public class BulkUpdateInventory : IAutomationJob
{ {
private readonly FtpService _ftpService; private readonly FtpService _ftpService;
private readonly ILogger<BulkUpdateInventory> _logger; private readonly ILogger<BulkUpdateInventory> _logger;
private readonly string _connectionString;
public BulkUpdateInventory(IConfiguration configuration, ILogger<BulkUpdateInventory> logger) public BulkUpdateInventory(IConfiguration configuration, ILogger<BulkUpdateInventory> logger)
{ {
FtpConfiguration ftpConfiguration = configuration.GetSection("FtpServers:AutomationConfiguration").Get<FtpConfiguration>(); FtpConfiguration ftpConfiguration = configuration.GetSection("FtpServers:AutomationConfiguration").Get<FtpConfiguration>();
_ftpService = new FtpService(ftpConfiguration); _ftpService = new FtpService(ftpConfiguration);
_logger = logger; _connectionString = configuration.GetConnectionString("PartSourceDatabase");
_logger = logger;
} }
public async Task Run(CancellationToken token, params string[] arguments) public async Task Run(CancellationToken token, params string[] arguments)
@@ -46,11 +49,11 @@ namespace PartSource.Automation.Jobs.POC
DataTable dataTable = GetDataTable(file); DataTable dataTable = GetDataTable(file);
using SqlConnection connection = new SqlConnection("Server=tcp:ps-whi.database.windows.net,1433;Initial Catalog=ps-whi-test;Persist Security Info=False;User ID=ps-whi;Password=9-^*N5dw!6:|.5Q;MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"); using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open(); connection.Open();
using SqlCommand command = new SqlCommand("TRUNCATE TABLE PartAvailability", connection); using SqlCommand command = new SqlCommand("TRUNCATE TABLE PartAvailability", connection);
await command.ExecuteNonQueryAsync(); await command.ExecuteNonQueryAsync(token);
using SqlBulkCopy bulk = new SqlBulkCopy(connection) using SqlBulkCopy bulk = new SqlBulkCopy(connection)
{ {
@@ -58,7 +61,7 @@ namespace PartSource.Automation.Jobs.POC
BulkCopyTimeout = 14400 BulkCopyTimeout = 14400
}; };
bulk.WriteToServer(dataTable); await bulk.WriteToServerAsync(dataTable, token);
_ftpService.Delete(lastUploadedFile.Filename); _ftpService.Delete(lastUploadedFile.Filename);
@@ -71,6 +74,7 @@ namespace PartSource.Automation.Jobs.POC
dataTable.Columns.Add("Store", typeof(int)); dataTable.Columns.Add("Store", typeof(int));
dataTable.Columns.Add("SKU", typeof(string)); dataTable.Columns.Add("SKU", typeof(string));
dataTable.Columns.Add("QTY", typeof(int)); dataTable.Columns.Add("QTY", typeof(int));
dataTable.Columns.Add("Updated", typeof(string));
using StreamReader reader = new StreamReader(filename); using StreamReader reader = new StreamReader(filename);
string line = reader.ReadLine(); // Burn the header row string line = reader.ReadLine(); // Burn the header row
@@ -86,11 +90,14 @@ namespace PartSource.Automation.Jobs.POC
} }
string sku = columns[1].Trim(); string sku = columns[1].Trim();
string updated = columns[3].Trim();
if (int.TryParse(columns[0], out int store) if (int.TryParse(columns[0], out int store)
&& !string.IsNullOrEmpty(sku) && !string.IsNullOrEmpty(sku)
&& int.TryParse(columns[2], out int quantity)) && int.TryParse(columns[2], out int quantity)
&& !string.IsNullOrEmpty(updated))
{ {
dataTable.Rows.Add(new object[] { store, sku, quantity }); dataTable.Rows.Add(new object[] { store, sku, quantity, updated });
} }
} }

View File

@@ -1,9 +1,11 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PartSource.Automation.Models.Configuration; using PartSource.Automation.Models.Configuration;
using PartSource.Automation.Models.Ftp;
using PartSource.Automation.Services; using PartSource.Automation.Services;
using Ratermania.Automation.Interfaces; using Ratermania.Automation.Interfaces;
using System; using System;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -17,11 +19,11 @@ namespace PartSource.Automation.Jobs
private readonly ILogger<ExecuteSsisPackages> _logger; private readonly ILogger<ExecuteSsisPackages> _logger;
// TODO: set from config // TODO: set from config
private readonly string[] _ssisPackages = { "Parts Availability" }; private readonly string[] _ssisPackages = {"Parts Price" };
public ExecuteSsisPackages(EmailService emailService, IConfiguration configuration, SsisService ssisService, ILogger<ExecuteSsisPackages> logger) public ExecuteSsisPackages(EmailService emailService, IConfiguration configuration, SsisService ssisService, ILogger<ExecuteSsisPackages> logger)
{ {
FtpConfiguration ftpConfiguration = configuration.GetSection("FtpServers:AzureConfiguration").Get<FtpConfiguration>(); FtpConfiguration ftpConfiguration = configuration.GetSection("FtpServers:AutomationConfiguration").Get<FtpConfiguration>();
_emailService = emailService; _emailService = emailService;
_ftpService = new FtpService(ftpConfiguration); _ftpService = new FtpService(ftpConfiguration);
@@ -36,7 +38,18 @@ namespace PartSource.Automation.Jobs
{ {
try try
{ {
_ftpService.Download($"{package}.txt"); FtpFileInfo lastUploadedFile = _ftpService.ListFilesExtended()
.Where(f => f.FileType == FtpFileType.File && f.Filename.IndexOf(package) > -1)
.OrderByDescending(f => f.Modified)
.FirstOrDefault();
if (lastUploadedFile == null)
{
_logger.LogInformation($"No {package} file available.");
return;
}
_ftpService.Download($"{package}.txt");
_ssisService.Execute($"{package}.dtsx"); _ssisService.Execute($"{package}.dtsx");
_logger.LogInformation($"Execution of SSIS package {package} completed successfully."); _logger.LogInformation($"Execution of SSIS package {package} completed successfully.");
@@ -45,7 +58,6 @@ namespace PartSource.Automation.Jobs
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError($"Execution of SSIS package {package} failed.", ex); _logger.LogError($"Execution of SSIS package {package} failed.", ex);
throw; throw;
} }
} }

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using PartSource.Data.Contexts;
using PartSource.Data.Models;
using PartSource.Data.Nexpart;
using PartSource.Services;
using Ratermania.Automation.Interfaces;
using Ratermania.Shopify;
using Ratermania.Shopify.Resources;
namespace PartSource.Automation.Jobs.POC
{
public class ImageList : IAutomationJob
{
private readonly NexpartService _nexpartService;
private readonly PartSourceContext _partSourceContext;
private readonly FitmentContext _fitmentContext;
public ImageList(NexpartService nexpartService, PartSourceContext partSourceContext, FitmentContext fitmentContext)
{
_nexpartService = nexpartService;
_partSourceContext = partSourceContext;
_fitmentContext = fitmentContext;
}
public async Task Run(CancellationToken token, params string[] arguments)
{
IList<string> rows = new List<string> {
"\"Line Code\", \"Part Number\", \"Image URL(s)\""
};
IList<Data.Models.Part> parts = await _fitmentContext.Parts.ToListAsync();
string oldLineCode = string.Empty;
IList<DcfMapping> mappings = new List<DcfMapping>();
foreach (Data.Models.Part part in parts)
{
if (part.LineCode != oldLineCode)
{
mappings = await _fitmentContext.DcfMappings.Where(d => d.LineCode == part.LineCode).ToListAsync();
}
;
foreach (DcfMapping mapping in mappings)
{
SmartPageDataSearch dataSearch = new SmartPageDataSearch
{
Items = new Item[]
{
new Item
{
MfrCode = mapping.WhiCode,
PartNumber = part.PartNumber
}
},
DataOption = new[] { "ALL" }
};
SmartPageDataSearchResponse response = await _nexpartService.SendRequest<SmartPageDataSearch, SmartPageDataSearchResponse>(dataSearch);
if (response.ResponseBody.Item?.Length > 0)
{
List<string> urls = new List<string>();
if (!string.IsNullOrEmpty(response.ResponseBody.Item[0].PrimaryImg?.ImgUrl))
{
urls.Add(response.ResponseBody.Item[0].PrimaryImg?.ImgUrl);
};
if (response.ResponseBody.Item[0].AddImgs?.AddImg?.Length > 0)
{
urls.AddRange(response.ResponseBody.Item[0].AddImgs.AddImg.Select(i => i.AddImgUrl));
}
if (urls.Count > 0)
{
rows.Add($"\"{part.LineCode}\", \"{part.PartNumber}\", \"{string.Join(";", urls)}\"");
}
}
}
}
await File.WriteAllLinesAsync("C:\\users\\Tommy\\desktop\\WHI Images.csv", rows);
}
}
}

View File

@@ -68,7 +68,7 @@ namespace PartSource.Automation.Jobs.POC
ApplicationSearchResponse response = await _nexpartService.SendRequest<ApplicationSearch, ApplicationSearchResponse>(applicationSearch); ApplicationSearchResponse response = await _nexpartService.SendRequest<ApplicationSearch, ApplicationSearchResponse>(applicationSearch);
if (response.ResponseBody != null) if (response.ResponseBody != null)
{ {
foreach (App app in response.ResponseBody.App) foreach (App app in ((Apps)response.ResponseBody).App)
{ {
try try
{ {

View File

@@ -68,7 +68,7 @@ namespace PartSource.Automation.Jobs.POC
ApplicationSearchResponse response = await _nexpartService.SendRequest<ApplicationSearch, ApplicationSearchResponse>(applicationSearch); ApplicationSearchResponse response = await _nexpartService.SendRequest<ApplicationSearch, ApplicationSearchResponse>(applicationSearch);
if (response.ResponseBody != null) if (response.ResponseBody != null)
{ {
foreach (App app in response.ResponseBody.App) foreach (App app in ((Apps)response.ResponseBody).App)
{ {
try try
{ {

View File

@@ -14,21 +14,25 @@ using PartSource.Automation.Models.Ftp;
using PartSource.Automation.Services; using PartSource.Automation.Services;
using Ratermania.Automation.Interfaces; using Ratermania.Automation.Interfaces;
namespace PartSource.Automation.Jobs.POC namespace PartSource.Automation.Jobs
{ {
public class PartialInventoryUpdate : IAutomationJob public class PartialInventoryUpdate : IAutomationJob
{ {
private readonly FtpService _ftpService; private readonly FtpService _ftpService;
private readonly ILogger<PartialInventoryUpdate> _logger; private readonly ILogger<PartialInventoryUpdate> _logger;
private readonly string _connectionString;
public PartialInventoryUpdate(IConfiguration configuration, ILogger<PartialInventoryUpdate> logger) public PartialInventoryUpdate(IConfiguration configuration, ILogger<PartialInventoryUpdate> logger)
{ {
FtpConfiguration ftpConfiguration = configuration.GetSection("FtpServers:AutomationConfiguration").Get<FtpConfiguration>(); FtpConfiguration ftpConfiguration = configuration.GetSection("FtpServers:AutomationConfiguration").Get<FtpConfiguration>();
_ftpService = new FtpService(ftpConfiguration); _ftpService = new FtpService(ftpConfiguration);
_connectionString = _connectionString = configuration.GetConnectionString("PartSourceDatabase");
_logger = logger; _logger = logger;
} }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA2100:Review SQL queries for security vulnerabilities")]
public async Task Run(CancellationToken token, params string[] arguments) public async Task Run(CancellationToken token, params string[] arguments)
{ {
FtpFileInfo lastUploadedFile = _ftpService.ListFilesExtended() FtpFileInfo lastUploadedFile = _ftpService.ListFilesExtended()
@@ -46,38 +50,70 @@ namespace PartSource.Automation.Jobs.POC
string file = _ftpService.Download($"{lastUploadedFile.Filename}"); string file = _ftpService.Download($"{lastUploadedFile.Filename}");
using SqlConnection connection = new SqlConnection("Server=tcp:ps-whi.database.windows.net,1433;Initial Catalog=ps-whi-test;Persist Security Info=False;User ID=ps-whi;Password=9-^*N5dw!6:|.5Q;MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"); using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open(); connection.Open();
using StreamReader reader = new StreamReader(file); using StreamReader reader = new StreamReader(file);
string line = reader.ReadLine(); // Burn the header row string line = reader.ReadLine(); // Burn the header row
IDictionary<string, object> parameters = new Dictionary<string, object>();
string command = string.Empty;
int i = 0;
while (reader.Peek() > 0) while (reader.Peek() > 0)
{ {
line = reader.ReadLine(); line = reader.ReadLine();
string[] columns = line.Split("|"); string[] columns = line.Split("|");
for (int i = 0; i < columns.Length; i++) for (int j = 0; j < columns.Length; j++)
{ {
columns[i] = columns[i].Replace("\"", string.Empty); columns[j] = columns[j].Replace("\"", string.Empty);
} }
string sku = columns[1].Trim();
string updated = columns[3].Trim();
if (int.TryParse(columns[0], out int store) if (int.TryParse(columns[0], out int store)
&& int.TryParse(columns[1], out int sku) && !string.IsNullOrEmpty(sku)
&& int.TryParse(columns[2], out int quantity)) && int.TryParse(columns[2], out int quantity)
&& !string.IsNullOrEmpty(updated))
{ {
using SqlCommand sqlCommand = new SqlCommand("UPDATE PartAvailability SET QTY = @qty WHERE SKU = @sku AND Store = @store", connection); command += $"UPDATE PartAvailability SET QTY = @qty_{i}, Updated = @updated_{i} WHERE SKU = @sku_{i} AND Store = @store_{i};";
sqlCommand.Parameters.Add(new SqlParameter("qty", quantity));
sqlCommand.Parameters.Add(new SqlParameter("sku", sku));
sqlCommand.Parameters.Add(new SqlParameter("store", store));
await sqlCommand.ExecuteNonQueryAsync(); parameters.Add($"qty_{i}", quantity);
parameters.Add($"store_{i}", store);
parameters.Add($"sku_{i}", sku);
parameters.Add($"updated_{i}", updated);
i++;
}
if (i == 250)
{
using SqlCommand nested = new SqlCommand(command, connection);
foreach (KeyValuePair<string, object> parameter in parameters)
{
nested.Parameters.Add(new SqlParameter(parameter.Key, parameter.Value));
}
await nested.ExecuteNonQueryAsync(token);
parameters.Clear();
command = string.Empty;
i = 0;
} }
} }
using SqlCommand sqlCommand = new SqlCommand(command, connection);
foreach (KeyValuePair<string, object> parameter in parameters)
{
sqlCommand.Parameters.Add(new SqlParameter(parameter.Key, parameter.Value));
}
await sqlCommand.ExecuteNonQueryAsync(token);
_ftpService.Delete(lastUploadedFile.Filename); _ftpService.Delete(lastUploadedFile.Filename);
return; return;
} }
} }
} }

View File

@@ -0,0 +1,89 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PartSource.Automation.Models.Jobs;
using PartSource.Automation.Services;
using PartSource.Data.Contexts;
using PartSource.Data.Models;
using Ratermania.Automation.Interfaces;
using Ratermania.Shopify;
using Ratermania.Shopify.Resources;
using Ratermania.Shopify.Resources.Enums;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Threading;
using System.Threading.Tasks;
namespace PartSource.Automation.Jobs
{
public class PartsSync : IAutomationJob
{
private readonly ILogger<UpdatePricing> _logger;
private readonly FitmentContext _fitmentContext;
private readonly ShopifyClient _shopifyClient;
public PartsSync(ILogger<UpdatePricing> logger, FitmentContext fitmentContext, ShopifyClient shopifyClient)
{
_logger = logger;
_fitmentContext = fitmentContext;
_shopifyClient = shopifyClient;
}
public async Task Run(CancellationToken token, params string[] arguments)
{
IEnumerable<Product> products = await _shopifyClient.Products.Get(new Dictionary<string, object> { { "limit", 250 } });
while (products != null && products.Any())
{
foreach (Product product in products)
{
try
{
IEnumerable<Metafield> metafields = await _shopifyClient.Metafields.Get(new Dictionary<string, object> { { "metafield[owner_id]", product.Id }, { "metafield[owner_resource]", "product" } });
Part part = new Part
{
LineCode = metafields.FirstOrDefault(m => m.Key == "custom_label_0")?.Value ?? string.Empty,
PartNumber = metafields.FirstOrDefault(m => m.Key == "custom_label_1")?.Value ?? string.Empty,
Sku = product.Variants[0].Sku // They know we can't do fitment for variants
};
// part.PartNumber = part.PartNumber.Replace("-", string.Empty);
if (
string.IsNullOrEmpty(part.LineCode)
|| string.IsNullOrEmpty(part.PartNumber)
|| int.TryParse(part.LineCode, out _)) //If the line code is numeric, it cannot have fitment data associated with it.
{
continue;
}
Part? existing = await _fitmentContext.Parts.FirstOrDefaultAsync(p => p.Sku == part.Sku);
if (existing == null)
{
await _fitmentContext.Parts.AddAsync(part);
await _fitmentContext.SaveChangesAsync();
}
}
catch (Exception ex)
{
_logger.LogInformation(ex.Message);
}
}
try
{
products = await _shopifyClient.Products.GetNext();
}
catch (Exception ex)
{
_logger.LogInformation(ex.Message);
}
}
}
}
}

View File

@@ -56,24 +56,25 @@ namespace PartSource.Automation.Jobs
fileGroups.Enqueue(fileGroup); fileGroups.Enqueue(fileGroup);
} }
Task[] taskArray = new Task[8]; Task[] taskArray = new Task[18];
for (int i = 0; i < taskArray.Length; i++) for (int i = 0; i < taskArray.Length; i++)
{ {
taskArray[i] = Task.Factory.StartNew(() => taskArray[i] = Task.Factory.StartNew(() =>
{ {
while (fileGroups.TryDequeue(out IGrouping<string, FileInfo> fileGroup)) while (fileGroups.TryDequeue(out IGrouping<string, FileInfo> fileGroup))
{ {
string tableName = string.Empty;
foreach (FileInfo fileInfo in fileGroup) foreach (FileInfo fileInfo in fileGroup)
{ {
try try
{ {
string filename = Decompress(fileInfo); string filename = Decompress(fileInfo);
string tableName = fileInfo.Name.Substring(0, fileInfo.Name.IndexOf('.')); DataTable dataTable = GetDataTable(filename, out tableName);
DataTable dataTable = GetDataTable(filename); string tempTable = $"Fitment_{Guid.NewGuid():N}_{tableName}";
_whiSeoService.BulkCopyFitment(dataTable, tableName); _whiSeoService.BulkCopyFitment(dataTable, tempTable);
_logger.LogInformation($"Copied {fileInfo.Name} to the database."); _logger.LogInformation($"Copied {fileInfo.Name} to the database.");
File.Delete(filename); File.Delete(filename);
@@ -85,19 +86,18 @@ namespace PartSource.Automation.Jobs
} }
} }
string fitmentTable = fileGroup.Key.Substring(0, fileGroup.Key.IndexOf('.')); _whiSeoService.CreateFitmentTable(tableName);
_whiSeoService.CreateFitmentTable(fitmentTable);
_logger.LogInformation($"Created fitment table for part group {fitmentTable}."); _logger.LogInformation($"Created fitment table for part group {tableName}.");
} }
}); });
} }
Task.WaitAll(taskArray); Task.WaitAll(taskArray);
_whiSeoService.SaveNotes(_noteDictionary); _whiSeoService.SaveNotes(_noteDictionary);
//_whiSeoService.CreateFitmentView();
_whiSeoService.CreateFitmentView();
} }
public string Decompress(FileInfo fileInfo) public string Decompress(FileInfo fileInfo)
@@ -112,8 +112,10 @@ namespace PartSource.Automation.Jobs
return decompressedFile; return decompressedFile;
} }
private DataTable GetDataTable(string filename) private DataTable GetDataTable(string filename, out string lineCode)
{ {
lineCode = string.Empty;
using DataTable dataTable = new DataTable(); using DataTable dataTable = new DataTable();
dataTable.Columns.Add("LineCode", typeof(string)); dataTable.Columns.Add("LineCode", typeof(string));
dataTable.Columns.Add("PartNumber", typeof(string)); dataTable.Columns.Add("PartNumber", typeof(string));
@@ -121,8 +123,9 @@ namespace PartSource.Automation.Jobs
dataTable.Columns.Add("EngineConfigId", typeof(int)); dataTable.Columns.Add("EngineConfigId", typeof(int));
dataTable.Columns.Add("Position", typeof(string)); dataTable.Columns.Add("Position", typeof(string));
dataTable.Columns.Add("FitmentNoteHash", typeof(string)); dataTable.Columns.Add("FitmentNoteHash", typeof(string));
dataTable.Columns.Add("PartTerminologyId", typeof(int));
using StreamReader reader = new StreamReader(filename); using StreamReader reader = new StreamReader(filename);
string line = reader.ReadLine(); // Burn the header row string line = reader.ReadLine(); // Burn the header row
while (reader.Peek() > 0) while (reader.Peek() > 0)
@@ -135,7 +138,7 @@ namespace PartSource.Automation.Jobs
columns[i] = columns[i].Replace("\"", string.Empty); columns[i] = columns[i].Replace("\"", string.Empty);
} }
string lineCode = Regex.Replace(columns[0], "[^a-zA-Z0-9]", string.Empty).Trim(); lineCode = Regex.Replace(columns[0], "[^a-zA-Z0-9]", string.Empty).Trim();
string partNumber = Regex.Replace(columns[1], "[^a-zA-Z0-9]", string.Empty).Trim(); string partNumber = Regex.Replace(columns[1], "[^a-zA-Z0-9]", string.Empty).Trim();
string position = columns[7].Trim(); string position = columns[7].Trim();
@@ -149,10 +152,11 @@ namespace PartSource.Automation.Jobs
if (!string.IsNullOrEmpty(lineCode) if (!string.IsNullOrEmpty(lineCode)
&& !string.IsNullOrEmpty(partNumber) && !string.IsNullOrEmpty(partNumber)
&& int.TryParse(columns[2], out int partTerminologyId)
&& int.TryParse(columns[5], out int baseVehicleId) && int.TryParse(columns[5], out int baseVehicleId)
&& int.TryParse(columns[6], out int engineConfigId)) && int.TryParse(columns[6], out int engineConfigId))
{ {
dataTable.Rows.Add(new object[] { lineCode, partNumber, baseVehicleId, engineConfigId, position, noteTextHash }); dataTable.Rows.Add(new object[] { lineCode, partNumber, baseVehicleId, engineConfigId, position, noteTextHash, partTerminologyId });
} }
} }

View File

@@ -1,26 +1,21 @@
using Microsoft.Extensions.Configuration; using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PartSource.Automation.Extensions; using PartSource.Automation.Extensions;
using PartSource.Automation.Models.Configuration; using PartSource.Automation.Models.Configuration;
using PartSource.Automation.Models.Enums; using PartSource.Automation.Models.Enums;
using PartSource.Automation.Services; using PartSource.Automation.Services;
using Ratermania.Automation.Interfaces; using Ratermania.Automation.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace PartSource.Automation.Jobs namespace PartSource.Automation.Jobs
{ {
public class ProcessWhiVehicles : IAutomationJob public class ProcessWhiVehicles : IAutomationJob
{ {
private readonly ILogger<ProcessWhiVehicles> _logger; private readonly ILogger<ProcessWhiVehicles> _logger;
private readonly WhiSeoService _whiSeoService; private readonly WhiSeoService _whiSeoService;

View File

@@ -24,7 +24,7 @@ namespace PartSource.Automation.Jobs
#pragma warning disable CS1998, CA1303 #pragma warning disable CS1998, CA1303
public async Task Run(CancellationToken token, params string[] arguments) public async Task Run(CancellationToken token, params string[] arguments)
{ {
// _emailService.Send("Automation Test Message", "This is a test email from the automation server. If this message was in your spam folder, whitelist the address that sent this email."); _emailService.Send("Automation Test Message", "This is a test email from the automation server. If this message was in your spam folder, whitelist the address that sent this email.");
_logger.LogInformation("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc scelerisque congue euismod. Curabitur enim eros, sollicitudin ac purus eget, dignissim mattis augue. In quam sapien, tincidunt et elementum vitae, interdum vitae sem."); _logger.LogInformation("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc scelerisque congue euismod. Curabitur enim eros, sollicitudin ac purus eget, dignissim mattis augue. In quam sapien, tincidunt et elementum vitae, interdum vitae sem.");
_logger.LogWarning("Praesent feugiat sapien non suscipit faucibus. Mauris fermentum ut augue a feugiat. Integer felis sem, laoreet et augue at, finibus maximus ex. Fusce sit amet erat non tortor porta condimentum condimentum quis ipsum."); _logger.LogWarning("Praesent feugiat sapien non suscipit faucibus. Mauris fermentum ut augue a feugiat. Integer felis sem, laoreet et augue at, finibus maximus ex. Fusce sit amet erat non tortor porta condimentum condimentum quis ipsum.");

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using PartSource.Automation.Services; using PartSource.Automation.Services;
@@ -13,6 +14,7 @@ using PartSource.Data.Models;
using Ratermania.Automation.Interfaces; using Ratermania.Automation.Interfaces;
using Ratermania.Shopify; using Ratermania.Shopify;
using Ratermania.Shopify.Resources; using Ratermania.Shopify.Resources;
using System.Web;
namespace PartSource.Automation.Jobs namespace PartSource.Automation.Jobs
{ {
@@ -36,216 +38,148 @@ namespace PartSource.Automation.Jobs
public async Task Run(CancellationToken token, params string[] arguments) public async Task Run(CancellationToken token, params string[] arguments)
{ {
IEnumerable<Product> products = null; IList<string> productTypes = await _fitmentContext.ProductTypes
.Where(p => p.Active)
.Select(p => HttpUtility.UrlEncode(p.Name))
.ToListAsync();
try foreach (string productType in productTypes)
{ {
//products = await _shopifyClient.Products.Get(new Dictionary<string, object> { { "limit", 250 } }); _logger.LogInformation("Processing {productType}", HttpUtility.UrlDecode(productType));
products = new List<Product>
{
await _shopifyClient.Products.GetById(7285013446703)
};
}
catch (Exception ex) IEnumerable<Product> products = null;
{
_logger.LogError("Failed to get products from Shopify", ex);
throw;
}
int i = 1;
while (products != null && products.Any())
{
foreach (Product product in products)
{
ImportData importData = null;
try
{
IEnumerable<Metafield> metafields = await _shopifyClient.Metafields.Get(new Dictionary<string, object> { { "metafield[owner_id]", product.Id }, { "metafield[owner_resource]", "product" } });
importData = new ImportData
{
LineCode = metafields.FirstOrDefault(m => m.Key == "custom_label_0")?.Value ?? string.Empty,
PartNumber = metafields.FirstOrDefault(m => m.Key == "custom_label_1")?.Value ?? string.Empty,
VariantSku = product.Variants[0].Sku // They know we can't do fitment for variants
};
bool isFitment = false;
string bodyHtml = string.IsNullOrEmpty(product.BodyHtml)
? "<ul></ul>"
: product.BodyHtml.Substring(0, product.BodyHtml.IndexOf("</ul>") + "</ul>".Length);
IList<Vehicle> vehicles = await _vehicleFitmentService.GetVehiclesForPart(importData.PartNumber, importData.LineCode);
IList<int> vehicleIdFitment = _vehicleFitmentService.GetVehicleIdFitment(vehicles);
if (vehicleIdFitment.Count == 0)
{
continue;
}
string vehicleIdString = string.Join(',', vehicleIdFitment.Select(j => $"v{j}"));
bodyHtml += $"<div id=\"vehicleIDs\" style=\"display:none;\">{vehicleIdString}</div>";
isFitment = true;
string json = JsonConvert.SerializeObject(vehicleIdFitment);
if (json.Length < 100000)
{
Metafield vehicleMetafield = new Metafield
{
Namespace = "fitment",
Key = "ids",
Value = json,
Type = "json_string",
OwnerResource = "product",
OwnerId = product.Id
};
await _shopifyClient.Metafields.Add(vehicleMetafield);
}
else
{
_logger.LogWarning($"Vehicle ID fitment data for SKU {importData.VariantSku} is too large for Shopify and cannot be added.");
continue;
}
IList<string> ymmFitment = _vehicleFitmentService.GetYmmFitment(vehicles);
if (ymmFitment.Count > 0)
{
isFitment = true;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine("<table><tr><th colspan=\"2\">This Part Fits</th></tr>");
foreach (string fitment in ymmFitment)
{
try
{
string[] parts = fitment.Split(' ', 2);
stringBuilder.AppendLine($"<tr><td>{parts[1]}</td><td>{parts[0].Replace("-", ", ")}</td></tr>");
}
catch
{
// This is still a POC at this point. Oh well...
}
}
stringBuilder.AppendLine("</table>");
bodyHtml += $"<div id=\"seoData\">{stringBuilder.ToString()}</div>";
json = JsonConvert.SerializeObject(ymmFitment);
if (json.Length < 100000)
{
Metafield ymmMetafield = new Metafield
{
Namespace = "fitment",
Key = "seo",
Value = json,
Type = "json_string",
OwnerResource = "product",
OwnerId = product.Id
};
await _shopifyClient.Metafields.Add(ymmMetafield);
}
else
{
_logger.LogWarning($"Year/make/model fitment data for SKU {importData.VariantSku} is too large for Shopify and cannot be added.");
continue;
}
}
Metafield isFitmentMetafield = new Metafield
{
Namespace = "Flags",
Key = "IsFitment",
Value = isFitment.ToString(),
Type = "string",
OwnerResource = "product",
OwnerId = product.Id
};
await _shopifyClient.Metafields.Add(isFitmentMetafield);
//Metafield lineCodeMetafield = new Metafield
//{
// Namespace = "google",
// Key = "custom_label_0",
// Value = importData.LineCode,
// Type = "string",
// OwnerResource = "product",
// OwnerId = product.Id
//};
//await _shopifyClient.Metafields.Add(lineCodeMetafield);
//Metafield partNumberMetafield = new Metafield
//{
// Namespace = "google",
// Key = "custom_label_1",
// Value = importData.PartNumber,
// Type = "string",
// OwnerResource = "product",
// OwnerId = product.Id
//};
//await _shopifyClient.Metafields.Add(partNumberMetafield);
List<string> tags = new List<string>();
for (int j = 0; j < vehicleIdFitment.Count; j += 25)
{
tags.Add(string.Join('-', vehicleIdFitment.Skip(j).Take(25).Select(j => $"v{j}")));
}
tags.AddRange(ymmFitment);
if (tags.Count > 249)
{
tags = tags.Take(249).ToList();
}
string zzzIsFitment = isFitment
? "zzzIsFitment=true"
: "zzzIsFitment=false";
tags.Add(zzzIsFitment);
product.Tags = string.Join(',', tags);
product.BodyHtml = bodyHtml;
await _shopifyClient.Products.Update(product);
importData.IsFitment = isFitment;
importData.UpdatedAt = DateTime.Now;
importData.UpdateType = "Fitment";
}
catch (Exception ex)
{
_logger.LogError($"Failed to updated fitment data for SKU {importData?.VariantSku} - {ex.Message}", ex);
}
}
try try
{ {
Console.WriteLine(i); products = await _shopifyClient.Products.Get(new Dictionary<string, object> { { "limit", 250 }, { "product_type", productType } });
//products = new List<Product>
_partSourceContext.SaveChanges(); //{
products = await _shopifyClient.Products.GetNext(); // await _shopifyClient.Products.GetById(7458071052335)
//};
i++;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Failed to get the next set of products. Retrying"); _logger.LogError("Failed to get products from Shopify", ex);
products = await _shopifyClient.Products.GetPrevious(); throw;
}
while (products != null && products.Any())
{
foreach (Product product in products)
{
ImportData importData = null;
bool isFitment = false;
try
{
IEnumerable<Metafield> metafields = await _shopifyClient.Metafields.Get(new Dictionary<string, object> { { "metafield[owner_id]", product.Id }, { "metafield[owner_resource]", "product" } });
importData = new ImportData
{
LineCode = metafields.FirstOrDefault(m => m.Key == "custom_label_0")?.Value ?? string.Empty,
PartNumber = metafields.FirstOrDefault(m => m.Key == "custom_label_1")?.Value ?? string.Empty,
VariantSku = product.Variants[0].Sku // They know we can't do fitment for variants
};
importData.PartNumber = importData.PartNumber.Replace("-", string.Empty);
//If the line code is numeric, it cannot have fitment data associated with it.
if (int.TryParse(importData.LineCode, out _))
{
continue;
}
// Extract Partsource bullet points if present.
string bodyHtml = string.IsNullOrEmpty(product.BodyHtml)
? string.Empty
: product.BodyHtml.Substring(0, product.BodyHtml.IndexOf("</ul>") + "</ul>".Length);
IList<Vehicle> vehicles = await _vehicleFitmentService.GetVehiclesForPart(importData.PartNumber, importData.LineCode);
IList<int> vehicleIdFitment = _vehicleFitmentService.GetVehicleIdFitment(vehicles);
if (!vehicleIdFitment.Any())
{
Console.WriteLine($"No fitment data for {importData.LineCode} {importData.PartNumber}");
continue;
}
string vehicleIdString = string.Join(',', vehicleIdFitment.Select(j => $"v{j}"));
bodyHtml += $"<div id=\"vehicleIDs\" style=\"display:none;\">{vehicleIdString}</div>";
isFitment = true;
IList<string> ymmFitment = _vehicleFitmentService.GetYmmFitment(vehicles);
if (ymmFitment.Count > 0)
{
isFitment = true;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine("<table><tr><th colspan=\"2\">This Part Fits</th></tr>");
foreach (string fitment in ymmFitment)
{
try
{
string[] parts = fitment.Split(' ', 2);
stringBuilder.AppendLine($"<tr><td>{parts[1]}</td><td>{parts[0].Replace("-", ", ")}</td></tr>");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "YMM fitment for {fitment} was in an invalid format", fitment);
}
}
stringBuilder.AppendLine("</table>");
bodyHtml += $"<div id=\"seoData\">{stringBuilder}</div>";
}
List<string> tags = new List<string>();
for (int j = 0; j < vehicleIdFitment.Count; j += 25)
{
tags.Add(string.Join('-', vehicleIdFitment.Skip(j).Take(25).Select(j => $"v{j}")));
}
tags.AddRange(ymmFitment);
if (tags.Count > 249)
{
tags = tags.Take(249).ToList();
}
string zzzIsFitment = isFitment
? "zzzIsFitment=true"
: "zzzIsFitment=false";
tags.Add(zzzIsFitment);
product.Tags = string.Join(',', tags);
product.BodyHtml = bodyHtml;
await _shopifyClient.Products.Update(product);
importData.IsFitment = isFitment;
importData.UpdatedAt = DateTime.Now;
importData.UpdateType = "Fitment";
}
catch (Exception ex)
{
_logger.LogError($"Failed to updated fitment data for SKU {importData?.VariantSku} - {ex.Message}", ex);
}
}
try
{
products = await _shopifyClient.Products.GetNext();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get the next set of products. Retrying");
products = await _shopifyClient.Products.GetPrevious();
}
} }
} }
} }

View File

@@ -76,7 +76,8 @@ namespace PartSource.Automation.Jobs
try try
{ {
_shopifyClient.BulkActions.Add(new Metafield
await _shopifyClient.Metafields.Add(new Metafield
{ {
Namespace = "Pricing", Namespace = "Pricing",
Key = "CorePrice", Key = "CorePrice",
@@ -85,24 +86,16 @@ namespace PartSource.Automation.Jobs
OwnerResource = "product", OwnerResource = "product",
OwnerId = product.Id OwnerId = product.Id
}); });
await _shopifyClient.Products.Update(product);
_logger.LogInformation("Updated product id {productId}", product.Id);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, $"Failed to update pricing for product ID {product.Id}"); _logger.LogWarning(ex, "Failed to update pricing for product ID {productId}", product.Id);
} }
}
try
{
_shopifyClient.BulkActions.Update(product);
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Failed to update pricing for product ID {product.Id}");
} }
} }
} }
@@ -119,14 +112,9 @@ namespace PartSource.Automation.Jobs
_logger.LogWarning(ex, "Failed to get the next set of products. Retrying"); _logger.LogWarning(ex, "Failed to get the next set of products. Retrying");
products = await _shopifyClient.Products.GetPrevious(); products = await _shopifyClient.Products.GetPrevious();
} }
}
while (_shopifyClient.BulkActions.PendingCount() > 0) _emailService.Send("Pricing Update Completed", $"The pricing update has completed.");
{
await Task.Delay(15 * 1000, token);
_logger.LogInformation(_shopifyClient.BulkActions.PendingCount().ToString());
} }
// _emailService.Send("Pricing Update Completed", $"The pricing update has completed.");
} }
} }
} }

View File

@@ -16,5 +16,7 @@ namespace PartSource.Automation.Models.Configuration
public string Username { get; set; } public string Username { get; set; }
public string Password { get; set; } public string Password { get; set; }
public int Port { get; set; }
} }
} }

View File

@@ -4,7 +4,6 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PartSource.Automation.Jobs; using PartSource.Automation.Jobs;
using PartSource.Automation.Jobs.POC;
using PartSource.Automation.Services; using PartSource.Automation.Services;
using PartSource.Data.AutoMapper; using PartSource.Data.AutoMapper;
using PartSource.Data.Contexts; using PartSource.Data.Contexts;
@@ -64,7 +63,7 @@ namespace PartSource.Automation
{ {
options.ApiKey = builder.Configuration["Shopify:ApiKey"]; options.ApiKey = builder.Configuration["Shopify:ApiKey"];
options.ApiSecret = builder.Configuration["Shopify:ApiSecret"]; options.ApiSecret = builder.Configuration["Shopify:ApiSecret"];
options.ApiVersion = "2022-10"; options.ApiVersion = "2024-10";
options.ShopDomain = builder.Configuration["Shopify:ShopDomain"]; options.ShopDomain = builder.Configuration["Shopify:ShopDomain"];
//options.ApiKey = "9a533dad460321c6ce8f30bf5b8691ed"; //options.ApiKey = "9a533dad460321c6ce8f30bf5b8691ed";
@@ -75,32 +74,41 @@ namespace PartSource.Automation
.AddAutomation(options => .AddAutomation(options =>
{ {
options.HasBaseInterval(new TimeSpan(0, 5, 0)) options.HasBaseInterval(new TimeSpan(0, 5, 0))
.HasMaxFailures(1) .HasMaxFailures(5)
//.HasJob<TestJob>(options => options.HasInterval(new TimeSpan(7, 0, 0, 0))); //.HasJob<TestJob>(options => options.HasInterval(new TimeSpan(7, 0, 0, 0)));
// //
//.HasJob<SyncronizeProducts>(options => options.HasInterval(new TimeSpan(24, 0, 0))) //.HasJob<SyncronizeProducts>(options => options.HasInterval(new TimeSpan(24, 0, 0)))
// .HasJob<ProcessWhiFitment>(options => options.HasInterval(new TimeSpan(24, 0, 0))); // .HasJob<ProcessWhiFitment>(options => options.HasInterval(new TimeSpan(24, 0, 0)));
//.HasJob<ProcessWhiVehicles>(options => options.HasInterval(new TimeSpan(24, 0, 0)) //.HasJob<ProcessWhiVehicles>(options => options.HasInterval(new TimeSpan(24, 0, 0))
//.HasDependency<SyncronizeProducts>() //.HasDependency<SyncronizeProducts>()
//.HasJob<UpdateFitment>(options => options.HasInterval(new TimeSpan(24, 0, 0))); //.HasJob<UpdateFitment>(options => options.HasInterval(new TimeSpan(24, 0, 0)));
//.HasJob<UpdatePositioning>(options => options.HasInterval(new TimeSpan(24, 0, 0)) //.HasJob<UpdatePositioning>(options => options.HasInterval(new TimeSpan(24, 0, 0))
// .HasDependency<UpdateFitment>() // .HasDependency<UpdateFitment>()
// .HasDependency<ProcessWhiFitment>() // .HasDependency<ProcessWhiFitment>()
// .HasDependency<SyncronizeProducts>() // .HasDependency<SyncronizeProducts>()
// .StartsAt(DateTime.Today.AddHours(8)) // .StartsAt(DateTime.Today.AddHours(8))
//) ; //) ;
//.HasJob<StatusCheck>(options => options.HasInterval(new TimeSpan(24, 0, 0)) //.HasJob<StatusCheck>(options => options.HasInterval(new TimeSpan(24, 0, 0))
// .StartsAt(DateTime.Parse("2021-04-01 08:00:00")) // .StartsAt(DateTime.Parse("2021-04-01 08:00:00"))
//) //)
.HasJob<BulkUpdateInventory>(options => //.HasJob<ExecuteSsisPackages>(options =>
options.HasInterval(new TimeSpan(1, 0, 0)) // options.HasInterval(new TimeSpan(24, 0, 0))
.StartsAt(DateTime.Today.AddHours(-27)) // .StartsAt(DateTime.Today.AddHours(-24))
) // )
.HasJob<PartialInventoryUpdate>(options => .HasJob<UpdatePricing>(options =>
options.HasInterval(new TimeSpan(1, 0, 0)) options.HasInterval(new TimeSpan(24, 0, 0))
.StartsAt(DateTime.Today.AddHours(-27).AddMinutes(30)) .StartsAt(DateTime.Today.AddHours(-22))
); //.HasDependency<ExecuteSsisPackages>()
);
//.HasJob<BulkUpdateInventory>(options =>
// options.HasInterval(new TimeSpan(1, 0, 0))
// .StartsAt(DateTime.Today.AddHours(-27))
// );
//.HasJob<TestJob>(options =>
// options.HasInterval(new TimeSpan(1, 0, 0))
// .StartsAt(DateTime.Today.AddHours(-27).AddMinutes(30))
//);
//.HasJob<PartialInventoryUpdate>(options => options.HasInterval(new TimeSpan(1, 0, 0)) //.HasJob<PartialInventoryUpdate>(options => options.HasInterval(new TimeSpan(1, 0, 0))
//.HasDependency<ExecuteSsisPackages>() //.HasDependency<ExecuteSsisPackages>()

View File

@@ -91,7 +91,9 @@ namespace PartSource.Automation.Services
public IList<int> GetVehicleIdFitment(IList<Vehicle> vehicles) public IList<int> GetVehicleIdFitment(IList<Vehicle> vehicles)
{ {
return vehicles.Select(v => v.VehicleToEngineConfigId).Distinct().ToArray(); return vehicles != null
? vehicles.Select(v => v.VehicleToEngineConfigId).Distinct().ToArray()
: new List<int>();
} }
public async Task<IList<Vehicle>> GetVehiclesForPart(string partNumber, string lineCode, int maxVehicles = 0) public async Task<IList<Vehicle>> GetVehiclesForPart(string partNumber, string lineCode, int maxVehicles = 0)

View File

@@ -5,167 +5,154 @@ using Microsoft.Extensions.Logging;
using PartSource.Automation.Models.Configuration; using PartSource.Automation.Models.Configuration;
using PartSource.Automation.Models.Enums; using PartSource.Automation.Models.Enums;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace PartSource.Automation.Services namespace PartSource.Automation.Services
{ {
public class WhiSeoService public class WhiSeoService
{ {
private readonly FtpService _ftpService; private readonly FtpService _ftpService;
private readonly string _connectionString; private readonly string _connectionString;
private readonly ILogger<WhiSeoService> _logger; private readonly ILogger<WhiSeoService> _logger;
public WhiSeoService(IConfiguration configuration, ILogger<WhiSeoService> logger) public WhiSeoService(IConfiguration configuration, ILogger<WhiSeoService> logger)
{ {
FtpConfiguration ftpConfiguration = configuration.GetSection("FtpServers:WhiConfiguration").Get<FtpConfiguration>(); FtpConfiguration ftpConfiguration = configuration.GetSection("FtpServers:WhiConfiguration").Get<FtpConfiguration>();
_ftpService = new FtpService(ftpConfiguration); _ftpService = new FtpService(ftpConfiguration);
_connectionString = configuration.GetConnectionString("FitmentDatabase"); _connectionString = configuration.GetConnectionString("FitmentDatabase");
_logger = logger; _logger = logger;
} }
public void GetFiles(SeoDataType seoDataType) public void GetFiles(SeoDataType seoDataType)
{ {
string seoDataTypeString = seoDataType.ToString().ToLowerInvariant(); string seoDataTypeString = seoDataType.ToString().ToLowerInvariant();
string[] files = _ftpService.ListFiles(seoDataTypeString);
// WHI changed the transfer protocol to SFTP and then messed with the directory structure.
// Since fitment isn't really all that automated anyway, just download the files manually with an SFTP client.
Console.WriteLine($"Remember to manually download the {seoDataTypeString} files with an SFTP client. Press any key to continue.");
Console.ReadLine();
}
foreach (string file in files) public void TruncateVehicleTable()
{ {
if (file.Contains(".csv")) using SqlConnection connection = new SqlConnection(_connectionString);
{ connection.Open();
try
{
_ftpService.Download($"{seoDataTypeString}/{file}");
_logger.LogInformation($"Finished downloading {file}.");
}
using SqlCommand command = new SqlCommand($"truncate table dbo.Vehicle", connection);
command.ExecuteNonQuery();
}
catch (Exception ex) public void TruncateFitmentTables()
{ {
_logger.LogWarning($"Failed to download {file}, quitting", ex); using SqlConnection connection = new SqlConnection(_connectionString);
throw; connection.Open();
}
}
}
}
public void TruncateVehicleTable() using SqlCommand command = new SqlCommand($"exec DropFitmentTables", connection);
{ command.ExecuteNonQuery();
using SqlConnection connection = new SqlConnection(_connectionString); }
connection.Open();
using SqlCommand command = new SqlCommand($"truncate table dbo.Vehicle", connection); public void SaveNotes(IDictionary<string, string> notes)
command.ExecuteNonQuery(); {
} using DataTable dataTable = new DataTable();
dataTable.Columns.Add("NoteText", typeof(string));
dataTable.Columns.Add("Hash", typeof(string));
public void TruncateFitmentTables() foreach (KeyValuePair<string, string> note in notes)
{ {
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlCommand command = new SqlCommand($"exec DropFitmentTables", connection); dataTable.Rows.Add(new string[] { note.Value, note.Key });
command.ExecuteNonQuery(); }
}
public void SaveNotes(IDictionary<string, string> notes) using SqlConnection connection = new SqlConnection(_connectionString);
{ connection.Open();
using DataTable dataTable = new DataTable();
dataTable.Columns.Add("NoteText", typeof(string));
dataTable.Columns.Add("Hash", typeof(string));
foreach (KeyValuePair<string, string> note in notes) using SqlBulkCopy bulk = new SqlBulkCopy(connection)
{ {
DestinationTableName = $"FitmentNote",
BulkCopyTimeout = 14400
};
dataTable.Rows.Add(new string[] { note.Value, note.Key }); bulk.WriteToServer(dataTable);
} }
using SqlConnection connection = new SqlConnection(_connectionString); public void BulkCopyFitment(DataTable dataTable, string tableName)
connection.Open(); {
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlBulkCopy bulk = new SqlBulkCopy(connection) string sql = string.Empty;
{
DestinationTableName = $"FitmentNote",
BulkCopyTimeout = 14400
};
bulk.WriteToServer(dataTable); using SqlCommand command = new SqlCommand($"EXEC CreateFitmentTempTable @tableName = '{tableName}'", connection);
} command.ExecuteNonQuery();
public void BulkCopyFitment(DataTable dataTable, string tableName) using SqlBulkCopy bulk = new SqlBulkCopy(connection)
{ {
using SqlConnection connection = new SqlConnection(_connectionString); DestinationTableName = $"FitmentTemp.{tableName}",
connection.Open(); BulkCopyTimeout = 14400
};
string sql = string.Empty; bulk.WriteToServer(dataTable);
}
using SqlCommand command = new SqlCommand($"EXEC CreateFitmentTempTable @tableName = '{tableName}'", connection); public void BulkCopyVehicle(DataTable dataTable, string tableName)
command.ExecuteNonQuery(); {
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlBulkCopy bulk = new SqlBulkCopy(connection) string sql = string.Empty;
{
DestinationTableName = $"FitmentTemp.{tableName}",
BulkCopyTimeout = 14400
};
bulk.WriteToServer(dataTable); using SqlCommand command = new SqlCommand($"EXEC CreateVehicleTempTable @tableName = '{tableName}'", connection);
} command.ExecuteNonQuery();
public void BulkCopyVehicle(DataTable dataTable, string tableName) using SqlBulkCopy bulk = new SqlBulkCopy(connection)
{ {
using SqlConnection connection = new SqlConnection(_connectionString); DestinationTableName = $"VehicleTemp.{tableName}",
connection.Open(); BulkCopyTimeout = 14400
};
string sql = string.Empty; bulk.WriteToServer(dataTable);
}
using SqlCommand command = new SqlCommand($"EXEC CreateVehicleTempTable @tableName = '{tableName}'", connection); public void CreateFitmentTable(string tableName)
command.ExecuteNonQuery(); {
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlBulkCopy bulk = new SqlBulkCopy(connection) using SqlCommand command = new SqlCommand($"exec CreateFitmentTable @tableName = '{tableName}'", connection);
{ command.CommandTimeout = 1800;
DestinationTableName = $"VehicleTemp.{tableName}", command.ExecuteNonQuery();
BulkCopyTimeout = 14400 }
};
bulk.WriteToServer(dataTable); public void CreateFitmentView()
} {
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
public void CreateFitmentTable(string tableName) using SqlCommand command = new SqlCommand($"exec CreateFitmentView", connection);
{ command.CommandTimeout = 1800;
using SqlConnection connection = new SqlConnection(_connectionString); command.ExecuteNonQuery();
connection.Open();
using SqlCommand command = new SqlCommand($"exec CreateFitmentTable @tableName = '{tableName}'", connection); using SqlCommand command2 = new SqlCommand($"exec CreateFitmentIndexes", connection);
command.CommandTimeout = 1800; command.CommandTimeout = 3600;
command.ExecuteNonQuery(); command2.ExecuteNonQuery();
} }
public void CreateFitmentView() public void CreateVehicleTable()
{ {
using SqlConnection connection = new SqlConnection(_connectionString); using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open(); connection.Open();
using SqlCommand command = new SqlCommand($"exec CreateFitmentView", connection); using SqlCommand command = new SqlCommand($"exec CreateVehicleTable", connection);
command.CommandTimeout = 1800; command.CommandTimeout = 1800;
command.ExecuteNonQuery(); command.ExecuteNonQuery();
}
using SqlCommand command2 = new SqlCommand($"exec CreateFitmentIndexes", connection); }
command.CommandTimeout = 1800;
command2.ExecuteNonQuery();
}
public void CreateVehicleTable()
{
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlCommand command = new SqlCommand($"exec CreateVehicleTable", connection);
command.CommandTimeout = 1800;
command.ExecuteNonQuery();
}
}
} }
#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities

View File

@@ -1,34 +1,28 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
//"FitmentDatabase": "Data Source=localhost;Initial Catalog=WhiFitment;Integrated Security=true;TrustServerCertificate=True", "FitmentDatabase": "Data Source=localhost;Initial Catalog=WhiFitment;Integrated Security=true;TrustServerCertificate=true",
//"FitmentDatabase": "Data Source=localhost;Initial Catalog=WhiFitment;User ID=stageuser;Password=FXepK^cFYS|[H<;Encrypt=True;TrustServerCertificate=True;Connection Timeout=300", //"FitmentDatabase": "Server=tcp:ps-automation.eastus2.cloudapp.azure.com,1433;Initial Catalog=WhiFitment;User ID=sa;Password=GZ0`-ekd~[2u;Encrypt=True;TrustServerCertificate=True;Connection Timeout=300",
"FitmentDatabase": "Data Source=localhost;User ID=stageuser;Password=FXepK^cFYS|[H<;Connect Timeout=30;Encrypt=True;Trust Server Certificate=True;Application Intent=ReadWrite;Multi Subnet Failover=False",
"PartSourceDatabase": "Server=tcp:ps-whi.database.windows.net,1433;Initial Catalog=ps-whi-stage;Persist Security Info=False;User ID=ps-whi;Password=9-^*N5dw!6:|.5Q;MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" "PartSourceDatabase": "Server=tcp:ps-whi.database.windows.net,1433;Initial Catalog=ps-whi-stage;Persist Security Info=False;User ID=ps-whi;Password=9-^*N5dw!6:|.5Q;MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
}, },
"emailConfiguration": { "emailConfiguration": {
"From": "alerts@ps-automation.eastus2.cloudapp.azure.com", "From": "alerts@ps-automation.eastus2.cloudapp.azure.com",
"To": "tom@soundpress.com,Anas.Bajwa@Partsource.ca,josh@soundpress.com,alex.au@partsource.ca,michael.massara@partsource.ca", "To": "tom@soundpress.com,josh@soundpress.com,alex.au@partsource.ca,michael.massara@partsource.ca",
//"To": "tom@tomraterman.com", //"To": "tom@tomraterman.com,josh@soundpress.com",
"SmtpHost": "localhost" "SmtpHost": "localhost"
}, },
"FtpServers": { "FtpServers": {
"AzureConfiguration": { "WhiConfiguration": {
"Username": "ps-ftp\\$ps-ftp", "Username": "ctc_seo",
"Password": "ycvXptffBxqkBXW4vuRYqn4Zi1soCvnvMMolTe5HNSeAlcl3bAyJYtNhG579", "Password": "YD3gtaQ5kPdtNKs",
"Url": "ftp://waws-prod-yq1-007.ftp.azurewebsites.windows.net/site/wwwroot", "Url": "ftp://ftp.whisolutions.com",
"Destination": "C:\\Partsource.Automation\\Downloads" "Destination": "C:\\Partsource.Automation\\Downloads\\WHI",
"Port": 3001
}, },
"AutomationConfiguration": { "AutomationConfiguration": {
"Username": "stageuser", "Username": "stageuser",
"Password": "FXepK^cFYS|[H<", "Password": "FXepK^cFYS|[H<",
"Url": "ftp://ps-automation-stage.eastus2.cloudapp.azure.com", "Url": "ftp://localhost",
"Destination": "C:\\Partsource.Automation\\Downloads\\Stage" "Destination": "C:\\Partsource.Automation\\Downloads"
},
"WhiConfiguration": {
"Username": "ctc_seo",
"Password": "be34hz64e4",
"Url": "ftp://ftp.whisolutions.com",
"Destination": "C:\\Partsource.Automation\\Downloads\\WHI"
} }
}, },
"ssisConfiguration": { "ssisConfiguration": {
@@ -43,7 +37,7 @@
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information",
// "Microsoft.EntityFrameworkCore.Database.Command": "Information" // "Microsoft.EntityFrameworkCore.Database.Command": "Information"
}, },
"EventLog": { "EventLog": {

View File

@@ -13,15 +13,19 @@ namespace PartSource.Data.Contexts
public DbSet<DcfMapping> DcfMappings { get; set; } public DbSet<DcfMapping> DcfMappings { get; set; }
public DbSet<Part> Parts { get; set; }
public DbSet<Fitment> Fitments { get; set; } public DbSet<Fitment> Fitments { get; set; }
public DbSet<FitmentNote> FitmentNotes { get; set; } public DbSet<FitmentNote> FitmentNotes { get; set; }
public DbSet<ProductType> ProductTypes { get; set; }
public DbSet<Vehicle> Vehicles { get; set; } public DbSet<Vehicle> Vehicles { get; set; }
public DbSet<VehicleFitment> VehicleFitments { get; set; } public DbSet<VehicleFitment> VehicleFitments { get; set; }
public DbSet<Wiper> Wipers { get; set; } public DbSet<Wiper> Wipers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -30,9 +34,9 @@ namespace PartSource.Data.Contexts
modelBuilder.Entity<DcfMapping>().HasKey(d => new { d.LineCode, d.WhiCode }); modelBuilder.Entity<DcfMapping>().HasKey(d => new { d.LineCode, d.WhiCode });
modelBuilder.Entity<Fitment>().HasKey(f => new { f.BaseVehicleId, f.EngineConfigId, f.LineCode, f.PartNumber }); modelBuilder.Entity<Fitment>().HasKey(f => new { f.BaseVehicleId, f.EngineConfigId, f.LineCode, f.PartNumber });
modelBuilder.Entity<Wiper>().HasKey(f => new { f.BaseVehicleId, f.PartNumber, f.LineCode, f.Position}); modelBuilder.Entity<Wiper>().HasKey(f => new { f.BaseVehicleId, f.PartNumber, f.LineCode, f.Position});
modelBuilder.Entity<VehicleFitment>().HasNoKey(); modelBuilder.Entity<VehicleFitment>().HasNoKey();
foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes())
{ {
entityType.SetTableName(entityType.ClrType.Name); entityType.SetTableName(entityType.ClrType.Name);
} }

View File

@@ -26,13 +26,13 @@ namespace PartSource.Data.Contexts
public DbSet<PartPrice> PartPrices { get; set; } public DbSet<PartPrice> PartPrices { get; set; }
public DbSet<Part> Parts { get; set; } // public DbSet<Part> Parts { get; set; }
public DbSet<ShopifyChangelog> ShopifyChangelogs { get; set; } public DbSet<ShopifyChangelog> ShopifyChangelogs { get; set; }
public DbSet<Vehicle> Vehicles { get; set; } public DbSet<Vehicle> Vehicles { get; set; }
public DbSet<PartsAvailability> PartAvailabilities { get; set; } public DbSet<PartAvailability> PartAvailabilities { get; set; }
public DbSet<BaseVehicle> BaseVehicles { get; set; } public DbSet<BaseVehicle> BaseVehicles { get; set; }
@@ -48,7 +48,7 @@ namespace PartSource.Data.Contexts
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PartsAvailability>().HasKey(p => new { p.Store, p.SKU }); modelBuilder.Entity<PartAvailability>().HasKey(p => new { p.Store, p.SKU });
modelBuilder.Entity<DcfMapping>().HasKey(d => new { d.LineCode, d.WhiCode }); modelBuilder.Entity<DcfMapping>().HasKey(d => new { d.LineCode, d.WhiCode });
modelBuilder.Entity<ShopifyChangelog>() modelBuilder.Entity<ShopifyChangelog>()

View File

@@ -5,8 +5,15 @@ using System.Text;
namespace PartSource.Data.Dtos namespace PartSource.Data.Dtos
{ {
public class VehicleFitmentDto : VehicleFitment public class VehicleFitmentDto : VehicleFitment
{ {
public string PartDescription { get; set; }
// May not be needed, but don't remove just yet
public IList<string> SubmodelNames { get; set; } public IList<string> SubmodelNames { get; set; }
public IList<string> DriveTypes { get; set; }
public IList<string> Notes { get; set; }
} }
} }

View File

@@ -1,9 +1,10 @@
using System.ComponentModel.DataAnnotations; using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace PartSource.Data.Models namespace PartSource.Data.Models
{ {
public class PartsAvailability public class PartAvailability
{ {
[Column(Order = 0)] [Column(Order = 0)]
[DatabaseGenerated(DatabaseGeneratedOption.None)] [DatabaseGenerated(DatabaseGeneratedOption.None)]
@@ -13,14 +14,8 @@ namespace PartSource.Data.Models
[DatabaseGenerated(DatabaseGeneratedOption.None)] [DatabaseGenerated(DatabaseGeneratedOption.None)]
public int SKU { get; set; } public int SKU { get; set; }
[Column("Line Code")]
[StringLength(50)]
public string Line_Code { get; set; }
[Column("Part Number")]
[StringLength(50)]
public string Part_Number { get; set; }
public int? QTY { get; set; } public int? QTY { get; set; }
public string Updated { get; set; }
} }
} }

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PartSource.Data.Models
{
public class ProductType
{
[Key]
public string Name { get; set; }
public bool Active { get; set; }
}
}

View File

@@ -1,4 +1,6 @@
namespace PartSource.Data.Models using Newtonsoft.Json;
namespace PartSource.Data.Models
{ {
public class VehicleFitment public class VehicleFitment
{ {
@@ -8,6 +10,7 @@
public string PartNumber { get; set; } public string PartNumber { get; set; }
[JsonIgnore]
public string NoteText { get; set; } public string NoteText { get; set; }
public int Year { get; set; } public int Year { get; set; }

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
namespace PartSource.Data.Nexpart
{
[XmlType(Namespace = "http://whisolutions.com/pss/common/helper/parts")]
public class Answer
{
[XmlAttribute]
public int Id { get; set; }
[XmlAttribute]
public int Count { get; set; }
[XmlText]
public string Value { get; set; }
}
}

View File

@@ -31,9 +31,15 @@ namespace PartSource.Data.Nexpart
public bool SecondaryDCF => true; public bool SecondaryDCF => true;
[XmlElement(Order = 6)] [XmlElement(Order = 6)]
public Criterion[] Criterion { get; set; } public string[] AppOption { get; set; }
[XmlElement(Order = 7)] [XmlElement(Order = 7)]
public Criterion[] Criterion { get; set; }
[XmlElement(Order = 8)]
public string GroupBy { get; set; } public string GroupBy { get; set; }
[XmlElement(Order = 9)]
public string QuestionOption { get; set; }
} }
} }

View File

@@ -9,13 +9,13 @@ using PartSource.Data.Nexpart.Interfaces;
namespace PartSource.Data.Nexpart namespace PartSource.Data.Nexpart
{ {
[XmlType(AnonymousType = true, Namespace = "http://whisolutions.com/pss/common/model/parts")] [XmlType(AnonymousType = true, Namespace = "http://whisolutions.com/pss/common/model/parts")]
public class ApplicationSearchResponse : IResponseElement<Apps> public class ApplicationSearchResponse : IResponseElement<object>
{ {
[XmlElement] [XmlElement]
public PSResponseHeader PSResponseHeader { get; set; } public PSResponseHeader PSResponseHeader { get; set; }
[XmlElement(ElementName = nameof(Apps))] [XmlElement(ElementName = nameof(Apps), Namespace = "http://whisolutions.com/pss/common/model/parts", Type = typeof(Apps))]
public Apps ResponseBody { get; set; } [XmlElement(ElementName = nameof(Questions), Namespace = "http://whisolutions.com/pss/common/model/parts", Type = typeof(Questions))]
public object ResponseBody { get; set; }
} }
} }

View File

@@ -13,7 +13,7 @@ namespace PartSource.Data.Nexpart
{ {
public PSRequestHeader() public PSRequestHeader()
{ {
this.SvcVersion = "1.0"; this.SvcVersion = "2.0";
this.ReturnWarnings = "true"; this.ReturnWarnings = "true";
} }

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
namespace PartSource.Data.Nexpart
{
[XmlType(Namespace = "http://whisolutions.com/pss/common/helper/parts")]
public class Question
{
[XmlAttribute(AttributeName = "Attrib")]
public string Attribute { get; set; }
[XmlAttribute]
public int Count { get; set; }
[XmlAttribute]
public string Text { get; set; }
[XmlElement]
public Answer[] Answer { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
namespace PartSource.Data.Nexpart
{
[XmlType(Namespace = "http://whisolutions.com/pss/common/model/parts")]
public class Questions
{
[XmlElement(Namespace = "http://whisolutions.com/pss/common/helper/parts")]
public Question[] Question { get; set; }
[XmlAttribute]
public int NumApps { get; set; }
}
}

View File

@@ -1,13 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using PartSource.Data.Contexts; using PartSource.Data.Contexts;
using PartSource.Data.Dtos; using PartSource.Data.Dtos;
using PartSource.Data.Models; using PartSource.Data.Models;
using PartSource.Data.Nexpart;
namespace PartSource.Services namespace PartSource.Services
{ {
@@ -35,21 +35,11 @@ namespace PartSource.Services
ModelName = vf.ModelName, ModelName = vf.ModelName,
BaseVehicleId = vf.BaseVehicleId, BaseVehicleId = vf.BaseVehicleId,
EngineConfigId = vf.EngineConfigId, EngineConfigId = vf.EngineConfigId,
VehicleToEngineConfigId = vf.VehicleToEngineConfigId VehicleToEngineConfigId = vf.VehicleToEngineConfigId,
SubmodelName = vf.SubmodelName
}) })
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (vehicleFitment == null)
{
return null;
}
vehicleFitment.SubmodelNames = await _fitmentContext.VehicleFitments
.Where(vf => vf.BaseVehicleId == vehicleFitment.BaseVehicleId && vf.Sku == sku)
.Select(vf => vf.SubmodelName)
.Distinct()
.ToListAsync();
return vehicleFitment; return vehicleFitment;
} }

View File

@@ -25,17 +25,20 @@ namespace PartSource.Services
U content; U content;
string x = textWriter.ToString(); string x = textWriter.ToString();
System.Diagnostics.Debug.WriteLine(x);
using (HttpClient client = new HttpClient())
using (HttpClient client = new HttpClient())
{ {
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", "QjM4ODAyMzM3QjQxNEM2QTk4M0RFMjM0Mjk4Rjk4M0UtOUIzNUUxNzNBQUYxNEE2QjhCQjI2RjZDOUY2ODk1NDU6MkMzOUVCOTYtRDBBRS00QkVBLTlCMzItMUYyNTA5MDJGQTE0"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", "QjM4ODAyMzM3QjQxNEM2QTk4M0RFMjM0Mjk4Rjk4M0UtOUIzNUUxNzNBQUYxNEE2QjhCQjI2RjZDOUY2ODk1NDU6MkMzOUVCOTYtRDBBRS00QkVBLTlCMzItMUYyNTA5MDJGQTE0");
try try
{ {
//HttpResponseMessage response = await client.PostAsync(ConfigurationManager.AppSettings["NexpartUrl"], (HttpContent)new StringContent(sb.ToString(), Encoding.UTF8, "text/xml")); //HttpResponseMessage response = await client.PostAsync(ConfigurationManager.AppSettings["NexpartUrl"], (HttpContent)new StringContent(sb.ToString(), Encoding.UTF8, "text/xml"));
HttpResponseMessage response = await client.PostAsync("http://acespssprod.nexpart.com:8085/partselect/2.0/services/PartSelectService.PartSelectHttpSoap11Endpoint", new StringContent(textWriter.ToString(), Encoding.UTF8)); HttpResponseMessage response = await client.PostAsync("http://acespssprod.nexpart.com:8085/partselect/2.0/services/PartSelectService.PartSelectHttpSoap11Endpoint", new StringContent(textWriter.ToString(), Encoding.UTF8, "text/xml"));
Stream result = await response.Content.ReadAsStreamAsync(); Stream result = await response.Content.ReadAsStreamAsync();
string str = await response.Content.ReadAsStringAsync(); string str = await response.Content.ReadAsStringAsync();
System.Diagnostics.Debug.WriteLine(str);
content = (U)((Envelope)serializer.Deserialize(result)).Body.Content; content = (U)((Envelope)serializer.Deserialize(result)).Body.Content;
; ;
} }
@@ -44,6 +47,7 @@ namespace PartSource.Services
throw; throw;
} }
} }
return content; return content;
} }
} }

View File

@@ -12,26 +12,28 @@ namespace PartSource.Services
{ {
public class PartService public class PartService
{ {
private readonly PartSourceContext _context; private readonly PartSourceContext _partSourceContext;
private readonly FitmentContext _fitmentContext;
public PartService(PartSourceContext context) public PartService(PartSourceContext partSourceContext, FitmentContext fitmentContext)
{ {
_context = context; _partSourceContext = partSourceContext;
_fitmentContext = fitmentContext;
} }
public async Task<PartsAvailability> GetInventory(int sku, int storeNumber) public async Task<PartAvailability> GetInventory(int sku, int storeNumber)
{ {
return await _context.PartAvailabilities.FirstOrDefaultAsync(s => s.Store == storeNumber && s.SKU == sku); return await _partSourceContext.PartAvailabilities.FirstOrDefaultAsync(s => s.Store == storeNumber && s.SKU == sku);
} }
public async Task<Part> GetPartBySku(string sku) public async Task<Part> GetPartBySku(string sku)
{ {
return await _context.Parts.SingleOrDefaultAsync(p => p.Sku == sku); return await _fitmentContext.Parts.SingleOrDefaultAsync(p => p.Sku == sku);
} }
public async Task<IList<DcfMapping>> GetDcfMapping(string partsourceLineCode) public async Task<IList<DcfMapping>> GetDcfMapping(string partsourceLineCode)
{ {
return await _context.DcfMappings return await _fitmentContext.DcfMappings
.Where(dcf => dcf.LineCode == partsourceLineCode) .Where(dcf => dcf.LineCode == partsourceLineCode)
.ToListAsync(); .ToListAsync();
} }