diff --git a/src/food-market.domain/Organizations/Employee.cs b/src/food-market.domain/Organizations/Employee.cs
new file mode 100644
index 0000000..12725ac
--- /dev/null
+++ b/src/food-market.domain/Organizations/Employee.cs
@@ -0,0 +1,41 @@
+using foodmarket.Domain.Common;
+
+namespace foodmarket.Domain.Organizations;
+
+/// Сотрудник организации. Может быть привязан к учётной записи
+/// (UserId), а может существовать без логина (например, кассир, которого
+/// добавили в HR, но логин ещё не выдали).
+public class Employee : TenantEntity
+{
+ /// FK на Identity-юзера. Заполняется когда сотруднику выдан логин.
+ /// Null — запись без учётки.
+ public Guid? UserId { get; set; }
+
+ public string LastName { get; set; } = "";
+ public string FirstName { get; set; } = "";
+ public string? MiddleName { get; set; }
+ public string? Position { get; set; }
+
+ public string? Email { get; set; }
+ public string? Phone { get; set; }
+
+ public Guid RoleId { get; set; }
+ public EmployeeRole Role { get; set; } = null!;
+
+ /// Активен ли сотрудник. False — заблокирован, не может логиниться.
+ /// Удаление физически не делаем (FK из документов), просто IsActive=false.
+ public bool IsActive { get; set; } = true;
+ public DateTime? FiredAt { get; set; }
+
+ public ICollection RetailPointAssignments { get; set; }
+ = new List();
+}
+
+/// Привязка сотрудника к кассе (для роли Кассир): к каким RetailPoint'ам
+/// он может вставать. Если назначений нет — может ко всем (поведение по умолчанию).
+public class EmployeeRetailPointAssignment : TenantEntity
+{
+ public Guid EmployeeId { get; set; }
+ public Employee Employee { get; set; } = null!;
+ public Guid RetailPointId { get; set; }
+}
diff --git a/src/food-market.domain/Organizations/EmployeeRole.cs b/src/food-market.domain/Organizations/EmployeeRole.cs
new file mode 100644
index 0000000..5a1ead7
--- /dev/null
+++ b/src/food-market.domain/Organizations/EmployeeRole.cs
@@ -0,0 +1,17 @@
+using foodmarket.Domain.Common;
+
+namespace foodmarket.Domain.Organizations;
+
+/// Роль сотрудника в организации. Системные (IsSystem=true) сидируются
+/// при создании Organization (Администратор/Менеджер/Кладовщик/Кассир/Закупщик/
+/// Бухгалтер) — нельзя удалить, имя менять можно. Кастомные — полный CRUD.
+public class EmployeeRole : TenantEntity
+{
+ public string Name { get; set; } = "";
+ public string? Description { get; set; }
+ public bool IsSystem { get; set; }
+ public int SortOrder { get; set; }
+
+ /// Permissions — owned JSON-колонка (см. EF config).
+ public RolePermissions Permissions { get; set; } = new();
+}
diff --git a/src/food-market.domain/Organizations/RolePermissions.cs b/src/food-market.domain/Organizations/RolePermissions.cs
new file mode 100644
index 0000000..616f48e
--- /dev/null
+++ b/src/food-market.domain/Organizations/RolePermissions.cs
@@ -0,0 +1,51 @@
+namespace foodmarket.Domain.Organizations;
+
+/// Набор флагов разрешений роли. Хранится JSON-колонкой
+/// (owned by EmployeeRole). Семантика: false = доступ запрещён.
+public class RolePermissions
+{
+ // Каталог
+ public bool ProductsView { get; set; }
+ public bool ProductsEdit { get; set; }
+ public bool ProductsDelete { get; set; }
+ public bool ProductGroupsManage { get; set; }
+ public bool PriceTypesManage { get; set; }
+
+ // Закупки
+ public bool SuppliesView { get; set; }
+ public bool SuppliesEdit { get; set; }
+ public bool SuppliesPost { get; set; }
+ public bool SuppliesDelete { get; set; }
+
+ // Продажи (POS)
+ public bool RetailSalesOperate { get; set; }
+ public bool RetailSalesRefund { get; set; }
+
+ // Контрагенты
+ public bool CounterpartiesView { get; set; }
+ public bool CounterpartiesEdit { get; set; }
+
+ // Отчёты и остатки
+ public bool ReportsView { get; set; }
+ public bool StocksView { get; set; }
+
+ // Настройки организации
+ public bool OrgSettingsManage { get; set; }
+ public bool EmployeesManage { get; set; }
+ public bool RolesManage { get; set; }
+ public bool StoresManage { get; set; }
+ public bool RetailPointsManage { get; set; }
+
+ /// Полный набор всех true — для системной роли «Администратор».
+ public static RolePermissions All() => new()
+ {
+ ProductsView = true, ProductsEdit = true, ProductsDelete = true,
+ ProductGroupsManage = true, PriceTypesManage = true,
+ SuppliesView = true, SuppliesEdit = true, SuppliesPost = true, SuppliesDelete = true,
+ RetailSalesOperate = true, RetailSalesRefund = true,
+ CounterpartiesView = true, CounterpartiesEdit = true,
+ ReportsView = true, StocksView = true,
+ OrgSettingsManage = true, EmployeesManage = true, RolesManage = true,
+ StoresManage = true, RetailPointsManage = true,
+ };
+}
diff --git a/src/food-market.infrastructure/Persistence/AppDbContext.cs b/src/food-market.infrastructure/Persistence/AppDbContext.cs
index e55f69f..0c531e2 100644
--- a/src/food-market.infrastructure/Persistence/AppDbContext.cs
+++ b/src/food-market.infrastructure/Persistence/AppDbContext.cs
@@ -46,6 +46,10 @@ public AppDbContext(DbContextOptions options, ITenantContext tenan
public DbSet RetailSales => Set();
public DbSet RetailSaleLines => Set();
+ public DbSet EmployeeRoles => Set();
+ public DbSet Employees => Set();
+ public DbSet EmployeeRetailPointAssignments => Set();
+
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
@@ -78,6 +82,7 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.ConfigureInventory();
builder.ConfigurePurchases();
builder.ConfigureSales();
+ builder.ConfigureOrganizationsHr();
// Apply multi-tenant query filter to every entity that implements ITenantEntity
foreach (var entityType in builder.Model.GetEntityTypes())
diff --git a/src/food-market.infrastructure/Persistence/Configurations/OrganizationsHrConfigurations.cs b/src/food-market.infrastructure/Persistence/Configurations/OrganizationsHrConfigurations.cs
new file mode 100644
index 0000000..cf23362
--- /dev/null
+++ b/src/food-market.infrastructure/Persistence/Configurations/OrganizationsHrConfigurations.cs
@@ -0,0 +1,44 @@
+using foodmarket.Domain.Organizations;
+using Microsoft.EntityFrameworkCore;
+
+namespace foodmarket.Infrastructure.Persistence.Configurations;
+
+public static class OrganizationsHrConfigurations
+{
+ public static void ConfigureOrganizationsHr(this ModelBuilder b)
+ {
+ b.Entity(e =>
+ {
+ e.ToTable("employee_roles");
+ e.Property(x => x.Name).HasMaxLength(100).IsRequired();
+ e.Property(x => x.Description).HasMaxLength(500);
+ // Permissions — owned JSON-колонка. Один объект = один сериализованный
+ // блоб на роль. Все поля булевы, по умолчанию false.
+ e.OwnsOne(x => x.Permissions, p => p.ToJson("permissions"));
+ e.HasIndex(x => new { x.OrganizationId, x.Name }).IsUnique();
+ });
+
+ b.Entity(e =>
+ {
+ e.ToTable("employees");
+ e.Property(x => x.LastName).HasMaxLength(100).IsRequired();
+ e.Property(x => x.FirstName).HasMaxLength(100).IsRequired();
+ e.Property(x => x.MiddleName).HasMaxLength(100);
+ e.Property(x => x.Position).HasMaxLength(150);
+ e.Property(x => x.Email).HasMaxLength(200);
+ e.Property(x => x.Phone).HasMaxLength(50);
+ e.HasOne(x => x.Role).WithMany().HasForeignKey(x => x.RoleId).OnDelete(DeleteBehavior.Restrict);
+ e.HasMany(x => x.RetailPointAssignments).WithOne(a => a.Employee)
+ .HasForeignKey(a => a.EmployeeId).OnDelete(DeleteBehavior.Cascade);
+ e.HasIndex(x => new { x.OrganizationId, x.UserId }).IsUnique()
+ .HasFilter("\"UserId\" IS NOT NULL");
+ e.HasIndex(x => new { x.OrganizationId, x.LastName });
+ });
+
+ b.Entity(e =>
+ {
+ e.ToTable("employee_retail_point_assignments");
+ e.HasIndex(x => new { x.EmployeeId, x.RetailPointId }).IsUnique();
+ });
+ }
+}
diff --git a/src/food-market.infrastructure/Persistence/Migrations/20260427010000_Phase4_EmployeesAndRoles.Designer.cs b/src/food-market.infrastructure/Persistence/Migrations/20260427010000_Phase4_EmployeesAndRoles.Designer.cs
new file mode 100644
index 0000000..6800585
--- /dev/null
+++ b/src/food-market.infrastructure/Persistence/Migrations/20260427010000_Phase4_EmployeesAndRoles.Designer.cs
@@ -0,0 +1,2001 @@
+//
+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("20260427010000_Phase4_EmployeesAndRoles")]
+ partial class Phase4_EmployeesAndRoles
+ {
+ ///
+ 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("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