Konektory dostaly vykonnou cast. Jeden skript je jeden soubor, ktery nese manifest (vstupni a vystupni parametry) i kod. Diky manifestu s nim umi pracovat strom automatizace, aniz by o kodu cokoliv vedel. Soubory jsou zamerne obycejny JavaScript, ne TypeScript. TypeScript by se musel prelozit a to je presne to otaceni, ktere tady nema byt. Registr sleduje cas zmeny souboru, takze uprava v portalu, rucni uprava souboru i novy soubor ve slozce funguji stejne a bez restartu. Pridano: - scripts/ se skripty konektoru, nazev souboru je zaroven ID operace - kontrola vstupu i vystupu proti manifestu, jedna funkce pro obe strany. Chybejici povinny vystup je chyba skriptu, ne uzivatele - jinak by strom veril parametru, ktery nikdy nedosel - ctx predavany skriptu: http nad adresou napojeni, util, log, config, idempotencyKey, fail a retry. Skript nedostane pristupove udaje - rozliseni opakovatelne a koncove chyby. Runner nikdy nevyhodi vyjimku, vzdy vraci vysledek vcetne retryable - redakce tajnych hodnot pred zapisem do logu. Cizi API rado vraci prijaty token v chybove zprave a log ticketu vidi klient - napojeni z environment variables vcetne iDokladu - sest ukazkovych skriptu pro iDoklad proti skutecnemu API sluzby services.csbot.cz/apps/idoklad, kazdy na jiny vzor - stranka /dashboard/skripty: seznam, manifest, editor, zkusebni spusteni. Formular testu se sklada z manifestu, nepise se pro kazdy skript - endpointy /api/dashboard/scripts vcetne Swaggeru Zmeneno: - katalog konektoru uz neni jen staticky seznam. Akce ze skriptu se domeruji prekryvem v src/data/connectors.ts, takze se naraz objevi ve validaci stromu, ve vypoctu scope i v sablonach. Pri stejnem ID vyhrava skript - ConnectorOperation ma implementation a scriptId - ApiError na klientovi nese cele telo odpovedi a umi z nej vytahnout issues - Dockerfile kopiruje scripts/ do vysledneho image Ukladani nemuze rozbit fungujici skript: kod se nejdriv zapise do docasneho souboru, ten se nacte a overi, a az pak prepise puvodni. K tomu tri dokumenty navrhu dalsich kroku: 09 datove modely a prava, 10 runtime a rozpocet na 150 klientu, 11 popis skriptu konektoru. Overeno: npm run typecheck prochazi na serveru i webu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
156 lines
5.1 KiB
JavaScript
156 lines
5.1 KiB
JavaScript
/**
|
|
* iDoklad: vystaveni vydane faktury s jednou polozkou.
|
|
*
|
|
* Sluzba: https://services.csbot.cz/apps/idoklad
|
|
* Endpointy: GET /issued-invoices/default, POST /issued-invoices
|
|
*
|
|
* Vzor **dvou volani za sebou**. iDoklad u faktury vyzaduje pole, ktera nikdo
|
|
* rucne vyplnovat nechce (`documentSerialNumber`, `isEet`, `isIncomeTax`),
|
|
* takze se nejdriv vezme predvyplneny vzor z `/issued-invoices/default`
|
|
* a prepisou se v nem jen ty veci, ktere prisly ze stromu.
|
|
*
|
|
* Kdyby se telo skladalo od nuly, rozbila by ho kazda zmena povinnych poli
|
|
* na strane iDokladu. Takhle se prepisuje jen to, cemu rozumime.
|
|
*/
|
|
|
|
export const manifest = {
|
|
id: 'idoklad.create-issued-invoice',
|
|
name: 'Vystavit vydanou fakturu',
|
|
description:
|
|
'Vystaví v iDokladu vydanou fakturu s jednou položkou. Chybějící údaje ' +
|
|
'se doplní z předvyplněného vzoru iDokladu.',
|
|
timeoutMs: 25000,
|
|
|
|
inputs: [
|
|
{
|
|
id: 'partnerId',
|
|
label: 'ID odběratele v iDokladu',
|
|
type: 'number',
|
|
required: true,
|
|
hint: 'Umí ho dohledat akce Najít kontakt.',
|
|
},
|
|
{
|
|
id: 'description',
|
|
label: 'Popis dokladu',
|
|
type: 'string',
|
|
required: true,
|
|
hint: 'Text v hlavičce faktury, například Objednávka {{orderNumber}}.',
|
|
},
|
|
{ id: 'itemName', label: 'Název položky', type: 'string', required: true },
|
|
{
|
|
id: 'unitPrice',
|
|
label: 'Cena za jednotku',
|
|
type: 'number',
|
|
required: true,
|
|
hint: 'V měně dokladu. Desetinná čárka i tečka projdou.',
|
|
},
|
|
{ id: 'amount', label: 'Počet jednotek', type: 'number', required: false, default: 1 },
|
|
{ id: 'unit', label: 'Jednotka', type: 'string', required: false, default: 'ks' },
|
|
{
|
|
id: 'dateOfIssue',
|
|
label: 'Datum vystavení',
|
|
type: 'date',
|
|
required: false,
|
|
hint: 'Nevyplněno = dnes.',
|
|
},
|
|
{
|
|
id: 'maturityDays',
|
|
label: 'Splatnost ve dnech',
|
|
type: 'number',
|
|
required: false,
|
|
default: 14,
|
|
},
|
|
{ id: 'variableSymbol', label: 'Variabilní symbol', type: 'string', required: false },
|
|
{ id: 'note', label: 'Poznámka', type: 'string', required: false, multiline: true },
|
|
{
|
|
id: 'vatRateType',
|
|
label: 'Kód sazby DPH',
|
|
type: 'number',
|
|
required: false,
|
|
default: 0,
|
|
hint: 'Číselný kód VatRateType z iDokladu. Když nevíte, nechte 0.',
|
|
},
|
|
{
|
|
id: 'priceType',
|
|
label: 'Kód typu ceny',
|
|
type: 'number',
|
|
required: false,
|
|
default: 0,
|
|
hint: 'Číselný kód PriceType z iDokladu (cena s DPH nebo bez). Když nevíte, nechte 0.',
|
|
},
|
|
],
|
|
|
|
outputs: [
|
|
{ id: 'invoiceId', label: 'ID faktury', type: 'number', required: true },
|
|
{ id: 'documentNumber', label: 'Číslo dokladu', type: 'string', required: true },
|
|
{ id: 'totalWithVat', label: 'Celkem s DPH', type: 'number', required: false },
|
|
{ id: 'dateOfMaturity', label: 'Datum splatnosti', type: 'date', required: true },
|
|
],
|
|
};
|
|
|
|
/** Datum ve tvaru, ktery iDoklad ceka. */
|
|
function isoDay(value) {
|
|
return new Date(value).toISOString().slice(0, 10);
|
|
}
|
|
|
|
function addDays(value, days) {
|
|
const date = new Date(value);
|
|
date.setUTCDate(date.getUTCDate() + days);
|
|
return date;
|
|
}
|
|
|
|
export async function run(inputs, ctx) {
|
|
const { unwrap, pick, text, num, date, need } = ctx.util;
|
|
|
|
if (inputs.unitPrice < 0) ctx.fail('Cena za jednotku nemůže být záporná.');
|
|
const amount = inputs.amount ?? 1;
|
|
if (amount <= 0) ctx.fail('Počet jednotek musí být větší než nula.');
|
|
|
|
// 1. Predvyplneny vzor. Nese povinna pole, ktera nechceme vyplnovat rucne.
|
|
const defaults = unwrap((await ctx.http.get('/issued-invoices/default')).body);
|
|
if (!defaults || typeof defaults !== 'object') {
|
|
ctx.retry('iDoklad nevrátil předvyplněný vzor faktury.');
|
|
}
|
|
|
|
const issuedAt = inputs.dateOfIssue ? new Date(inputs.dateOfIssue) : new Date();
|
|
const maturityAt = addDays(issuedAt, inputs.maturityDays ?? 14);
|
|
|
|
// 2. Prepisou se jen ta pole, kterym rozumime. Zbytek zustava ze vzoru.
|
|
const payload = {
|
|
...defaults,
|
|
partnerId: inputs.partnerId,
|
|
description: inputs.description,
|
|
dateOfIssue: isoDay(issuedAt),
|
|
dateOfTaxing: isoDay(issuedAt),
|
|
dateOfMaturity: isoDay(maturityAt),
|
|
items: [
|
|
{
|
|
name: inputs.itemName,
|
|
amount,
|
|
unit: inputs.unit ?? 'ks',
|
|
unitPrice: inputs.unitPrice,
|
|
discountPercentage: 0,
|
|
isTaxMovement: false,
|
|
priceType: inputs.priceType ?? 0,
|
|
vatRateType: inputs.vatRateType ?? 0,
|
|
},
|
|
],
|
|
};
|
|
|
|
if (inputs.variableSymbol) payload.variableSymbol = inputs.variableSymbol;
|
|
if (inputs.note) payload.note = inputs.note;
|
|
|
|
const { body } = await ctx.http.post('/issued-invoices', payload);
|
|
const invoice = unwrap(body);
|
|
|
|
ctx.log(`Faktura vystavena pro odběratele ${inputs.partnerId}.`);
|
|
|
|
return {
|
|
invoiceId: need(num(pick(invoice, 'id')), 'ID vystavené faktury'),
|
|
documentNumber: need(text(pick(invoice, 'documentNumber', 'number')), 'číslo dokladu'),
|
|
totalWithVat: num(pick(invoice, 'totalWithVat', 'totalWithVatHc', 'total')),
|
|
dateOfMaturity:
|
|
date(pick(invoice, 'dateOfMaturity')) ?? need(date(maturityAt), 'datum splatnosti'),
|
|
};
|
|
}
|