Sync products with old e5 CRM
This commit is contained in:
@@ -5,8 +5,8 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Bimix.API.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ProductsController(BimixDbContext context) : ControllerBase
|
||||
{
|
||||
private readonly BimixDbContext _context = context;
|
||||
|
||||
16
Bimix.API/Controllers/SyncController.cs
Normal file
16
Bimix.API/Controllers/SyncController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
using Bimix.Infrastructure.Data;
|
||||
using Bimix.Infrastructure.Sync;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||
builder.Services.AddDbContext<BimixDbContext>(options => options.UseSqlServer(connectionString));
|
||||
builder.Services.AddScoped<ProductSyncService>();
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
@@ -1,38 +1,11 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:54415",
|
||||
"sslPort": 44366
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"dev": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5090",
|
||||
"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",
|
||||
"applicationUrl": "https://localhost:7142;http://localhost:5142",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
namespace Bimix.Application;
|
||||
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
7
Bimix.Domain/Entities/SyncState.cs
Normal file
7
Bimix.Domain/Entities/SyncState.cs
Normal 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
|
||||
}
|
||||
@@ -6,12 +6,15 @@ namespace Bimix.Infrastructure.Data;
|
||||
public class BimixDbContext(DbContextOptions<BimixDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Product> Products { get; set; }
|
||||
public DbSet<SyncState> SyncStates { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<Product>().HasKey(p => p.Id);
|
||||
modelBuilder.Entity<Product>().Property(p => p.Name).IsRequired().HasMaxLength(200);
|
||||
modelBuilder.Entity<Product>().HasKey(x => x.Id);
|
||||
modelBuilder.Entity<Product>().Property(x => x.Name).IsRequired().HasMaxLength(512);
|
||||
|
||||
modelBuilder.Entity<SyncState>().HasKey((x => x.Entity));
|
||||
}
|
||||
}
|
||||
65
Bimix.Infrastructure/Migrations/20250623184943_AddSyncState.Designer.cs
generated
Normal file
65
Bimix.Infrastructure/Migrations/20250623184943_AddSyncState.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
65
Bimix.Infrastructure/Migrations/20250623194653_ResizeProductName.Designer.cs
generated
Normal file
65
Bimix.Infrastructure/Migrations/20250623194653_ResizeProductName.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,8 @@ namespace Bimix.Infrastructure.Migrations
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
@@ -43,6 +43,19 @@ namespace Bimix.Infrastructure.Migrations
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
74
Bimix.Infrastructure/Sync/ProductSyncService.cs
Normal file
74
Bimix.Infrastructure/Sync/ProductSyncService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Windows Machine": {
|
||||
"commandName": "MsixPackage",
|
||||
"nativeDebugging": false
|
||||
"dev": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:9600",
|
||||
"sslPort": 44314
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"dev": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5250",
|
||||
"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,
|
||||
"applicationUrl": "https://localhost:7246;http://localhost:5246",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user