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',
'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'],
$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();