Čo je nové oproti verzii 1.0 (apríl 2026)
  • 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 SynchronizationMessage sú camelCase, PascalCase platí len vnútri poľa data; doručenia a ACK sú MassTransit obálky.
  • Entity PeppolDocument = 30 a PeppolDocumentEvent = 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 Authorization so surovým kľúčom, odpoveď Registration/Validate v camelCase; viac prevádzok a suffixRoutingKey.
  • 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

☁ ASOL CLOUD 🖥️ Váš systém ERP · Účtovný systém · Portál konzument & producent správ 🐇 RabbitMQ AMQPS · async správy ⚡ QCloud HUB Synchronization Service Registrácie · Smerovanie · Audit HUB ☕ APEX Peppol Access Point (Java) Validácia · SMP lookup · AS4 · MLS AS4 AS4 / HTTPS 🌐 Peppol Network Európska eInvoicing sieť doručenia · eventy

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ávaentityTypeSmerČo prenáša
PeppolDocument30oboma smermiUBL dokument + Peppol identifikátory (odosielaný alebo prijatý dokument)
PeppolDocumentEvent32len APEX → váš systémbiznis výsledok dokumentu, ktorý ste odoslali: zaradený, doručený, zamietnutý, dead letter, MLS
SynchronizationResponseMessage (ACK)–príjemca správy → QHUBtechnické 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

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ógiaDeklarovať 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ýsledkuKonzumovať PeppolDocumentEvent z tej istej queue a viesť stav dokumentu.
Idempotencia a dohľadDeduplikovať 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

  1. 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.
  2. 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.)
  3. QHUB pozná váš systém — váš SystemCode sa objaví v katalógu QHUB po odoslaní popisu entít (3.1).
  4. Odosielateľ je participant vášho tenanta. Peppol participant, za ktorého posielate (napr. 0245:2024000001), musí byť v APEX zaregistrovaný a priradený k tenantovi z ownerTenantId. Registruje ho ASOL (napr. pri registrácii v SMP); neznámeho odosielateľa APEX sám nezaloží.
  5. 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žkaPoznámka
SystemCodeKód vášho systému v QHUB/ALIS (napr. MYERP).
TenantIdIAM tenant UUID zákazníka — pre každého zákazníka zvlášť.
Sériové číslo prevádzkyLen 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 brokeraCertifiká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

#KrokKto
1ALIS licencie na APEX a váš systém pre tenanta zákazníka; registrácia participanta (SMP)ASOL / partner
2Odoslanie popisu entít PeppolDocument (30) a PeppolDocumentEvent (32) do katalógu QHUBvy
3Vytvorenie registrácie APEX ↔ váš systém (prípadne pre každú prevádzku)ASOL / partner
4Vygenerovanie jednorazového registračného kľúča a jeho bezpečné odovzdanieASOL / partner
5POST /api/v1/Registration/Validate → RabbitMQ URL, meno a heslovy
6Inštalácia CA certifikátu (a klientskeho certifikátu, ak je mTLS)vy
7Deklarácia delivery exchange, queue a väzieb; spustenie konzumentavy
8Prijímanie + ACK, odosielanie, spracovanie eventov, deduplikácia, timeoutyvy
9Test 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:

http
POST {QHUB_REGISTRATION_URL}/api/v1/SyncEntityCatalogItem
Content-Type: application/json
Accept: application/json
Authorization: {API kľúč}

Authorization obsahuje surový API kľúč bez prefixu (žiadne Bearer ani Basic ). Hlavička X-Api-Key nie 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):

json
{
  "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 }
        ]
      }
    }
  ]
}

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:

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:

http
POST {QHUB_REGISTRATION_URL}/api/v1/Registration/Validate
Content-Type: application/json
json
{
  "participantSystem": "MYERP",
  "participantTenantId": "c0ffee00-1111-2222-3333-444455556666",
  "registrationKey": "REG-KEY-OD-ASOL",
  "routingKey": "MYERP.c0ffee00-1111-2222-3333-444455556666"
}

routingKey identifikuje 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:

json
{
  "participantSystem": "MYERP",
  "participantTenantId": "c0ffee00-1111-2222-3333-444455556666",
  "rabbitMqUrl": "amqps://messaging.qasida.sk:5671",
  "rabbitMqName": "myerp_c0ffee00",
  "rabbitMqPass": "••••••••••••",
  "errorCode": null,
  "success": true,
  "registeredEntities": [ … ]
}

3.4 Viac zákazníkov a viac prevádzok


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ázovTypKto ho deklarujeÚčel
clouderp-sync-queuefanout exchangeQHUB (existuje)Vy publikujete dokumenty na odoslanie (routing key "")
clouderp-sync-response-queuefanout exchangeQHUB (existuje)Vy publikujete ACK (routing key "")
clouderp-delivery-queuedirect exchangeQHUB (existuje)Centrálna exchange, do ktorej QHUB publikuje doručenia s routing key príjemcu
clouderp-delivery-queue.{RoutingKey}fanout exchangevyVaša per-inštanciová exchange
clouderp-delivery-queue.{RoutingKey}queuevyVaša delivery queue — odtiaľto čítate
clouderp.dlqdead-letter exchange— (overte s ASOL)Cieľ správ, ktoré vaša queue dead-letteruje

Bodkové názvy neexistujú. clouderp.sync, clouderp.sync.response ani clouderp.delivery na brokeri nie sú (publish do nich skončí 404 NOT_FOUND). Jediný objekt s bodkami je clouderp.dlq. Exchange a queue clouderp-delivery-queue.{RoutingKey} majú rovnaký názov — v RabbitMQ sú to oddelené menné priestory.

4.2 Ako to do seba zapadá

diagram
  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áciaOprávnenie
Deklarácia vlastnej exchange a queueconfigure na clouderp-delivery-queue.{RoutingKey}
Queue s dead-letter exchangewrite na clouderp.dlq
Binding queue → vaša exchangewrite na queue, read na vašej exchange
Exchange-to-exchange bindingwrite na vašej exchange, read na clouderp-delivery-queue
Konzumáciaread na vašej queue
Publikovanie dokumentov a ACKwrite 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.

postup
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}

4.5 TLS a mTLS

4.6 Nastavenia konzumenta, ktoré odporúčame

NastavenieHodnotaPrečo
prefetch10drží pamäť pod kontrolou
potvrdzovaniemanuálne/AUTO po úspešnom uloženísprávu potvrďte až po trvalom uložení
requeue pri chybefalsepri výnimke ide správa do DLX, nie donekonečna dokola
prevod správysurové bajty, žiadny typový konvertordoručenie je MassTransit obálka; napr. Spring Jackson2JsonMessageConverter na hlavičke __TypeId__ zlyhá a správa skončí v DLQ
spracovaniepersist-then-processv 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

tok
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

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:

json
{
  "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 message sú v camelCase; obsah poľa data je v PascalCase. data je JSON reťazec — deserializujte ho druhýkrát.

5.3 Obsah PeppolDocument prijatého dokumentu

PoleObsah
SenderParticipantId, ReceiverParticipantIdVždy kanonický tvar iso6523-actorid-upis::0245:2024000002. Pri porovnávaní odrežte všetko po :: vrátane.
DocumentTypeIdTyp dokumentu z AS4 hlavičky; spravidla s prefixom schémy busdox-docid-qns:: — pred porovnaním ho odstráňte.
ProcessIdProces bez schémy, napr. urn:fdc:peppol.eu:2017:poacc:billing:01:1.0.
UblDocumentCelý 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.
As4MessageIdAS4 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

  1. Uložte surové bajty/JSON do vlastnej tabuľky (persist-then-process).
  2. Prázdne telo, obálku bez message a obálky ACK len zalogujte a potvrďte na úrovni RabbitMQ.
  3. Deduplikujte (5.6).
  4. Rozvetvite podľa entityType: 30 → import dokumentu (rozbaliť SBDH, normalizovať identifikátory), 32 → aktualizácia stavu odoslaného dokumentu (kapitola 7), iné → zalogovať.
  5. 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-typeapplication/vnd.masstransit+json
hlavička MT-MessageTypeurn: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.transactionIdskopírovaný z doručenia (message.transactionId)
json
{
  "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.

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.transactionIdak je väčšie ako 0
obálkové correlationIdak je vyplnené
As4MessageId z datapri entite 30 — obsahová identita prijatého dokumentu
EventId z datapri 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

tok
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-queue sa posiela holá SynchronizationMessage v koreni JSON, nie MassTransit obálka (QHUB tu používa raw JSON deserializér).

AMQP vlastnosťHodnota
exchange / routing keyclouderp-sync-queue / ""
content-typeapplication/json
hlavička MT-MessageTypeurn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationMessage
message_idGUID — rovnaký pri opakovanom publikovaní tej istej správy
correlation_idGUID — jedinečný pre každý dokument (opakované použitie môže viesť k tomu, že APEX ďalší dokument zahodí ako duplikát)
delivery modepersistent
json
{
  "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ť
transactionId0 — QHUB pridelí vlastný
eventTypevždy 1. APEX eventType nevyhodnocuje — 2 (UPDATE) aj 4 (DELETE) by znamenali nové, nezvratné odoslanie do Peppolu
entityType30
routingKey, deliverySystemnull — dopĺňa QHUB
ownerTenantIdIAM tenant UUID zákazníka; APEX podľa neho nájde tenanta, licenciu a overí odosielateľa
ownerSystemváš SystemCode (presný tvar potvrdí ASOL pri onboardingu)
deliveryTenantIdtenant UUID alebo null (APEX ho nepoužíva)
syncKeynové 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
environmentoznačenie prostredia dohodnuté pri onboardingu (APEX ho nekontroluje)
domainSyncVersion1 (≥ 1); nie je to mechanizmus riešenia konfliktov
occurredAtčas vzniku u vás, ISO 8601 reťazec
dataentita PeppolDocument ako JSON reťazec v PascalCase
suffixRoutingKeyvoliteľné — označenie prevádzky, aby eventy prišli do jej queue (3.4)

6.3 Polia entity PeppolDocument pri odosielaní

PolePovinnéPravidlá
SenderParticipantIdánoKrá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ánoRovnaké 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ánoPeppol proces, napr. urn:fdc:peppol.eu:2017:poacc:billing:01:1.0.
UblDocumentánoXML text v UTF-8 (nie Base64) — holý UBL alebo UBL v SBDH. Úvodný BOM APEX odstráni.
As4MessageIdnechajte prázdneJe to deduplikačný kľúč APEX — opakovaná hodnota spôsobí, že APEX dokument ticho zahodí.
ReceivedAtnechajte prázdneAk 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):

#KontrolaEventTypeError
0data nie je platný JSON alebo sa nedá namapovať (napr. zlý ReceivedAt)žiadny event; po 3 pokusoch failure ACK ProcessingFailed—
1chýba message alebo dataPROCESSING_ERRORMissing inner message or data …
2prázdny UblDocumentVALIDATION_ERRORUblDocument is missing or blank …
3chýba odosielateľ alebo príjemcaVALIDATION_ERRORSenderParticipantId or ReceiverParticipantId is missing …
4ownerTenantId nezodpovedá tenantoviPROCESSING_ERRORTenant could not be resolved for ownerTenantId=…
5odosielateľ neexistuje, nie je priradený alebo patrí inému tenantoviAUTHORIZATION_ERRORParticipant not found: … / Authenticated tenant does not have access to participant: …
6chýba typ dokumentu alebo procesVALIDATION_ERRORDocument type ID is required / Process ID is required
7licencia tenantaLICENSE_ERRORNo active license exists for tenant …, License for tenant … has expired …
8XML, XSD, Schematron, biznis pravidláVALIDATION_ERROR + ValidationErrorsDocument validation failed: N error(s)
9strany dokladu (tabuľka nižšie)VALIDATION_ERROR + ValidationErrorsprvá nezhoda
10príjemca v SML/SMP (neexistuje, nepodporuje typ, SMP nedostupné) a iné chybyPROCESSING_ERRORFailed to queue document: …
11všetko v poriadkuMESSAGE_QUEUED a ďalší životný cyklus—

Pravidlá strán (odosielateľ a príjemca musia sedieť s cbc:EndpointID v doklade):

DokladOdosielateľ =Príjemca =Kódy nezhody
Faktúra, dobropis a iné doklady vystavené dodávateľomcac:AccountingSupplierPartycac:AccountingCustomerPartySENDER_SUPPLIER_MISMATCH, RECEIVER_CUSTOMER_MISMATCH
Samofaktúra (selfbilling v DocumentTypeId alebo cbc:CustomizationID)cac:AccountingCustomerPartycac:AccountingSupplierPartySENDER_CUSTOMER_MISMATCH, RECEIVER_SUPPLIER_MISMATCH
Order, OrderChange, OrderCancellationcac:BuyerCustomerPartycac:SellerSupplierPartySENDER_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


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.

7.1 Kedy event dostanete

priebeh
ú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):

EventTypeVýznamČo s tým
VALIDATION_ERRORChý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_ERRORTenant nemá platnú licenciu APEX.Kontaktujte ASOL / partnera
AUTHORIZATION_ERROROdosielateľ 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_ERRORNerozpoznaný 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:

EventTypeVýznamMessage
MESSAGE_QUEUEDDokument prešiel kontrolami a je vo fronteDocument queued for async AS4 sending to <endpoint>
QUEUE_PROCESSING_STARTEDZačal sa pokus o odoslanie (pri každom pokuse)Worker … started processing (attempt n)
QUEUE_PROCESSING_FAILEDPokus zlyhal, ďalší je naplánovaný — priebežný stavAttempt n/5 failed, retry in Ns: <príčina>
MESSAGE_DELIVEREDDoručené do prístupového bodu príjemcu (AS4 receipt)Message delivered successfully
QUEUE_DEAD_LETTERDefinití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_FAILEDAll 5 retry attempts exhausted: … / Not retried, no later attempt can succeed: …
MLS_RECEIVEDPrijímajúci prístupový bod potvrdil spracovanie (Peppol MLS AP/AB)MLS AP received from …
MLS_RECEIVED_NEGATIVEPrijímajúci prístupový bod dokument odmietol (MLS RE) — aj po MESSAGE_DELIVEREDMLS 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 hodnoty EventType buďte pripravení: zalogujte ich a správu potvrďte.

7.3 Ako event spárujete so svojím dokumentom

Pole eventuPorovnávate sSpoľahlivosť
PeppolDocumentKey.SyncKeysyncKey, ktoré ste poslali v SynchronizationMessageodporúčané — QHUB ho prenáša nezmenené
PeppolDocumentMessageCorrelationIdkorelačné ID doručenia, ktoré dostal APEXdoplnkové — či sa zhoduje s vaším AMQP correlation_id, závisí od QHUB
PeppolDocumentKey.BusinessKeyAs4MessageId, ktoré ste poslalipri 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):

json
{
  "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:

json
{
  "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"
  }
}

ValidationErrors a ValidationWarnings sú JSON reťazce — deserializujte ich samostatne. Položky majú polia code, message, location (camelCase; parsujte bez ohľadu na veľkosť písmen). Prázdny zoznam je null. Pri nezhode strán obsahuje code napr. SENDER_SUPPLIER_MISMATCH a location XPath strany.

7.5 Stav dokumentu u vás

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

SmerExchangeFormátcontent-type
QHUB → vy (doručenie dokumentu alebo eventu)vaša clouderp-delivery-queue.{RK}MassTransit obálkaapplication/vnd.masstransit+json
vy → QHUB (dokument na odoslanie)clouderp-sync-queueholá SynchronizationMessageapplication/json
vy → QHUB (ACK)clouderp-sync-response-queueMassTransit obálkaapplication/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.

PoleTypPopis
messageIdstring (GUID)Identifikátor správy; pri opakovaní tej istej správy rovnaký
correlationIdstring (GUID)Korelačné ID; v ACK skopírujte z doručenia
requestId, conversationId, initiatorIdstringNepoužívajú sa
sourceAddress, destinationAddress, responseAddress, faultAddressstringInformatívne
messageTypestring[]["urn:message:CloudERP.QHubApi.Hub.Abstractions.Messages:SynchronizationMessage"] alebo …:SynchronizationResponseMessage
messageobjectSynchronizationMessage alebo SynchronizationResponseMessage
expirationTimestringNepoužíva sa
sentTimestringISO 8601 UTC
headers, hostobjectVoliteľné / vypĺňa MassTransit

8.3 SynchronizationMessage

Používa sa v oboch smeroch; názvy polí sú camelCase.

PoleTypPri odosielaní (vy)Pri doručení (od APEX)
transactionIdlong0ID transakcie QHUB — skopírujte do ACK
eventTypeint alebo stringvždy 11
entityTypeint alebo string3030 dokument, 32 event
routingKeystringnullváš routing key (dopĺňa QHUB)
ownerTenantIdstring (UUID)IAM tenant zákazníkaIAM tenant zákazníka
ownerSystemstringváš SystemCodezdrojový systém (APEX) — nespoliehajte sa na presný tvar
deliverySystemstringnullváš systém (dopĺňa QHUB)
deliveryTenantIdstring (UUID)tenant alebo nullIAM tenant zákazníka
syncKeystringnové UUID pre každý dokumentdokument: AS4 MessageId; event: nové UUID
schemaVersionstring"1.0.0""1.0.0"
environmentstringpodľa dohody pri onboardinguoznačenie prostredia APEX
domainSyncVersionint11 alebo skopírovaná hodnota
occurredAtstring (ISO 8601)čas vzniku u vásčas publikovania
datastring (JSON)PeppolDocument v PascalCasePeppolDocument alebo PeppolDocumentEvent v PascalCase
ignoredPropertiesstring[] / nullnullvlastnosti, ktoré má cieľ ignorovať (dopĺňa QHUB)
suffixRoutingKeystring / nullvoliteľné označenie prevádzkypri evente skopírované z vášho dokumentu, inak null

8.4 SynchronizationResponseMessage (ACK)

PoleTypPopis
transactionIdlongSkopírované z doručenej SynchronizationMessage
result.successbooleantrue = spracované úspešne
result.errorCodestring / nullVáš kód chyby pri success=false
result.errorParamsstring[] / nullParametre 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)

PoleTypPovinnéBusiness keyPopis
SenderParticipantIdstringánoniePeppol ID odosielateľa (pri prijatých kanonický tvar)
ReceiverParticipantIdstringánoniePeppol ID príjemcu
DocumentTypeIdstringánoniePeppol typ dokumentu (pri prijatých s prefixom busdox-docid-qns::)
ProcessIdstringánoniePeppol proces
UblDocumentstringánonieXML text; pri prijatých celý StandardBusinessDocument, pri odosielaní holý UBL alebo SBDH
As4MessageIdstringnieánoAS4 MessageId prijatého dokumentu; pri odosielaní prázdne
ReceivedAtDateTimenienieČas prijatia v APEX; pri odosielaní prázdne

Príklad — prijatý dokument:

json
{
  "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).

PoleTypVždy vyplnenéPopis
EventIdstring (UUID)ánoJedinečné ID eventu (business key) — deduplikujte podľa neho
PeppolDocumentMessageCorrelationIdstringánoKorelačné ID doručenia vášho dokumentu do APEX
EventTypestringánoHodnoty v 7.2
MessagestringspravidlaČitateľný popis
Errorstring / nulllen pri zamietnutíTechnický detail
ValidationErrorsstring (JSON) / nullpri VALIDATION_ERROR z validácie a kontroly stránJSON pole {code, message, location}
ValidationWarningsstring (JSON) / nullako vyššieRovnaký formát
TimestampDateTimeánoČas vzniku eventu v APEX (ISO 8601 s posunom)
PeppolDocumentKeyobjectá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 / typSchéma (ICD)Príklad
Slovensko — DIČ (odporúčané)02450245:2024000001
Slovensko — IČ DPH99509950:SK2024000001
Česko — DIČ (VAT)99299929:CZ12345678
GLN00880088: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 nesieKedy príde
SynchronizationResponseMessage (ACK)technické prevzatie správypri úspešnom prevzatí a pri technickom zlyhaní
PeppolDocumentEvent (32)biznis výsledok dokumentupri 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ódPríčinaRiešenie
RegCodeNotFoundNeplatný kľúč (alebo ho zdieľa viac registrácií)Vyžiadajte nový kľúč
RegCodeExpiredKľúč expirovalVyžiadajte nový kľúč
SystemMismatchparticipantSystem nesedí s registráciouSkontrolujte SystemCode
TenantMismatchparticipantTenantId nesedí s registráciouSkontrolujte IAM tenant UUID
HubInternalErrorInterná chyba QHUBKontaktujte 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ódPríčina
EventTypeMissing / EventTypeWrongNeplatný eventType
EntityDataMissing / EntityDataInvalidChýba data alebo sa nedá deserializovať na entitu
TenantIdMissingChýba ownerTenantId
SchemaVersionMissingChýba schemaVersion
DomainSyncVersionWrongdomainSyncVersion < 1
DeliverySystemWrongPre správu neexistuje registrovaný cieľ (preskočené)
DuplicateVersion / StaleVersionSprá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):

errorCodeKedy
UnsupportedEntityTypeIný entityType ako 30 (vrátane 32 poslaného do APEX)
ProcessingFailedSpracovanie trikrát zlyhalo na technickej chybe (napr. nečitateľné data)
DeserializationFailedTelo sa nepodarilo deserializovať

10.5 Kódy vo ValidationErrors

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áciaPrečo neprišla odpoveď
APEX vyhodnotil správu ako duplikátRovnaké correlation_id, transactionId alebo As4MessageId — APEX správu ticho zahodí
data sa nedalo spracovať a transactionId je 0Failure ACK nemá ku ktorej transakcii priradiť; event nevznikne
Prázdne telo alebo obálka bez messageSpráva sa len zaloguje
Publikovanie do neexistujúcej exchangeNapr. bodkové názvy — broker vráti 404 NOT_FOUND
QHUB správu preskočilChýbajúca registrácia, duplicitná verzia (10.3)
APEX nepublikoval eventVý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

ČoPokusyOdstup
Spracovanie doručenej správy (APEX ju vyberá každé ~3 s)3, potom failure ACK ProcessingFailedkaždý cyklus
Publikovanie ACK z APEXbez limitu30 s → 60 s → 120 s → 240 s → max 300 s
Publikovanie dokumentov a eventov z APEX do QHUB5, potom sa správa stratí30 s → 60 s → 120 s → 240 s
AS4 odoslanie do Peppolu5, potom QUEUE_DEAD_LETTER30 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.


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:

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

java
@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

java
@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

java
@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

java
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 s EL1008E. Nepoužívajte Jackson2JsonMessageConverter na 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.

c#
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)

python
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 dokumentuDocumentTypeIdProcessId
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.1urn: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.1urn: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.1urn: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.1urn: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.1urn: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.1urn: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.1urn: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

Registrácia

Topológia

Prijímanie

ACK

Odosielanie

Výsledky a testy


Príloha CKontakty a podpora

Typ požiadavkyKontakt
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 chybyZá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:

  1. URL registračnej služby QHUB, API kľúč pre katalóg entít a či Registration/Validate vyžaduje hlavičku Authorization.
  2. Sieťová dostupnosť brokera messaging.qasida.sk:5671 z vášho prostredia (firewall), virtual host a či prostredie vyžaduje mTLS.
  3. Hodnota environment a presný tvar ownerSystem (holý SystemCode alebo celý routing key).
  4. Existencia exchange clouderp.dlq s naviazanou queue a oprávnenie zapisovať do nej.
  5. Maximálna veľkosť správy na brokeri (dokumenty s veľkými prílohami).
  6. Pri viacerých prevádzkach: smerovanie podľa suffixRoutingKey a názov queue prevádzky.
  7. Platnosť a jednorazovosť registračného kľúča.