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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c62902bfb6
commit
6297bbf480
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 : {},
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+62
-24
@@ -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<string>();
|
||||
const duplicates = new Set<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
+23
-15
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user