- Skutočné názvy exchange a queue (
clouderp-sync-queue,clouderp-delivery-queue,clouderp-sync-response-queue) a exchange-to-exchange väzba delivery queue — bodkové názvy z v1.0 na brokeri neexistujú. - Správny wire formát: obálka a
SynchronizationMessagesú camelCase, PascalCase platí len vnútri poľadata; doručenia a ACK sú MassTransit obálky. - Entity
PeppolDocument= 30 aPeppolDocumentEvent= 32 (v1.0 uvádzala 100) a nová kapitola 7. Výsledok odoslania — jediný spôsob, ako sa dozviete, či dokument odišiel do Peppolu. - Registračné API: hlavička
Authorizationso surovým kľúčom, odpoveďRegistration/Validatev camelCase; viac prevádzok asuffixRoutingKey. - Presný obsah prijatého dokumentu (SBDH obálka, kanonické identifikátory), deduplikácia, kontroly strán, retry politiky, prípady bez odpovede a opravené príklady v Jave, .NET a Pythone.
Kapitola 1Ako funguje integrácia
1.1 Celkový pohľad
Váš systém nikdy nekomunikuje s APEX-om priamo. Pri onboardingu použijete REST API registračnej služby QHUB, za behu už len AMQP 0-9-1 (RabbitMQ). QHUB doručí vaše dokumenty APEX-u a APEX vám cez QHUB posiela prijaté dokumenty aj výsledky odoslania.
1.2 Tri typy správ, ktoré si vymieňate
| Správa | entityType | Smer | Čo prenáša |
|---|---|---|---|
PeppolDocument | 30 | oboma smermi | UBL dokument + Peppol identifikátory (odosielaný alebo prijatý dokument) |
PeppolDocumentEvent | 32 | len APEX → váš systém | biznis výsledok dokumentu, ktorý ste odoslali: zaradený, doručený, zamietnutý, dead letter, MLS |
SynchronizationResponseMessage (ACK) | – | príjemca správy → QHUB | technické potvrdenie „správu som spracoval“ |
ACK nie je výsledok. ACK znamená len „prevzal som správu“. Či dokument naozaj odišiel do Peppolu, sa dozviete výhradne z
PeppolDocumentEvent(kapitola 7). Biznis zamietnutia (validácia, licencia, neznámy odosielateľ) APEX ACK-om vôbec nehlási.
1.3 Čo QHUB robí za vás — a čo nie
- Registrácie a autorizácia väzby — dvojica (váš systém, tenant) ↔ (APEX, tenant) musí byť v QHUB zaregistrovaná; neregistrovanú väzbu QHUB nepustí.
- Smerovanie — podľa routing key doručí správu z vášho systému do APEX-u a naopak; nemusíte vedieť, kde APEX beží.
- Audit — každá správa je zaznamenaná v transakčnej tabuľke QHUB so stavom a časom.
QHUB správy znova nedoručuje a nič nepresúva do DLQ. Ak ACK nepríde, transakcia v QHUB zostane v stave Pending — ďalší pokus ani notifikácia nenasledujú. Dead-letter je vlastnosť vašej queue (kapitola 10.9) a jej sledovanie je vaša zodpovednosť.
1.4 Čo musíte urobiť vy
| Oblasť | Vaša zodpovednosť |
|---|---|
| Topológia | Deklarovať vlastnú delivery exchange a queue s väzbami (kapitola 4). |
| Prijímanie (Peppol → váš systém) | Konzumovať doručenia, rozbaliť obálku aj SBDH, importovať UBL a vždy poslať ACK. |
| Odosielanie (váš systém → Peppol) | Pripraviť UBL, publikovať SynchronizationMessage s entitou PeppolDocument do clouderp-sync-queue. |
| Sledovanie výsledku | Konzumovať PeppolDocumentEvent z tej istej queue a viesť stav dokumentu. |
| Idempotencia a dohľad | Deduplikovať doručenia (QHUB negarantuje exactly-once), používať jedinečné identifikátory, mať timeouty a alerty pre dokumenty bez odpovede. |
Kapitola 2Predpoklady a onboarding
2.1 Čo musí platiť pred začatím
- Zákazník (tenant) je založený v APEX. Je naviazaný na IAM tenant UUID v ASOL Cloude — to je
TenantId, ktorý používate všade (participantTenantId,ownerTenantId, routing key). Nie je to interné ID vášho ERP. Ak obsluhujete viac zákazníkov, každý má vlastnéTenantId, vlastný routing key a vlastnú queue. - Platné ALIS licencie na tom istom tenante — pre APEX aj pre váš systém (
SystemCode), obe so sériovým číslom. Bez nich sa registrácia v QHUB nedá vytvoriť. (Multi-tenant systémy v katalógu QHUB vlastnú licenciu nepotrebujú; QHUB ju overí pri validácii kľúča.) - QHUB pozná váš systém — váš
SystemCodesa objaví v katalógu QHUB po odoslaní popisu entít (3.1). - Odosielateľ je participant vášho tenanta. Peppol participant, za ktorého posielate (napr.
0245:2024000001), musí byť v APEX zaregistrovaný a priradený k tenantovi zownerTenantId. Registruje ho ASOL (napr. pri registrácii v SMP); neznámeho odosielateľa APEX sám nezaloží. - AMQP 0-9-1 klient s podporou TLS 1.2+:
- Java: Spring AMQP / RabbitMQ Java Client
- .NET: RabbitMQ.Client (MassTransit v9 je komerčný; v8 je open source)
- Python: pika
- Node.js: amqplib
2.2 Čo dostanete od ASOL alebo od svojho partnera
| Položka | Poznámka |
|---|---|
SystemCode | Kód vášho systému v QHUB/ALIS (napr. MYERP). |
TenantId | IAM tenant UUID zákazníka — pre každého zákazníka zvlášť. |
| Sériové číslo prevádzky | Len ak má zákazník viac inštalácií (prevádzok) toho istého systému — 3.4. |
| URL registračnej služby QHUB + API kľúč | Pre zápis popisu entít (3.1). |
| Jednorazový registračný kľúč | Vygeneruje ho ASOL admin alebo partner pre konkrétnu registráciu; zobrazí sa raz a APEX ho neukladá. Každý nový kľúč zneplatní predchádzajúci a odpojí spárovaný systém, kým nezadáte nový. Podľa dokumentácie QHUB platí 24 hodín. |
| CA certifikáty brokera | Certifikát brokera vydáva privátna CA Asseco Solutions — jej certifikáty (intermediate aj root) pridajte do truststore. |
| Klientsky certifikát (mTLS) | Len ak to prostredie vyžaduje. Príde ako šifrovaný ZIP (ca.pem, client_cert.pem, client_key.pem, client.pfx), heslo iným kanálom. Certifikát dopĺňa kľúč, nenahrádza ho. |
2.3 Onboarding krok za krokom
| # | Krok | Kto |
|---|---|---|
| 1 | ALIS licencie na APEX a váš systém pre tenanta zákazníka; registrácia participanta (SMP) | ASOL / partner |
| 2 | Odoslanie popisu entít PeppolDocument (30) a PeppolDocumentEvent (32) do katalógu QHUB | vy |
| 3 | Vytvorenie registrácie APEX ↔ váš systém (prípadne pre každú prevádzku) | ASOL / partner |
| 4 | Vygenerovanie jednorazového registračného kľúča a jeho bezpečné odovzdanie | ASOL / partner |
| 5 | POST /api/v1/Registration/Validate → RabbitMQ URL, meno a heslo | vy |
| 6 | Inštalácia CA certifikátu (a klientskeho certifikátu, ak je mTLS) | vy |
| 7 | Deklarácia delivery exchange, queue a väzieb; spustenie konzumenta | vy |
| 8 | Prijímanie + ACK, odosielanie, spracovanie eventov, deduplikácia, timeouty | vy |
| 9 | Test inbound, outbound a negatívnych scenárov v testovacom prostredí | vy + ASOL |
Kapitola 3Registrácia systému v QHUB
Registrácia má tri časti: popis entít (robíte vy), registračná väzba (robí ASOL alebo partner) a validácia kľúča (robíte vy).
3.1 Odoslanie popisu entít
QHUB musí vedieť, aké entity váš systém podporuje. Pre Peppol ide o dve entity: PeppolDocument (dokument) a PeppolDocumentEvent (výsledok spracovania). QHUB smeruje len entity, ktoré majú zaregistrované obe strany — bez entity 32 vám eventy nebudú mať kam prísť.
Endpoint:
POST {QHUB_REGISTRATION_URL}/api/v1/SyncEntityCatalogItem Content-Type: application/json Accept: application/json Authorization: {API kľúč}
Authorizationobsahuje surový API kľúč bez prefixu (žiadneBeareraniBasic). HlavičkaX-Api-Keynie je podporovaná. Staršia dokumentácia QHUB uvádza aj cestu/api/v1/SyncEntityCatalogItem/StoreSyncEntityItems; používajte kratšiu formu, ktorú používa aj APEX.
Request body — rovnaká definícia, akú registruje APEX (názvy a príznaky Required musia sedieť na oboch stranách):
{
"System": "MYERP",
"SyncEntityDatas": [
{
"EntityCode": "PeppolDocument",
"EntityType": 30,
"DefaultAllowed": true,
"Version": "1.0.0",
"SyncEntityDefinitionData": {
"SyncPropertiesData": [
{ "Name": "SenderParticipantId", "OriginalName": "SenderParticipantId", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "ReceiverParticipantId", "OriginalName": "ReceiverParticipantId", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "DocumentTypeId", "OriginalName": "DocumentTypeId", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "ProcessId", "OriginalName": "ProcessId", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "UblDocument", "OriginalName": "UblDocument", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "As4MessageId", "OriginalName": "As4MessageId", "ProperyType": "String", "Required": false, "IsBusinessKey": true },
{ "Name": "ReceivedAt", "OriginalName": "ReceivedAt", "ProperyType": "DateTime", "Required": false, "IsBusinessKey": false }
],
"SyncForeignKeysData": []
}
},
{
"EntityCode": "PeppolDocumentEvent",
"EntityType": 32,
"DefaultAllowed": true,
"Version": "1.0.0",
"SyncEntityDefinitionData": {
"SyncPropertiesData": [
{ "Name": "EventId", "OriginalName": "eventId", "ProperyType": "String", "Required": true, "IsBusinessKey": true },
{ "Name": "PeppolDocumentMessageCorrelationId", "OriginalName": "peppolDocumentMessageCorrelationId", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "EventType", "OriginalName": "EventType", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "Message", "OriginalName": "Message", "ProperyType": "String", "Required": false, "IsBusinessKey": false },
{ "Name": "Error", "OriginalName": "Error", "ProperyType": "String", "Required": false, "IsBusinessKey": false },
{ "Name": "ValidationErrors", "OriginalName": "ValidationErrors", "ProperyType": "String", "Required": false, "IsBusinessKey": false },
{ "Name": "ValidationWarnings", "OriginalName": "ValidationWarnings", "ProperyType": "String", "Required": false, "IsBusinessKey": false },
{ "Name": "Timestamp", "OriginalName": "Timestamp", "ProperyType": "DateTime", "Required": true, "IsBusinessKey": false }
],
"SyncForeignKeysData": [
{ "Name": "PeppolDocumentKey", "EntityType": 30, "Required": true }
]
}
}
]
}ProperyType— áno, bez druhéhot; je to názov poľa v API QHUB.EntityType— hodnoty 30 a 32 sú pevne dané číselníkom QHUB. Iné číslo APEX odmietne (UnsupportedEntityType).SyncForeignKeysDatapriPeppolDocumentEventdeklaruje väzbuPeppolDocumentKeyna entitu 30 — podľa nej spárujete event s dokumentom.Version— posielajte"1.0.0"(rovnako akoschemaVersionv správach). QHUB zmenu katalógu prevezme len pri zmene verzie.- Úspech:
HTTP 200, telotrue. Opakované odoslanie (napr. pri štarte aplikácie) je neškodné.
3.2 Registračná väzba (vytvára ASOL alebo partner)
Registráciu medzi APEX-om a vaším systémom pre konkrétneho zákazníka nezakladáte vy cez API. Vytvorí ju:
- ASOL admin v APEX Management (sekcia notifikácií, „Nová QHub registrácia“ a „Generovať kľúč“),
- partner v tom istom rozhraní alebo cez Partner API (
POST /partner-api/v1/customers/{customerId}/qhub/registrationsaPOST /partner-api/v1/qhub/registrations/{id}/key), - pre QASIDA ERP a HOREC aj zákaznícky portál.
Registrácia spája vlastníka APEX a váš SystemCode na tom istom IAM tenante zákazníka, so sériovými číslami licencií a voliteľne s obdobím platnosti. Registráciu nemožno upraviť — mení sa zmazaním a vytvorením novej (zmazanie zneplatní aj kľúč). Pri vypršaní licencie alebo obdobia ju QHUB pri nočnej kontrole zneplatní.
3.3 Validácia registračného kľúča
Jednorazový registračný kľúč dostanete zabezpečeným kanálom. Vymeníte ho za prístup k RabbitMQ:
POST {QHUB_REGISTRATION_URL}/api/v1/Registration/Validate Content-Type: application/json
{
"participantSystem": "MYERP",
"participantTenantId": "c0ffee00-1111-2222-3333-444455556666",
"registrationKey": "REG-KEY-OD-ASOL",
"routingKey": "MYERP.c0ffee00-1111-2222-3333-444455556666"
}
routingKeyidentifikuje vašu inštanciu. Konvencia pre systémy mimo ASOL Cloudu je{SystemCode}.{TenantId}. Rozlišuje veľké a malé písmená a odvodzuje sa z neho názov vašej delivery exchange aj queue — používajte ho všade rovnako.
Úspešná odpoveď (HTTP 200) — v camelCase:
{
"participantSystem": "MYERP",
"participantTenantId": "c0ffee00-1111-2222-3333-444455556666",
"rabbitMqUrl": "amqps://messaging.qasida.sk:5671",
"rabbitMqName": "myerp_c0ffee00",
"rabbitMqPass": "••••••••••••",
"errorCode": null,
"success": true,
"registeredEntities": [ … ]
}- Parsujte ako camelCase. Pri PascalCase klient nespadne, ale dostanete
success = falsea saménull. rabbitMqUrlurčuje host (messaging.qasida.sk), port (AMQPS 5671) a prípadne virtual host — smerodajná je vždy hodnota z odpovede. Ak by prišla so schémouhttps://, použiteamqps://.rabbitMqPassje tajomstvo — uložte ho do trezoru (Kubernetes Secret, Vault…), nikdy nie do repozitára. Používateľa a oprávnenia na brokeri vytvorí QHUB automaticky.registeredEntitiesopisuje povolené entity a smerovanie; jeho presnú štruktúru si overte na reálnej odpovedi.- Chybové kódy: 10.2.
3.4 Viac zákazníkov a viac prevádzok
- Viac zákazníkov: každý zákazník (tenant) má vlastné
TenantId, vlastnú registráciu, vlastný routing key a vlastnú delivery queue. - Viac inštalácií u jedného zákazníka (napr. prevádzky s vlastnou inštaláciou systému): každá dostane vlastnú registráciu odlíšenú sériovým číslom licencie prevádzky. Registrácia je jednoznačne určená trojicou tenant +
SystemCode+ sériové číslo; bez sériového čísla môže mať tenant pre daný systém len jednu registráciu. Kľúč sa zadáva v inštalácii danej prevádzky. suffixRoutingKey(od októbra 2026): ak dokumenty jedného tenanta posielajú rôzne prevádzky, pridajte do odosielanejSynchronizationMessagepolesuffixRoutingKey(napr. sériové číslo prevádzky; najviac 128 znakov, bez bodky a riadiacich znakov). APEX ho uloží a vráti v obálke každéhoPeppolDocumentEventk tomuto dokumentu; ak má cieľová aplikácia v QHUB zapnuté smerovanie podľa suffixu, event príde do fronty prevádzky, ktorá dokument poslala. Dokumenty bez suffixu a všetky dokumenty prijaté z Peppolu idú do základnej queue tenanta. Presný názov queue prevádzky a nastavenie smerovania dohodnete s ASOL.
Kapitola 4Nastavenie RabbitMQ pripojenia
Broker QHUB beží na messaging.qasida.sk. Pripojenie prebieha cez AMQPS (TLS, port 5671) s URL, menom a heslom z Registration/Validate. Povoľte odchádzajúce spojenie na messaging.qasida.sk:5671 a dostupnosť z vášho prostredia si overte s ASOL pri onboardingu.
4.1 Topológia — exchanges a queues
Názvy sú doslovné AMQP identifikátory vrátane pomlčiek. Tri z nich sú exchange, hoci majú v názve „queue“.
| Názov | Typ | Kto ho deklaruje | Účel |
|---|---|---|---|
clouderp-sync-queue | fanout exchange | QHUB (existuje) | Vy publikujete dokumenty na odoslanie (routing key "") |
clouderp-sync-response-queue | fanout exchange | QHUB (existuje) | Vy publikujete ACK (routing key "") |
clouderp-delivery-queue | direct exchange | QHUB (existuje) | Centrálna exchange, do ktorej QHUB publikuje doručenia s routing key príjemcu |
clouderp-delivery-queue.{RoutingKey} | fanout exchange | vy | Vaša per-inštanciová exchange |
clouderp-delivery-queue.{RoutingKey} | queue | vy | Vaša delivery queue — odtiaľto čítate |
clouderp.dlq | dead-letter exchange | — (overte s ASOL) | Cieľ správ, ktoré vaša queue dead-letteruje |
Bodkové názvy neexistujú.
clouderp.sync,clouderp.sync.responseaniclouderp.deliveryna brokeri nie sú (publish do nich skončí404 NOT_FOUND). Jediný objekt s bodkami jeclouderp.dlq. Exchange a queueclouderp-delivery-queue.{RoutingKey}majú rovnaký názov — v RabbitMQ sú to oddelené menné priestory.
4.2 Ako to do seba zapadá
VY publikujete dokument VY čítate doručenia a eventy
│ ▲
▼ │
┌──────────────────────┐ ┌────────────────────────────────────┐
│ clouderp-sync-queue │ │ queue clouderp-delivery-queue.{RK} │
│ (fanout exchange) │ └─────────────────▲──────────────────┘
└──────────┬───────────┘ │ binding (fanout, "")
▼ ┌─────────────────┴──────────────────┐
┌───────────┐ │ fanout exchange │
│ QHUB │──────────────────────────▶│ clouderp-delivery-queue.{RK} │
│ Sync Svc │ publikuje do └─────────────────▲──────────────────┘
└─────▲─────┘ clouderp-delivery-queue │ exchange-to-exchange
│ (direct, routing key = {RK}) │ binding, routing key = {RK}
│ ┌─────────────────┴──────────────────┐
│ │ direct exchange │
│ │ clouderp-delivery-queue │
│ └────────────────────────────────────┘
┌──────────┴───────────────────┐
│ clouderp-sync-response-queue │ ◀── VY publikujete ACK
│ (fanout exchange) │
└──────────────────────────────┘Kľúčové: vaša queue nie je naviazaná priamo na clouderp-delivery-queue. Reťaz je clouderp-delivery-queue (direct, routing key = {RoutingKey}) → vaša fanout exchange → vaša queue.
4.3 Oprávnenia vášho RabbitMQ účtu
Používateľa a oprávnenia vytvorí QHUB pri validácii kľúča — na brokeri nič nenastavujete. Pre kontrolu, čo jednotlivé operácie potrebujú:
| Operácia | Oprávnenie |
|---|---|
| Deklarácia vlastnej exchange a queue | configure na clouderp-delivery-queue.{RoutingKey} |
| Queue s dead-letter exchange | write na clouderp.dlq |
| Binding queue → vaša exchange | write na queue, read na vašej exchange |
| Exchange-to-exchange binding | write na vašej exchange, read na clouderp-delivery-queue |
| Konzumácia | read na vašej queue |
| Publikovanie dokumentov a ACK | write na clouderp-sync-queue a clouderp-sync-response-queue |
Ak niečo zlyhá s ACCESS_REFUSED, kontaktujte ASOL.
4.4 Deklarácia topológie
Vašu exchange, queue a väzby deklaruje váš systém pred prvým consume. Deklarácie sú idempotentné — spúšťajte ich pri každom štarte.
1) fanout exchange clouderp-delivery-queue.{RoutingKey} durable=true, auto-delete=false
2) queue clouderp-delivery-queue.{RoutingKey} durable=true, exclusive=false, auto-delete=false
arguments: x-dead-letter-exchange = clouderp.dlq
3) queue binding queue clouderp-delivery-queue.{RoutingKey} ← exchange clouderp-delivery-queue.{RoutingKey}
routing key ""
4) exchange binding destination clouderp-delivery-queue.{RoutingKey} (EXCHANGE)
source clouderp-delivery-queue (direct)
routing key {RoutingKey}- Krok 4 je exchange-to-exchange binding (raw AMQP
exchange.bind(destination, source, routingKey), Springnew Binding(dest, DestinationType.EXCHANGE, source, rk, null)). Bez neho queue vznikne, ale nikdy do nej nič nepríde. - Centrálne exchange nedeklarujte (
clouderp-sync-queue,clouderp-delivery-queue,clouderp-sync-response-queue) — existujú a deklarácia vyžaduje oprávnenie configure, ktoré váš účet nemá (ACCESS_REFUSED). Ak ich chcete overiť, použite pasívnu deklaráciu. - Argumenty queue nemeňte. Opätovná deklarácia s inými argumentmi (napr. bez DLX) skončí
PRECONDITION_FAILED.
4.5 TLS a mTLS
- TLS je povinné (
amqps://, port 5671), TLS 1.2 alebo vyššie. Certifikát brokera (platný premessaging.qasida.sk) vydáva privátna CA Asseco Solutions — certifikáty CA (PEM, intermediate aj root) dostanete od ASOL a pridajte ich do truststore. Overovanie hostname nevypínajte. - mTLS závisí od prostredia. Ak ho ASOL zapne, dostanete klientsky certifikát a kľúč (PEM, PKCS#1 aj PKCS#8, resp.
client.pfx); používajte RSA kľúče. Certifikát sa prikladá k menu a heslu, nenahrádza ich.
4.6 Nastavenia konzumenta, ktoré odporúčame
| Nastavenie | Hodnota | Prečo |
|---|---|---|
| prefetch | 10 | drží pamäť pod kontrolou |
| potvrdzovanie | manuálne/AUTO po úspešnom uložení | správu potvrďte až po trvalom uložení |
| requeue pri chybe | false | pri výnimke ide správa do DLX, nie donekonečna dokola |
| prevod správy | surové bajty, žiadny typový konvertor | doručenie je MassTransit obálka; napr. Spring Jackson2JsonMessageConverter na hlavičke __TypeId__ zlyhá a správa skončí v DLQ |
| spracovanie | persist-then-process | v listeneri len uložte surový JSON, spracujte asynchrónne z vlastnej tabuľky |
Veľkosť správy: celý UBL dokument vrátane príloh (Base64 v
cac:AdditionalDocumentReference) ide v jednej AMQP správe. Limit určuje konfigurácia brokera (predvolený limit RabbitMQ je 16 MiB) — pri veľkých prílohách si skutočný limit overte s ASOL.
Kapitola 5Prijímanie dokumentov z Peppolu
Keď APEX prijme Peppol dokument pre vášho zákazníka, QHUB vám ho doručí na delivery queue ako SynchronizationMessage s entitou PeppolDocument (entityType = 30).
5.1 Tok prijímania
Peppol odosielateľ → AS4 → APEX
→ validácia (XSD + Schematron) a uloženie; odosielateľovi ide Peppol MLS
→ len platný dokument: APEX publikuje SynchronizationMessage do clouderp-sync-queue (cca do 5 s)
→ QHUB vytvorí doručenie pre každý zaregistrovaný cieľový systém tenanta
→ clouderp-delivery-queue (direct, routing key = váš RoutingKey)
→ clouderp-delivery-queue.{RoutingKey} (vaša fanout exchange) → vaša queue
→ VÁŠ KONZUMENT → import → ACK do clouderp-sync-response-queue- Doručujú sa všetky platné prijaté dokumenty tenanta: faktúry, dobropisy, samofaktúry, objednávky, ale aj obchodné odpovede (Invoice Response / IMR, MLR).
- Nedoručujú sa: dokumenty zamietnuté validáciou, technické Peppol MLS a duplicitne doručené AS4 správy.
- Ak pre tenanta neexistuje registrácia, QHUB doručenie preskočí.
5.2 Formát doručenej správy
Doručenie je MassTransit obálka (content-type: application/vnd.masstransit+json). Obsah je v poli message:
{
"messageId": "8f14e45f-ceea-467a-9575-9c4b9b8a1c3d",
"correlationId": "3b2f1c0a-9d8e-4f7a-b6c5-1a2b3c4d5e6f",
"messageType": [
"urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationMessage"
],
"sentTime": "2026-10-09T08:15:31.1234567Z",
"headers": {},
"host": { "machineName": "…", "processName": "…", "massTransitVersion": "…" },
"message": {
"transactionId": 4711,
"eventType": 1,
"entityType": 30,
"routingKey": "MYERP.c0ffee00-1111-2222-3333-444455556666",
"ownerTenantId": "c0ffee00-1111-2222-3333-444455556666",
"ownerSystem": "APEX",
"deliverySystem": "MYERP",
"deliveryTenantId": "c0ffee00-1111-2222-3333-444455556666",
"syncKey": "abc123@ap.sender.example",
"schemaVersion": "1.0.0",
"environment": "…",
"domainSyncVersion": 1,
"occurredAt": "2026-10-09T08:15:30.5Z",
"data": "{\"SenderParticipantId\":\"iso6523-actorid-upis::0245:2024000002\", … ,\"UblDocument\":\"<StandardBusinessDocument …>…\"}",
"ignoredProperties": null,
"suffixRoutingKey": null
}
}Dve konvencie v jednej správe. Obálka aj
messagesú v camelCase; obsah poľadataje v PascalCase.dataje JSON reťazec — deserializujte ho druhýkrát.
eventTypeaentityTypemôžu prísť ako číslo aj ako reťazec (30,"30", prípadne názov) — deserializér to musí zniesť.- Neznáme polia ignorujte; QHUB a MassTransit môžu pridávať ďalšie.
routingKey,ownerSystem,deliverySystemdopĺňa QHUB pri smerovaní; hodnoty v príklade sú ilustračné.- Pri prijatom dokumente je
syncKeyAS4 MessageId od odosielajúceho prístupového bodu (nie UUID). - Na queue môže prísť aj obálka, ktorej
messageTypeobsahuje…:SynchronizationResponseMessage(ACK). Nenesie entitu — potvrďte ju na úrovni RabbitMQ, neukladajte ju a neposielajte na ňu ACK.
5.3 Obsah PeppolDocument prijatého dokumentu
| Pole | Obsah |
|---|---|
SenderParticipantId, ReceiverParticipantId | Vždy kanonický tvar iso6523-actorid-upis::0245:2024000002. Pri porovnávaní odrežte všetko po :: vrátane. |
DocumentTypeId | Typ dokumentu z AS4 hlavičky; spravidla s prefixom schémy busdox-docid-qns:: — pred porovnaním ho odstráňte. |
ProcessId | Proces bez schémy, napr. urn:fdc:peppol.eu:2017:poacc:billing:01:1.0. |
UblDocument | Celý StandardBusinessDocument (SBDH obálka s UBL dokumentom vnútri) ako XML text, nie Base64. Biznis dokument nájdite podľa menného priestoru (napr. urn:oasis:names:specification:ubl:schema:xsd:Invoice-2); prefixy nie sú garantované. Prázdny reťazec = payload sa nepodarilo načítať — považujte ho za chybu. |
As4MessageId | AS4 MessageId pridelený odosielajúcim prístupovým bodom (formát je na ňom, napr. uuid@doména). |
ReceivedAt | Čas prijatia v APEX, ISO 8601 s posunom. |
Prílohy sú súčasťou UBL (cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject); samostatný kanál príloh neexistuje.
5.4 Čo urobiť s každým doručením
- Uložte surové bajty/JSON do vlastnej tabuľky (persist-then-process).
- Prázdne telo, obálku bez
messagea obálky ACK len zalogujte a potvrďte na úrovni RabbitMQ. - Deduplikujte (5.6).
- Rozvetvite podľa
entityType: 30 → import dokumentu (rozbaliť SBDH, normalizovať identifikátory), 32 → aktualizácia stavu odoslaného dokumentu (kapitola 7), iné → zalogovať. - Pošlite ACK pre každé spracované doručenie — úspech aj chybu.
5.5 Response ACK (povinné)
ACK publikujete do exchange clouderp-sync-response-queue s routing key "". Musí to byť MassTransit obálka — response endpoint QHUB holý application/json odmietne:
| Vlastnosť | Hodnota |
|---|---|
content-type | application/vnd.masstransit+json |
hlavička MT-MessageType | urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationResponseMessage |
messageId (v tele) | GUID, rovnaký pri opakovaní toho istého ACK (napr. odvodený z correlationId a transactionId) |
correlationId (v tele) | skopírovaný z doručenia |
message.transactionId | skopírovaný z doručenia (message.transactionId) |
{
"messageId": "5d0b3c9e-2f7a-5c1b-9e44-0a6f2d1c8b77",
"correlationId": "3b2f1c0a-9d8e-4f7a-b6c5-1a2b3c4d5e6f",
"messageType": [
"urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationResponseMessage"
],
"sentTime": "2026-10-09T08:15:35Z",
"message": {
"transactionId": 4711,
"result": { "success": true, "errorCode": null, "errorParams": null }
}
}Pri chybe: "result": { "success": false, "errorCode": "ImportFailed", "errorParams": ["INV-2026-001"] }. Do errorParams dávajte len parametre chybového kódu (z ktorých sa skladajú preložené texty), nie texty výnimiek.
- ACK posielajte vždy, aj pri chybe a aj na eventy (entita 32).
- Ak
transactionIdchýba alebo je0, ACK nemá QHUB k čomu priradiť — neposielajte ho. - Ak publikovanie ACK zlyhá, opakujte ho, kým neprejde (APEX to robí s odstupom 30 s → 60 s → 120 s → 240 s → max 300 s bez limitu pokusov). QHUB doručenie bez ACK znova nepošle — transakcia by zostala visieť.
5.6 Deduplikácia (povinné)
QHUB negarantuje doručenie práve raz. Duplicitu rozpoznajte podľa týchto kľúčov (APEX na svojej strane kontroluje prvé tri):
| Kľúč | Kedy |
|---|---|
message.transactionId | ak je väčšie ako 0 |
obálkové correlationId | ak je vyplnené |
As4MessageId z data | pri entite 30 — obsahová identita prijatého dokumentu |
EventId z data | pri entite 32 |
Aplikačnú kontrolu podoprite unikátnym indexom v databáze — pri paralelných konzumentoch je sama o sebe závodivá.
5.7 Obchodné odmietnutie prijatej faktúry
APEX potvrdí odosielajúcemu prístupovému bodu technické prevzatie (Peppol MLS) hneď po uložení dokumentu, nezávisle od výsledku vášho importu. Negatívny ACK do QHUB len označí transakciu — odosielateľ faktúry sa o ňom nedozvie. Ak chcete faktúru obchodne odmietnuť alebo potvrdiť, pošlite mu Invoice Response (IMR) ako bežný odosielaný PeppolDocument (kapitola 6, identifikátory v prílohe A).
Kapitola 6Odosielanie dokumentov do Peppolu
Dokument odošlete tak, že publikujete SynchronizationMessage s entitou PeppolDocument do exchange clouderp-sync-queue. QHUB ju doručí APEX-u, ten ju skontroluje, zaradí do frontu a odošle cez AS4. Výsledok vám príde ako PeppolDocumentEvent.
6.1 Tok odosielania
VÁŠ SYSTÉM → SynchronizationMessage (entityType 30) → clouderp-sync-queue
→ QHUB → delivery queue APEX-u
→ APEX (spracovanie každé ~3 s): kontroly 1–10 (6.4)
├─ zamietnutie → PeppolDocumentEvent VALIDATION_ERROR / LICENSE_ERROR / AUTHORIZATION_ERROR / PROCESSING_ERROR
└─ prevzatie → MESSAGE_QUEUED (+ ACK do QHUB)
→ AS4 odoslanie s opakovaním → MESSAGE_DELIVERED | QUEUE_PROCESSING_FAILED … | QUEUE_DEAD_LETTER
→ neskôr prípadne MLS_RECEIVED / MLS_RECEIVED_NEGATIVE
APEX → PeppolDocumentEvent (entityType 32) → QHUB → vaša delivery queue (alebo queue prevádzky pri suffixRoutingKey)6.2 Formát odosielanej správy
Rozdiel oproti doručeniu: do
clouderp-sync-queuesa posiela holáSynchronizationMessagev koreni JSON, nie MassTransit obálka (QHUB tu používa raw JSON deserializér).
| AMQP vlastnosť | Hodnota |
|---|---|
| exchange / routing key | clouderp-sync-queue / "" |
content-type | application/json |
hlavička MT-MessageType | urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationMessage |
message_id | GUID — rovnaký pri opakovanom publikovaní tej istej správy |
correlation_id | GUID — jedinečný pre každý dokument (opakované použitie môže viesť k tomu, že APEX ďalší dokument zahodí ako duplikát) |
| delivery mode | persistent |
{
"transactionId": 0,
"eventType": 1,
"entityType": 30,
"routingKey": null,
"ownerTenantId": "c0ffee00-1111-2222-3333-444455556666",
"ownerSystem": "MYERP",
"deliverySystem": null,
"deliveryTenantId": "c0ffee00-1111-2222-3333-444455556666",
"syncKey": "9a1d2c3e-6b7f-4c8d-9e0a-1b2c3d4e5f60",
"schemaVersion": "1.0.0",
"environment": "Production",
"domainSyncVersion": 1,
"occurredAt": "2026-10-09T08:15:30Z",
"data": "{\"SenderParticipantId\":\"0245:2024000001\",\"ReceiverParticipantId\":\"0245:2024000002\",\"DocumentTypeId\":\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1\",\"ProcessId\":\"urn:fdc:peppol.eu:2017:poacc:billing:01:1.0\",\"UblDocument\":\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?><Invoice xmlns=…>…</Invoice>\"}",
"ignoredProperties": null,
"suffixRoutingKey": "SN-001"
}| Pole | Čo tam dať |
|---|---|
transactionId | 0 — QHUB pridelí vlastný |
eventType | vždy 1. APEX eventType nevyhodnocuje — 2 (UPDATE) aj 4 (DELETE) by znamenali nové, nezvratné odoslanie do Peppolu |
entityType | 30 |
routingKey, deliverySystem | null — dopĺňa QHUB |
ownerTenantId | IAM tenant UUID zákazníka; APEX podľa neho nájde tenanta, licenciu a overí odosielateľa |
ownerSystem | váš SystemCode (presný tvar potvrdí ASOL pri onboardingu) |
deliveryTenantId | tenant UUID alebo null (APEX ho nepoužíva) |
syncKey | nové UUID pre každý dokument; QHUB druhú správu s rovnakým syncKey a verziou preskočí. Vráti sa vám v evente ako PeppolDocumentKey.SyncKey — uložte si ho |
schemaVersion | "1.0.0" — rovnako ako Version v katalógu |
environment | označenie prostredia dohodnuté pri onboardingu (APEX ho nekontroluje) |
domainSyncVersion | 1 (≥ 1); nie je to mechanizmus riešenia konfliktov |
occurredAt | čas vzniku u vás, ISO 8601 reťazec |
data | entita PeppolDocument ako JSON reťazec v PascalCase |
suffixRoutingKey | voliteľné — označenie prevádzky, aby eventy prišli do jej queue (3.4) |
6.3 Polia entity PeppolDocument pri odosielaní
| Pole | Povinné | Pravidlá |
|---|---|---|
SenderParticipantId | áno | Krátky (0245:2024000001) alebo kanonický tvar. Musí byť zaregistrovaný v APEX pod tenantom z ownerTenantId a sedieť s cbc:EndpointID odosielajúcej strany dokladu (6.4). |
ReceiverParticipantId | áno | Rovnaké tvary. Musí byť dohľadateľný v Peppol (SML/SMP) pre daný typ a proces a sedieť s cbc:EndpointID prijímajúcej strany. |
DocumentTypeId | áno | Úplný Peppol identifikátor, so schémou busdox-docid-qns:: aj bez nej (príloha A). |
ProcessId | áno | Peppol proces, napr. urn:fdc:peppol.eu:2017:poacc:billing:01:1.0. |
UblDocument | áno | XML text v UTF-8 (nie Base64) — holý UBL alebo UBL v SBDH. Úvodný BOM APEX odstráni. |
As4MessageId | nechajte prázdne | Je to deduplikačný kľúč APEX — opakovaná hodnota spôsobí, že APEX dokument ticho zahodí. |
ReceivedAt | nechajte prázdne | Ak ho vyplníte, musí byť platný ISO 8601 čas s posunom; inak sa správa nedá spracovať a nepríde žiadny event. |
6.4 Kontroly v APEX a pravidlá strán
APEX kontroluje v tomto poradí; prvé zlyhanie dokument zamietne a pošle event (bez ACK):
| # | Kontrola | EventType | Error |
|---|---|---|---|
| 0 | data nie je platný JSON alebo sa nedá namapovať (napr. zlý ReceivedAt) | žiadny event; po 3 pokusoch failure ACK ProcessingFailed | — |
| 1 | chýba message alebo data | PROCESSING_ERROR | Missing inner message or data … |
| 2 | prázdny UblDocument | VALIDATION_ERROR | UblDocument is missing or blank … |
| 3 | chýba odosielateľ alebo príjemca | VALIDATION_ERROR | SenderParticipantId or ReceiverParticipantId is missing … |
| 4 | ownerTenantId nezodpovedá tenantovi | PROCESSING_ERROR | Tenant could not be resolved for ownerTenantId=… |
| 5 | odosielateľ neexistuje, nie je priradený alebo patrí inému tenantovi | AUTHORIZATION_ERROR | Participant not found: … / Authenticated tenant does not have access to participant: … |
| 6 | chýba typ dokumentu alebo proces | VALIDATION_ERROR | Document type ID is required / Process ID is required |
| 7 | licencia tenanta | LICENSE_ERROR | No active license exists for tenant …, License for tenant … has expired … |
| 8 | XML, XSD, Schematron, biznis pravidlá | VALIDATION_ERROR + ValidationErrors | Document validation failed: N error(s) |
| 9 | strany dokladu (tabuľka nižšie) | VALIDATION_ERROR + ValidationErrors | prvá nezhoda |
| 10 | príjemca v SML/SMP (neexistuje, nepodporuje typ, SMP nedostupné) a iné chyby | PROCESSING_ERROR | Failed to queue document: … |
| 11 | všetko v poriadku | MESSAGE_QUEUED a ďalší životný cyklus | — |
Pravidlá strán (odosielateľ a príjemca musia sedieť s cbc:EndpointID v doklade):
| Doklad | Odosielateľ = | Príjemca = | Kódy nezhody |
|---|---|---|---|
| Faktúra, dobropis a iné doklady vystavené dodávateľom | cac:AccountingSupplierParty | cac:AccountingCustomerParty | SENDER_SUPPLIER_MISMATCH, RECEIVER_CUSTOMER_MISMATCH |
Samofaktúra (selfbilling v DocumentTypeId alebo cbc:CustomizationID) | cac:AccountingCustomerParty | cac:AccountingSupplierParty | SENDER_CUSTOMER_MISMATCH, RECEIVER_SUPPLIER_MISMATCH |
Order, OrderChange, OrderCancellation | cac:BuyerCustomerParty | cac:SellerSupplierParty | SENDER_CUSTOMER_MISMATCH, RECEIVER_SUPPLIER_MISMATCH |
Pri samofaktúre je teda tenantom odberateľ a odosielateľ (odberateľ) musí patriť tenantovi. Ak je dokument zabalený v SBDH, porovnávajú sa strany UBL vnútri obálky.
6.5 Identifikátory a opakované odoslanie
- Zamietnutie je konečné. Zamietnutý dokument APEX znova neskúša — opravte ho a pošlite ako novú správu (nový
syncKey, novécorrelation_id, novémessage_id).PROCESSING_ERRORmôže byť aj dočasný (napr. SMP nedostupné); opätovné odoslanie je bezpečné, lebo nič nebolo zaradené. - Opakované publikovanie tej istej správy (napr. po výpadku spojenia) robte s rovnakým
message_ida rovnakým telom. - Nikdy neposielajte dokument znova automaticky len preto, že neprišla odpoveď — ak prvá správa prešla, druhé odoslanie do Peppolu je nezvratné. Riešte to timeoutom a dohľadom (10.7).
Kapitola 7Výsledok odoslania — PeppolDocumentEvent
PeppolDocumentEvent (entityType = 32) posiela APEX pri každom podstatnom kroku spracovania dokumentu, ktorý ste vy odoslali cez QHUB. Bez jeho spracovania sa nedozviete, či faktúra naozaj odišla do Peppolu.
- Smer je výlučne APEX → QHUB → váš systém; eventy prichádzajú na tú istú delivery queue ako prijaté dokumenty (rozlíšite ich podľa
entityType == 32). - Obsah
dataje opäť PascalCase JSON reťazec. - Na prijaté dokumenty (Peppol → vy) žiadne eventy nechodia.
7.1 Kedy event dostanete
úspech: MESSAGE_QUEUED → QUEUE_PROCESSING_STARTED → MESSAGE_DELIVERED (→ MLS_RECEIVED) opakovanie: MESSAGE_QUEUED → QUEUE_PROCESSING_STARTED → QUEUE_PROCESSING_FAILED → … → MESSAGE_DELIVERED neúspech: MESSAGE_QUEUED → … → QUEUE_PROCESSING_FAILED (4×) → QUEUE_DEAD_LETTER zamietnutie: VALIDATION_ERROR | LICENSE_ERROR | AUTHORIZATION_ERROR | PROCESSING_ERROR (nič sa nezaradilo) neskôr: MESSAGE_DELIVERED → MLS_RECEIVED_NEGATIVE (prijímajúci AP dokument odmietol)
Prvý event príde zvyčajne do 5–15 sekúnd od publikovania (plus oneskorenie QHUB). AS4 odoslanie má 5 pokusov s odstupom 30, 60, 120 a 240 s — konečný výsledok poznáte spravidla do 8 minút.
7.2 Hodnoty EventType
Zamietnutie pri prevzatí (dokument sa do frontu nedostal — konečný stav):
EventType | Význam | Čo s tým |
|---|---|---|
VALIDATION_ERROR | Chýbajúce pole, zlyhaná XSD / Schematron / biznis validácia alebo nezhoda strán. Detaily v ValidationErrors a ValidationWarnings. | Opravte dokument a pošlite ako novú správu |
LICENSE_ERROR | Tenant nemá platnú licenciu APEX. | Kontaktujte ASOL / partnera |
AUTHORIZATION_ERROR | Odosielateľ v APEX neexistuje, nie je priradený k tenantovi alebo patrí inému tenantovi (dôvod v Error). | Skontrolujte SenderParticipantId; prípadne požiadajte ASOL o registráciu participanta |
PROCESSING_ERROR | Nerozpoznaný ownerTenantId, chýbajúce data, príjemca nie je v Peppol alebo nepodporuje typ dokumentu, dočasná chyba SMP (Error: Failed to queue document: …). | Skontrolujte tenanta a príjemcu; pri dočasnej chybe pošlite znova |
Životný cyklus odosielania:
EventType | Význam | Message |
|---|---|---|
MESSAGE_QUEUED | Dokument prešiel kontrolami a je vo fronte | Document queued for async AS4 sending to <endpoint> |
QUEUE_PROCESSING_STARTED | Začal sa pokus o odoslanie (pri každom pokuse) | Worker … started processing (attempt n) |
QUEUE_PROCESSING_FAILED | Pokus zlyhal, ďalší je naplánovaný — priebežný stav | Attempt n/5 failed, retry in Ns: <príčina> |
MESSAGE_DELIVERED | Doručené do prístupového bodu príjemcu (AS4 receipt) | Message delivered successfully |
QUEUE_DEAD_LETTER | Definitívne zlyhanie — pokusy vyčerpané alebo trvalá chyba (napr. certifikát príjemcu nevydaný CA OpenPeppol); môže prísť aj bez predchádzajúceho QUEUE_PROCESSING_FAILED | All 5 retry attempts exhausted: … / Not retried, no later attempt can succeed: … |
MLS_RECEIVED | Prijímajúci prístupový bod potvrdil spracovanie (Peppol MLS AP/AB) | MLS AP received from … |
MLS_RECEIVED_NEGATIVE | Prijímajúci prístupový bod dokument odmietol (MLS RE) — aj po MESSAGE_DELIVERED | MLS RE received from …: <dôvod> |
Peppol MLS je v sieti povinný až od 28. 2. 2027 (príjem) a 31. 3. 2027 (odosielanie) — dovtedy
MLS_RECEIVED*od mnohých prístupových bodov nepríde. Na neznáme hodnotyEventTypebuďte pripravení: zalogujte ich a správu potvrďte.
7.3 Ako event spárujete so svojím dokumentom
| Pole eventu | Porovnávate s | Spoľahlivosť |
|---|---|---|
PeppolDocumentKey.SyncKey | syncKey, ktoré ste poslali v SynchronizationMessage | odporúčané — QHUB ho prenáša nezmenené |
PeppolDocumentMessageCorrelationId | korelačné ID doručenia, ktoré dostal APEX | doplnkové — či sa zhoduje s vaším AMQP correlation_id, závisí od QHUB |
PeppolDocumentKey.BusinessKey | As4MessageId, ktoré ste poslali | pri odosielaní je prázdne — nepoužívajte |
Ak ste dokument poslali so suffixRoutingKey, obálka každého eventu ho nesie späť.
7.4 Príklad eventu
Obsah message v doručenej obálke (polia routingKey, deliverySystem a transactionId dopĺňa QHUB):
{
"transactionId": 4720,
"eventType": 1,
"entityType": 32,
"routingKey": "MYERP.c0ffee00-1111-2222-3333-444455556666",
"ownerTenantId": "c0ffee00-1111-2222-3333-444455556666",
"ownerSystem": "APEX",
"deliverySystem": "MYERP",
"deliveryTenantId": "c0ffee00-1111-2222-3333-444455556666",
"syncKey": "2f6d0a1e-4b3c-4d5e-8f70-a1b2c3d4e5f6",
"schemaVersion": "1.0.0",
"environment": "…",
"domainSyncVersion": 1,
"occurredAt": "2026-10-09T08:15:36Z",
"data": "{\"EventId\":\"f47ac10b-58cc-4372-a567-0e02b2c3d479\",\"EventType\":\"VALIDATION_ERROR\", … }",
"ignoredProperties": null,
"suffixRoutingKey": "SN-001"
}Deserializovaný obsah data:
{
"EventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"PeppolDocumentMessageCorrelationId": "3b2f1c0a-9d8e-4f7a-b6c5-1a2b3c4d5e6f",
"EventType": "VALIDATION_ERROR",
"Message": "Document send rejected — document validation failed",
"Error": "Document validation failed: 1 error(s)",
"ValidationErrors": "[{\"code\":\"PEPPOL-EN16931-R003\",\"message\":\"A buyer reference or purchase order reference MUST be provided.\",\"location\":\"/Invoice\"}]",
"ValidationWarnings": null,
"Timestamp": "2026-10-09T10:15:31.123456789+02:00",
"PeppolDocumentKey": {
"BusinessKey": null,
"SyncKey": "9a1d2c3e-6b7f-4c8d-9e0a-1b2c3d4e5f60"
}
}
ValidationErrorsaValidationWarningssú JSON reťazce — deserializujte ich samostatne. Položky majú poliacode,message,location(camelCase; parsujte bez ohľadu na veľkosť písmen). Prázdny zoznam jenull. Pri nezhode strán obsahujecodenapr.SENDER_SUPPLIER_MISMATCHalocationXPath strany.
7.5 Stav dokumentu u vás
- Konečné stavy:
MESSAGE_DELIVERED(úspech, ktorý môže ešte prepísaťMLS_RECEIVED_NEGATIVE),QUEUE_DEAD_LETTER,MLS_RECEIVED_NEGATIVEa štyri zamietnutia*_ERROR. - Poradie eventov nie je zaručené — publikujú ich viaceré inštancie APEX a QHUB ich spracúva paralelne;
QUEUE_PROCESSING_STARTEDmôže prísť poMESSAGE_DELIVERED. Zoraďujte podľaTimestampa z konečného stavu sa nevracajte do priebežného. - Deduplikujte podľa
EventId.
7.6 Aj na event pošlite ACK
PeppolDocumentEvent je bežná synchronizačná správa — po spracovaní pošlite ACK ako pri dokumente (5.5).
Kapitola 8Štruktúry správ
8.1 Prehľad — kam čo ide a v akom formáte
| Smer | Exchange | Formát | content-type |
|---|---|---|---|
| QHUB → vy (doručenie dokumentu alebo eventu) | vaša clouderp-delivery-queue.{RK} | MassTransit obálka | application/vnd.masstransit+json |
| vy → QHUB (dokument na odoslanie) | clouderp-sync-queue | holá SynchronizationMessage | application/json |
| vy → QHUB (ACK) | clouderp-sync-response-queue | MassTransit obálka | application/vnd.masstransit+json |
Asymetria je skutočné správanie QHUB: sync konzument používa raw JSON deserializér, response konzument nie. Pri odosielaní aj ACK pridajte hlavičku MT-MessageType s príslušným urn:message:….
8.2 MassTransit obálka
Všetky polia sú camelCase. Pri ACK sú pre vás podstatné tučné polia.
| Pole | Typ | Popis |
|---|---|---|
messageId | string (GUID) | Identifikátor správy; pri opakovaní tej istej správy rovnaký |
correlationId | string (GUID) | Korelačné ID; v ACK skopírujte z doručenia |
requestId, conversationId, initiatorId | string | Nepoužívajú sa |
sourceAddress, destinationAddress, responseAddress, faultAddress | string | Informatívne |
messageType | string[] | ["urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationMessage"] alebo …:SynchronizationResponseMessage |
message | object | SynchronizationMessage alebo SynchronizationResponseMessage |
expirationTime | string | Nepoužíva sa |
sentTime | string | ISO 8601 UTC |
headers, host | object | Voliteľné / vypĺňa MassTransit |
8.3 SynchronizationMessage
Používa sa v oboch smeroch; názvy polí sú camelCase.
| Pole | Typ | Pri odosielaní (vy) | Pri doručení (od APEX) |
|---|---|---|---|
transactionId | long | 0 | ID transakcie QHUB — skopírujte do ACK |
eventType | int alebo string | vždy 1 | 1 |
entityType | int alebo string | 30 | 30 dokument, 32 event |
routingKey | string | null | váš routing key (dopĺňa QHUB) |
ownerTenantId | string (UUID) | IAM tenant zákazníka | IAM tenant zákazníka |
ownerSystem | string | váš SystemCode | zdrojový systém (APEX) — nespoliehajte sa na presný tvar |
deliverySystem | string | null | váš systém (dopĺňa QHUB) |
deliveryTenantId | string (UUID) | tenant alebo null | IAM tenant zákazníka |
syncKey | string | nové UUID pre každý dokument | dokument: AS4 MessageId; event: nové UUID |
schemaVersion | string | "1.0.0" | "1.0.0" |
environment | string | podľa dohody pri onboardingu | označenie prostredia APEX |
domainSyncVersion | int | 1 | 1 alebo skopírovaná hodnota |
occurredAt | string (ISO 8601) | čas vzniku u vás | čas publikovania |
data | string (JSON) | PeppolDocument v PascalCase | PeppolDocument alebo PeppolDocumentEvent v PascalCase |
ignoredProperties | string[] / null | null | vlastnosti, ktoré má cieľ ignorovať (dopĺňa QHUB) |
suffixRoutingKey | string / null | voliteľné označenie prevádzky | pri evente skopírované z vášho dokumentu, inak null |
8.4 SynchronizationResponseMessage (ACK)
| Pole | Typ | Popis |
|---|---|---|
transactionId | long | Skopírované z doručenej SynchronizationMessage |
result.success | boolean | true = spracované úspešne |
result.errorCode | string / null | Váš kód chyby pri success=false |
result.errorParams | string[] / null | Parametre chybového kódu |
Kapitola 9Entity PeppolDocument a PeppolDocumentEvent
Obe entity sa prenášajú ako JSON reťazec v poli data a obe používajú PascalCase.
9.1 PeppolDocument (entityType = 30, schéma 1.0.0)
| Pole | Typ | Povinné | Business key | Popis |
|---|---|---|---|---|
SenderParticipantId | string | áno | nie | Peppol ID odosielateľa (pri prijatých kanonický tvar) |
ReceiverParticipantId | string | áno | nie | Peppol ID príjemcu |
DocumentTypeId | string | áno | nie | Peppol typ dokumentu (pri prijatých s prefixom busdox-docid-qns::) |
ProcessId | string | áno | nie | Peppol proces |
UblDocument | string | áno | nie | XML text; pri prijatých celý StandardBusinessDocument, pri odosielaní holý UBL alebo SBDH |
As4MessageId | string | nie | áno | AS4 MessageId prijatého dokumentu; pri odosielaní prázdne |
ReceivedAt | DateTime | nie | nie | Čas prijatia v APEX; pri odosielaní prázdne |
Príklad — prijatý dokument:
{
"SenderParticipantId": "iso6523-actorid-upis::0245:2024000002",
"ReceiverParticipantId": "iso6523-actorid-upis::0245:2024000001",
"DocumentTypeId": "busdox-docid-qns::urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
"ProcessId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
"UblDocument": "<StandardBusinessDocument xmlns=\"http://www.unece.org/cefact/namespaces/StandardBusinessDocumentHeader\"><StandardBusinessDocumentHeader>…</StandardBusinessDocumentHeader><Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\">…</Invoice></StandardBusinessDocument>",
"As4MessageId": "abc123@ap.sender.example",
"ReceivedAt": "2026-10-09T08:15:29.871Z"
}9.2 PeppolDocumentEvent (entityType = 32, schéma 1.0.0)
Posiela výlučne APEX (kapitola 7).
| Pole | Typ | Vždy vyplnené | Popis |
|---|---|---|---|
EventId | string (UUID) | áno | Jedinečné ID eventu (business key) — deduplikujte podľa neho |
PeppolDocumentMessageCorrelationId | string | áno | Korelačné ID doručenia vášho dokumentu do APEX |
EventType | string | áno | Hodnoty v 7.2 |
Message | string | spravidla | Čitateľný popis |
Error | string / null | len pri zamietnutí | Technický detail |
ValidationErrors | string (JSON) / null | pri VALIDATION_ERROR z validácie a kontroly strán | JSON pole {code, message, location} |
ValidationWarnings | string (JSON) / null | ako vyššie | Rovnaký formát |
Timestamp | DateTime | áno | Čas vzniku eventu v APEX (ISO 8601 s posunom) |
PeppolDocumentKey | object | áno | { "BusinessKey": …, "SyncKey": … } — SyncKey je syncKey vášho dokumentu; BusinessKey je As4MessageId, ktoré ste poslali (spravidla null) |
9.3 Verzie entít
Version v katalógu aj schemaVersion v správach je "1.0.0". Pri zmene schémy ASOL vydá novú verziu a oznámi ju.
9.4 Peppol participant ID
| Krajina / typ | Schéma (ICD) | Príklad |
|---|---|---|
| Slovensko — DIČ (odporúčané) | 0245 | 0245:2024000001 |
| Slovensko — IČ DPH | 9950 | 9950:SK2024000001 |
| Česko — DIČ (VAT) | 9929 | 9929:CZ12345678 |
| GLN | 0088 | 0088:1234567890123 |
Kanonický tvar je iso6523-actorid-upis:: + krátky tvar. Ktorú schému má príjemca skutočne zaregistrovanú, overte v SMP (napr. cez lookup v APEX API).
Kapitola 10Chyby, timeouty a retry
10.1 Dva kanály — nepomýľte si ich
| Kanál | Čo nesie | Kedy príde |
|---|---|---|
SynchronizationResponseMessage (ACK) | technické prevzatie správy | pri úspešnom prevzatí a pri technickom zlyhaní |
PeppolDocumentEvent (32) | biznis výsledok dokumentu | pri zamietnutí aj počas celého odosielania |
Biznis chyby (validácia, licencia, odosielateľ, príjemca) sa hlásia výhradne eventom. Kto čaká na ACK so success=false, nedočká sa.
10.2 Chyby registrácie (QHUB)
| Kód | Príčina | Riešenie |
|---|---|---|
RegCodeNotFound | Neplatný kľúč (alebo ho zdieľa viac registrácií) | Vyžiadajte nový kľúč |
RegCodeExpired | Kľúč expiroval | Vyžiadajte nový kľúč |
SystemMismatch | participantSystem nesedí s registráciou | Skontrolujte SystemCode |
TenantMismatch | participantTenantId nesedí s registráciou | Skontrolujte IAM tenant UUID |
HubInternalError | Interná chyba QHUB | Kontaktujte podporu |
10.3 Chyby synchronizácie (QHUB)
Ak QHUB vašu správu nespracuje, označí transakciu ako Failed alebo Skipped. Odosielateľ o tom správu nedostane — preto potrebujete timeouty (10.7).
| Kód | Príčina |
|---|---|
EventTypeMissing / EventTypeWrong | Neplatný eventType |
EntityDataMissing / EntityDataInvalid | Chýba data alebo sa nedá deserializovať na entitu |
TenantIdMissing | Chýba ownerTenantId |
SchemaVersionMissing | Chýba schemaVersion |
DomainSyncVersionWrong | domainSyncVersion < 1 |
DeliverySystemWrong | Pre správu neexistuje registrovaný cieľ (preskočené) |
DuplicateVersion / StaleVersion | Správa s rovnakým syncKey a verziou už bola spracovaná |
10.4 Chybové kódy v ACK od APEX
APEX posiela success=false len pri technických problémoch (a nikdy, ak je transactionId 0):
errorCode | Kedy |
|---|---|
UnsupportedEntityType | Iný entityType ako 30 (vrátane 32 poslaného do APEX) |
ProcessingFailed | Spracovanie trikrát zlyhalo na technickej chybe (napr. nečitateľné data) |
DeserializationFailed | Telo sa nepodarilo deserializovať |
10.5 Kódy vo ValidationErrors
- Kódy pravidiel Schematron (
BR-…,BR-CO-…,PEPPOL-EN16931-R…, …) a správy XSD. - Kódy APEX:
XML_001(nečitateľné XML),SCHEMA_001,SCHEMATRON_001(pre typ dokumentu neexistujú pravidlá),SCHEMATRON_002,BUSINESS_*a kódy nezhody strán (6.4). - Dočasné chyby validátora —
VALIDATION_IO,VALIDATION_ERROR,SCHEMA_ERROR,SCHEMATRON_ERROR,SCHEMATRON_000,BUSINESS_ERROR: dokument nie je nutne chybný, pošlite ho znova neskôr.
10.6 Vaše chybové kódy (v ACK)
V result.errorCode môžete použiť ľubovoľný reťazec (anglicky, bez medzier), napr. ImportFailed, DuplicateDocument, ValidationFailed, UnsupportedDocumentType, UnexpectedError. errorParams = len parametre kódu.
10.7 Keď nepríde vôbec nič
| Situácia | Prečo neprišla odpoveď |
|---|---|
| APEX vyhodnotil správu ako duplikát | Rovnaké correlation_id, transactionId alebo As4MessageId — APEX správu ticho zahodí |
data sa nedalo spracovať a transactionId je 0 | Failure ACK nemá ku ktorej transakcii priradiť; event nevznikne |
Prázdne telo alebo obálka bez message | Správa sa len zaloguje |
| Publikovanie do neexistujúcej exchange | Napr. bodkové názvy — broker vráti 404 NOT_FOUND |
| QHUB správu preskočil | Chýbajúca registrácia, duplicitná verzia (10.3) |
| APEX nepublikoval event | Výpadok spojenia s QHUB dlhší ako 5 pokusov (cca 7,5 min) — event sa stratí |
Odporúčanie: každý odoslaný dokument si označte časom. Ak do 15 minút nepríde žiadny event alebo do 60 minút konečný event, eskalujte (alert, podpora) — neposielajte ho automaticky znova, hrozí duplicitné odoslanie do Peppolu. Pre prijímanie odporúčame vedieť si stav overiť aj inou cestou (napr. inbox v APEX API), ak máte k API prístup.
10.8 Retry politiky APEX
| Čo | Pokusy | Odstup |
|---|---|---|
| Spracovanie doručenej správy (APEX ju vyberá každé ~3 s) | 3, potom failure ACK ProcessingFailed | každý cyklus |
| Publikovanie ACK z APEX | bez limitu | 30 s → 60 s → 120 s → 240 s → max 300 s |
| Publikovanie dokumentov a eventov z APEX do QHUB | 5, potom sa správa stratí | 30 s → 60 s → 120 s → 240 s |
| AS4 odoslanie do Peppolu | 5, potom QUEUE_DEAD_LETTER | 30 s → 60 s → 120 s → 240 s |
10.9 Dead-letter queue
Vaša delivery queue má argument x-dead-letter-exchange: clouderp.dlq. Správy, ktoré odmietnete (basic.nack/reject s requeue=false) alebo pri ktorých konzument zlyhá bez requeue, presunie RabbitMQ do tejto exchange.
clouderp.dlqnedeklaruje APEX ani QHUB — overte s ASOL, že existuje a má naviazanú queue, inak sa dead-letterované správy ticho stratia.- QHUB do DLQ nič nepresúva a nikoho neupozorňuje — DLQ sledujte sami (MassTransit si navyše vytvára vlastnú
…_errorqueue).
Kapitola 11Príklady implementácie
Príklady sú skrátené na podstatné; logovanie, transakcie a perzistenciu doplňte podľa svojho systému. Prístupové údaje čítajte z prostredia alebo trezoru, nikdy ich nevkladajte do kódu. Spoločné konštanty:
ROUTING_KEY = SYSTEM_CODE + "." + TENANT_ID EXCHANGE_DELIVERY = "clouderp-delivery-queue" (direct, existuje) EXCHANGE_SYNC = "clouderp-sync-queue" (fanout, existuje) EXCHANGE_SYNC_RESPONSE = "clouderp-sync-response-queue" (fanout, existuje) DELIVERY = "clouderp-delivery-queue." + ROUTING_KEY (vaša fanout exchange aj queue) MT_SYNC = "urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationMessage" MT_RESPONSE = "urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationResponseMessage" ENTITY_PEPPOL_DOCUMENT = 30, ENTITY_PEPPOL_DOCUMENT_EVENT = 32
11.1 Java (Spring AMQP)
Konfigurácia — pripojenie a vlastná topológia
@Configuration @EnableRabbit public class QHubConfig { static final String EXCHANGE_DELIVERY = "clouderp-delivery-queue"; // direct, existuje @Value("${qhub.routing-key}") String routingKey; // MYERP.{TenantId} @Value("${qhub.url}") String url; // amqps://messaging.qasida.sk:5671 @Value("${qhub.username}") String username; @Value("${qhub.password}") String password; @Value("${qhub.ca-cert-path}") String caCertPath; // CA brokera (PEM) String deliveryName() { return EXCHANGE_DELIVERY + "." + routingKey; } @Bean CachingConnectionFactory qhubConnectionFactory() throws Exception { CachingConnectionFactory cf = new CachingConnectionFactory(); cf.setUri(url); cf.setUsername(username); cf.setPassword(password); // SSLContext s truststore z CA brokera (pri mTLS aj s klientskym RSA kľúčom) cf.getRabbitConnectionFactory().useSslProtocol(sslContextFromPem(caCertPath)); return cf; } @Bean RabbitAdmin qhubAdmin(CachingConnectionFactory cf) { return new RabbitAdmin(cf); } // Deklarujú sa len vlastné objekty. Centrálne exchange ako @Bean NEDEFINUJTE — // RabbitAdmin by ich deklaroval a skončil s ACCESS_REFUSED. @Bean FanoutExchange myDeliveryExchange() { return new FanoutExchange(deliveryName(), true, false); } @Bean Queue myDeliveryQueue() { return QueueBuilder.durable(deliveryName()) .withArgument("x-dead-letter-exchange", "clouderp.dlq") .build(); } @Bean Binding queueToMyExchange() { return BindingBuilder.bind(myDeliveryQueue()).to(myDeliveryExchange()); } /** Exchange-to-exchange binding: bez neho do queue nikdy nič nepríde. */ @Bean Binding myExchangeToCentral() { return new Binding(deliveryName(), Binding.DestinationType.EXCHANGE, EXCHANGE_DELIVERY, routingKey, null); } @Bean SimpleRabbitListenerContainerFactory qhubListenerFactory(CachingConnectionFactory cf) { SimpleRabbitListenerContainerFactory f = new SimpleRabbitListenerContainerFactory(); f.setConnectionFactory(cf); f.setPrefetchCount(10); f.setAcknowledgeMode(AcknowledgeMode.AUTO); f.setDefaultRequeueRejected(false); // výnimka → DLX, nie nekonečný requeue // žiadny MessageConverter: listener dostane surové bajty MassTransit obálky return f; } @Bean RabbitTemplate qhubTemplate(CachingConnectionFactory cf) { return new RabbitTemplate(cf); } }
Prijímanie — persist-then-process
@Component @RequiredArgsConstructor public class QHubDeliveryListener { private final ObjectMapper mapper; // FAIL_ON_UNKNOWN_PROPERTIES = false private final InboxRepository inbox; // vaša tabuľka s unikátnymi indexmi // Listener len uloží surovú správu; spracovanie robí QHubInboxProcessor. @RabbitListener(queues = "#{@myDeliveryQueue.name}", containerFactory = "qhubListenerFactory") public void onDelivery(Message amqp) throws IOException { byte[] body = amqp.getBody(); if (body.length == 0) { return; } JsonNode envelope = mapper.readTree(body); if (envelope.path("messageType").toString().contains("SynchronizationResponseMessage") || !envelope.hasNonNull("message")) { return; // ACK obálka alebo prázdna správa: neukladať, neodpovedať } inbox.saveIfNew( // pri duplicite (transactionId / correlationId) nič neurobí envelope.path("message").path("transactionId").asLong(), envelope.path("correlationId").asText(null), new String(body, StandardCharsets.UTF_8)); } } @Slf4j @Component @RequiredArgsConstructor public class QHubInboxProcessor { private final ObjectMapper mapper; private final InboxRepository inbox; private final QHubAckSender ack; @Scheduled(fixedDelay = 3000) public void process() throws IOException { for (InboxRow row : inbox.findPending(50)) { JsonNode msg = mapper.readTree(row.rawJson()).path("message"); String entityType = msg.path("entityType").asText(); // číslo aj reťazec JsonNode data = mapper.readTree(msg.path("data").asText()); // 2. deserializácia, PascalCase try { if (entityType.equals("30") || entityType.equals("PeppolDocument")) { importDocument(data); // UblDocument = StandardBusinessDocument, ID v kanonickom tvare } else if (entityType.equals("32") || entityType.equals("PeppolDocumentEvent")) { updateSentDocument(data.path("PeppolDocumentKey").path("SyncKey").asText(), data.path("EventType").asText(), data); } else { log.warn("Neznámy entityType {}", entityType); } ack.send(row.correlationId(), row.transactionId(), true, null, null); } catch (Exception e) { ack.send(row.correlationId(), row.transactionId(), false, "ImportFailed", List.of(String.valueOf(row.transactionId()))); } inbox.markProcessed(row.id()); } } }
Response ACK
@Component @RequiredArgsConstructor public class QHubAckSender { private static final String MT_RESPONSE = "urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationResponseMessage"; private final RabbitTemplate qhubTemplate; private final ObjectMapper mapper; public void send(String correlationId, long transactionId, boolean success, String errorCode, List<String> errorParams) throws IOException { if (transactionId <= 0) { return; // QHUB by ACK nemal ku ktorej transakcii priradiť } // stabilné messageId: pri opakovaní toho istého ACK rovnaké String messageId = UUID.nameUUIDFromBytes( ("ack:" + correlationId + ":" + transactionId).getBytes(StandardCharsets.UTF_8)).toString(); Map<String, Object> result = new LinkedHashMap<>(); result.put("success", success); result.put("errorCode", errorCode); result.put("errorParams", errorParams); Map<String, Object> envelope = new LinkedHashMap<>(); envelope.put("messageId", messageId); envelope.put("correlationId", correlationId); envelope.put("messageType", List.of(MT_RESPONSE)); envelope.put("sentTime", Instant.now().toString()); envelope.put("message", Map.of("transactionId", transactionId, "result", result)); MessageProperties props = new MessageProperties(); props.setContentType("application/vnd.masstransit+json"); // POVINNÉ props.setHeader("MT-MessageType", MT_RESPONSE); props.setMessageId(messageId); props.setCorrelationId(correlationId); props.setDeliveryMode(MessageDeliveryMode.PERSISTENT); // ak publikovanie zlyhá, uložte ACK a opakujte, kým neprejde qhubTemplate.send("clouderp-sync-response-queue", "", new Message(mapper.writeValueAsBytes(envelope), props)); } }
Odosielanie dokumentu do Peppolu
public void sendToPeppol(String ublXml, String sender, String receiver, String documentTypeId, String processId) throws IOException { Map<String, Object> data = new LinkedHashMap<>(); // PascalCase data.put("SenderParticipantId", sender); data.put("ReceiverParticipantId", receiver); data.put("DocumentTypeId", documentTypeId); data.put("ProcessId", processId); data.put("UblDocument", ublXml); // XML text, nie Base64 String syncKey = UUID.randomUUID().toString(); // nové pre každý dokument Map<String, Object> msg = new LinkedHashMap<>(); // camelCase, bez obálky msg.put("transactionId", 0); msg.put("eventType", 1); // vždy 1 msg.put("entityType", 30); msg.put("routingKey", null); msg.put("ownerTenantId", tenantId); // IAM tenant UUID msg.put("ownerSystem", systemCode); msg.put("deliverySystem", null); msg.put("deliveryTenantId", tenantId); msg.put("syncKey", syncKey); msg.put("schemaVersion", "1.0.0"); msg.put("environment", environment); msg.put("domainSyncVersion", 1); msg.put("occurredAt", Instant.now().toString()); msg.put("data", mapper.writeValueAsString(data)); msg.put("ignoredProperties", null); // outbox: stabilné message_id pre opakované publikovanie, jedinečné correlation_id pre dokument OutboxRow row = outbox.save(syncKey, UUID.randomUUID().toString(), mapper.writeValueAsBytes(msg)); MessageProperties props = new MessageProperties(); props.setContentType("application/json"); // NIE masstransit+json props.setHeader("MT-MessageType", "urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationMessage"); props.setMessageId(row.id().toString()); props.setCorrelationId(row.correlationId()); props.setDeliveryMode(MessageDeliveryMode.PERSISTENT); qhubTemplate.send("clouderp-sync-queue", "", new Message(row.body(), props)); }
SpEL:
#{@myDeliveryQueue.name}— znak@je povinný, inak aplikácia spadne sEL1008E. NepoužívajteJackson2JsonMessageConverterna listeneri.
11.2 .NET (RabbitMQ.Client 7)
Príklad používa oficiálneho klienta RabbitMQ.Client 7.x (bez MassTransit), takže topológiu aj obálky máte plne pod kontrolou. Ak používate MassTransit, dbajte na to, aby nedeklaroval centrálne exchange a aby ACK aj odosielané správy mali formát z kapitoly 8.
using System.Security.Authentication; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using RabbitMQ.Client; using RabbitMQ.Client.Events; const string ExchangeDelivery = "clouderp-delivery-queue"; const string ExchangeSync = "clouderp-sync-queue"; const string ExchangeSyncResponse = "clouderp-sync-response-queue"; const string MtSync = "urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationMessage"; const string MtResponse = "urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationResponseMessage"; var routingKey = $"{systemCode}.{tenantId}"; var delivery = $"{ExchangeDelivery}.{routingKey}"; var uri = new Uri(config["QHub:Url"]!); // amqps://messaging.qasida.sk:5671 var factory = new ConnectionFactory { Uri = uri, UserName = config["QHub:Username"]!, Password = config["QHub:Password"]!, AutomaticRecoveryEnabled = true, Ssl = new SslOption { Enabled = true, ServerName = uri.Host, Version = SslProtocols.Tls12 | SslProtocols.Tls13, // CA brokera pridajte do úložiska certifikátov (alebo overte v CertificateValidationCallback); // pri mTLS: CertPath = "client.pfx", CertPassphrase = … } }; await using var connection = await factory.CreateConnectionAsync(); await using var channel = await connection.CreateChannelAsync(); // ── Topológia: len vlastné objekty ── await channel.ExchangeDeclareAsync(delivery, ExchangeType.Fanout, durable: true, autoDelete: false); await channel.QueueDeclareAsync(delivery, durable: true, exclusive: false, autoDelete: false, arguments: new Dictionary<string, object?> { ["x-dead-letter-exchange"] = "clouderp.dlq" }); await channel.QueueBindAsync(delivery, delivery, routingKey: ""); await channel.ExchangeBindAsync(destination: delivery, source: ExchangeDelivery, routingKey: routingKey); await channel.BasicQosAsync(prefetchSize: 0, prefetchCount: 10, global: false); // ── Prijímanie: uložiť a potvrdiť (persist-then-process) ── var consumer = new AsyncEventingBasicConsumer(channel); consumer.ReceivedAsync += async (_, ea) => { var envelope = ea.Body.Length > 0 ? JsonNode.Parse(ea.Body.Span) : null; var types = envelope?["messageType"]?.ToJsonString() ?? ""; if (envelope?["message"] is JsonObject msg && !types.Contains("SynchronizationResponseMessage")) { var transactionId = long.Parse(msg["transactionId"]!.ToString()); var correlationId = envelope["correlationId"]?.GetValue<string>(); await inbox.SaveIfNewAsync(transactionId, correlationId, Encoding.UTF8.GetString(ea.Body.Span)); } await channel.BasicAckAsync(ea.DeliveryTag, multiple: false); }; await channel.BasicConsumeAsync(delivery, autoAck: false, consumer); // ── Spracovanie uloženej správy (napr. z BackgroundService) ── async Task ProcessAsync(InboxRow row) { var msg = JsonNode.Parse(row.RawJson)!["message"]!; var entityType = msg["entityType"]!.ToString(); // číslo aj reťazec var data = JsonNode.Parse(msg["data"]!.GetValue<string>())!; // 2. deserializácia, PascalCase try { if (entityType is "30" or "PeppolDocument") await ImportDocumentAsync(data); // UblDocument = StandardBusinessDocument else if (entityType is "32" or "PeppolDocumentEvent") await UpdateSentDocumentAsync(data["PeppolDocumentKey"]!["SyncKey"]!.GetValue<string>(), data["EventType"]!.GetValue<string>(), data); await SendAckAsync(row.CorrelationId, row.TransactionId, true, null, null); } catch (Exception) { await SendAckAsync(row.CorrelationId, row.TransactionId, false, "ImportFailed", new[] { row.TransactionId.ToString() }); } } // ── Response ACK: MassTransit obálka ── async Task SendAckAsync(string? correlationId, long transactionId, bool success, string? errorCode, string[]? errorParams) { if (transactionId <= 0) return; // stabilné messageId pri opakovaní toho istého ACK var messageId = new Guid(MD5.HashData(Encoding.UTF8.GetBytes($"ack:{correlationId}:{transactionId}"))); var body = JsonSerializer.SerializeToUtf8Bytes(new { messageId, correlationId, messageType = new[] { MtResponse }, sentTime = DateTimeOffset.UtcNow.ToString("O"), message = new { transactionId, result = new { success, errorCode, errorParams } } }); var props = new BasicProperties { ContentType = "application/vnd.masstransit+json", // POVINNÉ MessageId = messageId.ToString(), CorrelationId = correlationId, DeliveryMode = DeliveryModes.Persistent, Headers = new Dictionary<string, object?> { ["MT-MessageType"] = MtResponse } }; await channel.BasicPublishAsync(ExchangeSyncResponse, "", mandatory: false, basicProperties: props, body: body); } // ── Odoslanie dokumentu: holá SynchronizationMessage ── async Task SendToPeppolAsync(OutboxRow row) // row: uložený dokument s Id, CorrelationId a SyncKey { var data = JsonSerializer.Serialize(new Dictionary<string, string?> // PascalCase kľúče { ["SenderParticipantId"] = row.Sender, ["ReceiverParticipantId"] = row.Receiver, ["DocumentTypeId"] = row.DocumentTypeId, ["ProcessId"] = row.ProcessId, ["UblDocument"] = row.UblXml // XML text, nie Base64 }); var body = JsonSerializer.SerializeToUtf8Bytes(new // camelCase, bez obálky { transactionId = 0, eventType = 1, entityType = 30, routingKey = (string?)null, ownerTenantId = tenantId, ownerSystem = systemCode, deliverySystem = (string?)null, deliveryTenantId = tenantId, syncKey = row.SyncKey, // nové UUID pre každý dokument schemaVersion = "1.0.0", environment, domainSyncVersion = 1, occurredAt = DateTimeOffset.UtcNow.ToString("O"), data, ignoredProperties = (string[]?)null }); var props = new BasicProperties { ContentType = "application/json", // NIE masstransit+json MessageId = row.Id.ToString(), // rovnaké pri opakovanom publikovaní CorrelationId = row.CorrelationId, // jedinečné pre dokument DeliveryMode = DeliveryModes.Persistent, Headers = new Dictionary<string, object?> { ["MT-MessageType"] = MtSync } }; await channel.BasicPublishAsync(ExchangeSync, "", mandatory: false, basicProperties: props, body: body); }
11.3 Python (pika)
import json, os, ssl, uuid import xml.etree.ElementTree as ET from datetime import datetime, timezone import pika SYSTEM_CODE = os.environ["QHUB_SYSTEM_CODE"] # napr. MYERP TENANT_ID = os.environ["QHUB_TENANT_ID"] # IAM tenant UUID ROUTING_KEY = f"{SYSTEM_CODE}.{TENANT_ID}" EXCHANGE_DELIVERY = "clouderp-delivery-queue" EXCHANGE_SYNC = "clouderp-sync-queue" EXCHANGE_SYNC_RESPONSE = "clouderp-sync-response-queue" DELIVERY = f"{EXCHANGE_DELIVERY}.{ROUTING_KEY}" MT_SYNC = "urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationMessage" MT_RESPONSE = "urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationResponseMessage" UBL_INVOICE = "{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice" # ── Pripojenie (hodnoty z Registration/Validate, uložené v trezore) ── params = pika.URLParameters(os.environ["QHUB_URL"]) # amqps://user:pass@messaging.qasida.sk:5671 ctx = ssl.create_default_context(cafile=os.environ["QHUB_CA_CERT"]) # privátna CA brokera # pri mTLS: ctx.load_cert_chain("client_cert.pem", "client_key.pem") params.ssl_options = pika.SSLOptions(ctx, params.host) connection = pika.BlockingConnection(params) channel = connection.channel() # ── Topológia: len vlastné objekty, centrálne exchange nedeklarovať ── channel.exchange_declare(DELIVERY, exchange_type="fanout", durable=True) channel.queue_declare(DELIVERY, durable=True, arguments={"x-dead-letter-exchange": "clouderp.dlq"}) channel.queue_bind(queue=DELIVERY, exchange=DELIVERY, routing_key="") channel.exchange_bind(destination=DELIVERY, source=EXCHANGE_DELIVERY, routing_key=ROUTING_KEY) channel.basic_qos(prefetch_count=10) def short_id(value): # iso6523-actorid-upis::0245:… → 0245:…, busdox-docid-qns::urn:… → urn:… if value.startswith(("iso6523-actorid-upis::", "busdox-docid-qns::")): return value.split("::", 1)[1] return value def business_document(sbdh_xml): # UblDocument prijatého dokumentu je StandardBusinessDocument; hľadajte podľa menného priestoru root = ET.fromstring(sbdh_xml.encode("utf-8")) return next(root.iter(UBL_INVOICE), None) def send_ack(correlation_id, transaction_id, success, error_code=None, error_params=None): if transaction_id <= 0: return message_id = str(uuid.uuid5(uuid.NAMESPACE_URL, f"ack:{correlation_id}:{transaction_id}")) # stabilné envelope = { "messageId": message_id, "correlationId": correlation_id, "messageType": [MT_RESPONSE], "sentTime": datetime.now(timezone.utc).isoformat(), "message": {"transactionId": transaction_id, "result": {"success": success, "errorCode": error_code, "errorParams": error_params}}, } channel.basic_publish( exchange=EXCHANGE_SYNC_RESPONSE, routing_key="", body=json.dumps(envelope).encode("utf-8"), properties=pika.BasicProperties( content_type="application/vnd.masstransit+json", # POVINNÉ headers={"MT-MessageType": MT_RESPONSE}, message_id=message_id, correlation_id=correlation_id, delivery_mode=2)) def on_message(ch, method, props, body): envelope = json.loads(body) if body else {} msg = envelope.get("message") if msg is None or any("SynchronizationResponseMessage" in t for t in envelope.get("messageType", [])): ch.basic_ack(method.delivery_tag) # prázdna správa alebo ACK obálka return correlation_id = envelope.get("correlationId") transaction_id = int(msg.get("transactionId") or 0) if not save_if_new(transaction_id, correlation_id, body): # vaša tabuľka s unikátnymi indexmi ch.basic_ack(method.delivery_tag) # duplikát return try: entity_type = str(msg["entityType"]) # číslo aj reťazec data = json.loads(msg["data"]) # 2. deserializácia, PascalCase if entity_type in ("30", "PeppolDocument"): import_document(short_id(data["SenderParticipantId"]), business_document(data["UblDocument"])) elif entity_type in ("32", "PeppolDocumentEvent"): update_sent_document(data["PeppolDocumentKey"]["SyncKey"], data["EventType"], data) send_ack(correlation_id, transaction_id, True) except Exception: send_ack(correlation_id, transaction_id, False, "ImportFailed", [str(transaction_id)]) ch.basic_ack(method.delivery_tag) def send_to_peppol(ubl_xml, sender, receiver, document_type_id, process_id, outbox_id, correlation_id, sync_key): data = {"SenderParticipantId": sender, "ReceiverParticipantId": receiver, # PascalCase "DocumentTypeId": document_type_id, "ProcessId": process_id, "UblDocument": ubl_xml} message = { # camelCase, bez obálky "transactionId": 0, "eventType": 1, "entityType": 30, "routingKey": None, "ownerTenantId": TENANT_ID, "ownerSystem": SYSTEM_CODE, "deliverySystem": None, "deliveryTenantId": TENANT_ID, "syncKey": sync_key, "schemaVersion": "1.0.0", "environment": os.environ["QHUB_ENVIRONMENT"], "domainSyncVersion": 1, "occurredAt": datetime.now(timezone.utc).isoformat(), "data": json.dumps(data), "ignoredProperties": None} channel.basic_publish( exchange=EXCHANGE_SYNC, routing_key="", body=json.dumps(message).encode("utf-8"), properties=pika.BasicProperties( content_type="application/json", # NIE masstransit+json headers={"MT-MessageType": MT_SYNC}, message_id=outbox_id, # rovnaké pri opakovanom publikovaní correlation_id=correlation_id, # jedinečné pre dokument delivery_mode=2)) channel.basic_consume(queue=DELIVERY, on_message_callback=on_message) channel.start_consuming()
Príklad v Pythone pre jednoduchosť spracúva správu priamo v callbacku; v produkcii ju najprv uložte a spracujte asynchrónne.
Kapitola 12Časté otázky
Ako zistím, že mi prišla faktúra?
Konzumujte queue clouderp-delivery-queue.{SystemCode}.{TenantId}, rozbaľte MassTransit obálku a pozrite message.entityType — 30 je Peppol dokument. Faktúru nájdete vo vnútri StandardBusinessDocument v poli UblDocument.
Ako zistím, či moja faktúra odišla do Peppolu?
Z PeppolDocumentEvent (entityType = 32) na tej istej queue: úspech je MESSAGE_DELIVERED, definitívne zlyhanie QUEUE_DEAD_LETTER alebo MLS_RECEIVED_NEGATIVE. ACK vám to nepovie.
Prečo mi na odoslaný dokument neprišla žiadna odpoveď?
Najčastejšie: (a) APEX ho vyhodnotil ako duplikát (opakované correlation_id alebo vyplnené As4MessageId), (b) publikovali ste do neexistujúcej exchange (bodkové názvy), (c) chýba registrácia alebo QHUB správu preskočil, (d) nemáte zaregistrovanú entitu 32. Pozri 10.7.
Je odoslaný dokument validovaný?
Áno — XSD, Schematron podľa typu dokumentu, biznis pravidlá a kontrola strán. Pri neúspechu príde VALIDATION_ERROR s detailmi v ValidationErrors, nie ACK so success=false.
Čo ak príjemca nie je v Peppol sieti?
APEX ho overí v SML/SMP pred zaradením; ak ho nenájde alebo príjemca neprijíma daný typ dokumentu, príde PROCESSING_ERROR s Error „Failed to queue document: …“.
Môžem poslať UPDATE alebo DELETE?
Nie. Posielajte vždy eventType = 1. APEX eventType nevyhodnocuje a každá správa znamená nové, nezvratné odoslanie. Opravu faktúry riešte podľa pravidiel Peppol BIS (napr. dobropisom).
Ako odmietnem prijatú faktúru?
Pošlite odosielateľovi Invoice Response (IMR) ako bežný odosielaný dokument — negatívny ACK do QHUB sa k nemu nedostane (5.7).
Môžem posielať aj iné dokumenty ako faktúry?
Áno — dobropisy, samofaktúry, objednávky, odpovede na faktúru a ďalšie Peppol BIS dokumenty; nastavte správne DocumentTypeId a ProcessId (príloha A) a dodržte pravidlá strán (6.4).
Musím zachovávať poradie správ?
QHUB ani APEX poradie negarantujú; každý dokument aj event je samostatná správa. Stav dokumentu odvodzujte z Timestamp a konečných stavov.
Obsluhujem viac zákazníkov. Stačí mi jedna queue?
Nie. Každý zákazník má vlastné TenantId, registráciu, routing key a queue. Pri viacerých prevádzkach jedného zákazníka pozri 3.4.
Prečo QHUB odmieta môj ACK?
Takmer vždy kvôli formátu: ACK musí byť MassTransit obálka s content-type: application/vnd.masstransit+json a hlavičkou MT-MessageType. Väčšina knižníc nastaví application/json — musíte ho prepísať.
Aký Peppol identifikátor má slovenská firma?
Odporúčaná schéma je 0245 (DIČ), napr. 0245:2024000001; niektoré firmy sú registrované aj pod 9950 (IČ DPH). Overte v SMP.
Príloha APeppol Document Type identifikátory
| Typ dokumentu | DocumentTypeId | ProcessId |
|---|---|---|
| Faktúra (Invoice) | urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1 | urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 |
| Dobropis (Credit Note) | urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1 | urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 |
| Samofaktúra (Self-billing) | urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:selfbilling:3.0::2.1 | urn:fdc:peppol.eu:2017:poacc:selfbilling:01:1.0 |
| Objednávka (Order) | urn:oasis:names:specification:ubl:schema:xsd:Order-2::Order##urn:fdc:peppol.eu:poacc:trns:order:3::2.1 | urn:fdc:peppol.eu:poacc:bis:order_only:3 (resp. …:bis:ordering:3) |
| Odpoveď na objednávku (Order Response) | urn:oasis:names:specification:ubl:schema:xsd:OrderResponse-2::OrderResponse##urn:fdc:peppol.eu:poacc:trns:order_response:3::2.1 | urn:fdc:peppol.eu:poacc:bis:ordering:3 |
| Odpoveď na faktúru (IMR) | urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:fdc:peppol.eu:poacc:trns:invoice_response:3::2.1 | urn:fdc:peppol.eu:poacc:bis:invoice_response:3 |
| MLR (Message Level Response) | urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:fdc:peppol.eu:poacc:trns:mlr:3::2.1 | urn:fdc:peppol.eu:poacc:bis:mlr:3 |
Technický Peppol MLS (
urn:peppol:edec:mls) posiela APEX sám — vy ho neposielate ani nedostávate ako dokument. Identifikátory sa s vydaniami Peppol BIS menia; pred nasadením ich overte voči aktuálnemu Peppol code listu.
Príloha BKontrolný zoznam integrácie
Onboarding
-
SystemCodea IAMTenantIdpre každého zákazníka (a sériové číslo pre každú prevádzku) - ALIS licencie na APEX a váš systém; odosielajúci participant zaregistrovaný v APEX pod tenantom
- URL a API kľúč registračnej služby, CA certifikáty brokera (intermediate aj root), prípadne klientsky certifikát (mTLS)
- Overená sieťová dostupnosť
messaging.qasida.sk:5671a hodnotaenvironment
Registrácia
- Odoslaný popis oboch entít (30 a 32) s
Version: "1.0.0"a hlavičkouAuthorization: {kľúč} - ASOL/partner vytvoril registráciu a odovzdal jednorazový kľúč
- Zavolaný
Registration/Validate, odpoveď parsovaná ako camelCase, heslo uložené v trezore
Topológia
- Vlastná fanout exchange a queue
clouderp-delivery-queue.{RoutingKey}(queue sx-dead-letter-exchange: clouderp.dlq) - Binding queue → vlastná exchange a exchange-to-exchange binding z
clouderp-delivery-queues routing key - Centrálne exchange sa aktívne nedeklarujú; overená existencia
clouderp.dlqa sledovanie DLQ
Prijímanie
- Surové bajty bez typového konvertora, persist-then-process, prefetch ~10
- ACK obálky sa ignorujú; deduplikácia podľa
transactionId,correlationId,As4MessageId,EventId -
datasa deserializuje druhýkrát (PascalCase); rozvetvenie podľaentityType30 / 32 - SBDH sa rozbaľuje podľa menného priestoru; prefixy
iso6523-actorid-upis::abusdox-docid-qns::sa normalizujú; prázdnyUblDocumentje chyba
ACK
- MassTransit obálka,
application/vnd.masstransit+json,MT-MessageType, stabilnémessageId, skopírovanécorrelationIdatransactionId - ACK sa posiela aj na eventy a pri zlyhaní publikovania sa opakuje, kým neprejde
Odosielanie
- Holá
SynchronizationMessage,application/json,MT-MessageType, stabilnémessage_id, jedinečnécorrelation_id -
eventType = 1,entityType = 30, IAMownerTenantId, novýsyncKey, prázdneAs4MessageIdaReceivedAt - Strany sedia s
EndpointIDv doklade (vrátane samofaktúr a objednávok);suffixRoutingKeypri prevádzkach
Výsledky a testy
- Stavový automat nad všetkými
EventTypevrátaneMLS_*; párovanie podľaPeppolDocumentKey.SyncKey -
ValidationErrorssa zobrazujú používateľovi; dočasné chyby validátora sa riešia opätovným odoslaním - Timeouty a alerty (15 / 60 min), žiadne automatické opätovné odoslanie
- Otestované: prijatie, odoslanie,
VALIDATION_ERROR,AUTHORIZATION_ERROR(cudzí odosielateľ),PROCESSING_ERROR(neznámy príjemca), samofaktúra, prevádzka so suffixom
Príloha CKontakty a podpora
| Typ požiadavky | Kontakt |
|---|---|
Onboarding (SystemCode, TenantId, API kľúč, registrácia a registračný kľúč) | ASOL integračný tím alebo váš partner |
| Technická podpora QHUB (RabbitMQ, prístupy, certifikáty, DLQ) | ASOL DevOps / Cloud tím |
| Technická podpora Peppol (SMP registrácia, validácia, doručovanie) | ASOL Peppol tím |
| Nahlásenie chyby | Zákaznícky portál |
Príloha DČo si overiť pri onboardingu
Tieto hodnoty závisia od prostredia alebo od nastavenia QHUB a dostanete ich od ASOL:
- URL registračnej služby QHUB, API kľúč pre katalóg entít a či
Registration/Validatevyžaduje hlavičkuAuthorization. - Sieťová dostupnosť brokera
messaging.qasida.sk:5671z vášho prostredia (firewall), virtual host a či prostredie vyžaduje mTLS. - Hodnota
environmenta presný tvarownerSystem(holýSystemCodealebo celý routing key). - Existencia exchange
clouderp.dlqs naviazanou queue a oprávnenie zapisovať do nej. - Maximálna veľkosť správy na brokeri (dokumenty s veľkými prílohami).
- Pri viacerých prevádzkach: smerovanie podľa
suffixRoutingKeya názov queue prevádzky. - Platnosť a jednorazovosť registračného kľúča.