diff --git a/PartSource.Api/Controllers/WebhooksController.cs b/PartSource.Api/Controllers/WebhooksController.cs new file mode 100644 index 0000000..9a09a62 --- /dev/null +++ b/PartSource.Api/Controllers/WebhooksController.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using PartSource.Data.Dtos; +using PartSource.Data.Models; +using PartSource.Services; +using Ratermania.Shopify.Resources; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace PartSource.Api.Controllers +{ + /// + /// This endpoint handles Shopify webhooks + /// + [Route("v2/[controller]")] + [ApiController] + [ApiExplorerSettings(GroupName = "v2")] + public class WebhooksController : BaseApiController + { + private readonly ShopifyChangelogService _shopifyChangelogService; + + public WebhooksController(ShopifyChangelogService shopifyChangelogService) + { + _shopifyChangelogService = shopifyChangelogService; + } + + [HttpPost] + [Route("products/update")] + public async Task OnProductUpdate([FromBody]Product product) + { + await _shopifyChangelogService.AddEntry(product); + + return Ok(); + } + } +} diff --git a/PartSource.Api/PartSource.Api.csproj b/PartSource.Api/PartSource.Api.csproj index 9edd31e..b081ba9 100644 --- a/PartSource.Api/PartSource.Api.csproj +++ b/PartSource.Api/PartSource.Api.csproj @@ -28,8 +28,11 @@ + + + diff --git a/PartSource.Api/PartSource.Api.xml b/PartSource.Api/PartSource.Api.xml index a364bd7..9521af0 100644 --- a/PartSource.Api/PartSource.Api.xml +++ b/PartSource.Api/PartSource.Api.xml @@ -106,5 +106,10 @@ OK: The engine with the provided EngineConfigId. Not Found: No engine was found matching the provided EngineConfigId. + + + This endpoint handles Shopify webhooks + + diff --git a/PartSource.Api/Startup.cs b/PartSource.Api/Startup.cs index 7197ffe..1ba5b6a 100644 --- a/PartSource.Api/Startup.cs +++ b/PartSource.Api/Startup.cs @@ -6,9 +6,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; -using PartSource.Api.Formatters; -using PartSource.Data; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using PartSource.Data.AutoMapper; +using PartSource.Data.Contexts; using PartSource.Services; using System.IO; @@ -30,6 +31,14 @@ namespace PartSource.Api { options.OutputFormatters.RemoveType(typeof(StringOutputFormatter)); options.EnableEndpointRouting = false; + }) + .AddNewtonsoftJson(options => + { + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + options.SerializerSettings.ContractResolver = new DefaultContractResolver() + { + NamingStrategy = new SnakeCaseNamingStrategy() + }; }); services.AddSwaggerGen(c => @@ -46,6 +55,7 @@ namespace PartSource.Api services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddCors(o => o.AddPolicy("Default", builder => { @@ -57,6 +67,9 @@ namespace PartSource.Api services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("PartSourceDatabase")) ); + services.AddDbContext(options => + options.UseSqlServer(Configuration.GetConnectionString("FitmentDatabase")) + ); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/PartSource.Api/appsettings.json b/PartSource.Api/appsettings.json index 77e9658..d92f19a 100644 --- a/PartSource.Api/appsettings.json +++ b/PartSource.Api/appsettings.json @@ -1,7 +1,8 @@ { - "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;" - }, + "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;", + "FitmentDatabase": "Data Source=localhost;Initial Catalog=WhiFitment;Integrated Security=true" + }, "Logging": { "LogLevel": { "Default": "Warning" diff --git a/PartSource.Automation/Jobs/ProcessWhiFitment.cs b/PartSource.Automation/Jobs/ProcessWhiFitment.cs index f5a8ea3..175d6a4 100644 --- a/PartSource.Automation/Jobs/ProcessWhiFitment.cs +++ b/PartSource.Automation/Jobs/ProcessWhiFitment.cs @@ -42,96 +42,38 @@ namespace PartSource.Automation.Jobs string directory = Path.Combine(_ftpConfiguration.Destination, _seoDataType.ToString().ToLowerInvariant()); DirectoryInfo directoryInfo = new DirectoryInfo(directory); - ConcurrentQueue files = new ConcurrentQueue(directoryInfo.GetFiles().Where(f => f.Name.EndsWith("csv.gz")).OrderBy(f => f.Length)); + IEnumerable> fileGroups = directoryInfo.GetFiles().Where(f => f.Name.EndsWith("csv.gz")).GroupBy(x => x.Name.Split('_').Last()); - while (files.Count > 0) + foreach (IGrouping fileGroup in fileGroups) { - Parallel.For(0, 4, index => + foreach (FileInfo fileInfo in fileGroup) { - if (!files.TryDequeue(out FileInfo fileInfo)) - { - return; - } - - string filename = Decompress(fileInfo); - - using DataTable dataTable = new DataTable(); - dataTable.Columns.Add("LineCode", typeof(string)); - dataTable.Columns.Add("PartNumber", typeof(string)); - dataTable.Columns.Add("BaseVehicleId", typeof(int)); - dataTable.Columns.Add("EngineConfigId", typeof(int)); - dataTable.Columns.Add("Position", typeof(string)); - dataTable.Columns.Add("NoteText", typeof(string)); - - using StreamReader reader = new StreamReader(filename); - string line = reader.ReadLine(); // Burn the header row - try { - int skippedLines = 0; - while (reader.Peek() > 0) - { - line = reader.ReadLine(); - - string[] columns = line.Split("\",\""); - - if (columns.Length != 8) - { - skippedLines++; - continue; - } - - for (int i = 0; i < columns.Length; i++) - { - columns[i] = columns[i].Replace("\"", string.Empty); - } - - string 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 position = columns[7].Trim(); - string noteText = columns[4].Trim(); - - if (!string.IsNullOrEmpty(lineCode) - && !string.IsNullOrEmpty(partNumber) - && int.TryParse(columns[5], out int baseVehicleId) - && int.TryParse(columns[6], out int engineConfigId)) - { - dataTable.Rows.Add(new object[] { lineCode, partNumber, baseVehicleId, engineConfigId, position, noteText }); - } - } + string filename = Decompress(fileInfo); + DataTable dataTable = GetDataTable(filename); string tableName = fileInfo.Name.Substring(0, fileInfo.Name.IndexOf('.')); - + _whiSeoService.BulkCopy(_seoDataType, dataTable, tableName); - if (skippedLines == 0) - { - _logger.LogInformation($"Copied {filename} to the database."); - } + _logger.LogInformation($"Copied {fileInfo.Name} to the database."); - else - { - _logger.LogWarning($"Copied {filename} to the database with warnings. {skippedLines} lines contained errors and could not be processed."); - } - - File.Delete(fileInfo.FullName); - } - - catch (Exception ex) - { - _logger.LogError($"Failed to copy {filename} to the database.", ex); - } - - try - { - reader.Close(); File.Delete(filename); } - catch (Exception ex) { } - }); - } + catch (Exception ex) + { + _logger.LogError($"Failed to write {fileInfo.Name} to the database - {ex.Message}", ex); + } + } + string fitmentTable = fileGroup.Key.Substring(0, fileGroup.Key.IndexOf('.')); + _whiSeoService.CreateFitmentTable(fitmentTable); + + _logger.LogInformation($"Created fitment table for part group {fitmentTable}."); + } + _whiSeoService.CreateFitmentView(); } @@ -146,5 +88,45 @@ namespace PartSource.Automation.Jobs return decompressedFile; } + + private DataTable GetDataTable(string filename) + { + using DataTable dataTable = new DataTable(); + dataTable.Columns.Add("LineCode", typeof(string)); + dataTable.Columns.Add("PartNumber", typeof(string)); + dataTable.Columns.Add("BaseVehicleId", typeof(int)); + dataTable.Columns.Add("EngineConfigId", typeof(int)); + //dataTable.Columns.Add("Position", typeof(string)); + //dataTable.Columns.Add("NoteText", typeof(string)); + + using StreamReader reader = new StreamReader(filename); + string line = reader.ReadLine(); // Burn the header row + + while (reader.Peek() > 0) + { + line = reader.ReadLine(); + + string[] columns = line.Split("\",\""); + for (int i = 0; i < columns.Length; i++) + { + columns[i] = columns[i].Replace("\"", string.Empty); + } + + string 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 position = columns[7].Trim(); + string noteText = columns[4].Trim(); + + if (!string.IsNullOrEmpty(lineCode) + && !string.IsNullOrEmpty(partNumber) + && int.TryParse(columns[5], out int baseVehicleId) + && int.TryParse(columns[6], out int engineConfigId)) + { + dataTable.Rows.Add(new object[] { lineCode, partNumber, baseVehicleId, engineConfigId }); //, position, noteText }); + } + } + + return dataTable; + } } } \ No newline at end of file diff --git a/PartSource.Automation/Jobs/SyncronizeProducts.cs b/PartSource.Automation/Jobs/SyncronizeProducts.cs index f71a04f..10263d5 100644 --- a/PartSource.Automation/Jobs/SyncronizeProducts.cs +++ b/PartSource.Automation/Jobs/SyncronizeProducts.cs @@ -1,17 +1,14 @@ -using Ratermania.Automation.Interfaces; -using PartSource.Automation.Models; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using PartSource.Data; -using PartSource.Services; +using PartSource.Data.Contexts; +using PartSource.Data.Models; +using Ratermania.Automation.Interfaces; using Ratermania.Shopify; using Ratermania.Shopify.Resources; -using PartSource.Data.Models; +using System; +using System.Collections.Generic; using System.Linq; -using Microsoft.EntityFrameworkCore; +using System.Threading.Tasks; namespace PartSource.Automation.Jobs { diff --git a/PartSource.Automation/Jobs/UpdateFitment.cs b/PartSource.Automation/Jobs/UpdateFitment.cs index cba0dfb..c8eef00 100644 --- a/PartSource.Automation/Jobs/UpdateFitment.cs +++ b/PartSource.Automation/Jobs/UpdateFitment.cs @@ -74,7 +74,7 @@ namespace PartSource.Automation.Jobs bool isFitment = false; - IList vehicles = _vehicleService.GetVehiclesForPart(importData.PartNumber, importData.LineCode, 255); + IList vehicles = _vehicleService.GetVehiclesForPart(importData.PartNumber, importData.LineCode); //if (vehicles.Count > 250) //{ @@ -166,9 +166,9 @@ namespace PartSource.Automation.Jobs tags.AddRange(ymmFitment); - if (tags.Count > 250) + if (tags.Count > 249) { - tags = tags.Take(250).ToList(); + tags = tags.Take(249).ToList(); } string zzzIsFitment = isFitment @@ -188,7 +188,7 @@ namespace PartSource.Automation.Jobs catch (Exception ex) { - _logger.LogError($"Failed to updated fitment data for SKU {importData?.VariantSku}", ex); + _logger.LogError($"Failed to updated fitment data for SKU {importData?.VariantSku} - {ex.Message}", ex); } } diff --git a/PartSource.Automation/Jobs/UpdatePricing.cs b/PartSource.Automation/Jobs/UpdatePricing.cs index 1ee53c9..d5f917f 100644 --- a/PartSource.Automation/Jobs/UpdatePricing.cs +++ b/PartSource.Automation/Jobs/UpdatePricing.cs @@ -1,9 +1,9 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Ratermania.Automation.Interfaces; -using PartSource.Automation.Models; -using PartSource.Data; +using Microsoft.Extensions.Logging; +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; @@ -11,8 +11,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using PartSource.Automation.Services; namespace PartSource.Automation.Jobs { @@ -46,7 +44,7 @@ namespace PartSource.Automation.Jobs catch (Exception ex) { _logger.LogError(ex, "Failed to get the initial set of products from Shopify."); - + throw; } @@ -56,35 +54,45 @@ namespace PartSource.Automation.Jobs { if (product.Variants.Length > 0) { - Variant variant = product.Variants[0]; - PartPrice partPrice = prices.Where(p => p.SKU == variant.Sku).FirstOrDefault(); + bool hasUpdate = false; - if (partPrice == null || !partPrice.Your_Price.HasValue || !partPrice.Compare_Price.HasValue) + for (int i = 0; i < product.Variants.Length; i++) { - continue; + Variant variant = product.Variants[i]; + PartPrice partPrice = prices.Where(p => p.SKU == variant.Sku).FirstOrDefault(); + + if (partPrice == null || !partPrice.Your_Price.HasValue || !partPrice.Compare_Price.HasValue) + { + continue; + } + + if (product.Variants[i].Price.ToString("G29") != partPrice.Your_Price.Value.ToString("G29") || product.Variants[i].CompareAtPrice.ToString("G29") != partPrice.Compare_Price.Value.ToString("G29")) + { + product.Variants[i].Price = partPrice.Your_Price.Value; + product.Variants[i].CompareAtPrice = partPrice.Compare_Price.Value; + + product.PublishedAt = partPrice.Active.Trim().ToUpperInvariant() == "Y" ? (DateTime?)DateTime.Now : null; + product.PublishedScope = PublishedScope.Global; + + //Metafield metafield = new Metafield + //{ + // Namespace = "Pricing", + // Key = "CorePrice", + // Value = partPrice.Core_Price.HasValue ? partPrice.Core_Price.Value.ToString() : "0.00", + // ValueType = "string", + // OwnerResource = "product", + // OwnerId = product.Id + //}; + + hasUpdate = true; + } } - if (product.Variants[0].Price != partPrice.Your_Price.Value || product.Variants[0].CompareAtPrice != partPrice.Compare_Price.Value) + if (hasUpdate) { - product.Variants[0].Price = partPrice.Your_Price.Value; - product.Variants[0].CompareAtPrice = partPrice.Compare_Price.Value; - - product.PublishedAt = partPrice.Active.Trim().ToUpperInvariant() == "Y" ? (DateTime?)DateTime.Now : null; - product.PublishedScope = PublishedScope.Global; - - Metafield metafield = new Metafield - { - Namespace = "Pricing", - Key = "CorePrice", - Value = partPrice.Core_Price.HasValue ? partPrice.Core_Price.Value.ToString() : "0.00", - ValueType = "string", - OwnerResource = "product", - OwnerId = product.Id - }; - try { - await _shopifyClient.Metafields.Add(metafield); + //await _shopifyClient.Metafields.Add(metafield); await _shopifyClient.Products.Update(product); updateCount++; @@ -92,7 +100,7 @@ namespace PartSource.Automation.Jobs catch (Exception ex) { - _logger.LogWarning(ex, $"Failed to update pricing for SKU {variant.Sku}"); + _logger.LogWarning(ex, $"Failed to update pricing for product ID {product.Id}"); } } } @@ -100,7 +108,7 @@ namespace PartSource.Automation.Jobs try { - products = await _shopifyClient.Products.GetNext(); + products = await _shopifyClient.Products.GetNext(); _logger.LogInformation($"Total updated: {updateCount}"); } diff --git a/PartSource.Automation/Program.cs b/PartSource.Automation/Program.cs index 3a165aa..2c5b64b 100644 --- a/PartSource.Automation/Program.cs +++ b/PartSource.Automation/Program.cs @@ -70,23 +70,23 @@ namespace PartSource.Automation { options.HasBaseInterval(new TimeSpan(0, 15, 0)) .HasMaxFailures(5) - // - //.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) - // .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) - //.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) - // .HasDependency() - // .StartsAt(DateTime.Today.AddHours(8)) - //) - .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) - .StartsAt(DateTime.Parse("2021-04-01 08:00:00")) - ) - .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) - .StartsAt(DateTime.Today.AddHours(26)) - ) + // + //.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) + // .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) + //.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) + // .HasDependency() + // .StartsAt(DateTime.Today.AddHours(8)) + //) + //.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) + // .StartsAt(DateTime.Parse("2021-04-01 08:00:00")) + //) + .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) + .StartsAt(DateTime.Today.AddHours(26)) + ) .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0)) - .HasDependency() - .StartsAt(DateTime.Today.AddHours(27) - ) + .HasDependency() + .StartsAt(DateTime.Today.AddHours(27) + ) ); //.AddApiServer(); }) diff --git a/PartSource.Automation/Services/WhiSeoService.cs b/PartSource.Automation/Services/WhiSeoService.cs index bd62a0f..264f972 100644 --- a/PartSource.Automation/Services/WhiSeoService.cs +++ b/PartSource.Automation/Services/WhiSeoService.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.Configuration; +#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities + +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using PartSource.Automation.Models.Configuration; using PartSource.Automation.Models.Enums; @@ -65,20 +67,28 @@ namespace PartSource.Automation.Services using SqlConnection connection = new SqlConnection(_connectionString); connection.Open(); -#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities - using SqlCommand command = new SqlCommand($"EXEC AddFitmentTable @tableName = '{tableName}'", connection); + + using SqlCommand command = new SqlCommand($"EXEC CreateFitmentTempTable @tableName = '{tableName}'", connection); command.ExecuteNonQuery(); -#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities using SqlBulkCopy bulk = new SqlBulkCopy(connection) { - DestinationTableName = $"{seoDataType}.{tableName}", + DestinationTableName = $"FitmentTemp.{tableName}", BulkCopyTimeout = 14400 }; bulk.WriteToServer(dataTable); } + public void CreateFitmentTable(string tableName) + { + using SqlConnection connection = new SqlConnection(_connectionString); + connection.Open(); + + using SqlCommand command = new SqlCommand($"exec CreateFitmentTable @tableName = '{tableName}'", connection); + command.ExecuteNonQuery(); + } + public void CreateFitmentView() { using SqlConnection connection = new SqlConnection(_connectionString); @@ -89,3 +99,4 @@ namespace PartSource.Automation.Services } } } +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities \ No newline at end of file diff --git a/PartSource.Data/PartSourceContext.cs b/PartSource.Data/Contexts/PartSourceContext.cs similarity index 84% rename from PartSource.Data/PartSourceContext.cs rename to PartSource.Data/Contexts/PartSourceContext.cs index b223dea..1f84836 100644 --- a/PartSource.Data/PartSourceContext.cs +++ b/PartSource.Data/Contexts/PartSourceContext.cs @@ -2,9 +2,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using PartSource.Data.Converters; using PartSource.Data.Models; -namespace PartSource.Data +namespace PartSource.Data.Contexts { public class PartSourceContext : DbContext { @@ -29,6 +30,8 @@ namespace PartSource.Data public DbSet Parts { get; set; } + public DbSet ShopifyChangelogs { get; set; } + public DbSet Vehicles { get; set; } public DbSet PartAvailabilities { get; set; } @@ -55,11 +58,18 @@ namespace PartSource.Data modelBuilder.Entity().HasKey(p => new { p.Store, p.SKU }); + modelBuilder.Entity() + .Property(s => s.ResourceType) + .HasConversion(new TypeToStringConverter()); + + modelBuilder.Entity() + .Property(s => s.Data) + .HasConversion(new ObjectToJsonConverter()); + foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) { entityType.Relational().TableName = entityType.ClrType.Name; } - } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) diff --git a/PartSource.Data/Converters/ObjectToJsonConverter.cs b/PartSource.Data/Converters/ObjectToJsonConverter.cs new file mode 100644 index 0000000..339b80f --- /dev/null +++ b/PartSource.Data/Converters/ObjectToJsonConverter.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; + +namespace PartSource.Data.Converters +{ + public class ObjectToJsonConverter : ValueConverter + { + private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new DefaultContractResolver() + { + NamingStrategy = new SnakeCaseNamingStrategy() + } + }; + + public ObjectToJsonConverter() : base(ObjectToJson, JsonToObject) { } + + public static Expression> JsonToObject = value => JsonConvert.DeserializeObject(value, _serializerSettings); + public static Expression> ObjectToJson = value => JsonConvert.SerializeObject(value, _serializerSettings); + } +} diff --git a/PartSource.Data/Converters/TypeToStringConverter.cs b/PartSource.Data/Converters/TypeToStringConverter.cs new file mode 100644 index 0000000..fc630af --- /dev/null +++ b/PartSource.Data/Converters/TypeToStringConverter.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; + +namespace PartSource.Data.Converters +{ + public class TypeToStringConverter : ValueConverter + { + public TypeToStringConverter() : base(TypeToString, StringToType) { } + + public static Expression> StringToType = value => Type.GetType(value); + public static Expression> TypeToString = value => value.AssemblyQualifiedName; + } +} diff --git a/PartSource.Data/Migrations/20190811020941_ImportDateDcfMapping.Designer.cs b/PartSource.Data/Migrations/20190811020941_ImportDateDcfMapping.Designer.cs index 47989b4..3f73e9b 100644 --- a/PartSource.Data/Migrations/20190811020941_ImportDateDcfMapping.Designer.cs +++ b/PartSource.Data/Migrations/20190811020941_ImportDateDcfMapping.Designer.cs @@ -1,15 +1,14 @@ // -using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using PartSource.Data; +using PartSource.Data.Contexts; +using System; namespace PartSource.Data.Migrations { - [DbContext(typeof(PartSourceContext))] + [DbContext(typeof(PartSourceContext))] [Migration("20190811020941_ImportDateDcfMapping")] partial class ImportDateDcfMapping { diff --git a/PartSource.Data/Migrations/PartSourceContextModelSnapshot.cs b/PartSource.Data/Migrations/PartSourceContextModelSnapshot.cs index a089ce0..06256f0 100644 --- a/PartSource.Data/Migrations/PartSourceContextModelSnapshot.cs +++ b/PartSource.Data/Migrations/PartSourceContextModelSnapshot.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using PartSource.Data; +using PartSource.Data.Contexts; namespace PartSource.Data.Migrations { diff --git a/PartSource.Data/Models/ShopifyChangelog.cs b/PartSource.Data/Models/ShopifyChangelog.cs new file mode 100644 index 0000000..20fb572 --- /dev/null +++ b/PartSource.Data/Models/ShopifyChangelog.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace PartSource.Data.Models +{ + public class ShopifyChangelog + { + public int Id { get; set; } + + public Type ResourceType { get; set; } + + public long ShopifyId { get; set; } + + public DateTime Timestamp { get; set; } + + public object Data { get; set; } + } +} diff --git a/PartSource.Services/MetafieldService.cs b/PartSource.Services/MetafieldService.cs deleted file mode 100644 index a7b2eed..0000000 --- a/PartSource.Services/MetafieldService.cs +++ /dev/null @@ -1,73 +0,0 @@ -//using Newtonsoft.Json; -//using PartSource.Data.Shopify; -//using PartSource.Services.Integrations; -//using Ratermania.Shopify; -//using System; -//using System.Collections.Generic; -//using System.Diagnostics.CodeAnalysis; -//using System.Text; -//using System.Threading.Tasks; - -//namespace PartSource.Services -//{ -// public class MetafieldService -// { -// private readonly ShopifyClient _shopifyClient; - -// public MetafieldService(ShopifyClient shopifyClient) -// { -// _shopifyClient = shopifyClient; -// } - -// [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Lowercase is expected by Shopify")] -// public async Task SaveMetafield(T shopifyEntity, IList vehicleIds, string @namespace) where T : ShopifyEntity -// { -// if (vehicleIds.Count == 0) -// { -// return; -// } - -// string json = JsonConvert.SerializeObject(vehicleIds); -// if (json.Length >= 100000) -// { -// // TODO: Logging -// return; -// } - -// string key = @namespace.ToLowerInvariant().Replace(" ", "_"); -// if (key.Length > 20) -// { -// key = key.Substring(0, 20); -// } - -// Metafield metafield = new Metafield -// { -// Namespace = "position", -// Key = key, -// Value = json, -// ValueType = "json_string", -// OwnerResource = "product", -// OwnerId = shopifyEntity.Id -// }; - -// await _shopifyClient.Metafields.Add(metafield); -// } - -// public async Task DeleteMetafields(long shopifyId) where T : ShopifyEntity -// { -// IDictionary parameters = new Dictionary -// { -// { "metafield[owner_id]", shopifyId}, -// { "metafield[owner_resource]", "product" }, -// { "namespace", "position" }, -// }; - -// IEnumerable metafields = await _shopifyClient.Metafields.Get(parameters); - -// foreach (Metafield metafield in metafields) -// { -// await _shopifyClient.Metafields.Delete(metafield); -// } -// } -// } -//} diff --git a/PartSource.Services/PartService.cs b/PartSource.Services/PartService.cs index 6c5335c..5b7c2c9 100644 --- a/PartSource.Services/PartService.cs +++ b/PartSource.Services/PartService.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using PartSource.Data; +using PartSource.Data.Contexts; using PartSource.Data.Dtos; using PartSource.Data.Models; using System; diff --git a/PartSource.Services/PartSource.Services.csproj b/PartSource.Services/PartSource.Services.csproj index a3887df..6c9dd00 100644 --- a/PartSource.Services/PartSource.Services.csproj +++ b/PartSource.Services/PartSource.Services.csproj @@ -8,6 +8,7 @@ + diff --git a/PartSource.Services/SecurityService.cs b/PartSource.Services/SecurityService.cs index e119521..27e5d20 100644 --- a/PartSource.Services/SecurityService.cs +++ b/PartSource.Services/SecurityService.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using PartSource.Data; +using PartSource.Data.Contexts; using PartSource.Data.Models; using System.Threading.Tasks; diff --git a/PartSource.Services/ShopifyChangelogService.cs b/PartSource.Services/ShopifyChangelogService.cs new file mode 100644 index 0000000..22e77af --- /dev/null +++ b/PartSource.Services/ShopifyChangelogService.cs @@ -0,0 +1,34 @@ +using PartSource.Data.Contexts; +using PartSource.Data.Models; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Ratermania.Shopify.Resources; + +namespace PartSource.Services +{ + public class ShopifyChangelogService + { + private readonly PartSourceContext _partsourceContext; + + public ShopifyChangelogService(PartSourceContext partsourceContext) + { + _partsourceContext = partsourceContext; + } + + public async Task AddEntry(T data) where T : BaseShopifyResource + { + ShopifyChangelog shopifyChangelog = new ShopifyChangelog + { + ResourceType = typeof(T), + ShopifyId = data.Id, + Data = data, + Timestamp = DateTime.Now + }; + + await _partsourceContext.AddAsync(shopifyChangelog); + await _partsourceContext.SaveChangesAsync(); + } + } +}