Import a synchronizace dat

Přehled

Projekt pracuje se třemi zdroji dat, každý má jiný charakter:

Zdroj Typ Frekvence Kde
Starý e-shop (MariaDB) Jednorázová migrace 1x (test + ostrý přechod) ESHOP/import/
Pohoda (XML) Full sync + inkrementální změny 1× denně (full) + každých 20 min (diff) products/Workers/
Dodavatelé XML (11×) Ceny, dostupnost 1× denně, někteří max 2× products/Workers/

Kde žije importní kód

ESHOP/import/ — jednorázová migrace

Samostatný projekt pro přechod ze starého e-shopu. Po úspěšném spuštění se archivuje. Migrační kód nepatří do produkční domény, nepatří do workers, nepatří do core modelu.

ESHOP/import/
  src/
    MariaDB/
      ProductMigrator.php
      CategoryMigrator.php
      BrandMigrator.php
  run.php

products/Workers/ — živé workery

Workery patří do tohoto projektu, protože sdílejí databázi, konfiguraci a triggerují search reindex.

products/Workers/
  MeilisearchSetup.php        # hotovo
  SearchIndexWorker.php       # hotovo
  PohodaFullImportWorker.php  # plán
  PohodaDiffImportWorker.php  # plán
  SupplierImportWorker.php    # plán (sdílený runner, konfigurace per dodavatel)

Architektura průtoku

zdroj (MariaDB / Pohoda XML / dodavatel XML)
  → system.import_raw           # surová data, beze změny — střed systému
  → normalizer                  # mapování na nový model
  → catalog.* / pricing.* / inventory.*
  → system.migration_error      # chyby s raw_data pro debug
  → system.migration_batch      # stav a statistika dávky

Zásada: raw data se nikdy nepřepisují.

Zachrání to při: - změně mapování nebo opravě parseru - rollbacku importu - debugování — porovnání starý vs. nový systém - edge cases, které se objeví až za provozu

Architektonická rozhodnutí

system.import_raw je střed systému

Buffer pro všechna příchozí data. Nic nejde přímo do finálních tabulek.

CREATE TABLE system.import_raw (
    id              bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    source          varchar NOT NULL,
    entity_type     varchar NOT NULL,
    external_id     varchar,
    import_batch_id uuid REFERENCES system.migration_batch(id),
    payload         jsonb NOT NULL,
    payload_hash    varchar,
    imported_at     timestamptz NOT NULL DEFAULT now(),
    processed_at    timestamptz,
    status          varchar NOT NULL DEFAULT 'pending',
    error_message   text
);

Správný průtok:

XML → RAW → NORMALIZER → UPSERT

Špatný průtok (nedělat):

XML → rovnou catalog.product

Idempotence je kritická

Pohoda diff i dodavatelé musí zvládnout stejný feed pustit 10× bez bordelu. Zajišťuje to:

  • payload_hash — skip pokud se nic nezměnilo
  • external_id — párování na existující záznamy
  • unique constraints v DB
  • deterministic upsert (ne insert)

Identita produktu je největší budoucí problém

Mapování produktu přes zdroje nebude triviální. Typické problémy:

  • jiný nebo chybějící EAN
  • duplicitní EAN u různých dodavatelů
  • varianty bez variant (dodavatel nezná varianty)
  • změna SKU v čase
  • různý naming stejného produktu
  • dodavatel tvrdí jiný brand

Model mapování identity se vyladí až po prvních reálných XML a edge cases. Teď neřešit — cíl je průtok dat, ne dokonalá architektura.

Tracking v databázi

Máme hotové (migrace 011):

  • system.migration_batch — dávka importu (source, status, started_at, processed_rows, total_rows)
  • system.migration_error — chyby s legacy_id, entity_type, raw_data

Chybí — první co udělat:

  • system.import_raw — viz struktura výše

Datová pole u importovaných entit

Každý importovaný záznam nese:

  • source — odkud pochází (legacy_mariadb, pohoda, supplier_xyz)
  • external_id — původní ID ze zdrojového systému
  • import_batch_id — reference na migration_batch
  • payload_hash — hash pro skip při opakovaném importu beze změn

Plán — co je třeba udělat

Fáze 0: Infrastruktura importu (udělat jako první)

  • [ ] Vytvořit migraci 014_import_raw.sql — tabulka system.import_raw
  • [ ] Ověřit že migration_batch a migration_error jsou v DB

Fáze 1: Migrace ze starého e-shopu

  • [ ] Zmapovat tabulky starého MariaDB (produkty, kategorie, brandy, ceny, sklad)
  • [ ] Navrhnout mapování starý model → nový PostgreSQL model
  • [ ] Napsat ProductMigrator — plní import_raw, pak normalizuje do catalog.*
  • [ ] Napsat CategoryMigrator
  • [ ] Napsat BrandMigrator
  • [ ] Spustit testovací import, ověřit přes public API
  • [ ] Ostrý přechod

Fáze 2: Pohoda (hlavní sklad — full + inkrementální)

Pohoda exportuje dva typy XML:

  • Full — kompletní snapshot, 1× denně (noc / ráno)
  • Diff — jen změněné záznamy, každých 20 minut přes URL — musí být idempotentní

  • [ ] Zjistit formát Pohoda XML (full vs. diff — stejné elementy?)

  • [ ] Napsat PohodaFullImportWorker
  • [ ] Napsat PohodaDiffImportWorker
  • [ ] Párování přes EAN / SKU → catalog.product_variant
  • [ ] Zápis cen přes pricing.price_upsert()
  • [ ] Zápis skladu do inventory.*
  • [ ] Nastavit docker crony

Fáze 3: Dodavatelé (11× — URL → XML)

  • Každý dodavatel má vlastní URL na XML export
  • Frekvence: 1× denně, někteří max 2×
  • Jeden sdílený runner, konfigurace per dodavatel

  • [ ] Definovat konfigurační strukturu (URL, frekvence, parser per dodavatel)

  • [ ] Napsat SupplierImportWorker — sdílený runner
  • [ ] Specifické XML parsery per dodavatel
  • [ ] Párování přes EAN
  • [ ] Nastavit crony

Přímý přístup do zdrojů

  • Starý e-shop: přímý přístup do MariaDB (connection string v .env)
  • Pohoda (full): URL na XML export — 1× denně
  • Pohoda (diff): URL na XML diff — každých 20 min
  • Dodavatelé: 11× URL na XML, každý vlastní endpoint a frekvence