diff --git a/Bimix.API/Controllers/ProductsController.cs b/Bimix.API/Controllers/ProductsController.cs index 11397b0..573f12a 100644 --- a/Bimix.API/Controllers/ProductsController.cs +++ b/Bimix.API/Controllers/ProductsController.cs @@ -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; diff --git a/Bimix.API/Controllers/SyncController.cs b/Bimix.API/Controllers/SyncController.cs new file mode 100644 index 0000000..af13761 --- /dev/null +++ b/Bimix.API/Controllers/SyncController.cs @@ -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 RunProductSync() + { + await productSyncService.RunAsync(); + return Ok(); + } +} \ No newline at end of file diff --git a/Bimix.API/Program.cs b/Bimix.API/Program.cs index e9e885d..72aa51f 100644 --- a/Bimix.API/Program.cs +++ b/Bimix.API/Program.cs @@ -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(options => options.UseSqlServer(connectionString)); +builder.Services.AddScoped(); +builder.Services.AddHttpClient(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/Bimix.API/Properties/launchSettings.json b/Bimix.API/Properties/launchSettings.json index d92ae0d..8e97836 100644 --- a/Bimix.API/Properties/launchSettings.json +++ b/Bimix.API/Properties/launchSettings.json @@ -1,41 +1,14 @@ { - "$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" } } } -} +} \ No newline at end of file diff --git a/Bimix.API/appsettings.Development.json b/Bimix.API/appsettings.Development.json index 3c9a030..5d472c1 100644 Binary files a/Bimix.API/appsettings.Development.json and b/Bimix.API/appsettings.Development.json differ diff --git a/Bimix.Application/Bimix.Application.csproj b/Bimix.Application/Bimix.Application.csproj index f97e5bb..e6b0ca9 100644 --- a/Bimix.Application/Bimix.Application.csproj +++ b/Bimix.Application/Bimix.Application.csproj @@ -1,7 +1,7 @@  - - + + diff --git a/Bimix.Application/Class1.cs b/Bimix.Application/Class1.cs deleted file mode 100644 index 8b43d7c..0000000 --- a/Bimix.Application/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bimix.Application; - -public class Class1 -{ - -} diff --git a/Bimix.Domain/Entities/SyncState.cs b/Bimix.Domain/Entities/SyncState.cs new file mode 100644 index 0000000..0149952 --- /dev/null +++ b/Bimix.Domain/Entities/SyncState.cs @@ -0,0 +1,7 @@ +namespace Bimix.Domain.Entities; + +public class SyncState +{ + public required string Entity { get; set; } + public required long LastSynced { get; set; } // UnixTimestamp +} \ No newline at end of file diff --git a/Bimix.Infrastructure/Data/BimixDbContext.cs b/Bimix.Infrastructure/Data/BimixDbContext.cs index a794375..dd09f5f 100644 --- a/Bimix.Infrastructure/Data/BimixDbContext.cs +++ b/Bimix.Infrastructure/Data/BimixDbContext.cs @@ -6,12 +6,15 @@ namespace Bimix.Infrastructure.Data; public class BimixDbContext(DbContextOptions options) : DbContext(options) { public DbSet Products { get; set; } + public DbSet SyncStates { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.Entity().HasKey(p => p.Id); - modelBuilder.Entity().Property(p => p.Name).IsRequired().HasMaxLength(200); + modelBuilder.Entity().HasKey(x => x.Id); + modelBuilder.Entity().Property(x => x.Name).IsRequired().HasMaxLength(512); + + modelBuilder.Entity().HasKey((x => x.Entity)); } } \ No newline at end of file diff --git a/Bimix.Infrastructure/Migrations/20250623184943_AddSyncState.Designer.cs b/Bimix.Infrastructure/Migrations/20250623184943_AddSyncState.Designer.cs new file mode 100644 index 0000000..2b3e72a --- /dev/null +++ b/Bimix.Infrastructure/Migrations/20250623184943_AddSyncState.Designer.cs @@ -0,0 +1,65 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Bimix.Domain.Entities.SyncState", b => + { + b.Property("Entity") + .HasColumnType("nvarchar(450)"); + + b.Property("LastSynced") + .HasColumnType("bigint"); + + b.HasKey("Entity"); + + b.ToTable("SyncStates"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Bimix.Infrastructure/Migrations/20250623184943_AddSyncState.cs b/Bimix.Infrastructure/Migrations/20250623184943_AddSyncState.cs new file mode 100644 index 0000000..456a24e --- /dev/null +++ b/Bimix.Infrastructure/Migrations/20250623184943_AddSyncState.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bimix.Infrastructure.Migrations +{ + /// + public partial class AddSyncState : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SyncStates", + columns: table => new + { + Entity = table.Column(type: "nvarchar(450)", nullable: false), + LastSynced = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SyncStates", x => x.Entity); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SyncStates"); + } + } +} diff --git a/Bimix.Infrastructure/Migrations/20250623194653_ResizeProductName.Designer.cs b/Bimix.Infrastructure/Migrations/20250623194653_ResizeProductName.Designer.cs new file mode 100644 index 0000000..f14b2cf --- /dev/null +++ b/Bimix.Infrastructure/Migrations/20250623194653_ResizeProductName.Designer.cs @@ -0,0 +1,65 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Bimix.Domain.Entities.SyncState", b => + { + b.Property("Entity") + .HasColumnType("nvarchar(450)"); + + b.Property("LastSynced") + .HasColumnType("bigint"); + + b.HasKey("Entity"); + + b.ToTable("SyncStates"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Bimix.Infrastructure/Migrations/20250623194653_ResizeProductName.cs b/Bimix.Infrastructure/Migrations/20250623194653_ResizeProductName.cs new file mode 100644 index 0000000..fb882ec --- /dev/null +++ b/Bimix.Infrastructure/Migrations/20250623194653_ResizeProductName.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bimix.Infrastructure.Migrations +{ + /// + public partial class ResizeProductName : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + table: "Products", + type: "nvarchar(512)", + maxLength: 512, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(200)", + oldMaxLength: 200); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + table: "Products", + type: "nvarchar(200)", + maxLength: 200, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(512)", + oldMaxLength: 512); + } + } +} diff --git a/Bimix.Infrastructure/Migrations/BimixDbContextModelSnapshot.cs b/Bimix.Infrastructure/Migrations/BimixDbContextModelSnapshot.cs index a5add14..e7b2441 100644 --- a/Bimix.Infrastructure/Migrations/BimixDbContextModelSnapshot.cs +++ b/Bimix.Infrastructure/Migrations/BimixDbContextModelSnapshot.cs @@ -33,8 +33,8 @@ namespace Bimix.Infrastructure.Migrations b.Property("Name") .IsRequired() - .HasMaxLength(200) - .HasColumnType("nvarchar(200)"); + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); b.Property("UpdatedAt") .HasColumnType("datetime2"); @@ -43,6 +43,19 @@ namespace Bimix.Infrastructure.Migrations b.ToTable("Products"); }); + + modelBuilder.Entity("Bimix.Domain.Entities.SyncState", b => + { + b.Property("Entity") + .HasColumnType("nvarchar(450)"); + + b.Property("LastSynced") + .HasColumnType("bigint"); + + b.HasKey("Entity"); + + b.ToTable("SyncStates"); + }); #pragma warning restore 612, 618 } } diff --git a/Bimix.Infrastructure/Sync/ProductSyncService.cs b/Bimix.Infrastructure/Sync/ProductSyncService.cs new file mode 100644 index 0000000..6afa9ac --- /dev/null +++ b/Bimix.Infrastructure/Sync/ProductSyncService.cs @@ -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>(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(); + } +} \ No newline at end of file diff --git a/Bimix.UI.Mobile/Properties/launchSettings.json b/Bimix.UI.Mobile/Properties/launchSettings.json index edf8aad..8d6c11a 100644 --- a/Bimix.UI.Mobile/Properties/launchSettings.json +++ b/Bimix.UI.Mobile/Properties/launchSettings.json @@ -1,8 +1,8 @@ { "profiles": { - "Windows Machine": { - "commandName": "MsixPackage", - "nativeDebugging": false + "dev": { + "commandName": "Project", + "dotnetRunMessages": true } } } \ No newline at end of file diff --git a/Bimix.UI.Web/Properties/launchSettings.json b/Bimix.UI.Web/Properties/launchSettings.json index e809b95..2e11377 100644 --- a/Bimix.UI.Web/Properties/launchSettings.json +++ b/Bimix.UI.Web/Properties/launchSettings.json @@ -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": { - "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, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } + "profiles": { + "dev": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7246;http://localhost:5246", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" } } } +} \ No newline at end of file