Eshop — návrh datového modelu v5
Inspirační zdroje
| Oblast |
Inspirace |
| Katalog |
Vendure |
| Pricing |
Medusa |
| Inventory |
Medusa |
| Attribute/Option rozlišení |
Sylius |
| API |
vlastní REST |
Rozhodnutí z review (souhrn všech verzí)
PK strategie
UUID pro veřejnou identitu (API, URL, externí systémy). bigint generated always as identity pro interní PK joinovacích a překladových tabulek.
Typy pro peníze a sazby
| Pole |
Typ |
amount (ceny, poplatky) |
int — v haléřích/centech |
exchange_rate |
numeric(18,8) |
tax_rate |
numeric(5,2) |
discount_value |
numeric(12,4) |
weight |
numeric(10,3) |
width, height, length |
numeric(10,2) |
Enumy jako PostgreSQL enum typ
Pro stabilní domény (publication_status, price.type, media.type) používáme PostgreSQL ENUM — menší storage, rychlejší porovnání, přidání hodnoty non-blocking přes ALTER TYPE ... ADD VALUE. Pro proměnlivé domény (reason v dirty queue, EAV klíče) varchar.
Stavový model produktu
publication_status vyjadřuje aktuální stav produktu. Flags vyjadřují původ nebo důvod stavu. Není to duplicita.
deleted není fyzický delete. Fyzický delete probíhá až po archivaci a auditu.
publication_status |
Popis |
draft |
Rozpracovaný, nezveřejněný |
published |
Aktivní na webu |
hidden |
Dočasně skrytý (viz hide_until) |
blocked |
Blokovaný — viz flag is_blocked_by_editor |
discontinued |
Vyřazený z nabídky |
deleted |
Soft delete — označen ke smazání, fyzicky zůstává do archivace |
Flags říkají proč:
- is_blocked_by_editor — editor ručně zablokoval
- is_deleted_by_import — import označil ke smazání
- is_clearance — doprodej (může být published i discontinued)
- hide_until — dočasně skrýt do data
Soft delete strategie
Rozhodnutí platí jednotně pro celý model:
- product — publication_status = deleted, fyzický delete až po auditu
- ostatní entity — deleted_at timestamptz NULL kde je potřeba obnova
- join tabulky bez historické hodnoty — fyzický delete + audit log zachytí
Slug unikátnost
UNIQUE(locale, slug) globálně + UNIQUE(entity_id, locale) aby jedna entita neměla dva slugy pro stejný jazyk.
Audit log entity_id
entity_id text — univerzální, pokryje uuid i bigint PK bez JOIN problémů.
reserved_quantity a oversell
CHECK (reserved_quantity <= stocked_quantity) záměrně chybí. Inventura a vrácení dodavateli jsou legitimní operace které by constraint rozbil. Oversell a backorder jsou obchodní rozhodnutí — hlídají se aplikačně při vytváření rezervace.
Uzavřená architektonická rozhodnutí
| Oblast |
Rozhodnutí |
| Closure table |
PL/pgSQL procedury v DB — nikdy přímý zápis do category_closure |
| Inventory rezervace |
inventory_reservation = zdroj pravdy, reserved_quantity = cache udržovaná DB triggerem |
| Price overlap |
DB procedura price_upsert() při každém zápisu — přímý INSERT do price zakázán |
| Facety |
Odvozovat z product_attribute_value při indexaci — samostatná facet vrstva se nedělá |
| Order item snapshot |
Flat základní sloupce + jsonb snapshot_detail pro zbytek |
| Raw import log |
Logovat pouze chyby — volitelný debug mód per supplier přes supplier.debug_mode |
Vrstva 1 — produkt, identita, stavy, dodavatel
product
| Pole |
Typ |
Popis |
id |
uuid PK |
Veřejná identita |
code |
varchar UK |
Obchodní kód — hlavní klíč pro import a párování |
ean |
varchar |
EAN kód |
brand_id |
uuid FK RESTRICT |
|
web_id |
bigint |
Mapovací klíč na starý systém |
publication_status |
enum |
draft, published, hidden, blocked, discontinued, deleted |
is_blocked_by_editor |
boolean |
Původ stavu blocked |
is_deleted_by_import |
boolean |
Původ stavu deleted |
is_clearance |
boolean |
Doprodej — nezávislé na publication_status |
new_until |
timestamptz |
Produkt je "nový" do tohoto data |
hide_until |
timestamptz |
Dočasně skrýt do tohoto data |
created_at |
timestamptz |
|
updated_at |
timestamptz |
|
brand
| Pole |
Typ |
Popis |
id |
uuid PK |
|
name |
varchar |
|
slug |
varchar UK |
Výchozí slug — pokud brand nemá lokalizaci |
website |
varchar |
|
logo |
varchar |
|
is_local |
boolean |
|
web_id |
bigint |
Mapovací klíč na starý systém |
Pokud brand potřebuje lokalizovaný slug, přidat brand_translation.slug s UNIQUE(locale, slug). Zatím řešeno výchozím brand.slug.
product_slug
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
|
locale |
varchar |
cs, sk, de, hu, pl |
slug |
varchar |
|
|
|
UNIQUE(product_id, locale) |
|
|
UNIQUE(locale, slug) |
supplier
| Pole |
Typ |
Popis |
id |
uuid PK |
|
name |
varchar |
|
code |
varchar UK |
|
debug_mode |
boolean |
Ukládat raw import payload pro debugging |
product_supplier_feed
| Pole |
Typ |
Popis |
id |
uuid PK |
|
product_id |
uuid FK CASCADE |
|
supplier_id |
uuid FK RESTRICT |
|
supplier_code |
varchar |
Kód produktu u dodavatele |
supplier_ean |
varchar |
|
lock_content |
boolean |
Nepřepisovat textová data (neprepsat) |
last_imported_at |
timestamptz |
|
|
|
UNIQUE(supplier_id, supplier_code) |
|
|
UNIQUE(supplier_id, supplier_ean) — pouze pokud je EAN u dodavatele spolehlivý |
Import logika: dodavatel insertuje pokud neexistuje code/ean → produkt vznikne jako draft. Po prvním insertu přepisuje pouze ceny a skladovou dostupnost. lock_content = true chrání textová data.
product_supplier_feed_raw
Loguje pouze chyby importu. Pokud supplier.debug_mode = true, loguje i úspěšné záznamy pro debugging. Není to log všeho — je to log problémů.
| Pole |
Typ |
Popis |
id |
bigint PK |
|
supplier_feed_id |
uuid FK CASCADE |
|
is_error |
boolean |
True = chyba importu, False = debug záznam |
error_message |
text NULL |
Popis chyby |
raw_data |
jsonb |
Surová data z importu |
imported_at |
timestamptz |
|
Vrstva 2 — lokalizace
Fallback na výchozí jazyk cs řeší aplikační vrstva.
product_translation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
|
locale |
varchar |
cs, sk, de, hu, pl |
name |
varchar |
|
name_heureka |
varchar |
Název pro Heureka feed |
description_short |
text |
|
description_long |
text |
|
description_tech |
text |
|
out_of_stock_text |
text |
Obchodní sdělení pro zákazníka |
is_machine_translated |
boolean |
Příznak pro editorial workflow |
created_at |
timestamptz |
|
updated_at |
timestamptz |
|
|
|
UNIQUE(product_id, locale) |
brand_translation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
brand_id |
uuid FK CASCADE |
|
locale |
varchar |
|
name |
varchar |
|
slug |
varchar NULL |
Lokalizovaný slug — pokud se liší od brand.slug |
created_at |
timestamptz |
|
updated_at |
timestamptz |
|
|
|
UNIQUE(brand_id, locale) |
|
|
UNIQUE(locale, slug) WHERE slug IS NOT NULL |
Vrstva 3 — varianty a atributy
Rozlišení Option vs. Attribute (Sylius)
| Typ |
Definice |
Vliv na SKU |
| Option |
Osa varianty — velikost, barva |
Ano |
| Attribute |
Popisná vlastnost — materiál, váha |
Ne |
Historické typy varianty a barvy mapují jako option_group.type = variant a option_group.type = color.
product_variant
| Pole |
Typ |
Popis |
id |
uuid PK |
|
product_id |
uuid FK CASCADE |
|
code |
varchar UK |
|
ean |
varchar |
|
is_active |
boolean |
|
is_default |
boolean |
|
sort_order |
int |
|
created_at |
timestamptz |
|
updated_at |
timestamptz |
|
|
|
UNIQUE(product_id) WHERE is_default = true — jen jedna výchozí varianta |
product_variant_translation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
variant_id |
uuid FK CASCADE |
|
locale |
varchar |
|
name |
varchar |
|
|
|
UNIQUE(variant_id, locale) |
option_group
| Pole |
Typ |
Popis |
id |
uuid PK |
|
product_id |
uuid FK CASCADE |
|
code |
varchar |
|
type |
varchar |
variant, color |
sort_order |
int |
|
|
|
UNIQUE(product_id, code) |
option_group_translation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
option_group_id |
uuid FK CASCADE |
|
locale |
varchar |
|
name |
varchar |
|
|
|
UNIQUE(option_group_id, locale) |
option_value
| Pole |
Typ |
Popis |
id |
uuid PK |
|
option_group_id |
uuid FK CASCADE |
|
code |
varchar |
|
color_hex |
varchar |
Pro type = color |
image |
varchar |
Vzorník |
sort_order |
int |
|
|
|
UNIQUE(option_group_id, code) |
option_value_translation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
option_value_id |
uuid FK CASCADE |
|
locale |
varchar |
|
name |
varchar |
|
|
|
UNIQUE(option_value_id, locale) |
product_variant_option_value
| Pole |
Typ |
Popis |
variant_id |
uuid FK CASCADE |
|
option_value_id |
uuid FK RESTRICT |
|
|
|
PRIMARY KEY (variant_id, option_value_id) |
Constrainty aplikační vrstvou / procedurou:
- Jedna varianta nesmí mít dvě hodnoty ze stejné option_group.
- Jedna kombinace option values nesmí existovat dvakrát pro stejný produkt.
attribute
| Pole |
Typ |
Popis |
id |
uuid PK |
|
code |
varchar UK |
|
type |
varchar |
text, number, boolean |
attribute_translation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
attribute_id |
uuid FK CASCADE |
|
locale |
varchar |
|
name |
varchar |
|
|
|
UNIQUE(attribute_id, locale) |
product_attribute_value
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
|
attribute_id |
uuid FK RESTRICT |
|
value |
varchar |
|
locale |
varchar |
|
|
|
UNIQUE(product_id, attribute_id, locale) |
Vrstva 4 — ceny (Medusa pricing model)
Filosofie
Varianta nemá jednu cenu — má price_set ze kterého se vybírá cena podle kontextu. Všechny peněžní hodnoty jsou int v haléřích/centech. Ceny se nikdy fyzicky nepřepisují — ukončuje se jejich platnost (valid_to).
region
| Pole |
Typ |
Popis |
id |
uuid PK |
|
code |
varchar UK |
CZ, SK, DE, HU, PL |
name |
varchar |
|
currency_code |
varchar |
CZK, EUR |
tax_mode |
varchar |
inclusive, exclusive |
exchange_rate |
numeric(18,8) |
Přepočet z CZK |
|
|
CHECK (exchange_rate > 0) |
price_set
| Pole |
Typ |
Popis |
id |
uuid PK |
|
variant_id |
uuid FK CASCADE |
UNIQUE — 1:1 s variantou |
created_at |
timestamptz |
|
price
| Pole |
Typ |
Popis |
id |
uuid PK |
|
price_set_id |
uuid FK CASCADE |
|
region_id |
uuid FK RESTRICT NULL |
NULL = platí pro všechny regiony |
currency_code |
varchar |
|
amount |
int |
V haléřích/centech |
type |
enum |
sale, original, cost, install |
min_quantity |
int NULL |
|
max_quantity |
int NULL |
|
valid_from |
timestamptz NULL |
|
valid_to |
timestamptz NULL |
|
created_at |
timestamptz |
|
updated_at |
timestamptz |
|
|
|
CHECK (amount >= 0) |
|
|
CHECK (min_quantity IS NULL OR min_quantity > 0) |
|
|
CHECK (max_quantity IS NULL OR max_quantity >= min_quantity) |
|
|
CHECK (valid_to IS NULL OR valid_to > valid_from) |
|
|
Index: (price_set_id, region_id, currency_code, type, min_quantity, valid_from, valid_to) |
Přímý INSERT do price je zakázán. Všechny zápisy jdou přes proceduru price_upsert(price_set_id, region_id, currency_code, type, amount, ...) která ověří překryvy valid_from/valid_to a min/max_quantity před zápisem.
Mapování: cena_a → sale, cena_b → original, cena_c → cost, cena_montaze → install.
tax_rate
| Pole |
Typ |
Popis |
id |
uuid PK |
|
region_id |
uuid FK RESTRICT |
|
name |
varchar |
|
rate |
numeric(5,2) |
|
is_default |
boolean |
|
|
|
CHECK (rate >= 0) |
|
|
UNIQUE(region_id) WHERE is_default = true |
Nahrazuje polymorfní target_type/target_id explicitními pivot tabulkami.
| Pole |
Typ |
Popis |
id |
uuid PK |
|
code |
varchar UK |
|
name |
varchar |
|
discount_type |
enum |
percent, fixed |
discount_value |
numeric(12,4) |
|
priority |
int |
Řeší konflikt více akcí |
only_in_stock |
boolean |
Akce jen pokud skladem |
valid_from |
timestamptz |
|
valid_to |
timestamptz |
|
is_active |
boolean |
|
created_at |
timestamptz |
|
updated_at |
timestamptz |
|
|
|
CHECK (discount_value >= 0) |
|
|
CHECK (discount_type <> 'percent' OR discount_value <= 100) |
| Pole |
Typ |
promotion_id |
uuid FK CASCADE |
product_id |
uuid FK CASCADE |
|
PRIMARY KEY (promotion_id, product_id) |
| Pole |
Typ |
promotion_id |
uuid FK CASCADE |
brand_id |
uuid FK CASCADE |
|
PRIMARY KEY (promotion_id, brand_id) |
| Pole |
Typ |
promotion_id |
uuid FK CASCADE |
category_id |
uuid FK CASCADE |
|
PRIMARY KEY (promotion_id, category_id) |
Vrstva 5 — sklad a dostupnost (Medusa inventory model)
Filosofie
Fyzický stock je oddělen od obchodní dostupnosti. inventory_reservation je zdroj pravdy pro rezervace. reserved_quantity na inventory_level je cache jejich součtu, udržovaná DB triggerem — nikdy se nezapisuje přímo.
inventory_item
| Pole |
Typ |
Popis |
id |
uuid PK |
|
variant_id |
uuid FK CASCADE |
UNIQUE — 1:1 s variantou |
sku |
varchar UK |
|
requires_shipping |
boolean |
|
weight |
numeric(10,3) |
kg |
width |
numeric(10,2) |
cm |
height |
numeric(10,2) |
cm |
length |
numeric(10,2) |
cm |
created_at |
timestamptz |
|
updated_at |
timestamptz |
|
stock_location
| Pole |
Typ |
Popis |
id |
uuid PK |
|
code |
varchar UK |
|
name |
varchar |
|
is_central |
boolean |
Mapuje eshop_produkty_sklad_centrala |
inventory_level
| Pole |
Typ |
Popis |
id |
bigint PK |
|
inventory_item_id |
uuid FK RESTRICT |
|
location_id |
uuid FK RESTRICT |
|
stocked_quantity |
int |
Fyzicky na skladě |
reserved_quantity |
int |
Cache — udržuje DB trigger z inventory_reservation, nezapisovat přímo |
incoming_quantity |
int |
Na cestě (na_ceste) |
updated_at |
timestamptz |
|
|
|
UNIQUE(inventory_item_id, location_id) |
|
|
CHECK (stocked_quantity >= 0) |
|
|
CHECK (reserved_quantity >= 0) |
|
|
CHECK (incoming_quantity >= 0) |
Dostupné množství = stocked_quantity - reserved_quantity. CHECK (reserved <= stocked) záměrně chybí — inventura nebo vrácení dodavateli může dočasně vytvořit záporný rozdíl. Hlídáno aplikačně při vytváření rezervace.
inventory_reservation
Zdroj pravdy pro rezervace. Po každé změně (INSERT/UPDATE/DELETE) DB trigger přepočítá inventory_level.reserved_quantity jako SUM(quantity) WHERE status != 'released'.
| Pole |
Typ |
Popis |
id |
uuid PK |
|
inventory_item_id |
uuid FK RESTRICT |
|
location_id |
uuid FK RESTRICT |
|
order_id |
uuid NULL |
Vazba na objednávku |
quantity |
int |
|
status |
varchar |
pending, confirmed, released |
created_at |
timestamptz |
|
released_at |
timestamptz NULL |
|
|
|
CHECK (quantity > 0) |
supplier_availability
| Pole |
Typ |
Popis |
id |
bigint PK |
|
inventory_item_id |
uuid FK RESTRICT |
|
supplier_id |
uuid FK RESTRICT |
|
availability_code |
varchar |
in_stock, on_order, unavailable |
availability_text |
varchar |
Textový popis od dodavatele |
eta |
timestamptz |
Očekávané datum naskladnění |
updated_at |
timestamptz |
|
availability_text
| Pole |
Typ |
Popis |
id |
bigint PK |
|
inventory_item_id |
uuid FK CASCADE |
|
locale |
varchar |
|
text |
varchar |
Text zobrazený zákazníkovi |
display_type |
varchar |
in_stock, low_stock, out_of_stock, on_order |
|
|
UNIQUE(inventory_item_id, locale) |
Vrstva 6 — kategorie a kolekce
Filosofie
category_closure je zdroj pravdy pro stromové dotazy. parent_id je záměrně redundantní convenience cache. Nikdy nezapisovat přímo do category_closure — vždy přes procedury.
Closure table — PL/pgSQL procedury (povinné)
Přímý zápis do category_closure je zakázán. Veškerá manipulace se stromem jde přes tyto funkce.
category_insert_node(parent_id uuid, code varchar, ...) → uuid
-- Vloží uzel, zapíše všechny řádky closure (self + cesta od kořene)
category_move_subtree(node_id uuid, new_parent_id uuid)
-- Přesune podstrom: smaže staré closure záznamy, zapíše nové
category_delete_subtree(node_id uuid)
-- Smaže uzel a všechny potomky včetně closure záznamů
category_rebuild_closure()
-- Záchranná funkce — přepočítá celou closure tabulku z parent_id
category
| Pole |
Typ |
Popis |
id |
uuid PK |
|
parent_id |
uuid FK NULL |
Convenience cache |
code |
varchar UK |
|
legacy_id |
bigint |
Staré ID pro migraci |
depth |
int |
|
sort_order |
int |
|
is_active |
boolean |
|
is_internal |
boolean |
Nezobrazuje se na webu |
created_at |
timestamptz |
|
updated_at |
timestamptz |
|
category_closure
| Pole |
Typ |
Popis |
ancestor_id |
uuid FK |
|
descendant_id |
uuid FK |
|
depth |
int |
0 = uzel sám na sebe |
|
|
PRIMARY KEY (ancestor_id, descendant_id) |
|
|
CHECK (depth >= 0) |
category_translation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
category_id |
uuid FK CASCADE |
|
locale |
varchar |
|
name |
varchar |
|
description |
text |
|
slug |
varchar |
|
meta_title |
varchar |
|
meta_description |
varchar |
|
created_at |
timestamptz |
|
updated_at |
timestamptz |
|
|
|
UNIQUE(category_id, locale) |
|
|
UNIQUE(locale, slug) |
product_category_assignment
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
|
category_id |
uuid FK RESTRICT |
|
is_primary |
boolean |
|
sort_order |
int |
|
|
|
UNIQUE(product_id, category_id) |
|
|
UNIQUE(product_id) WHERE is_primary = true |
category_migration_map
| Pole |
Typ |
Popis |
id |
bigint PK |
|
legacy_id |
bigint |
Staré číselné ID |
new_category_id |
uuid FK |
|
legacy_pipe_value |
varchar |
\|123\| |
is_mapped |
boolean |
|
collection
| Pole |
Typ |
Popis |
id |
uuid PK |
|
code |
varchar UK |
|
type |
varchar |
manual, automated, seasonal |
is_active |
boolean |
|
sort_order |
int |
|
collection_translation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
collection_id |
uuid FK CASCADE |
|
locale |
varchar |
|
name |
varchar |
|
slug |
varchar |
|
description |
text |
|
|
|
UNIQUE(collection_id, locale) |
|
|
UNIQUE(locale, slug) |
collection_product
| Pole |
Typ |
Popis |
collection_id |
uuid FK CASCADE |
|
product_id |
uuid FK CASCADE |
|
sort_order |
int |
|
|
|
PRIMARY KEY (collection_id, product_id) |
| Pole |
Typ |
Popis |
id |
uuid PK |
|
product_id |
uuid FK CASCADE |
|
variant_id |
uuid FK NULL |
Nullable — médium může patřit variantě |
type |
enum |
image, video, document |
url |
varchar |
|
alt |
varchar |
|
sort_order |
int |
|
is_primary |
boolean |
|
created_at |
timestamptz |
|
|
|
UNIQUE(product_id) WHERE is_primary = true AND variant_id IS NULL |
|
|
UNIQUE(variant_id) WHERE is_primary = true |
variant_id musí patřit ke stejnému product_id. Hlídáno triggerem — FK to nezaručí.
product_relation
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
|
related_product_id |
uuid FK CASCADE |
|
relation_type |
varchar |
related, category_related |
sort_order |
int |
|
created_at |
timestamptz |
|
|
|
UNIQUE(product_id, related_product_id, relation_type) |
|
|
CHECK (product_id <> related_product_id) |
product_accessory
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
|
accessory_product_id |
uuid FK CASCADE |
|
sort_order |
int |
|
|
|
UNIQUE(product_id, accessory_product_id) |
|
|
CHECK (product_id <> accessory_product_id) |
product_merchandising
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
UNIQUE — 1:1 s produktem |
is_featured |
boolean |
|
is_bestseller |
boolean |
|
is_recommended |
boolean |
|
is_sale |
boolean |
|
priority |
int |
|
badge_label |
varchar |
Štítek (stitek) |
in_slider_1 |
boolean |
|
in_slider_2 |
boolean |
|
slider_1_priority |
int |
|
slider_2_priority |
int |
|
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
|
channel |
varchar |
google, heureka, zbozi |
key |
varchar |
|
value |
text |
|
created_at |
timestamptz |
|
updated_at |
timestamptz |
|
|
|
UNIQUE(product_id, channel, key) |
product_compliance
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
|
fee_type |
varchar |
phe, recycling |
fee_code |
varchar |
|
linked_product_id |
uuid FK NULL |
Přidružená položka do košíku |
fee_amount |
int |
V haléřích |
|
|
CHECK (fee_amount >= 0) |
|
|
CHECK (product_id <> linked_product_id) |
product_search_document
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
|
locale |
varchar |
|
search_text |
text |
Fulltext — název, kód, EAN, výrobce |
structured_data |
jsonb |
Cena, dostupnost, facety |
indexed_at |
timestamptz |
|
|
|
UNIQUE(product_id, locale) |
product_search_dirty
Dirty queue pro async reindex. Trigger pouze zapíše sem, worker provede reindex.
| Pole |
Typ |
Popis |
id |
bigint PK |
|
product_id |
uuid FK CASCADE |
|
locale |
varchar |
|
reasons |
varchar[] |
{price_changed, stock_changed, content_changed, …} |
created_at |
timestamptz |
|
processed_at |
timestamptz NULL |
NULL = čeká |
attempts |
int |
Počet pokusů |
last_error |
text NULL |
Poslední chyba |
locked_at |
timestamptz NULL |
Kdy si job vzal worker |
locked_by |
varchar NULL |
Identifikace workeru |
|
|
UNIQUE(product_id, locale) WHERE processed_at IS NULL |
Při vícenásobné změně: INSERT ... ON CONFLICT DO UPDATE SET reasons = array(SELECT DISTINCT unnest(reasons || EXCLUDED.reasons)) — distinct merge zamezí duplicitám v array.
Audit log
entity_audit_log
| Pole |
Typ |
Popis |
id |
bigint PK |
|
entity_type |
varchar |
product, price, inventory_level, category, translation, … |
entity_id |
text |
Univerzální — pokryje uuid i bigint PK |
action |
varchar |
insert, update, delete |
old_data |
jsonb |
|
new_data |
jsonb |
|
actor_type |
varchar |
editor, import, system |
actor_id |
uuid NULL |
|
request_id |
uuid NULL |
Pro dohledání konkrétního HTTP requestu nebo importní dávky |
source |
varchar NULL |
api, admin, import, cron |
created_at |
timestamptz |
|
Migrační tracking
migration_batch
| Pole |
Typ |
Popis |
id |
uuid PK |
|
source |
varchar |
Název zdrojového systému / tabulky |
started_at |
timestamptz |
|
finished_at |
timestamptz NULL |
|
status |
varchar |
running, done, failed |
total_rows |
int |
|
processed_rows |
int |
|
migration_error
| Pole |
Typ |
Popis |
id |
bigint PK |
|
batch_id |
uuid FK |
|
entity_type |
varchar |
|
legacy_id |
varchar |
|
error |
text |
|
raw_data |
jsonb |
|
created_at |
timestamptz |
|
ON DELETE pravidla — přehled
| Vztah |
Pravidlo |
product → product_translation |
CASCADE |
product → product_variant |
CASCADE |
product → product_slug |
CASCADE |
product → product_supplier_feed |
CASCADE |
product → product_category_assignment |
CASCADE |
product → product_media |
CASCADE |
product → product_search_document |
CASCADE |
product → order snapshot |
nikdy FK na živý produkt |
brand → product |
RESTRICT |
supplier → product_supplier_feed |
RESTRICT |
category → product_category_assignment |
RESTRICT |
option_group → option_value |
CASCADE |
option_value → product_variant_option_value |
RESTRICT |
price_set → price |
CASCADE |
inventory_item → inventory_level |
CASCADE |
promotion → promotion_product/brand/category |
CASCADE |
Vrstva 8 — order item snapshot
Filosofie
Objednávka nesmí spoléhat na to, že produkt zůstane nezměněný. Order item ukládá flat základní sloupce pro dotazování a reporting, plus snapshot_detail jsonb pro úplný stav v době objednávky. Nikdy FK na živý product nebo product_variant z order item — pouze product_id a variant_id jako referenční hodnoty bez FK constraint.
order_item
| Pole |
Typ |
Popis |
id |
uuid PK |
|
order_id |
uuid FK CASCADE |
|
product_id |
uuid NULL |
Reference bez FK — produkt může být smazán |
variant_id |
uuid NULL |
Reference bez FK |
product_code |
varchar |
Snapshot product.code |
variant_code |
varchar NULL |
Snapshot product_variant.code |
ean |
varchar NULL |
Snapshot EAN |
name |
varchar |
Snapshot názvu v jazyce objednávky |
unit_price |
int |
Cena za kus v haléřích v době objednávky |
tax_rate |
numeric(5,2) |
Sazba DPH v době objednávky |
quantity |
int |
|
currency_code |
varchar |
|
locale |
varchar |
Jazyk objednávky |
snapshot_detail |
jsonb |
Úplný snapshot — kategorie, výrobce, atributy, aliasy, feed data |
created_at |
timestamptz |
|
|
|
CHECK (quantity > 0) |
|
|
CHECK (unit_price >= 0) |
snapshot_detail obsahuje vše ostatní co se může změnit: brand_name, category_path, option_values, attributes, slug, image_url. Struktura je volná — schema dokumentovat v kódu, ne v DB.
Doporučené pořadí implementace
1. product, brand, supplier, product_supplier_feed, product_supplier_feed_raw
2. product_translation, brand_translation, product_slug
3. product_variant, option_group, option_value, product_variant_option_value
attribute, product_attribute_value
4. region, price_set, tax_rate
procedura price_upsert()
promotion, promotion_product, promotion_brand, promotion_category
5. inventory_item, stock_location, inventory_level, inventory_reservation
trigger: inventory_reservation → reserved_quantity cache
supplier_availability, availability_text
6. category + category_closure
PL/pgSQL: category_insert_node, category_move_subtree,
category_delete_subtree, category_rebuild_closure
category_translation, product_category_assignment, category_migration_map
7. collection, product_media, product_relation, product_accessory
product_merchandising, product_external_metadata, product_compliance
8. product_search_document, product_search_dirty
9. order_item (snapshot)
10. entity_audit_log
11. migration_batch, migration_error
Zbývající otevřené otázky
- Constraint překryvu cen —
price_upsert() procedura ověřuje překryvy: použít EXCLUDE USING gist (vyžaduje btree_gist extension) nebo explicitní SELECT + chyba?
product_supplier_feed_raw retence — automatické mazání starých debug záznamů: cron job nebo partition by month?
- Order item / order struktura —
order a order_item jsou zatím mimo scope tohoto dokumentu, order_item je zde jen jako interface contract.