Jak Deedspan funguje a jak ho provozovat

Deedspan je integrační framework pro Magento se zdrojovým kódem k dispozici. Tady najdete, z čeho se instalace skládá, co obsahuje projekt, základní pojmy, první integraci, příkazy, provoz a rozšíření.

Jak Deedspan funguje

Deedspan přenáší data mezi e-shopem a jeho ERP, skladem, účetnictvím, PIM nebo CRM. Každý systém se napojuje na společný datový model nezávislý na e-shopu. Každé prostředí obchodníka má vlastní instalaci, staging a produkce jsou dvě instalace stejného projektu.

Co se stane se záznamem

  1. Spouštěč zahájí běh flow.
  2. Načte se zdroj: soubory, výsledek dotazu, odpověď API nebo data z webhooku.
  3. Každý řádek se namapuje na záznam společného modelu, třeba stock_level.
  4. Volitelné PHP hooky záznamy upraví, rozdělí nebo přeskočí.
  5. Nezměněné záznamy se označí a neposílají se.
  6. Cíl ověří, že odkazovaná SKU existují.
  7. Konektor zapisuje hromadně, nejvýš 1 000 záznamů v dávce.
  8. U každého záznamu se uloží výsledek.

Z čeho se instalace skládá

ČástK čemu slouží
AplikaceJádro (deedhold/deedspan, Symfony bundle) a váš projekt
WorkerySpouštějí běhy, zpracovávají dávky a běhy dokončují; může jich běžet víc
PlánovačSpouští flow podle cronu; běží právě jeden
MariaDBBěhy, výsledky záznamů, co se naposledy odeslalo, data zpráv, šifrovaná připojení
RabbitMQPovinný; nese všechny fáze běhu
Archiv souborůŠifrované soubory na disku nebo v úložišti kompatibilním s S3
Modul pro MagentoZachytávání změn, podepsaná upozornění, hromadný zápis skladu, sloupec v přehledu objednávek

Co obsahuje projekt

Malý projekt pro Composer založený z kostry. Je v git repozitáři obchodníka a nasazuje ho CI.

acme-integrations/

deedspan.yaml     připojení a balíčky typů záznamů
flows/            jeden YAML soubor na každý flow
records/          vlastní typy záznamů a pole navíc
connectors/       konektory jen pro tento projekt
lookups/          převodní tabulky v CSV
src/              PHP hooky, operace, přenosy
samples/          vzorové soubory pro zkoušky nanečisto
composer.json     jádro a doplňky; composer.lock je v gitu
Dockerfile        runtime image a vaše balíčky
compose.yaml      app, worker, scheduler, MariaDB, RabbitMQ

deedspan.yaml

Připojení k ostatním systémům a vypnuté balíčky typů záznamů. U přístupových údajů je jen název proměnné prostředí, takže se hesla nedostanou do gitu, do flow ani k hookům.

deedspan.yaml

connections:
  erp-ftp:
    settings: { host: sftp.erp.example, user: deedspan, root: /upload }
    secrets: { password: ERP_FTP_PASSWORD }
  shop:
    settings: { url: "https://shop.example.com" }
    secrets:
      consumer_key: MAGENTO_CONSUMER_KEY
      consumer_secret: MAGENTO_CONSUMER_SECRET
      token: MAGENTO_TOKEN
      token_secret: MAGENTO_TOKEN_SECRET
records:
  disabled: [pricing]

Převodní tabulky

CSV s hlavičkou key,value, ze kterého čte lookup().

lookups/warehouses.csv

key,value
01,main
02,east

Základní pojmy

Flow a spouštění

Každá integrace se skládá z jednoho nebo více flow. Flow přenáší jeden typ záznamů jedním směrem: zdroj, mapování, volitelné hooky a cíl. Uvádí, kterému systému data patří (owner:), a dva flow nesmí zapisovat stejné pole do stejného cíle.

flows/prices-from-erp.yaml

flow: prices-from-erp
owner: erp
record: price
triggers:
  schedule: "0 * * * *"
source:
  transport: sftp
  connection: erp-ftp
  path: /export/prices_*.csv
  format: { type: csv, delimiter: ";" }
map:
  sku:      "trim(row['sku'])"
  channel:  "lower(trim(row['channel']))"
  base:     "decimal(row['price'], ',')"
  currency: "upper(trim(row['currency']))"
target:
  connector: magento
  operation: price.update
  connection: shop
  channels: { eu: eu_store, uk: uk_store }
rules: { unknown_references: fail, compare_with: target }

Flow se spouští jedním ze čtyř způsobů, které se zapisují pod triggers:.

SpouštěčFlow se spustí, když
schedulenastane čas podle cronu, třeba "*/15 * * * *"
shop_signalmodul v Magentu ohlásí změněné objednávky nebo produkty
webhookjiný systém zavolá podepsaný POST /webhooks/<name>
ručněvždycky: z příkazové řádky, přes API, nanečisto nebo jako opakování

Webhook má dva režimy. V režimu signal jen oznámí, co se změnilo, a flow si aktuální data načte sám, takže duplicitní nebo opožděná volání nevadí. V režimu payload nese data přímo volání; uloží se a flow je čte přes source: { transport: webhook }. Webhooky se občas ztratí, proto k nim nechte běžet i pomalý plán:

triggers

triggers:
  webhook: { name: payments, mode: payload }
  schedule: "0 3 * * *"     # noční kontrola
KlíčObsahuje
sourcePřenos a formát, nebo stream konektoru. group_by spojí sousední řádky do jednoho záznamu.
mapJeden výraz na každé pole.
hooksPHP třídy v bodech after_read, after_map, before_send, on_error.
targetOperace konektoru, nebo přenos, který zapíše soubory, zavolá webhook nebo naplní mezitabulku.
rulesunknown_references, compare_with a bezpečnostní limity jako max_change_share.

Každý flow se kontroluje při načtení projektu, takže chyby zachytí už CI.

Záznamy

Záznam je jedna přenášená položka, třeba stav TENT-200 na skladovém místě main. Typy záznamů jsou definice v YAML s klíčem a typovanými poli.

TypKlíčHlavní pole
stock_levelsku + locationmnožství, skladem, datum naskladnění
pricesku + channelzákladní cena, měna, množstevní a akční ceny
productskutyp, sada atributů, kanály, kategorie, viditelnost, varianty, související
categorynázevpřeložené názvy
orderčíslo objednávkyzákazník, adresy, položky, součty, platba, doprava, stav
order_updatečíslo objednávkystav, zásilka se sledovacím číslem, faktura
  • Záznam nese jen pole, která flow mapuje. Chybějící pole zůstane beze změny, null ho vymaže a prázdná buňka v CSV data nikdy nesmaže.
  • Pole může mít hodnotu pro každý prodejní kanál nebo jazyk.
  • extends: přidá pole k typu z jádra.
  • references: product zajistí kontrolu, že SKU v cíli existuje.

Konektory

ČástMožnosti
Přenossftp, ftp, local, http, soap, mssql a další databáze, email, s3, webhook
Formátcsv (jakékoli kódování), xml, json, jsonl, pevná šířka
Konektor systémumagento (jádro); Pohoda, Helios, Fakturoid (doplňky); vlastní

Operace pro Magento: stock.update, price.update, product.update, product.upsert, category.update. Zapisují přes REST API Magenta podepsané OAuth 1.0a a přes tenký modul Deedspanu. Do databází cizích systémů se zapisuje jen do mezitabulek nebo uložených procedur.

Výrazy

Každé mapované pole má jeden výraz nad zdrojovým řádkem row. Výrazy se nedostanou k souborům, síti ani databázi a kontrolují se při načtení projektu.

SkupinaFunkce
Texttrim, upper, lower, replace, split, extract, substr, pad, join, regex_match, regex_replace
Číslaint, decimal, round, abs, min, max
Datumdate, format_date, add_days, now
Logikacoalesce, empty, in, ? :, ??
Převodylookup, xref, setting
Kolekcemap, filter, first, count, sum

Když výraz selže, skončí chybou jen jeho záznam:

záznam s chybou

map.quantity: int(): "many" is not a whole number. (expression: max(0, int(row['qty'])))

Běhy a výsledky

Každé spuštění flow je běh. Dělí se na fáze v RabbitMQ a na dávky nejvýš po 1 000 záznamech.

  • Zápis je idempotentní podle klíče, třeba TENT-200|main.
  • Posílají se jen změněné záznamy; compare_with: target porovnává s aktuálním stavem v cíli.
  • Od jednoho flow běží vždy jen jeden běh.
  • Běh, který by toho změnil nebo smazal příliš, počká na schválení.
VýsledekVýznam
writtenOdesláno a přijato
unchangedStejné jako minule, neodesláno
skippedVynechal ho hook nebo pravidlo
failedOdmítnuto; uloží se pole a chyba
deletedSmazáno, protože ve zdroji už není

Běh skončí jako completed, completed_with_failures nebo failed.

Jak začít

Stačí Docker, PHP běží v kontejneru.

  1. Založte projekt

    terminál

    $ composer create-project deedhold/deedspan-skeleton acme-integrations
    $ cd acme-integrations
    $ cp .env.example .env.local     # doplňte nastavení a hesla
    $ docker compose up -d --build
  2. Napište flow

    Začněte s target: { transport: none }: záznamy se namapují a zkontrolují, ale nikam se nepošlou.

    flows/stock-from-erp.yaml

    flow: stock-from-erp
    owner: erp
    record: stock_level
    source:
      transport: sftp
      connection: erp-ftp
      path: /export/stock_*.csv
      format: { type: csv, delimiter: ";", encoding: windows-1250 }
    map:
      sku:      "trim(row['sku'])"
      location: "lookup('warehouses', row['warehouse'])"
      quantity: "max(0, int(row['qty']))"
    target: { transport: none }
  3. Zkontrolujte projekt

    terminál

    $ bin/console deedspan:flow:validate
    Record types: category, price, product, stock_level
    Connectors: magento
    Flows: stock-from-erp
  4. Vyzkoušejte ho nanečisto

    Na cíl ani databázi nesáhne. Když některý záznam selže, skončí s kódem 1, takže se hodí do CI.

    terminál

    $ bin/console deedspan:flow:dry-run stock-from-erp samples/stock.csv
    TENT-200|main: sku=TENT-200, location=main, quantity=15
    BOOT-42|east: sku=BOOT-42, location=east, quantity=3
  5. Spusťte ho dvakrát

    Druhý běh už nic nového nenajde.

    terminál

    $ bin/console deedspan:flow:run stock-from-erp --sync
    Run 01a1d5f1 completed: written 2, unchanged 0, skipped 0, failed 0.
    $ bin/console deedspan:flow:run stock-from-erp --sync
    Run 01a1d5f9 completed: written 0, unchanged 2, skipped 0, failed 0.
  6. Posílejte sklad do Magenta podle plánu

    flows/stock-from-erp.yaml

    triggers: { schedule: "*/15 * * * *" }
    target:
      connector: magento
      operation: stock.update
      connection: shop
      locations: { main: default, east: east }   # zdroj MSI pro každé místo

Příkazová řádka

Každý příkaz nejdřív načte celý projekt. Logy jdou na stderr jako JSON, výstup na stdout.

PříkazCo dělá
deedspan:flow:validateZkontroluje typy záznamů, konektory a flow
deedspan:flow:dry-run <flow> <sample>Ukáže, co by se zapsalo; --json vypíše celý report
deedspan:flow:run <flow>Zařadí běh do fronty; --sync ho provede hned, --force i přes běžící běh
messenger:consume control bulkWorker
messenger:consume scheduler_flowsPlánovač (jen jeden)
debug:schedulerČasy dalších běhů
messenger:statsPočet zpráv ve frontách
messenger:failed:show / retryZprávy, které vyčerpaly pokusy
doctrine:migrations:migrateTabulky v databázi; po každém upgradu

Stejným nástrojem se dají spravovat připojení, nasazovat a vracet verze flow, znovu odesílat chyby, schvalovat pozastavené běhy a exportovat celou instalaci. Totéž umí API pro vaše nástroje i portál Deedholdu.

Provoz

Nasazení
Docker Compose na linuxovém serveru. CI zkontroluje a vyzkouší flow, sestaví image a nasadí ho. Přístup k placeným doplňkům jde jako build secret.
Prostředí
Nejdřív staging, produkce dostane stejný image po výslovném schválení.
Verze
Každé nasazení uloží verzi flow, vrátit ji jde jedním příkazem. Upgrade jádra: composer update deedhold/deedspan.
Hesla
Šifrovaně v databázi instalace; nikdy v gitu, hooky k nim nemají přístup.
Logy
JSON na stderr s během, flow, verzí, fází, dávkou a klíčem záznamu.
Upozornění
Podepsané webhooky a e-mail: run.failed, record.failed, flow.silent, approval.needed a další.
Telemetrie
OpenTelemetry přes OTLP, heartbeat URL pro každý flow, Sentry SDK.
Administrace Magenta
Sloupec Integrace v přehledu objednávek; u neúspěšné objednávky chyba a tlačítko pro opakování.
Chyby
Vadný řádek shodí jen svůj záznam. Neúspěšné zprávy se zkusí znovu třikrát (po 1, 3 a 9 s). Neúspěšné záznamy čekají na seznamu, odkud je pošlete znovu.
Uchovávání
Data a soubory 30 dní šifrovaně, potom historie a výsledky.
Odchod
Export přidá k repozitáři zálohu databáze a archiv; stejný Compose soubor spustíte kdekoli.

stderr

{"level_name":"WARNING","message":"Record failed.","context":{"event":"record.failed"},
 "extra":{"run_id":"01a1d668-1076-746d-9a19","flow":"stock-from-erp","stage":"send","chunk":3,"record_key":"STOVE-9|east"}}

Rozšíření

Nejdřív YAML a výrazy, PHP jen tam, kde nestačí. Instalace patří jednomu obchodníkovi, takže PHP z projektu běží bez sandboxu.

Vlastní konektory

Manifest connector.yaml nad obecnými přenosy, PHP jen v pojmenovaných místech. Konektor je ve složce connectors/, nebo jako balíček pro Composer.

connectors/trailhub/connector.yaml

schema_version: 1
connector: trailhub
version: 1.0.0
connection:
  url:   { type: string }
  token: { type: string, secret: true }
api:
  base_url: "{{ connection.url }}/api"
  auth: { type: bearer, token: "{{ connection.token }}" }
operations:
  claim.create:
    method: POST
    path: /claims
    class: App\Connector\CreateClaim
    record: warranty_claim

Třída operace dostane přihlášeného klienta API, nikdy samotné heslo, a vrací výsledek pro každý záznam:

RecordOperation

interface RecordOperation
{
    public function checkOptions(array $options): void;

    /** @return list<RecordResult> one per record, in the same order */
    public function send(array $records, OperationContext $context): array;
}

Volitelně: ComparesWithTarget pro compare_with: target, #[AsReferenceIndex] pro kontrolu odkazů, #[AsTransport] a #[AsFormat] pro nové přenosy a formáty.

PHP hooky

Hook dostane jeden záznam a vrátí ho, vrátí několik záznamů, nebo null, když se má přeskočit. K dispozici má nastavení, převodní tabulky, tabulku křížových odkazů a logger, nikdy ne připojení.

src/Hook/SkipZeroStock.php

final class SkipZeroStock implements Hook
{
    public function __invoke(Record $record, HookContext $context): ?Record
    {
        return '0' === $record->get('quantity') ? null : $record;
    }
}

Ve flow ho zapíšete jako hooks: { before_send: App\Hook\SkipZeroStock }.

Vlastní typy záznamů

Běhy, opakování, porovnání změn i zkoušky nanečisto fungují pro vlastní typy stejně.

records/warranty_claim.yaml

schema_version: 1
record: warranty_claim
version: 1.0.0
key: [claim_no]
fields:
  claim_no:   { type: string, required: true }
  order_no:   { type: string, required: true }
  sku:        { type: string, references: product }
  created_at: { type: date }

Typy polí: string, decimal, int, bool, date, enum, object, list.

Správa od Deedholdu

Integrace vám nastavíme a staging i produkci provozujeme za pevný měsíční poplatek za každý napojený systém. Kód zůstává vám.