Загрузка данных


Ты выступаешь как senior Laravel-разработчик. Нужно реализовать микросервис, который получает данные о лекарствах от нескольких аптечных сетей в разных форматах, нормализует их к единой модели и объединяет дубликаты в общий каталог.

Входные данные (во вложении три файла — используй их как основу маппинга)

pharmacy_a.json — плоский JSON-массив:

json
[{
  "id": "a-100",
  "name": "Nurofen 200 mg Tablets",
  "manufacturer": "Reckitt Benckiser",
  "gtin": "05050123456789",
  "dosage_mg": 200,
  "package_size": 24,
  "form": "tablet",
  "prescription_required": false,
  "country": "GB",
  "registration_date": "2023-01-15"
}]

pharmacy_b.xml — XML с вложенным элементом <medicine>:

xml
<medicines>
  <medicine>
    <externalId>b-777</externalId>
    <title>Nurofen Tablets 200mg</title>
    <producer>Reckitt Benckiser Ltd.</producer>
    <barcode>0505-0123-4567-89</barcode>
    <dosage>200 mg</dosage>
    <pack>24 tablets</pack>
    <dosageForm>tablets</dosageForm>
    <rx>false</rx>
    <origin>United Kingdom</origin>
    <registeredAt>15.01.2023</registeredAt>
  </medicine>
</medicines>

pharmacy_c.json — вложенный JSON с оберткой items:

json
{
  "items": [{
    "product_uid": "c_991",
    "details": {
      "trade_name": "Nurofen Film-coated Tabs",
      "manufacturer": { "name": "Reckitt Benckiser Healthcare" },
      "strength": { "value": 200, "unit": "mg" },
      "identifiers": { "gtin": "05050123456789" },
      "packaging": { "count": 24, "type": "tabs" },
      "requires_prescription": "no",
      "country_code": "UK",
      "registered": "2023/01/15"
    }
  }]
}
Установленные расхождения форматов (используй именно эти варианты в нормализаторах и тестах, не абстрактные примеры)
Идентификатор (GTIN/штрихкод): у A и C значение идентично побайтово ("05050123456789"). У B тот же код, но с разделителями-дефисами ("0505-0123-4567-89") — перед сравнением обязательна нормализация (удаление всех нецифровых символов).
Дозировка: A отдаёт готовое число в mg (dosage_mg: 200), B — строку "200 mg" для парсинга, C — структуру {value, unit}. Итоговое каноническое значение должно совпасть у всех трёх после нормализации.
Форма выпуска: три разных строки для одного и того же — "tablet" (A), "tablets" (B, множественное число), "tabs" (C). Плюс в названии C встречается "Film-coated Tabs". Нормализатор должен свести все три к одному enum-значению.
Рецептурность: три разных кодирования одного и того же логического значения — false (булево, A), "false" (строка, B), "no" (строка, другой словарь, C). Нормализатор должен привести все три к единому bool.
Страна: "GB" (ISO 3166-1 alpha-2, A), "United Kingdom" (полное название, B), "UK" (общеупотребимое, но не строгий ISO-код, C). Все три должны свестись к одному ISO-коду.
Дата регистрации: три разных формата — "2023-01-15" (ISO, A), "15.01.2023" (DD.MM.YYYY, B), "2023/01/15" (YYYY/MM/DD, C).
Название препарата нигде не совпадает буквально (три разные строки), поэтому матчинг между источниками должен в первую очередь опираться на нормализованный GTIN (после удаления разделителей), а fuzzy-сравнение названия — на резервный случай, когда GTIN отсутствует или не совпал.
Задача — выполняй строго по этапам, не переходи к следующему без явного вывода предыдущего
Этап 1. Подтверждение маппинга

Прежде чем писать код, выведи финальную таблицу "поле источника → каноническое поле" для всех трёх файлов (можешь опираться на анализ выше, но перепроверь самостоятельно по вложенным файлам) и явно распиши правила нормализации для каждого проблемного поля (дозировка, форма выпуска, рецептурность, страна, дата, идентификатор). Дождись моего подтверждения, прежде чем переходить к Этапу 2.

Этап 2. Архитектура и модель данных
Структура Laravel-проекта: Domain / Infrastructure / Application / Http (без очередей — обработка синхронная, по HTTP-запросу).
Миграции:
products — каноническая запись препарата (canonical_name, inn nullable, dosage_value, dosage_unit, release_form enum, manufacturer, country_code, registration_date, gtin_normalized nullable).
product_sources — сырые данные + связь с источником: source_code, source_external_id, raw_payload (json), matched_by (exact_gtin | fuzzy_name | manual), confidence (float), UNIQUE (source_code, source_external_id) — база идемпотентности на уровне записи.
import_batches — file_hash (sha256), source_code, status, items_total, items_new, items_merged, UNIQUE (source_code, file_hash) — идемпотентность на уровне файла: повторная загрузка того же файла не создаёт дублей и возвращает результат предыдущей обработки.
Интерфейсы: ImporterInterface (parse: string $path → Collection), NormalizerInterface, MatcherInterface.
Этап 3. Реализация
Три парсера под реальные форматы из вложения: PharmacyAImporter (JSON массив), PharmacyBImporter (XML с единичным/множественным <medicine>), PharmacyCImporter (JSON с оберткой items и вложенными объектами). Файл другого типа/структуры — исключение → HTTP 422 с понятным сообщением, что формат не поддерживается.
Нормализаторы для каждого из 7 расхождений, перечисленных выше, с юнит-тестами на конкретных значениях из вложенных файлов (не абстрактных).
CompositeMatcher с порядком правил:
точное совпадение по нормализованному GTIN (сначала убрать все нецифровые символы у обеих сторон, включая случай дефисов в barcode);
совпадение по (нормализованное название/INN + дозировка + форма выпуска + производитель);
fuzzy-сравнение нормализованного названия (укажи алгоритм — например, Jaro-Winkler — и обоснуй порог на трёх реальных названиях из вложения: "Nurofen 200 mg Tablets" / "Nurofen Tablets 200mg" / "Nurofen Film-coated Tabs" — все три должны попасть выше порога совпадения).
REST API:
POST /api/import (multipart: file + source_code) — синхронно парсит, нормализует, матчит и сохраняет; идемпотентно по хэшу файла.
GET /api/catalog — список канонических препаратов с пагинацией.
GET /api/catalog/{id}/sources — все источники, слитые в этот препарат, с их сырыми payload — для проверки корректности слияния.
GET /api/catalog/export — весь каталог одним JSON.
Прогони весь пайплайн на всех трёх приложенных файлах и покажи итоговый JSON: ожидается один канонический препарат с тремя записями в product_sources (matched_by = exact_gtin для всех трёх, после нормализации barcode у B).
Этап 4. Docker Compose

docker-compose.yaml, поднимающий php-fpm/nginx (или Octane) + Postgres/MySQL, с автоприменением миграций при старте — чтобы docker compose up сразу давал рабочий сервис без ручных шагов.

Этап 5. Проверка корректности
Unit-тесты нормализаторов на всех 7 расхождениях, перечисленных выше, со значениями из вложенных файлов.
Unit-тест матчера: подтверди, что все три записи из вложения матчатся друг с другом через exact_gtin (после нормализации), и отдельно добавь контрольный "отрицательный" кейс — препарат с похожим названием, но другой дозировкой или GTIN, который НЕ должен слиться (защита от false positive fuzzy-матчинга).
Feature-тест: импорт всех трёх файлов из вложения → в каталоге ровно одна запись Product с тремя связанными ProductSource.
Общие требования
Не хардкодь словари синонимов (форма выпуска, страна, рецептурность) в коде — вынеси в конфиг, чтобы расширять без правки логики.
Явно покажи точку расширения под добавление четвёртого источника с новым форматом в будущем.
Комментарии и docblocks — на русском.