getAttributes() as $attribute) { $values[strtolower($attribute->getName())] ??= $attribute->getValue(); } return $values; } public static function applies(ProductDTO $product): bool { $a = self::attributes($product); if (!empty($a['aic']) || !empty($a['aic_package']) || ($a['media_type'] ?? '') === 'medicine') { return false; } $text = $product->getName() . ' ' . $product->getDescription() . ' ' . ($a['category'] ?? ''); foreach ($product->getCategories() as $category) { $text .= ' ' . $category->getName(); } $kind = '(?:integrator[ei] (?:alimentar[ei]|di sali minerali)|(?:food|dietary) supplements?)'; if (preg_match('/\b(?:non\s+(?:è|e)|not)\s+(?:un\s+|a\s+)?' . $kind . '\b/iu', $text)) { return false; } return preg_match('/\b' . $kind . '\b/iu', $text) === 1; } /** Preserve the source in the description; use only complete sentences in SEO. */ public static function completeExcerpt(?string $text): ?string { if (!self::truncated($text)) { return $text; } $prefix = trim((string) preg_replace('/(?:\.{3}|…)\s*$/u', '', (string) $text)); if (preg_match('/^(.+[.!?])\s+[^.!?]*$/us', $prefix, $match)) { return trim($match[1]); } return null; } public static function truncated(?string $text): bool { return preg_match('/(?:\.{3}|…)\s*$/u', trim((string) $text)) === 1; } public static function medicineClaim(?string $text): bool { // Do not flag instructions such as "consultare il medico se si assumono medicinali". return preg_match('/\b(?:è|e\x{0300}|is)\s+(?:un\s+|a\s+)?(?:medicinale|farmaco|medicine|medicinal product)\b/iu', (string) $text) === 1; } public static function editable(ProductDTO $product): bool { $a = self::attributes($product); return !in_array($a['classification_authority'] ?? '', ['operator_explicit_domain', 'operator_category_decision'], true); } /** Only compare records carrying the very same physical barcode. */ public static function candidates(array $products): array { $first = $products[0] ?? null; if (!$first instanceof ProductDTO || !self::editable($first)) { return []; } $attributes = self::attributes($first); if (!empty($attributes['aic']) || !empty($attributes['aic_package']) || ($attributes['media_type'] ?? '') === 'medicine') { return []; } $barcode = $first->getBarcode()?->getValue(); if (!$barcode) { return []; } $same = array_values(array_filter($products, static fn (ProductDTO $p): bool => $p->getBarcode()?->getValue() === $barcode)); if (!array_filter($same, [self::class, 'applies'])) { return []; } $compatible = array_values(array_filter($same, static fn (ProductDTO $p): bool => self::compatible($first, $p))); return array_filter($compatible, [self::class, 'applies']) ? $compatible : []; } private static function tokens(string $text): array { return array_values(array_unique(preg_split('/[^\p{L}\p{N}]+/u', mb_strtolower($text)) ?: [])); } private static function compatible(ProductDTO $first, ProductDTO $candidate): bool { $left = self::attributes($first); $right = self::attributes($candidate); foreach (['brand', 'size', 'package_quantity'] as $field) { if (!empty($left[$field]) && !empty($right[$field]) && mb_strtolower(trim($left[$field])) !== mb_strtolower(trim($right[$field]))) { return false; } } $pattern = '/\b(\d+)\s*(bustine|compresse|ovalette|capsule|sachets|tablets)\b/iu'; if (preg_match($pattern, $first->getName(), $a) && preg_match($pattern, $candidate->getName(), $b) && (int) $a[1] !== (int) $b[1]) { return false; } $tokens = array_filter(self::tokens($first->getName()), static fn (string $v): bool => mb_strlen($v) > 1); return array_diff($tokens, self::tokens($candidate->getName())) === []; } public static function title(array $products): string { $best = $products[0]->getName(); foreach ($products as $product) { $candidate = $product->getName(); if (count(self::tokens($candidate)) > count(self::tokens($best)) && !self::truncated($candidate)) { $best = $candidate; } } return $best; } public static function description(array $products): ?string { $best = null; $bestScore = -1; foreach ($products as $product) { $text = trim((string) $product->getDescription()); if ($text === '' || self::medicineClaim($text)) { continue; } $a = self::attributes($product); $language = strtolower($a['content_language'] ?? $a['language'] ?? ''); // Completeness first, declared Italian second, length only as a tie-breaker. $score = (self::truncated($text) ? 0 : 1000000) + (preg_match('/^it(?:[-_]|$)/', $language) ? 100000 : 0) + min(20000, mb_strlen($text)); if ($score > $bestScore) { $best = $text; $bestScore = $score; } } return $best; } }