Sync products with old e5 CRM

This commit is contained in:
Michał Zieliński
2025-06-23 21:52:09 +02:00
parent 8cb3b50fe7
commit 63b7ed51d1
17 changed files with 339 additions and 80 deletions

View File

@@ -5,8 +5,8 @@ using Microsoft.EntityFrameworkCore;
namespace Bimix.API.Controllers; namespace Bimix.API.Controllers;
[Route("api/[controller]")]
[ApiController] [ApiController]
[Route("api/[controller]")]
public class ProductsController(BimixDbContext context) : ControllerBase public class ProductsController(BimixDbContext context) : ControllerBase
{ {
private readonly BimixDbContext _context = context; private readonly BimixDbContext _context = context;

View File

@@ -0,0 +1,16 @@
using Bimix.Infrastructure.Sync;
using Microsoft.AspNetCore.Mvc;
namespace Bimix.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class SyncController(ProductSyncService productSyncService) : ControllerBase
{
[HttpPost("run-product-sync")]
public async Task<IActionResult> RunProductSync()
{
await productSyncService.RunAsync();
return Ok();
}
}

View File

@@ -1,11 +1,14 @@
using Bimix.Infrastructure.Data; using Bimix.Infrastructure.Data;
using Bimix.Infrastructure.Sync;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<BimixDbContext>(options => options.UseSqlServer(connectionString)); builder.Services.AddDbContext<BimixDbContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddScoped<ProductSyncService>();
builder.Services.AddHttpClient();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();

View File

@@ -1,38 +1,11 @@
{ {
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:54415",
"sslPort": 44366
}
},
"profiles": { "profiles": {
"http": { "dev": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "swagger", "launchUrl": "swagger",
"applicationUrl": "http://localhost:5090", "applicationUrl": "https://localhost:7142;http://localhost:5142",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7139;http://localhost:5090",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@@ -1,6 +0,0 @@
namespace Bimix.Application;
public class Class1
{
}

View File

@@ -0,0 +1,7 @@
namespace Bimix.Domain.Entities;
public class SyncState
{
public required string Entity { get; set; }
public required long LastSynced { get; set; } // UnixTimestamp
}

View File

@@ -6,12 +6,15 @@ namespace Bimix.Infrastructure.Data;
public class BimixDbContext(DbContextOptions<BimixDbContext> options) : DbContext(options) public class BimixDbContext(DbContextOptions<BimixDbContext> options) : DbContext(options)
{ {
public DbSet<Product> Products { get; set; } public DbSet<Product> Products { get; set; }
public DbSet<SyncState> SyncStates { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Product>().HasKey(p => p.Id); modelBuilder.Entity<Product>().HasKey(x => x.Id);
modelBuilder.Entity<Product>().Property(p => p.Name).IsRequired().HasMaxLength(200); modelBuilder.Entity<Product>().Property(x => x.Name).IsRequired().HasMaxLength(512);
modelBuilder.Entity<SyncState>().HasKey((x => x.Entity));
} }
} }

View File

@@ -0,0 +1,65 @@
// <auto-generated />
using System;
using Bimix.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Bimix.Infrastructure.Migrations
{
[DbContext(typeof(BimixDbContext))]
[Migration("20250623184943_AddSyncState")]
partial class AddSyncState
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.17")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Bimix.Domain.Entities.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Products");
});
modelBuilder.Entity("Bimix.Domain.Entities.SyncState", b =>
{
b.Property<string>("Entity")
.HasColumnType("nvarchar(450)");
b.Property<long>("LastSynced")
.HasColumnType("bigint");
b.HasKey("Entity");
b.ToTable("SyncStates");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bimix.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddSyncState : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SyncStates",
columns: table => new
{
Entity = table.Column<string>(type: "nvarchar(450)", nullable: false),
LastSynced = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SyncStates", x => x.Entity);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SyncStates");
}
}
}

View File

@@ -0,0 +1,65 @@
// <auto-generated />
using System;
using Bimix.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Bimix.Infrastructure.Migrations
{
[DbContext(typeof(BimixDbContext))]
[Migration("20250623194653_ResizeProductName")]
partial class ResizeProductName
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.17")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Bimix.Domain.Entities.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Products");
});
modelBuilder.Entity("Bimix.Domain.Entities.SyncState", b =>
{
b.Property<string>("Entity")
.HasColumnType("nvarchar(450)");
b.Property<long>("LastSynced")
.HasColumnType("bigint");
b.HasKey("Entity");
b.ToTable("SyncStates");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bimix.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class ResizeProductName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Products",
type: "nvarchar(512)",
maxLength: 512,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(200)",
oldMaxLength: 200);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Products",
type: "nvarchar(200)",
maxLength: 200,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(512)",
oldMaxLength: 512);
}
}
}

View File

@@ -33,8 +33,8 @@ namespace Bimix.Infrastructure.Migrations
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(200) .HasMaxLength(512)
.HasColumnType("nvarchar(200)"); .HasColumnType("nvarchar(512)");
b.Property<DateTime>("UpdatedAt") b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@@ -43,6 +43,19 @@ namespace Bimix.Infrastructure.Migrations
b.ToTable("Products"); b.ToTable("Products");
}); });
modelBuilder.Entity("Bimix.Domain.Entities.SyncState", b =>
{
b.Property<string>("Entity")
.HasColumnType("nvarchar(450)");
b.Property<long>("LastSynced")
.HasColumnType("bigint");
b.HasKey("Entity");
b.ToTable("SyncStates");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@@ -0,0 +1,74 @@
using System.Text.Json;
using Bimix.Domain.Entities;
using Bimix.Infrastructure.Data;
using Microsoft.Extensions.Configuration;
namespace Bimix.Infrastructure.Sync;
public class ProductSyncService(HttpClient httpClient, BimixDbContext db, IConfiguration configuration)
{
private readonly HttpClient _httpClient = httpClient;
private readonly BimixDbContext _db = db;
private readonly IConfiguration _configuration = configuration;
public async Task RunAsync()
{
var apiKey = _configuration["E5_CRM:ApiKey"];
var syncState = _db.SyncStates.FirstOrDefault(x => x.Entity == "Product") ?? new SyncState { Entity = "Product", LastSynced = 0};
var url = $"https://crm.e5.pl/REST/index.php?key={apiKey}&action=export.products.list&since={syncState.LastSynced}";
var response = await _httpClient.GetStringAsync(url);
var products = JsonSerializer.Deserialize<List<JsonElement>>(response);
if (products == null) return;
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
foreach (var p in products)
{
var idStr = p.GetProperty("id").GetString() ?? "";
var name = p.GetProperty("name").GetString() ?? "";
if (!Guid.TryParse(idStr, out Guid id))
{
Console.WriteLine($"[SYNC] Skipping product with wrong ID: '{idStr}', Name: '{name}'");
continue;
}
var existing = _db.Products.FirstOrDefault(x => x.Id == id);
if (existing == null)
{
var product = new Product
{
Id = id,
Name = name,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
_db.Products.Add(product);
}
else
{
existing.Name = name;
existing.UpdatedAt = DateTime.UtcNow;
}
Console.WriteLine($"[SYNC] Updated product ID: '{idStr}', Name: '{name}'");
var exportedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var updateUrl = $"https://crm.e5.pl/REST/index.php?key={apiKey}&action=export.products.setExportedAt&id={id}&exportedAt={exportedAt}";
await _httpClient.GetAsync(updateUrl);
}
syncState.LastSynced = now;
if (_db.SyncStates.FirstOrDefault(x => x.Entity == "Product") == null)
{
_db.SyncStates.Add(syncState);
}
else
{
_db.SyncStates.Update(syncState);
}
await _db.SaveChangesAsync();
}
}

View File

@@ -1,8 +1,8 @@
{ {
"profiles": { "profiles": {
"Windows Machine": { "dev": {
"commandName": "MsixPackage", "commandName": "Project",
"nativeDebugging": false "dotnetRunMessages": true
} }
} }
} }

View File

@@ -1,35 +1,10 @@
{ {
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:9600",
"sslPort": 44314
}
},
"profiles": { "profiles": {
"http": { "dev": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "http://localhost:5250", "applicationUrl": "https://localhost:7246;http://localhost:5246",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7094;http://localhost:5250",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }