0
0

Compare commits

..

No commits in common. "3ab838e27f488088d32eb9a1bd239521185a131d" and "402ffae02d2ee0c48e8220500c03e588a5d57f15" have entirely different histories.

21 changed files with 334 additions and 120 deletions

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RhSolutions.Api\RhSolutions.Api.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,25 @@
using RhSolutions.Api.Models;
namespace RhSolutions.Tests
{
public class SkuExtensionsTests
{
[Theory]
[InlineData("11600011001")]
[InlineData(" 11600011001")]
[InlineData("11600011001 ")]
[InlineData("string 11600011001")]
[InlineData("11600011001 string")]
[InlineData("160001-001")]
[InlineData("string 160001-001")]
[InlineData("160001-001 string")]
[InlineData("160001001")]
[InlineData("string 160001001")]
[InlineData("160001001 string")]
public void TestName(string input)
{
Sku.TryParse(input, out IEnumerable<Sku> sku);
Assert.Equal(new Sku("160001", "001"), sku.FirstOrDefault());
}
}
}

View File

@ -0,0 +1 @@
global using Xunit;

View File

@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using RhSolutions.Models;
using RhSolutions.Api.Models;
using RhSolutions.Api.Services;
using System.Linq;
namespace RhSolutions.Api.Controllers
{
@ -32,25 +31,17 @@ namespace RhSolutions.Api.Controllers
}
[HttpPost]
public IActionResult PostProductsFromXls()
public async Task<IActionResult> PostProductsFromXls()
{
try
{
var products = parser.GetProducts(HttpContext).GroupBy(p => p.ProductSku)
.Select(g => new Product(g.Key)
{
Name = g.First().Name,
DeprecatedSkus = g.SelectMany(p => p.DeprecatedSkus).ToList(),
ProductLines = g.SelectMany(p => p.ProductLines).Distinct().ToList(),
IsOnWarehouse = g.Any(p => p.IsOnWarehouse == true),
ProductMeasure = g.First().ProductMeasure,
DeliveryMakeUp = g.First().DeliveryMakeUp,
Price = g.First().Price
});
foreach (var p in products)
var products = parser.GetProducts(HttpContext);
await foreach (var p in products)
{
dbContext.Add<Product>(p);
using (p)
{
dbContext.Add<Product>(p);
}
}
dbContext.SaveChanges();

View File

@ -1,27 +1,28 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RhSolutions.Models;
using RhSolutions.Api.Models;
namespace RhSolutions.Api.Controllers
{
[Route("api/[controller]")]
public class SearchController : ControllerBase
{
private RhSolutionsContext context;
[Route("api/[controller]")]
public class SearchController : ControllerBase
{
private RhSolutionsContext context;
public SearchController(RhSolutionsContext context)
{
this.context = context;
}
public SearchController(RhSolutionsContext context)
{
this.context = context;
}
[HttpGet]
public IAsyncEnumerable<Product> SearchProducts([FromQuery] string query)
{
return context.Products
.Where(p => EF.Functions.ToTsVector(
"russian", string.Join(' ', new[] { p.Name, string.Join(' ', p.ProductLines)}))
.Matches(EF.Functions.WebSearchToTsQuery("russian", query)))
.AsAsyncEnumerable();
}
}
[HttpGet]
public IAsyncEnumerable<Product> SearchProducts([FromQuery] string query)
{
return context.Products
.Where(p => EF.Functions.ToTsVector(
"russian", string.Join(' ',
new [] {p.ProductLine ?? string.Empty, p.Name ?? string.Empty }))
.Matches(EF.Functions.WebSearchToTsQuery("russian", query)))
.AsAsyncEnumerable();
}
}
}

View File

@ -6,14 +6,14 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using RhSolutions.Models;
using RhSolutions.Api.Models;
#nullable disable
namespace RhSolutions.Api.Migrations
{
[DbContext(typeof(RhSolutionsContext))]
[Migration("20230511043408_Init")]
[Migration("20221201071323_Init")]
partial class Init
{
/// <inheritdoc />
@ -21,15 +21,18 @@ namespace RhSolutions.Api.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("ProductVersion", "7.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("RhSolutions.Models.Product", b =>
modelBuilder.Entity("RhSolutions.Api.Models.Product", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<double?>("DeliveryMakeUp")
.HasColumnType("double precision");
@ -38,7 +41,7 @@ namespace RhSolutions.Api.Migrations
.IsRequired()
.HasColumnType("text[]");
b.Property<bool>("IsOnWarehouse")
b.Property<bool?>("IsOnWarehouse")
.HasColumnType("boolean");
b.Property<string>("Name")
@ -46,17 +49,15 @@ namespace RhSolutions.Api.Migrations
.HasColumnType("text");
b.Property<decimal>("Price")
.HasColumnType("numeric");
.HasColumnType("decimal(8,2)");
b.Property<List<string>>("ProductLines")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("ProductLine")
.HasColumnType("text");
b.Property<int>("ProductMeasure")
.HasColumnType("integer");
b.Property<string>("ProductSku")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
@ -15,15 +16,16 @@ namespace RhSolutions.Api.Migrations
name: "Products",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
ProductSku = table.Column<string>(type: "text", nullable: false),
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ProductSku = table.Column<string>(type: "text", nullable: true),
DeprecatedSkus = table.Column<List<string>>(type: "text[]", nullable: false),
ProductLines = table.Column<List<string>>(type: "text[]", nullable: false),
IsOnWarehouse = table.Column<bool>(type: "boolean", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
ProductLine = table.Column<string>(type: "text", nullable: true),
IsOnWarehouse = table.Column<bool>(type: "boolean", nullable: true),
ProductMeasure = table.Column<int>(type: "integer", nullable: false),
DeliveryMakeUp = table.Column<double>(type: "double precision", nullable: true),
Price = table.Column<decimal>(type: "numeric", nullable: false)
Price = table.Column<decimal>(type: "numeric(8,2)", nullable: false)
},
constraints: table =>
{

View File

@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using RhSolutions.Models;
using RhSolutions.Api.Models;
#nullable disable
@ -18,15 +18,18 @@ namespace RhSolutions.Api.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("ProductVersion", "7.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("RhSolutions.Models.Product", b =>
modelBuilder.Entity("RhSolutions.Api.Models.Product", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<double?>("DeliveryMakeUp")
.HasColumnType("double precision");
@ -35,7 +38,7 @@ namespace RhSolutions.Api.Migrations
.IsRequired()
.HasColumnType("text[]");
b.Property<bool>("IsOnWarehouse")
b.Property<bool?>("IsOnWarehouse")
.HasColumnType("boolean");
b.Property<string>("Name")
@ -43,17 +46,15 @@ namespace RhSolutions.Api.Migrations
.HasColumnType("text");
b.Property<decimal>("Price")
.HasColumnType("numeric");
.HasColumnType("decimal(8,2)");
b.Property<List<string>>("ProductLines")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("ProductLine")
.HasColumnType("text");
b.Property<int>("ProductMeasure")
.HasColumnType("integer");
b.Property<string>("ProductSku")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");

View File

@ -0,0 +1,4 @@
namespace RhSolutions.Api.Models
{
public enum Measure { Kg, M, M2, P }
}

View File

@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.Text.Json.Serialization;
namespace RhSolutions.Api.Models
{
public partial class Product : IDisposable
{
[Key]
[JsonIgnore]
public int Id { get; set; }
public string ProductSku { get; set; } = string.Empty;
public List<string> DeprecatedSkus { get; set; } = new();
public string Name { get; set; } = string.Empty;
public string ProductLine { get; set; } = string.Empty;
public bool? IsOnWarehouse { get; set; }
public Measure ProductMeasure { get; set; }
public double? DeliveryMakeUp { get; set; }
[Column(TypeName = "decimal(8,2)")]
public decimal Price { get; set; }
public void Dispose()
{
Debug.WriteLine($"{this} disposed");
}
public override string ToString()
{
return $"({ProductSku}) {Name}";
}
}
}

View File

@ -1,21 +1,11 @@
using Microsoft.EntityFrameworkCore;
namespace RhSolutions.Models;
namespace RhSolutions.Api.Models;
public class RhSolutionsContext : DbContext
{
public RhSolutionsContext(DbContextOptions<RhSolutionsContext> options)
: base(options) { }
: base(options) { }
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Product>()
.Property(e => e.ProductSku)
.HasConversion(v => v.ToString(), v => new ProductSku(v));
builder.Entity<Product>()
.Property(e => e.DeprecatedSkus)
.HasPostgresArrayConversion<ProductSku, string>(v => v.ToString(), v => new ProductSku(v));
}
}

View File

@ -0,0 +1,127 @@
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace RhSolutions.Api.Models
{
public class Sku
{
private const string matchPattern = @"([1\D]|\b)(?<Article>\d{6})([1\s-]|)(?<Variant>\d{3})\b";
private string? _article;
private string? _variant;
public Sku(string article, string variant)
{
Article = article;
Variant = variant;
}
[Key]
public string Id
{
get
{
return $"1{Article}1{Variant}";
}
set
{
if (TryParse(value, out IEnumerable<Sku> skus))
{
if (skus.Count() > 1)
{
throw new ArgumentException($"More than one valid sku detected: {value}");
}
else
{
this.Article = skus.First().Article;
this.Variant = skus.First().Variant;
}
}
else
{
throw new ArgumentException($"Invalid sku input: {value}");
}
}
}
public string? Article
{
get
{
return _article;
}
set
{
if (value == null || value.Length != 6 || value.Where(c => char.IsDigit(c)).Count() != 6)
{
throw new ArgumentException($"Wrong Article: {Article}");
}
else
{
_article = value;
}
}
}
public string? Variant
{
get
{
return _variant;
}
set
{
if (value == null || value.Length != 3 || value.Where(c => char.IsDigit(c)).Count() != 3)
{
throw new ArgumentException($"Wrong Variant: {Variant}");
}
else _variant = value;
}
}
public static IEnumerable<Sku> GetValidSkus(string line)
{
MatchCollection matches = Regex.Matches(line, matchPattern);
if (matches.Count == 0)
{
yield break;
}
else
{
foreach (Match m in matches)
{
yield return new Sku(m.Groups["Article"].Value, m.Groups["Variant"].Value);
}
}
}
public static bool TryParse(string line, out IEnumerable<Sku> skus)
{
MatchCollection matches = Regex.Matches(line, matchPattern);
if (matches.Count == 0)
{
skus = Enumerable.Empty<Sku>();
return false;
}
else
{
skus = GetValidSkus(line);
return true;
}
}
public override bool Equals(object? obj)
{
return obj is Sku sku &&
Article!.Equals(sku.Article) &&
Variant!.Equals(sku.Variant);
}
public override int GetHashCode()
{
return HashCode.Combine(Article, Variant);
}
public override string ToString()
{
return $"1{Article}1{Variant}";
}
}
}

View File

@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore;
using RhSolutions.Models;
using RhSolutions.Api.Models;
using RhSolutions.Api.Services;
var builder = WebApplication.CreateBuilder(args);

View File

@ -8,14 +8,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.101.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
<PackageReference Include="ClosedXML" Version="0.97.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
<PackageReference Include="Rhsolutions.ProductSku" Version="0.2.13" />
</ItemGroup>
</Project>

View File

@ -1,36 +1,35 @@
using ClosedXML.Excel;
using RhSolutions.Models;
using RhSolutions.Api.Models;
namespace RhSolutions.Api.Services
{
public class ClosedXMLParser : IPricelistParser
{
public List<Product> GetProducts(HttpContext context)
public async IAsyncEnumerable<Product> GetProducts(HttpContext context)
{
using (var memoryStream = new MemoryStream())
{
if (context == null)
{
return new List<Product>();
yield break;
}
context.Request.Body.CopyToAsync(memoryStream).GetAwaiter().GetResult();
List<Product> products = new();
await context.Request.Body.CopyToAsync(memoryStream);
using (var wb = new XLWorkbook(memoryStream))
{
Dictionary<ProductSku, Product> collected = new();
var table = GetTable(wb);
var rows = table.DataRange.Rows();
foreach (var row in rows)
var enumerator = rows.GetEnumerator();
while (enumerator.MoveNext())
{
if (ProductSku.TryParse(row.Field("Актуальный материал")
.GetString(), out _))
if (Sku.TryParse(enumerator.Current.Field("Актуальный материал")
.GetString(), out IEnumerable<Sku> skus))
{
products.Add(ParseRow(row));
yield return ParseRow(enumerator.Current);
}
}
}
yield break;
}
return products;
}
}
private IXLTable GetTable(XLWorkbook wb)
@ -61,10 +60,10 @@ namespace RhSolutions.Api.Services
.GetString()
.Split('\n')
.First();
ProductSku.TryParse(row.Field("Актуальный материал")
.GetString(), out IEnumerable<ProductSku> productSkus);
ProductSku.TryParse(row.Field("Прежний материал")
.GetString(), out IEnumerable<ProductSku> deprecatedSkus);
Sku.TryParse(row.Field("Актуальный материал")
.GetString(), out IEnumerable<Sku> productSkus);
Sku.TryParse(row.Field("Прежний материал")
.GetString(), out IEnumerable<Sku> deprecatedSkus);
string measureField = new string(row.Field("Ед. изм.")
.GetString()
@ -101,7 +100,7 @@ namespace RhSolutions.Api.Services
string onWarehouseField = row.Field("Складская программа")
.GetString();
bool IsOnWarehouse;
bool? IsOnWarehouse;
switch (onWarehouseField)
{
@ -112,7 +111,7 @@ namespace RhSolutions.Api.Services
IsOnWarehouse = false;
break;
default:
IsOnWarehouse = false;
IsOnWarehouse = null;
break;
}
@ -124,11 +123,12 @@ namespace RhSolutions.Api.Services
price = 0.0M;
}
return new Product(productSkus.First())
return new Product
{
ProductLines = new List<string>() { productLine },
ProductLine = productLine,
Name = productName,
DeprecatedSkus = deprecatedSkus.ToList(),
ProductSku = productSkus.First().Id,
DeprecatedSkus = deprecatedSkus.Select(s => s.Id).ToList(),
ProductMeasure = productMeasure,
DeliveryMakeUp = productWarehouseCount,
IsOnWarehouse = IsOnWarehouse,

View File

@ -1,9 +1,9 @@
using RhSolutions.Models;
using RhSolutions.Api.Models;
namespace RhSolutions.Api.Services
{
public interface IPricelistParser
{
public List<Product> GetProducts(HttpContext context);
public IAsyncEnumerable<Product> GetProducts(HttpContext context);
}
}

View File

@ -1,5 +1,5 @@
{
"sdk": {
"version": "6.0.311"
"version": "6.0.402"
}
}

View File

@ -7,19 +7,19 @@ CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
START TRANSACTION;
CREATE TABLE "Products" (
"Id" text NOT NULL,
"Name" text NOT NULL,
"ProductSku" text NOT NULL,
"Id" integer GENERATED BY DEFAULT AS IDENTITY,
"ProductSku" text NULL,
"DeprecatedSkus" text[] NOT NULL,
"ProductLines" text[] NOT NULL,
"IsOnWarehouse" boolean NOT NULL,
"Name" text NOT NULL,
"ProductLine" text NULL,
"IsOnWarehouse" boolean NULL,
"ProductMeasure" integer NOT NULL,
"DeliveryMakeUp" double precision NULL,
"Price" numeric NOT NULL,
"Price" numeric(8,2) NOT NULL,
CONSTRAINT "PK_Products" PRIMARY KEY ("Id")
);
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20230511043408_Init', '7.0.5');
VALUES ('20221201071323_Init', '7.0.0');
COMMIT;

View File

@ -1,8 +1,9 @@
version: '3'
services:
rhsolutions-api:
build: ..
build: ../RhSolutions.Api
container_name: rhsolutions-api
ports:
- 5000:5000
@ -15,13 +16,16 @@ services:
depends_on:
- rhsolutions-db
restart: unless-stopped
rhsolutions-db:
container_name: rhsolutions-db
ports:
- 5432:5432
build: ./Database
build: ./database
environment:
- POSTGRES_USER=chebser
- POSTGRES_PASSWORD=Rehau-987
- POSTGRES_DB=rhsolutions
restart: unless-stopped
restart: unless-stopped
networks:
default:
name: rhsolutions

View File

@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhSolutions.Api", "RhSolutions.Api\RhSolutions.Api.csproj", "{FD778359-7E92-4B5C-A4F9-7942A28E58F5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RhSolutions.Api.Tests", "RhSolutions.Api.Tests\RhSolutions.Api.Tests.csproj", "{A71CCD18-1D47-4DF5-B883-AF4D1CFB4F4E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -18,5 +20,9 @@ Global
{FD778359-7E92-4B5C-A4F9-7942A28E58F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD778359-7E92-4B5C-A4F9-7942A28E58F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD778359-7E92-4B5C-A4F9-7942A28E58F5}.Release|Any CPU.Build.0 = Release|Any CPU
{A71CCD18-1D47-4DF5-B883-AF4D1CFB4F4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A71CCD18-1D47-4DF5-B883-AF4D1CFB4F4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A71CCD18-1D47-4DF5-B883-AF4D1CFB4F4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A71CCD18-1D47-4DF5-B883-AF4D1CFB4F4E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal