diff --git a/src/food-market.domain/Organizations/Organization.cs b/src/food-market.domain/Organizations/Organization.cs index d8353b7..e44689b 100644 --- a/src/food-market.domain/Organizations/Organization.cs +++ b/src/food-market.domain/Organizations/Organization.cs @@ -13,6 +13,17 @@ public class Organization : Entity public string? Email { get; set; } public bool IsActive { get; set; } = true; + /// Архивирована ли организация. Архивные не видны пользователям + /// но данные сохраняются. Из архива можно восстановить или (после 30 дней) + /// удалить навсегда — этим управляет SuperAdmin. + public bool IsArchived { get; set; } + public DateTime? ArchivedAt { get; set; } + + /// Account owner (главный владелец, не путать с админами роли). + /// Это конкретный AppUser, который считается «хозяином» организации; + /// SuperAdmin может сменить через отдельный action c reason в audit-log. + public Guid? AccountOwnerUserId { get; set; } + /// Персональный API-токен MoySklad. Храним per-organization чтобы /// пользователю не нужно было вводить его каждый раз при импорте. public string? MoySkladToken { get; set; } diff --git a/src/food-market.domain/Organizations/SuperAdminAuditLog.cs b/src/food-market.domain/Organizations/SuperAdminAuditLog.cs new file mode 100644 index 0000000..636e598 --- /dev/null +++ b/src/food-market.domain/Organizations/SuperAdminAuditLog.cs @@ -0,0 +1,20 @@ +using foodmarket.Domain.Common; + +namespace foodmarket.Domain.Organizations; + +/// Журнал действий SuperAdmin'а: создание/правка/архивирование +/// организаций, смена аккаунт-владельца, правки в режиме «войти как». +/// Не tenant-scoped — лог общий для всей системы. +public class SuperAdminAuditLog : Entity +{ + public Guid SuperAdminUserId { get; set; } + public string ActionType { get; set; } = ""; + public Guid? OrganizationId { get; set; } + public string? EntityType { get; set; } + public Guid? EntityId { get; set; } + public string? Description { get; set; } + public string? Reason { get; set; } + /// JSON с diff'ом before/after или другим payload'ом действия. + public string ChangesJson { get; set; } = "{}"; + public string IpAddress { get; set; } = ""; +} diff --git a/src/food-market.infrastructure/Persistence/AppDbContext.cs b/src/food-market.infrastructure/Persistence/AppDbContext.cs index 0c531e2..a62633b 100644 --- a/src/food-market.infrastructure/Persistence/AppDbContext.cs +++ b/src/food-market.infrastructure/Persistence/AppDbContext.cs @@ -49,6 +49,7 @@ public AppDbContext(DbContextOptions options, ITenantContext tenan public DbSet EmployeeRoles => Set(); public DbSet Employees => Set(); public DbSet EmployeeRetailPointAssignments => Set(); + public DbSet SuperAdminAuditLogs => Set(); protected override void OnModelCreating(ModelBuilder builder) { @@ -76,6 +77,21 @@ protected override void OnModelCreating(ModelBuilder builder) b.Property(o => o.MoySkladToken).HasMaxLength(200); b.HasOne(o => o.DefaultCurrency).WithMany().HasForeignKey(o => o.DefaultCurrencyId).OnDelete(DeleteBehavior.Restrict); b.HasIndex(o => o.Name); + b.HasIndex(o => o.IsArchived); + }); + + builder.Entity(b => + { + b.ToTable("super_admin_audit_log"); + b.Property(x => x.ActionType).HasMaxLength(50).IsRequired(); + b.Property(x => x.EntityType).HasMaxLength(100); + b.Property(x => x.Description).HasMaxLength(500); + b.Property(x => x.Reason).HasMaxLength(1000); + b.Property(x => x.ChangesJson).HasColumnType("jsonb"); + b.Property(x => x.IpAddress).HasMaxLength(45); + b.HasIndex(x => x.CreatedAt); + b.HasIndex(x => new { x.SuperAdminUserId, x.CreatedAt }); + b.HasIndex(x => new { x.OrganizationId, x.CreatedAt }); }); builder.ConfigureCatalog(); diff --git a/src/food-market.infrastructure/Persistence/Migrations/20260427040000_Phase4_SuperAdminConsole.Designer.cs b/src/food-market.infrastructure/Persistence/Migrations/20260427040000_Phase4_SuperAdminConsole.Designer.cs new file mode 100644 index 0000000..de3b1b8 --- /dev/null +++ b/src/food-market.infrastructure/Persistence/Migrations/20260427040000_Phase4_SuperAdminConsole.Designer.cs @@ -0,0 +1,2048 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using foodmarket.Infrastructure.Persistence; + +#nullable disable + +namespace foodmarket.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260427040000_Phase4_SuperAdminConsole")] + partial class Phase4_SuperAdminConsole + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", "public"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", "public"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "public"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "public"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.Counterparty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BankName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Bik") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Bin") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ContactPerson") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CountryId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Iin") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("LegalName") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Phone") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TaxNumber") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CountryId"); + + b.HasIndex("OrganizationId", "Bin"); + + b.HasIndex("OrganizationId", "Name"); + + b.ToTable("counterparties", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultCurrencyId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VatRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DefaultCurrencyId"); + + b.ToTable("countries", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.Currency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MinorUnit") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("currencies", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.PriceType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRequired") + .HasColumnType("boolean"); + + b.Property("IsRetail") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("price_types", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Article") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CountryOfOriginId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultSupplierId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsMarked") + .HasColumnType("boolean"); + + b.Property("IsService") + .HasColumnType("boolean"); + + b.Property("Packaging") + .HasColumnType("boolean"); + + b.Property("MaxStock") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("MinStock") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProductGroupId") + .HasColumnType("uuid"); + + b.Property("Cost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("LastSupplyAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PurchaseCurrencyId") + .HasColumnType("uuid"); + + b.Property("ReferencePrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReferencePriceUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UnitOfMeasureId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Vat") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("VatEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.HasKey("Id"); + + b.HasIndex("CountryOfOriginId"); + + b.HasIndex("DefaultSupplierId"); + + b.HasIndex("ProductGroupId"); + + b.HasIndex("PurchaseCurrencyId"); + + b.HasIndex("UnitOfMeasureId"); + + b.HasIndex("OrganizationId", "Article"); + + b.HasIndex("OrganizationId", "Name"); + + b.HasIndex("OrganizationId", "ProductGroupId"); + + b.ToTable("products", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.ProductBarcode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("OrganizationId", "Code") + .IsUnique(); + + b.ToTable("product_barcodes", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.ProductGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MarkupPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("OrganizationId", "ParentId"); + + b.HasIndex("OrganizationId", "Path"); + + b.ToTable("product_groups", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.ProductImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsMain") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("product_images", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.ProductPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PriceTypeId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.HasIndex("PriceTypeId"); + + b.HasIndex("ProductId", "PriceTypeId") + .IsUnique(); + + b.ToTable("product_prices", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.RetailPoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FiscalRegNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("FiscalSerial") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Phone") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("StoreId"); + + b.HasIndex("OrganizationId", "Name"); + + b.ToTable("retail_points", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Code") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsMain") + .HasColumnType("boolean"); + + b.Property("ManagerName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Phone") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name"); + + b.ToTable("stores", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.UnitOfMeasure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Code") + .IsUnique(); + + b.ToTable("units_of_measure", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Inventory.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("ReservedQuantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("StoreId"); + + b.HasIndex("OrganizationId", "StoreId"); + + b.HasIndex("OrganizationId", "ProductId", "StoreId") + .IsUnique(); + + b.ToTable("stocks", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Inventory.StockMovement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DocumentNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("StoreId"); + + b.HasIndex("DocumentType", "DocumentId"); + + b.HasIndex("OrganizationId", "ProductId", "OccurredAt"); + + b.HasIndex("OrganizationId", "StoreId", "OccurredAt"); + + b.ToTable("stock_movements", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Organizations.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("AllowFractionalPrices") + .HasColumnType("boolean"); + + b.Property("Bin") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("CountryCode") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("DefaultCurrencyId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("ArchivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AccountOwnerUserId") + .HasColumnType("uuid"); + + b.Property("MoySkladToken") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MultiCurrencyEnabled") + .HasColumnType("boolean"); + + b.Property("ShowCountryOfOriginOnProduct") + .HasColumnType("boolean"); + + b.Property("ShowDescriptionOnProduct") + .HasColumnType("boolean"); + + b.Property("ShowMarkedOnProduct") + .HasColumnType("boolean"); + + b.Property("ShowReferencePriceOnProduct") + .HasColumnType("boolean"); + + b.Property("ShowMinMaxStock") + .HasColumnType("boolean"); + + b.Property("ShowServiceOnProduct") + .HasColumnType("boolean"); + + b.Property("ShowVatEnabledOnProduct") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DefaultCurrencyId"); + + b.HasIndex("Name"); + + b.HasIndex("IsArchived"); + + b.ToTable("organizations", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Purchases.Supply", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PostedByUserId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.HasIndex("StoreId"); + + b.HasIndex("SupplierId"); + + b.HasIndex("OrganizationId", "Date"); + + b.HasIndex("OrganizationId", "Number") + .IsUnique(); + + b.HasIndex("OrganizationId", "Status"); + + b.HasIndex("OrganizationId", "SupplierId"); + + b.ToTable("supplies", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Purchases.SupplyLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RetailPriceManuallyOverridden") + .HasColumnType("boolean"); + + b.Property("RetailPriceOverride") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("SupplyId") + .HasColumnType("uuid"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("SupplyId"); + + b.HasIndex("OrganizationId", "ProductId"); + + b.ToTable("supply_lines", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Sales.RetailSale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CashierUserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrencyId") + .HasColumnType("uuid"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaidCard") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("PaidCash") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Payment") + .HasColumnType("integer"); + + b.Property("PostedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PostedByUserId") + .HasColumnType("uuid"); + + b.Property("RetailPointId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("uuid"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("Total") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CurrencyId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("RetailPointId"); + + b.HasIndex("StoreId"); + + b.HasIndex("OrganizationId", "CashierUserId"); + + b.HasIndex("OrganizationId", "Date"); + + b.HasIndex("OrganizationId", "Number") + .IsUnique(); + + b.HasIndex("OrganizationId", "Status"); + + b.ToTable("retail_sales", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Sales.RetailSaleLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Discount") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("RetailSaleId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VatPercent") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.HasIndex("RetailSaleId"); + + b.HasIndex("OrganizationId", "ProductId"); + + b.ToTable("retail_sale_lines", "public"); + }); + + modelBuilder.Entity("foodmarket.Infrastructure.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", "public"); + }); + + modelBuilder.Entity("foodmarket.Infrastructure.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("foodmarket.Infrastructure.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("foodmarket.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("foodmarket.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("foodmarket.Infrastructure.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("foodmarket.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("foodmarket.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.Counterparty", b => + { + b.HasOne("foodmarket.Domain.Catalog.Country", "Country") + .WithMany() + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.Product", b => + { + b.HasOne("foodmarket.Domain.Catalog.Country", "CountryOfOrigin") + .WithMany() + .HasForeignKey("CountryOfOriginId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("foodmarket.Domain.Catalog.Counterparty", "DefaultSupplier") + .WithMany() + .HasForeignKey("DefaultSupplierId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("foodmarket.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany() + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("foodmarket.Domain.Catalog.Currency", "PurchaseCurrency") + .WithMany() + .HasForeignKey("PurchaseCurrencyId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("foodmarket.Domain.Catalog.UnitOfMeasure", "UnitOfMeasure") + .WithMany() + .HasForeignKey("UnitOfMeasureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CountryOfOrigin"); + + b.Navigation("DefaultSupplier"); + + b.Navigation("ProductGroup"); + + b.Navigation("PurchaseCurrency"); + + b.Navigation("UnitOfMeasure"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.ProductBarcode", b => + { + b.HasOne("foodmarket.Domain.Catalog.Product", "Product") + .WithMany("Barcodes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.ProductGroup", b => + { + b.HasOne("foodmarket.Domain.Catalog.ProductGroup", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.ProductImage", b => + { + b.HasOne("foodmarket.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.ProductPrice", b => + { + b.HasOne("foodmarket.Domain.Catalog.Currency", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("foodmarket.Domain.Catalog.PriceType", "PriceType") + .WithMany() + .HasForeignKey("PriceTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("foodmarket.Domain.Catalog.Product", "Product") + .WithMany("Prices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Currency"); + + b.Navigation("PriceType"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.RetailPoint", b => + { + b.HasOne("foodmarket.Domain.Catalog.Store", "Store") + .WithMany() + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("foodmarket.Domain.Inventory.Stock", b => + { + b.HasOne("foodmarket.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("foodmarket.Domain.Catalog.Store", "Store") + .WithMany() + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("foodmarket.Domain.Inventory.StockMovement", b => + { + b.HasOne("foodmarket.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("foodmarket.Domain.Catalog.Store", "Store") + .WithMany() + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("foodmarket.Domain.Purchases.Supply", b => + { + b.HasOne("foodmarket.Domain.Catalog.Currency", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("foodmarket.Domain.Catalog.Store", "Store") + .WithMany() + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("foodmarket.Domain.Catalog.Counterparty", "Supplier") + .WithMany() + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Currency"); + + b.Navigation("Store"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("foodmarket.Domain.Purchases.SupplyLine", b => + { + b.HasOne("foodmarket.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("foodmarket.Domain.Purchases.Supply", "Supply") + .WithMany("Lines") + .HasForeignKey("SupplyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Supply"); + }); + + modelBuilder.Entity("foodmarket.Domain.Sales.RetailSale", b => + { + b.HasOne("foodmarket.Domain.Catalog.Currency", "Currency") + .WithMany() + .HasForeignKey("CurrencyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("foodmarket.Domain.Catalog.Counterparty", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("foodmarket.Domain.Catalog.RetailPoint", "RetailPoint") + .WithMany() + .HasForeignKey("RetailPointId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("foodmarket.Domain.Catalog.Store", "Store") + .WithMany() + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Currency"); + + b.Navigation("Customer"); + + b.Navigation("RetailPoint"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("foodmarket.Domain.Sales.RetailSaleLine", b => + { + b.HasOne("foodmarket.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("foodmarket.Domain.Sales.RetailSale", "RetailSale") + .WithMany("Lines") + .HasForeignKey("RetailSaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("RetailSale"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.Product", b => + { + b.Navigation("Barcodes"); + + b.Navigation("Images"); + + b.Navigation("Prices"); + }); + + modelBuilder.Entity("foodmarket.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("foodmarket.Domain.Purchases.Supply", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("foodmarket.Domain.Sales.RetailSale", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("foodmarket.Domain.Organizations.SuperAdminAuditLog", b => + { + b.Property("Id").ValueGeneratedOnAdd().HasColumnType("uuid"); + b.Property("SuperAdminUserId").HasColumnType("uuid"); + b.Property("ActionType").IsRequired().HasMaxLength(50).HasColumnType("character varying(50)"); + b.Property("OrganizationId").HasColumnType("uuid"); + b.Property("EntityType").HasMaxLength(100).HasColumnType("character varying(100)"); + b.Property("EntityId").HasColumnType("uuid"); + b.Property("Description").HasMaxLength(500).HasColumnType("character varying(500)"); + b.Property("Reason").HasMaxLength(1000).HasColumnType("character varying(1000)"); + b.Property("ChangesJson").HasColumnType("jsonb"); + b.Property("IpAddress").HasMaxLength(45).HasColumnType("character varying(45)"); + b.Property("CreatedAt").HasColumnType("timestamp with time zone"); + b.Property("UpdatedAt").HasColumnType("timestamp with time zone"); + b.HasKey("Id"); + b.HasIndex("CreatedAt"); + b.HasIndex("SuperAdminUserId", "CreatedAt"); + b.HasIndex("OrganizationId", "CreatedAt"); + b.ToTable("super_admin_audit_log", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Organizations.EmployeeRole", b => + { + b.Property("Id").ValueGeneratedOnAdd().HasColumnType("uuid"); + b.Property("OrganizationId").HasColumnType("uuid"); + b.Property("Name").IsRequired().HasMaxLength(100).HasColumnType("character varying(100)"); + b.Property("Description").HasMaxLength(500).HasColumnType("character varying(500)"); + b.Property("IsSystem").HasColumnType("boolean"); + b.Property("SortOrder").HasColumnType("integer"); + b.Property("CreatedAt").HasColumnType("timestamp with time zone"); + b.Property("UpdatedAt").HasColumnType("timestamp with time zone"); + b.HasKey("Id"); + b.HasIndex("OrganizationId", "Name").IsUnique(); + b.ToTable("employee_roles", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Organizations.EmployeeRole", b => + { + b.OwnsOne("foodmarket.Domain.Organizations.RolePermissions", "Permissions", o => + { + o.Property("EmployeeRoleId").HasColumnType("uuid"); + o.Property("ProductsView").HasColumnType("boolean"); + o.Property("ProductsEdit").HasColumnType("boolean"); + o.Property("ProductsDelete").HasColumnType("boolean"); + o.Property("ProductGroupsManage").HasColumnType("boolean"); + o.Property("PriceTypesManage").HasColumnType("boolean"); + o.Property("UnitsManage").HasColumnType("boolean"); + o.Property("SuppliesView").HasColumnType("boolean"); + o.Property("SuppliesEdit").HasColumnType("boolean"); + o.Property("SuppliesPost").HasColumnType("boolean"); + o.Property("SuppliesDelete").HasColumnType("boolean"); + o.Property("DemandsView").HasColumnType("boolean"); + o.Property("DemandsEdit").HasColumnType("boolean"); + o.Property("DemandsPost").HasColumnType("boolean"); + o.Property("RetailSalesOperate").HasColumnType("boolean"); + o.Property("RetailSalesRefund").HasColumnType("boolean"); + o.Property("CounterpartiesView").HasColumnType("boolean"); + o.Property("CounterpartiesEdit").HasColumnType("boolean"); + o.Property("CounterpartiesDelete").HasColumnType("boolean"); + o.Property("StocksView").HasColumnType("boolean"); + o.Property("InventoryEdit").HasColumnType("boolean"); + o.Property("LossEdit").HasColumnType("boolean"); + o.Property("EnterEdit").HasColumnType("boolean"); + o.Property("ReportsView").HasColumnType("boolean"); + o.Property("ReportsFinanceView").HasColumnType("boolean"); + o.Property("ReportsStockView").HasColumnType("boolean"); + o.Property("OrgSettingsManage").HasColumnType("boolean"); + o.Property("EmployeesManage").HasColumnType("boolean"); + o.Property("RolesManage").HasColumnType("boolean"); + o.Property("StoresManage").HasColumnType("boolean"); + o.Property("RetailPointsManage").HasColumnType("boolean"); + o.Property("CashRegistersManage").HasColumnType("boolean"); + o.Property("IntegrationsManage").HasColumnType("boolean"); + o.HasKey("EmployeeRoleId"); + o.ToJson("permissions"); + o.WithOwner().HasForeignKey("EmployeeRoleId"); + }); + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("foodmarket.Domain.Organizations.Employee", b => + { + b.Property("Id").ValueGeneratedOnAdd().HasColumnType("uuid"); + b.Property("OrganizationId").HasColumnType("uuid"); + b.Property("UserId").HasColumnType("uuid"); + b.Property("LastName").IsRequired().HasMaxLength(100).HasColumnType("character varying(100)"); + b.Property("FirstName").IsRequired().HasMaxLength(100).HasColumnType("character varying(100)"); + b.Property("MiddleName").HasMaxLength(100).HasColumnType("character varying(100)"); + b.Property("Position").HasMaxLength(150).HasColumnType("character varying(150)"); + b.Property("Email").HasMaxLength(200).HasColumnType("character varying(200)"); + b.Property("Phone").HasMaxLength(50).HasColumnType("character varying(50)"); + b.Property("Salary").HasPrecision(18, 2).HasColumnType("numeric(18,2)"); + b.Property("TaxNumber").HasMaxLength(20).HasColumnType("character varying(20)"); + b.Property("Description").HasMaxLength(2000).HasColumnType("character varying(2000)"); + b.Property("ImageUrl").HasMaxLength(500).HasColumnType("character varying(500)"); + b.Property("RoleId").HasColumnType("uuid"); + b.Property("IsActive").HasColumnType("boolean"); + b.Property("FiredAt").HasColumnType("timestamp with time zone"); + b.Property("CreatedAt").HasColumnType("timestamp with time zone"); + b.Property("UpdatedAt").HasColumnType("timestamp with time zone"); + b.HasKey("Id"); + b.HasIndex("RoleId"); + b.HasIndex("OrganizationId", "LastName"); + b.HasIndex("OrganizationId", "UserId").IsUnique().HasFilter("\"UserId\" IS NOT NULL"); + b.ToTable("employees", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Organizations.Employee", b => + { + b.HasOne("foodmarket.Domain.Organizations.EmployeeRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + b.Navigation("Role"); + }); + + modelBuilder.Entity("foodmarket.Domain.Organizations.EmployeeRetailPointAssignment", b => + { + b.Property("Id").ValueGeneratedOnAdd().HasColumnType("uuid"); + b.Property("OrganizationId").HasColumnType("uuid"); + b.Property("EmployeeId").HasColumnType("uuid"); + b.Property("RetailPointId").HasColumnType("uuid"); + b.Property("CreatedAt").HasColumnType("timestamp with time zone"); + b.Property("UpdatedAt").HasColumnType("timestamp with time zone"); + b.HasKey("Id"); + b.HasIndex("EmployeeId", "RetailPointId").IsUnique(); + b.ToTable("employee_retail_point_assignments", "public"); + }); + + modelBuilder.Entity("foodmarket.Domain.Organizations.EmployeeRetailPointAssignment", b => + { + b.HasOne("foodmarket.Domain.Organizations.Employee", "Employee") + .WithMany("RetailPointAssignments") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("Employee"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/food-market.infrastructure/Persistence/Migrations/20260427040000_Phase4_SuperAdminConsole.cs b/src/food-market.infrastructure/Persistence/Migrations/20260427040000_Phase4_SuperAdminConsole.cs new file mode 100644 index 0000000..a5e64ea --- /dev/null +++ b/src/food-market.infrastructure/Persistence/Migrations/20260427040000_Phase4_SuperAdminConsole.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace foodmarket.Infrastructure.Persistence.Migrations +{ + /// SuperAdmin console: organizations.IsArchived/ArchivedAt/ + /// AccountOwnerUserId + новая таблица super_admin_audit_log для журнала + /// действий супер-админа (создание/правка/архивирование орг, + /// смена аккаунт-владельца, правки в режиме «войти как»). + public partial class Phase4_SuperAdminConsole : Migration + { + protected override void Up(MigrationBuilder b) + { + b.AddColumn( + name: "IsArchived", schema: "public", table: "organizations", + type: "boolean", nullable: false, defaultValue: false); + b.AddColumn( + name: "ArchivedAt", schema: "public", table: "organizations", + type: "timestamp with time zone", nullable: true); + b.AddColumn( + name: "AccountOwnerUserId", schema: "public", table: "organizations", + type: "uuid", nullable: true); + b.CreateIndex( + name: "IX_organizations_IsArchived", + schema: "public", + table: "organizations", + column: "IsArchived"); + + b.CreateTable( + name: "super_admin_audit_log", + schema: "public", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + SuperAdminUserId = table.Column(type: "uuid", nullable: false), + ActionType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + OrganizationId = table.Column(type: "uuid", nullable: true), + EntityType = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + EntityId = table.Column(type: "uuid", nullable: true), + Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + Reason = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + ChangesJson = table.Column(type: "jsonb", nullable: false), + IpAddress = table.Column(type: "character varying(45)", maxLength: 45, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + }, + constraints: table => table.PrimaryKey("PK_super_admin_audit_log", x => x.Id)); + b.CreateIndex( + name: "IX_super_admin_audit_log_CreatedAt", + schema: "public", table: "super_admin_audit_log", column: "CreatedAt"); + b.CreateIndex( + name: "IX_super_admin_audit_log_SuperAdminUserId_CreatedAt", + schema: "public", table: "super_admin_audit_log", + columns: new[] { "SuperAdminUserId", "CreatedAt" }); + b.CreateIndex( + name: "IX_super_admin_audit_log_OrganizationId_CreatedAt", + schema: "public", table: "super_admin_audit_log", + columns: new[] { "OrganizationId", "CreatedAt" }); + } + + protected override void Down(MigrationBuilder b) + { + b.DropTable(name: "super_admin_audit_log", schema: "public"); + b.DropIndex(name: "IX_organizations_IsArchived", schema: "public", table: "organizations"); + b.DropColumn(name: "AccountOwnerUserId", schema: "public", table: "organizations"); + b.DropColumn(name: "ArchivedAt", schema: "public", table: "organizations"); + b.DropColumn(name: "IsArchived", schema: "public", table: "organizations"); + } + } +} diff --git a/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs b/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index dd1d512..a2c0b75 100644 --- a/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/src/food-market.infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -1086,6 +1086,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsActive") .HasColumnType("boolean"); + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("ArchivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AccountOwnerUserId") + .HasColumnType("uuid"); + b.Property("MoySkladToken") .HasMaxLength(200) .HasColumnType("character varying(200)"); @@ -1131,6 +1140,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Name"); + b.HasIndex("IsArchived"); + b.ToTable("organizations", "public"); }); @@ -1889,6 +1900,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Lines"); }); + modelBuilder.Entity("foodmarket.Domain.Organizations.SuperAdminAuditLog", b => + { + b.Property("Id").ValueGeneratedOnAdd().HasColumnType("uuid"); + b.Property("SuperAdminUserId").HasColumnType("uuid"); + b.Property("ActionType").IsRequired().HasMaxLength(50).HasColumnType("character varying(50)"); + b.Property("OrganizationId").HasColumnType("uuid"); + b.Property("EntityType").HasMaxLength(100).HasColumnType("character varying(100)"); + b.Property("EntityId").HasColumnType("uuid"); + b.Property("Description").HasMaxLength(500).HasColumnType("character varying(500)"); + b.Property("Reason").HasMaxLength(1000).HasColumnType("character varying(1000)"); + b.Property("ChangesJson").HasColumnType("jsonb"); + b.Property("IpAddress").HasMaxLength(45).HasColumnType("character varying(45)"); + b.Property("CreatedAt").HasColumnType("timestamp with time zone"); + b.Property("UpdatedAt").HasColumnType("timestamp with time zone"); + b.HasKey("Id"); + b.HasIndex("CreatedAt"); + b.HasIndex("SuperAdminUserId", "CreatedAt"); + b.HasIndex("OrganizationId", "CreatedAt"); + b.ToTable("super_admin_audit_log", "public"); + }); + modelBuilder.Entity("foodmarket.Domain.Organizations.EmployeeRole", b => { b.Property("Id").ValueGeneratedOnAdd().HasColumnType("uuid");