fix(db): reconcile stage schema — drop TrackingType, add IsMarked
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 39s
CI / Web (React + Vite) (push) Successful in 22s
Docker Images / API image (push) Successful in 36s
Docker Images / Web image (push) Successful in 4s
Docker Images / Deploy stage (push) Successful in 18s

Phase2c2_MoySkladAlignment и Phase2c3_MsStrict остались в
__EFMigrationsHistory на стейдже, но .cs-файлы были удалены при откате
кода (8fc9ef1). В результате:
- снапшот не соответствовал актуальной БД
- колонка TrackingType висела в БД, а код ждал IsMarked
- /api/admin/moysklad/import-products валился с 42703

Эта миграция:
1. Добавляет IsMarked bool NOT NULL DEFAULT false
2. Если TrackingType есть — бэкфиллит IsMarked = (TrackingType <> 0)
   и удаляет колонку (idempotent через information_schema check)
3. Auto-scaffold также синхронизировал snapshot (был устаревшим —
   содержал VatRate/IsAlcohol/Kind/Symbol и пр., которых в коде давно нет).

Локально применилось без ошибок.
This commit is contained in:
nurdotnet 2026-04-23 21:23:45 +05:00
parent 8346c9a72e
commit 5f0692587a
3 changed files with 1933 additions and 74 deletions

View file

@ -0,0 +1,70 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace foodmarket.Infrastructure.Persistence.Migrations
{
/// <summary>Reconciliation migration.
///
/// Предыдущие миграции Phase2c2_MoySkladAlignment и Phase2c3_MsStrict были применены
/// на стейдже, но их исходные .cs файлы были удалены при откате кода (commit 8fc9ef1
/// стёр их, но __EFMigrationsHistory уже содержал записи). В результате:
/// - snapshot был неактуальным (ссылался на VatRate, IsAlcohol, Kind, и т.п.)
/// - БД в состоянии пост-2c3 (поля Vat, VatEnabled, TrackingType; без VatRate,
/// без Kind, без IsAlcohol, без Symbol/DecimalPlaces/IsBase)
/// - код ожидает IsMarked вместо TrackingType
///
/// Задача этой миграции — добить различие в одной колонке: заменить TrackingType
/// (добавленный в Phase2c2) на IsMarked. Всё остальное уже совпадает.
/// EF-scaffold предложил много мусорной работы из-за рассинхрона snapshot'а — это
/// тело переписано вручную.</summary>
public partial class Phase2c4_ReconcileStage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// 1. Добавляем IsMarked с дефолтом false.
migrationBuilder.AddColumn<bool>(
name: "IsMarked",
schema: "public",
table: "products",
type: "boolean",
nullable: false,
defaultValue: false);
// 2. Если TrackingType есть в БД (стейдж) — бэкфиллим и удаляем.
// На свежей БД (dev, где migrations 2c2/2c3 не применялись отдельно)
// колонки не будет — IF EXISTS защищает от ошибки.
migrationBuilder.Sql("""
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'products'
AND column_name = 'TrackingType') THEN
UPDATE public.products SET "IsMarked" = ("TrackingType" <> 0);
ALTER TABLE public.products DROP COLUMN "TrackingType";
END IF;
END $$;
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "TrackingType",
schema: "public",
table: "products",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.Sql("""UPDATE public.products SET "TrackingType" = CASE WHEN "IsMarked" THEN 99 ELSE 0 END;""");
migrationBuilder.DropColumn(
name: "IsMarked",
schema: "public",
table: "products");
}
}
}

View file

@ -380,9 +380,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<int>("Kind")
.HasColumnType("integer");
b.Property<string>("LegalName")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
@ -418,8 +415,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("OrganizationId", "Bin");
b.HasIndex("OrganizationId", "Kind");
b.HasIndex("OrganizationId", "Name");
b.ToTable("counterparties", "public");
@ -568,9 +563,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<bool>("IsAlcohol")
.HasColumnType("boolean");
b.Property<bool>("IsMarked")
.HasColumnType("boolean");
@ -612,8 +604,11 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("VatRateId")
.HasColumnType("uuid");
b.Property<int>("Vat")
.HasColumnType("integer");
b.Property<bool>("VatEnabled")
.HasColumnType("boolean");
b.HasKey("Id");
@ -627,8 +622,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("UnitOfMeasureId");
b.HasIndex("VatRateId");
b.HasIndex("OrganizationId", "Article");
b.HasIndex("OrganizationId", "IsActive");
@ -876,9 +869,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<bool>("IsMain")
.HasColumnType("boolean");
b.Property<int>("Kind")
.HasColumnType("integer");
b.Property<string>("ManagerName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
@ -919,15 +909,13 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("DecimalPlaces")
.HasColumnType("integer");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<bool>("IsBase")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
@ -936,11 +924,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<string>("Symbol")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
@ -952,47 +935,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("units_of_measure", "public");
});
modelBuilder.Entity("foodmarket.Domain.Catalog.VatRate", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<bool>("IsDefault")
.HasColumnType("boolean");
b.Property<bool>("IsIncludedInPrice")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<decimal>("Percent")
.HasPrecision(5, 2)
.HasColumnType("numeric(5,2)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("OrganizationId", "Name")
.IsUnique();
b.ToTable("vat_rates", "public");
});
modelBuilder.Entity("foodmarket.Domain.Inventory.Stock", b =>
{
b.Property<Guid>("Id")
@ -1652,12 +1594,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("foodmarket.Domain.Catalog.VatRate", "VatRate")
.WithMany()
.HasForeignKey("VatRateId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CountryOfOrigin");
b.Navigation("DefaultSupplier");
@ -1667,8 +1603,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Navigation("PurchaseCurrency");
b.Navigation("UnitOfMeasure");
b.Navigation("VatRate");
});
modelBuilder.Entity("foodmarket.Domain.Catalog.ProductBarcode", b =>