]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
refactor(ERP-467): DB-бэкстоп уникальности названия + чистка мёртвого теста hotfix-fomichev-ERP-467-supplier-mappings-by-name origin/hotfix-fomichev-ERP-467-supplier-mappings-by-name
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 15 Jul 2026 11:46:05 +0000 (14:46 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 15 Jul 2026 11:46:05 +0000 (14:46 +0300)
- миграция: expression-индекс uq_pm_product_supplier_name
  (product_guid, supplier_id, COALESCE(plantation_id,0), supplier_product_name)
  вместо удалённых строгих индексов — защита от гонок/bulk-путей
- ProductMapping: уточнён комментарий к default=null (нормализация для БД)
- тесты: удалён testArticleBarcodeDefaultRunsBeforeUniqueCheck
  (охранял неактуальный порядок правил)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
erp24/migrations/m260715_120000_widen_product_mappings_unique.php
erp24/records/ProductMapping.php
erp24/tests/unit/records/ProductMappingTest.php

index a7d9cee051515a3f7e649c7384fbd41e13b0a4f0..affb567201e5d557b415c78ab696270b70efbd90 100644 (file)
@@ -5,32 +5,42 @@ declare(strict_types=1);
 use yii\db\Migration;
 
 /**
- * ERP-467: Уникальность маппинга по названию/артикулу/штрихкоду.
+ * ERP-467: Уникальность маппинга по названию у поставщика.
  *
  * Раньше маппинг был уникален по (product_guid, supplier_id[, plantation_id]),
  * поэтому к одной карточке нельзя было привязать того же поставщика с той же
- * плантацией под другим названием. Убираем эти строгие индексы — теперь в рамках
- * объёма «товар + поставщик + плантация» на уникальность проверяется только
- * название (ProductMapping::validatePartialUnique()); артикул и штрихкод могут
- * повторяться.
+ * плантацией под другим названием. Меняем объём уникальности: в рамках
+ * «товар + поставщик + плантация» уникальным должно быть название
+ * (supplier_product_name); артикул и штрихкод могут повторяться.
  *
- * Partial/expression UNIQUE-индекс PostgreSQL (WHERE ... / COALESCE(...)) нельзя
- * выразить методами Yii2 Migration, поэтому DB-бэкстопа здесь нет — валидатора
- * достаточно для ручной формы справочника.
+ * PHP-валидатор (ProductMapping::validatePartialUnique) прикрывает форму, но
+ * оставляем и DB-бэкстоп от гонок/bulk-путей. COALESCE(plantation_id, 0)
+ * схлопывает оба сценария (NULL и не-NULL плантация) в один expression-индекс
+ * — fluent-методами Yii2 такой индекс не выразить, поэтому здесь execute().
  */
 class m260715_120000_widen_product_mappings_unique extends Migration
 {
     private const TABLE = 'erp24.product_mappings';
+    private const NAME_INDEX = 'uq_pm_product_supplier_name';
 
     public function safeUp(): void
     {
         $this->dropIndex('uq_pm_product_supplier', self::TABLE);
         $this->dropIndex('uq_pm_product_supplier_plantation', self::TABLE);
+
+        // Старые индексы гарантировали уникальность (товар, поставщик, плантация),
+        // так что дублей названий в этом объёме нет — CREATE не упадёт.
+        $this->execute(
+            'CREATE UNIQUE INDEX ' . self::NAME_INDEX . ' ON ' . self::TABLE
+            . ' (product_guid, supplier_id, COALESCE(plantation_id, 0), supplier_product_name)'
+        );
     }
 
     public function safeDown(): void
     {
-        // Восстанавливаем строгую уникальность (без partial WHERE — методами Yii это недоступно).
+        // Внимание: откат упадёт, если за время работы нового индекса накопились
+        // строки, нарушающие старую строгую уникальность (товар+поставщик[+плантация]).
+        $this->dropIndex(self::NAME_INDEX, self::TABLE);
         $this->createIndex('uq_pm_product_supplier', self::TABLE, ['product_guid', 'supplier_id'], true);
         $this->createIndex(
             'uq_pm_product_supplier_plantation',
index 2784cc5ea75e84a644e79f2af15ca2fbb4d574fa..fa796bdfb8efbe969b70cd76b05338a8ab6b0ab3 100644 (file)
@@ -96,8 +96,8 @@ class ProductMapping extends ActiveRecord
                 'skipOnEmpty' => true,
                 'message' => 'Плантация не найдена',
             ],
-            // default до validatePartialUnique: пустые article/barcode нормализуются в null
-            // ещё до проверки уникальности, чтобы '' и null не считались разными значениями.
+            // Пустые необязательные поля нормализуем в null перед сохранением,
+            // чтобы в БД не попадали '' вместо NULL.
             [['article', 'barcode', 'plantation_id'], 'default', 'value' => null],
             ['supplier_product_name', 'validatePartialUnique'],
             ['is_active', 'boolean'],
index fede1af685fc8669f135101f9762e4eb7733dd4a..af25ee080d1ffcc3fbdc0d5950dfe2aab701b25c 100644 (file)
@@ -107,39 +107,6 @@ class ProductMappingTest extends Unit
         $this->assertTrue(method_exists(ProductMapping::class, 'getProductNomenclature'));
     }
 
-    /**
-     * ERP-467: пустые article/barcode должны нормализоваться в null ДО проверки
-     * уникальности, иначе '' и null считались бы разными значениями.
-     */
-    public function testArticleBarcodeDefaultRunsBeforeUniqueCheck(): void
-    {
-        $rules = (new ProductMapping())->rules();
-
-        $defaultIndex = null;
-        $uniqueIndex = null;
-        foreach ($rules as $i => $rule) {
-            if (
-                ($rule[1] ?? null) === 'default'
-                && array_key_exists('value', $rule) && $rule['value'] === null
-                && in_array('article', (array)$rule[0], true)
-                && in_array('barcode', (array)$rule[0], true)
-            ) {
-                $defaultIndex = $i;
-            }
-            if (($rule[1] ?? null) === 'validatePartialUnique') {
-                $uniqueIndex = $i;
-            }
-        }
-
-        $this->assertNotNull($defaultIndex, 'Нужно правило default=null для article/barcode');
-        $this->assertNotNull($uniqueIndex, 'Нужно правило validatePartialUnique');
-        $this->assertLessThan(
-            $uniqueIndex,
-            $defaultIndex,
-            'default=null для article/barcode должен идти до validatePartialUnique'
-        );
-    }
-
     public function testMarkingIdsRuleHasDefault(): void
     {
         $rules = (new ProductMapping())->rules();