From 6297bbf480c7e122db266953d5591470090d0341 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:01:57 +0200 Subject: [PATCH] Vystup z vetve plati i za podminkou, samostatny krok Zalozit kontakt Bezny postup nesel poskladat z kroku: najdi podle ICO, kdyz neni zkus mail, kdyz porad neni zaloz - ID dava jednou jedna vetev a jednou druha, ale rozsah vystupy z vetvi za podminku nepoustel. Slucovat kvuli tomu hledani a zakladani do jednoho kroku bylo obejiti nasi chyby, ne reseni. - Vystup z vetve je za podminkou k dispozici, jen jako nepovinny. Ze muze chybet, se neztratilo: builder to u pole ukaze a pri behu se dosadi prazdno. - Vystup se stejnym jmenem uz z nabidky nemaze ten starsi. Po druhem hledani kontaktu zmizelo ID z prvniho, tedy to, co je v tu chvili potreba. Odkaz se jmenem kroku je jednoznacny. - Duplicitni jmena u vystupu kroku uz nejsou nedodelek. Konflikt zustava mezi parametry spoustece, kde zadny prefix neni. - Novy krok Zalozit kontakt. Nic nedohledava, hledani je vlastni krok. Najit nebo zalozit zustava pro toho, komu staci jistota jednim krokem. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/05-dashboard-a-builder.md | 22 +++++ documentation/99-zmeny.md | 31 +++++++ scripts/idoklad.create-contact.js | 105 ++++++++++++++++++++++++ src/data/automationStore.ts | 8 +- src/data/flowScope.ts | 86 +++++++++++++------ web/src/lib/flow.ts | 38 +++++---- 6 files changed, 249 insertions(+), 41 deletions(-) create mode 100644 scripts/idoklad.create-contact.js diff --git a/documentation/05-dashboard-a-builder.md b/documentation/05-dashboard-a-builder.md index 59f3650..9c210be 100644 --- a/documentation/05-dashboard-a-builder.md +++ b/documentation/05-dashboard-a-builder.md @@ -179,6 +179,28 @@ Builder i katalog ji vezmou automaticky. | Historie behu a logy | prazdne, chybi runtime | | Drag and drop | presouvani je zatim tlacitky nahoru a dolu | +## Co je videt v kterem kroku + +Krok vidi parametry spoustece a vystupy kroku pred nim. **Vystupy z vetvi +podminky plati i za podminkou**, jen jako nepovinne - probehla prece jen jedna +vetev. + +Diky tomu jde poskladat bezny postup, kde hodnotu dava jednou jedna vetev +a jednou druha: + +``` +Najit kontakt (podle ICO) +podminka: nenalezeno? + ano -> Najit kontakt (podle e-mailu) + podminka: porad nenalezeno? + ano -> Zalozit kontakt +Vlastni skript: partnerId = prvni vyplnene z ICO / mailu / zalozeni +``` + +Odkaz se jmenem kroku je jednoznacny (`{{st_ico.contactId}}`), takze dva kroky +se stejne pojmenovanym vystupem si neprekazi. Hole jmeno (`{{contactId}}`) +znamena ten posledni vystup, ktery probehl. + ## Prace nad celym modelem Odesilatel neposila ploche telo. Objednavka ze Shoptetu ma zanoreni, ceny diff --git a/documentation/99-zmeny.md b/documentation/99-zmeny.md index fc1d32e..8b2d065 100644 --- a/documentation/99-zmeny.md +++ b/documentation/99-zmeny.md @@ -2,6 +2,37 @@ Nejnovejsi nahore. +## 2026-08-20 - vystup z vetve plati i za podminkou + +Pri stavbe cesty "objednavka -> faktura" vyslo najevo, ze se bezny postup neda +poskladat z kroku: **najdi zakaznika podle ICO, kdyz neni, zkus e-mail, kdyz +porad neni, zaloz ho** konci tim, ze ID odberatele dava jednou jedna vetev +a jednou druha. Rozsah ale vystupy z vetvi za podminku nepoustel, takze se dal +pouzit jen tak, ze se hledani a zakladani sloucilo do jednoho kroku. + +To bylo obejiti nasi vlastni chyby, ne reseni. + +### Zmeneno + +- **Vystup z vetve je za podminkou k dispozici**, jen jako nepovinny + (`required: false`). Ze hodnota muze chybet, se neztratilo - builder to u pole + ukaze a pri behu se dosadi prazdno, stejne jako u ceho jineho, co neprislo. +- **Vystup se stejnym jmenem uz z nabidky nemaze ten starsi.** Po druhem hledani + kontaktu zmizelo ID z toho prvniho, tedy presne to, co je v tu chvili potreba. + Odkaz se jmenem kroku (`{{st_ico.contactId}}`) je jednoznacny, duvod k mazani + neni. Hole jmeno (`{{contactId}}`) porad znamena ten posledni. +- **Duplicitni jmena u vystupu kroku uz nejsou nedodelek.** Dva kroky, ktere + vraci `contactId`, jsou bezna vec. Konflikt zustava tam, kde opravdu je: + mezi parametry spoustece, kde zadny prefix neni. + +### Pridano + +- **Krok Zalozit kontakt** (`idoklad.create-contact`). Nic nedohledava - kdyz uz + kontakt existuje, vznikne druhy, a to je spravne chovani teto operace. Hledani + je vlastni krok, takze si strom sam rekne, kdy hledat a kdy zakladat. +- `idoklad.upsert-contact` (Najit nebo zalozit) zustava pro toho, komu staci + "chci mit jistotu, ze tam je". Obojí ma smysl, ani jedno nenahrazuje druhe. + ## 2026-08-20 - vlastni skripty firmy: prevod dat v JS Klikaci pravidla jsou u peti poli rychlejsi, ale u modelu objednavky je jich diff --git a/scripts/idoklad.create-contact.js b/scripts/idoklad.create-contact.js new file mode 100644 index 0000000..e91135c --- /dev/null +++ b/scripts/idoklad.create-contact.js @@ -0,0 +1,105 @@ +/** + * iDoklad: zalozeni odberatele. + * + * Sluzba: https://services.csbot.cz/apps/idoklad + * Endpoint: POST /contacts + * + * **Nic nedohledava.** Kdyz uz kontakt existuje, vznikne druhy se stejnym ICO - + * a to je spravne chovani teto operace, ne chyba. Hledani je vlastni krok + * (`Najit kontakt`), takze si strom sam rekne, kdy hledat a kdy zakladat: + * + * 1. Najit kontakt (podle ICO) + * 2. podminka: nenalezeno? + * ano -> Najit kontakt (podle e-mailu) + * 3. podminka: porad nenalezeno? + * ano -> Zalozit kontakt + * + * Kdyz nekomu staci "chci mit jistotu, ze tam je", pouzije jednim krokem + * `Najit nebo zalozit kontakt`. Obojí ma smysl a ani jedno nenahrazuje druhe. + */ + +export const manifest = { + id: 'idoklad.create-contact', + name: 'Založit kontakt', + description: + 'Založí v iDokladu nového odběratele a vrátí jeho ID. Nic nedohledává - ' + + 'na to je krok Najít kontakt.', + timeoutMs: 25000, + + inputs: [ + { + id: 'companyName', + label: 'Název odběratele', + type: 'string', + required: true, + hint: 'U firmy název, u fyzické osoby jméno.', + }, + { + id: 'identificationNumber', + label: 'IČO', + type: 'string', + required: false, + pattern: '^[0-9]{6,12}$', + hint: 'Jen číslice. U fyzické osoby zůstane prázdné.', + }, + { id: 'vatNumber', label: 'DIČ', type: 'string', required: false }, + { id: 'email', label: 'E-mail', type: 'string', required: false }, + { id: 'phone', label: 'Telefon', type: 'string', required: false }, + { id: 'street', label: 'Ulice', type: 'string', required: false }, + { id: 'city', label: 'Město', type: 'string', required: false }, + { id: 'postalCode', label: 'PSČ', type: 'string', required: false }, + { + id: 'countryCode', + label: 'Země', + type: 'string', + required: false, + default: 'CZ', + hint: 'Dvoupísmenný kód. Nevyplněno = CZ.', + }, + ], + + outputs: [ + { id: 'contactId', label: 'ID kontaktu', type: 'number', required: true }, + { id: 'companyName', label: 'Název odběratele', type: 'string', required: true }, + { id: 'contact', label: 'Celý kontakt', type: 'object', required: true }, + ], +}; + +export async function run(inputs, ctx) { + const { unwrap, pick, text, num, need } = ctx.util; + + const companyName = text(inputs.companyName); + if (!companyName) { + ctx.fail('Bez názvu odběratele iDoklad kontakt nezaloží.'); + } + + /* + * Posilaji se jen vyplnena pole. Prazdny retezec neni totez co "neuvedeno" - + * u DIC by z nej iDoklad udelal platce DPH s prazdnym cislem. + */ + const body = { + CompanyName: companyName, + CountryId: null, + ...(text(inputs.identificationNumber) + ? { IdentificationNumber: text(inputs.identificationNumber) } + : {}), + ...(text(inputs.vatNumber) ? { VatIdentificationNumber: text(inputs.vatNumber) } : {}), + ...(text(inputs.email) ? { Email: text(inputs.email) } : {}), + ...(text(inputs.phone) ? { Mobile: text(inputs.phone) } : {}), + ...(text(inputs.street) ? { Street: text(inputs.street) } : {}), + ...(text(inputs.city) ? { City: text(inputs.city) } : {}), + ...(text(inputs.postalCode) ? { PostalCode: text(inputs.postalCode) } : {}), + ...(text(inputs.countryCode) ? { CountryCode: text(inputs.countryCode) } : {}), + }; + + const response = await ctx.http.post('/contacts', body); + const created = unwrap(response.body); + + ctx.log(`Kontakt ${companyName} založen.`); + + return { + contactId: need(num(pick(created, 'id')), 'ID založeného kontaktu'), + companyName: text(pick(created, 'companyName', 'name')) || companyName, + contact: created && typeof created === 'object' ? created : {}, + }; +} diff --git a/src/data/automationStore.ts b/src/data/automationStore.ts index 6e91f50..2461b0b 100644 --- a/src/data/automationStore.ts +++ b/src/data/automationStore.ts @@ -395,8 +395,12 @@ export function collectFlowIssues(flow: AutomationFlow): string[] { } } - walk(step.yes, new Set(roots)); - walk(step.no, new Set(roots)); + /* + * Koreny z vetvi se **nesou dal**. Vystup z vetve je za podminkou + * k dispozici, jen nepovinne - viz data/flowScope.ts. + */ + walk(step.yes, roots); + walk(step.no, roots); } }; walk(flow.steps, sampleRoots); diff --git a/src/data/flowScope.ts b/src/data/flowScope.ts index 473960f..9e91bd3 100644 --- a/src/data/flowScope.ts +++ b/src/data/flowScope.ts @@ -5,10 +5,19 @@ * vrati `customerKnown`, "zalozit ticket" vrati `newTicketId`. Podminka i sablona * se pak muzou odkazat na cokoliv, co uz **pred nimi** vzniklo. * - * Dve pravidla, na kterych to stoji: - * - krok vidi jen to, co je pred nim ve stejne sekvenci, a to, co videl jeho rodic, - * - vetev podminky NEPRIDAVA nic do sekvence za podminkou. Vetev totiz nemusela - * probehnout, takze spolehat se na jeji vystup by byla past. + * Pravidla: + * - krok vidi to, co je pred nim ve stejne sekvenci, a to, co videl jeho rodic, + * - vystupy z vetvi podminky jsou za podminkou **k dispozici, ale nepovinne**. + * + * Druhe pravidlo bylo drive prisnejsi: vetev nepridavala nic. Znelo to opatrne, + * ale znamenalo to, ze zcela bezny postup nesel poskladat z kroku - "najdi + * zakaznika, a kdyz neni, zaloz ho, pak pouzij jeho ID" konci tim, ze ID dava + * jednou jedna vetev a jednou druha. Prisnost se tim neziskala, jen se obchazela + * tim, ze se dva kroky slouci do jednoho. + * + * Ze hodnota **muze chybet**, se neztratilo: pole je `required: false`, takze + * builder u nej ukaze, ze vznika jen ve vetvi. Kdo se na nej odkaze, dostane pri + * behu prazdno - stejne jako u ceho jineho, co neprislo. * * POZOR: stejna logika je na klientovi v web/src/lib/flow.ts. * Server je autorita, kopie na klientovi existuje jen kvuli UI. @@ -46,15 +55,46 @@ export function collectScopes(flow: AutomationFlow): FlowScopes { const triggerFields = flow.trigger?.fields ?? []; for (const field of triggerFields) all.set(field.id, field); - const walk = (steps: FlowStep[], inherited: TriggerField[]) => { + /** + * Projde sekvenci a vrati pole, ktera v ni **vznikla**. + * + * Vraci se jen pribytek, ne cely rozsah. Volajici z nej sklada, co je videt + * za podminkou, a zdedena pole uz tam samozrejme jsou. + */ + const walk = (steps: FlowStep[], inherited: TriggerField[]): TriggerField[] => { let available = inherited; + const produced: TriggerField[] = []; + + /** + * Prida vystupy do rozsahu. + * + * **Nic se neodebira.** Driv novejsi vystup starsi se stejnym jmenem + * z nabidky vyhodil, takze po druhem hledani kontaktu zmizelo ID z toho + * prvniho - presne to, co clovek v tu chvili potrebuje. Odkaz s jmenem + * kroku (`{{st_ico.contactId}}`) je jednoznacny, takze duvod k mazani neni. + * + * Hole jmeno (`{{contactId}}`) porad znamena ten posledni vystup; tak to + * dela runtime a tady se to neresi. + */ + const add = (outputs: TriggerField[]) => { + for (const output of outputs) all.set(output.id, output); + available = [...available, ...outputs]; + produced.push(...outputs); + }; for (const step of steps) { byStep.set(step.id, available); if (step.kind === 'condition') { - walk(step.yes, available); - walk(step.no, available); + const fromYes = walk(step.yes, available); + const fromNo = walk(step.no, available); + + /* + * Za podminkou plati obojí, ale jako nepovinne - probehla jen jedna + * vetev. Kdyz obe vetve daji pole stejneho jmena, je to ten bezny + * pripad "jednou najdeno, jednou zalozeno" a duplicita to neni. + */ + add([...fromYes, ...fromNo].map((field) => ({ ...field, required: false }))); continue; } @@ -74,29 +114,17 @@ export function collectScopes(flow: AutomationFlow): FlowScopes { * Po smycce je k dispozici seznam vysledku za kazdou polozku. Prave to * se posila dal jako radky objednavky nebo faktury. */ - const results: TriggerField[] = [ + add([ { id: `${step.id}.results`, name: `${step.id}.results`, type: 'list', required: true }, { id: `${step.id}.count`, name: `${step.id}.count`, type: 'number', required: true }, - ]; - for (const output of results) all.set(output.id, output); - available = [...available, ...results]; + ]); continue; } - const outputs = outputsOf(step); - if (outputs.length === 0) continue; - - for (const output of outputs) all.set(output.id, output); - /* - * Novejsi vystup **prekryje** starsi se stejnym jmenem. Presne to dela - * i runtime pri dosazovani do sablon, takze `{{assigneeId}}` znamena - * vzdycky ten posledni. Kdyby se tady jen pricitalo, hlasil by builder - * konflikt tam, kde zadny neni - napriklad kdyz krok vraci totez, co uz - * dal spoustec. - */ - const names = new Set(outputs.map((output) => output.name)); - available = [...available.filter((field) => !names.has(field.name)), ...outputs]; + add(outputsOf(step)); } + + return produced; }; walk(flow.steps, triggerFields); @@ -116,7 +144,17 @@ export function scopeFor(scopes: FlowScopes, stepId: string): TriggerField[] { export function duplicateNames(fields: TriggerField[]): string[] { const seen = new Set(); const duplicates = new Set(); + for (const field of fields) { + /* + * Vystupy kroku se sem nepocitaji. Dva kroky, ktere vraci `contactId`, jsou + * bezna vec (hledani a zalozeni) a odkaz `{{st_zaloz.contactId}}` je + * jednoznacny. Hlasit to jako konflikt by znamenalo psat varovani k tomu, + * co je spravne. Duplicita mezi **parametry spoustece** konflikt je - tam + * zadny prefix neni a v sablone by neslo poznat, ktery se mysli. + */ + if (field.id.includes('.')) continue; + if (seen.has(field.name)) duplicates.add(field.name); seen.add(field.name); } diff --git a/web/src/lib/flow.ts b/web/src/lib/flow.ts index 5cf24f6..f418521 100644 --- a/web/src/lib/flow.ts +++ b/web/src/lib/flow.ts @@ -165,42 +165,50 @@ export function collectScopes( })); }; - const walk = (steps: FlowStep[], inherited: TriggerField[]) => { + /** + * Projde sekvenci a vrati pole, ktera v ni vznikla. + * Musi sedet se serverem, viz src/data/flowScope.ts. + */ + const walk = (steps: FlowStep[], inherited: TriggerField[]): TriggerField[] => { let available = inherited; + const produced: TriggerField[] = []; + + const add = (outputs: TriggerField[]) => { + if (outputs.length === 0) return; + // Nic se neodebira: odkaz se jmenem kroku je jednoznacny a starsi vystup + // muze byt prave ten, ktery clovek potrebuje. Viz src/data/flowScope.ts. + available = [...available, ...outputs]; + produced.push(...outputs); + }; for (const step of steps) { byStep.set(step.id, available); if (step.kind === 'condition') { - walk(step.yes, available); - walk(step.no, available); + const fromYes = walk(step.yes, available); + const fromNo = walk(step.no, available); + // Za podminkou plati obojí, ale jako nepovinne - probehla jen jedna vetev. + add([...fromYes, ...fromNo].map((field) => ({ ...field, required: false }))); continue; } if (step.kind === 'foreach') { - // Musi sedet se serverem, viz src/data/flowScope.ts. walk(step.steps, [ ...available, { id: `${step.id}.item`, name: 'item', type: 'object', required: true }, { id: `${step.id}.index`, name: 'index', type: 'number', required: true }, ]); - available = [ - ...available, + add([ { id: `${step.id}.results`, name: `${step.id}.results`, type: 'list', required: true }, { id: `${step.id}.count`, name: `${step.id}.count`, type: 'number', required: true }, - ]; + ]); continue; } - const outputs = outputsOf(step); - if (outputs.length > 0) { - // Novejsi vystup prekryje starsi se stejnym jmenem, stejne jako to - // dela runtime pri dosazovani do sablon. Musi to sedet se serverem, - // viz src/data/flowScope.ts. - const names = new Set(outputs.map((output) => output.name)); - available = [...available.filter((field) => !names.has(field.name)), ...outputs]; - } + add(outputsOf(step)); } + + return produced; }; walk(flow.steps, triggerFields);