diff --git a/src/food-market.api/Controllers/Catalog/ProductsController.cs b/src/food-market.api/Controllers/Catalog/ProductsController.cs
index 167094a..b0fda43 100644
--- a/src/food-market.api/Controllers/Catalog/ProductsController.cs
+++ b/src/food-market.api/Controllers/Catalog/ProductsController.cs
@@ -25,6 +25,25 @@ public ProductsController(AppDbContext db, ITenantContext tenant)
// Проверка пересечения штрихкодов с другими товарами организации.
// Возвращает первый конфликт «код → товар» либо null если всё чисто.
+ /// Проверяет что у каждого PriceType с IsRequired=true есть
+ /// соответствующая запись в input.Prices с Amount > 0. Возвращает имя
+ /// первого нарушенного типа либо null если всё ок.
+ private async Task FindMissingRequiredPriceAsync(
+ IReadOnlyList? prices, CancellationToken ct)
+ {
+ var required = await _db.PriceTypes
+ .Where(pt => pt.IsRequired)
+ .Select(pt => new { pt.Id, pt.Name })
+ .ToListAsync(ct);
+ if (required.Count == 0) return null;
+ foreach (var pt in required)
+ {
+ var price = prices?.FirstOrDefault(p => p.PriceTypeId == pt.Id);
+ if (price is null || price.Amount <= 0m) return pt.Name;
+ }
+ return null;
+ }
+
private async Task<(string Code, string ProductName)?> FindBarcodeConflictAsync(
IEnumerable codes, Guid? excludeProductId, CancellationToken ct)
{
@@ -98,6 +117,8 @@ private async Task ResolveDefaultVatAsync(CancellationToken ct)
[FromQuery] decimal? referencePriceTo,
[FromQuery] int? shelfLifeDaysFrom,
[FromQuery] int? shelfLifeDaysTo,
+ [FromQuery] decimal? systemPriceFrom,
+ [FromQuery] decimal? systemPriceTo,
CancellationToken ct)
{
var q = QueryIncludes().AsNoTracking();
@@ -127,6 +148,11 @@ private async Task ResolveDefaultVatAsync(CancellationToken ct)
if (refTo is not null) q = q.Where(p => p.ReferencePrice <= refTo);
if (shelfLifeDaysFrom is not null) q = q.Where(p => p.ShelfLifeDays >= shelfLifeDaysFrom);
if (shelfLifeDaysTo is not null) q = q.Where(p => p.ShelfLifeDays <= shelfLifeDaysTo);
+ // Фильтр по системной (главной розничной) цене — берём Prices c PriceType.IsSystem=true.
+ if (systemPriceFrom is not null)
+ q = q.Where(p => p.Prices.Any(pr => pr.PriceType!.IsSystem && pr.Amount >= systemPriceFrom));
+ if (systemPriceTo is not null)
+ q = q.Where(p => p.Prices.Any(pr => pr.PriceType!.IsSystem && pr.Amount <= systemPriceTo));
if (!string.IsNullOrWhiteSpace(req.Search))
{
@@ -150,6 +176,8 @@ private async Task ResolveDefaultVatAsync(CancellationToken ct)
("packaging", true) => q.OrderByDescending(p => p.Packaging).ThenBy(p => p.Name),
("purchasePrice", false) => q.OrderBy(p => p.ReferencePrice).ThenBy(p => p.Name),
("purchasePrice", true) => q.OrderByDescending(p => p.ReferencePrice).ThenBy(p => p.Name),
+ ("systemPrice", false) => q.OrderBy(p => p.Prices.Where(pr => pr.PriceType!.IsSystem).Select(pr => (decimal?)pr.Amount).FirstOrDefault()).ThenBy(p => p.Name),
+ ("systemPrice", true) => q.OrderByDescending(p => p.Prices.Where(pr => pr.PriceType!.IsSystem).Select(pr => (decimal?)pr.Amount).FirstOrDefault()).ThenBy(p => p.Name),
("vat", false) => q.OrderBy(p => p.Vat).ThenBy(p => p.Name),
("vat", true) => q.OrderByDescending(p => p.Vat).ThenBy(p => p.Name),
("name", true) => q.OrderByDescending(p => p.Name),
@@ -174,6 +202,8 @@ public async Task> Create([FromBody] ProductInput input
{
if (input.Barcodes is null || input.Barcodes.Count == 0)
return BadRequest(new { error = "У товара должен быть хотя бы один штрихкод." });
+ if (await FindMissingRequiredPriceAsync(input.Prices, ct) is { } missing)
+ return BadRequest(new { error = $"Цена «{missing}» обязательна и должна быть больше 0." });
var conflict = await FindBarcodeConflictAsync(input.Barcodes.Select(b => b.Code), null, ct);
if (conflict is { } c)
return BadRequest(new { error = $"Штрихкод {c.Code} уже используется товаром «{c.ProductName}»." });
@@ -209,6 +239,8 @@ public async Task Update(Guid id, [FromBody] ProductInput input,
{
if (input.Barcodes is null || input.Barcodes.Count == 0)
return BadRequest(new { error = "У товара должен быть хотя бы один штрихкод." });
+ if (await FindMissingRequiredPriceAsync(input.Prices, ct) is { } missing)
+ return BadRequest(new { error = $"Цена «{missing}» обязательна и должна быть больше 0." });
var conflict = await FindBarcodeConflictAsync(input.Barcodes.Select(b => b.Code), id, ct);
if (conflict is { } c)
return BadRequest(new { error = $"Штрихкод {c.Code} уже используется товаром «{c.ProductName}»." });
diff --git a/src/food-market.api/Seed/DevDataSeeder.cs b/src/food-market.api/Seed/DevDataSeeder.cs
index 816ef9a..f8673c4 100644
--- a/src/food-market.api/Seed/DevDataSeeder.cs
+++ b/src/food-market.api/Seed/DevDataSeeder.cs
@@ -97,12 +97,16 @@ private static async Task SeedTenantReferencesAsync(AppDbContext db, Guid orgId,
);
}
+ // Сидируем PriceType ТОЛЬКО если у организации не было ни одной записи.
+ // Если есть — никогда не создаём «системную копию», корректность IsSystem
+ // обеспечивает миграция Phase3b_FixPriceTypeIsSystem (выбор по факту:
+ // запись с максимумом ProductPrice).
var anyPriceType = await db.PriceTypes.IgnoreQueryFilters().AnyAsync(p => p.OrganizationId == orgId, ct);
if (!anyPriceType)
{
db.PriceTypes.AddRange(
- new PriceType { OrganizationId = orgId, Name = "Розничная", IsDefault = true, IsRetail = true, SortOrder = 1 },
- new PriceType { OrganizationId = orgId, Name = "Оптовая", SortOrder = 2 }
+ new PriceType { OrganizationId = orgId, Name = "Розничная цена", IsSystem = true, IsRequired = true, IsDefault = true, IsRetail = true, SortOrder = 0 },
+ new PriceType { OrganizationId = orgId, Name = "Оптовая", SortOrder = 1 }
);
}
diff --git a/src/food-market.infrastructure/Persistence/Migrations/20260425190000_Phase3b_FixPriceTypeIsSystem.Designer.cs b/src/food-market.infrastructure/Persistence/Migrations/20260425190000_Phase3b_FixPriceTypeIsSystem.Designer.cs
new file mode 100644
index 0000000..a586030
--- /dev/null
+++ b/src/food-market.infrastructure/Persistence/Migrations/20260425190000_Phase3b_FixPriceTypeIsSystem.Designer.cs
@@ -0,0 +1,1904 @@
+//
+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("20260425190000_Phase3b_FixPriceTypeIsSystem")]
+ partial class Phase3b_FixPriceTypeIsSystem
+ {
+ ///
+ 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("IsDefault")
+ .HasColumnType("boolean");
+
+ 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("ShelfLifeDays")
+ .HasColumnType("integer");
+
+ 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("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.ToTable("organizations", "public");
+ });
+
+ modelBuilder.Entity("foodmarket.Domain.Purchases.Supply", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property