Integrační příručka: Napojení třetí strany na QHUB a příjem/odeslání Peppol dokumentů přes APEX
Určeno pro: Vývojáře externích systémů (ERP, účetní systémy, portály), kteří se chtějí napojit na QCloud HUB a vyměňovat Peppol dokumenty (faktury, …) prostřednictvím APEX Access Pointu.
1. Jak funguje integrace
Celkový pohled
Co QHUB dělá za vás
- Směrování – QHUB doručí zprávu z vašeho systému do APEX-u a naopak; nemusíte vědět, kde APEX běží.
- Retry a DLQ – při dočasném výpadku QHUB zprávu zopakuje; pokud všechny pokusy selžou, zpráva půjde do Dead-Letter Queue.
- Audit – každá zpráva je zaznamenána v transakční tabulce se stavem a časem.
Co musíte udělat vy
| Směr | Vaše zodpovědnost |
|---|---|
| Příjem (Peppol → váš systém) | Konzumovat zprávy z vaší delivery queue; zpracovat UBL XML; odeslat response zpět. |
| Odesílání (váš systém → Peppol) | Připravit UBL XML dokument; odeslat synchronizační zprávu do sync queue; sledovat stav. |
2. Předpoklady a onboarding
2.1 Co potřebujete před začátkem
-
Peppol participant ID – váš klient musí být registrován v Peppol síti. Identifikátor má formát
{schemeId}:{participantId}, například0245:2024000001.Schéma
0245= slovenské DIČ v Peppol. Pokud neznáte své Peppol ID, kontaktujte svého Peppol Service Provider (SMP). -
ASOL kontakt – požádejte ASOL tým o:
- přidělení
SystemCodevašeho systému (např.MYERP), - přidělení
TenantId(UUID) vašeho tenanta v QHUB, - přístup k QHUB Registration API (URL + API klíč).
- přidělení
-
RabbitMQ klient – váš systém musí umět komunikovat s RabbitMQ (AMQP 0-9-1 protokol). Doporučujeme:
- Java: Spring AMQP / RabbitMQ Java Client
- .NET: MassTransit / RabbitMQ.Client
- Python: pika
- Node.js: amqplib
2.2 Onboarding kroky (přehled)
1. Dohodnutie SystemCode + TenantId s ASOL
↓
2. Odoslanie popisu entity PeppolDocument do QHUB Registration API
↓
3. Administrátor ASOL vygeneruje jednorazový registračný kľúč
↓
4. Váš systém odošle kľúč na Registration API → získa RabbitMQ credentials
↓
5. Nakonfigurujete RabbitMQ v svojom systéme
↓
6. Otestujete end-to-end komunikáciu3. Registrace systému v QHUB
Registrace probíhá ve dvou krocích přes HTTP REST API.
3.1 Odeslání popisu entity
Před vytvořením registrace musíte QHUB informovat, jaké entity váš systém podporuje. Pro Peppol dokumenty jde o entitu PeppolDocument.
Endpoint:
POST {QHUB_REGISTRATION_URL}/api/v1/SyncEntityCatalogItem/StoreSyncEntityItems Content-Type: application/json X-Api-Key: {váš API kľúč}
Request body:
{
"System": "MYERP",
"SyncEntityDatas": [
{
"EntityCode": "PeppolDocument",
"EntityType": 100,
"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": []
}
}
]
}
EntityType: 100– přesná hodnota bude přidělena ASOL týmem. Použijte hodnotu, kterou obdržíte od ASOL při onboardingu. Číslo musí být konzistentní mezi všemi systémy.
Úspěšná odpověď: HTTP 200, body true
Toto volání opakujte při každém startu vaší aplikace i při každé změně schématu (a zvyšte přitom Version).
3.2 Validace registračního klíče
Administrátor ASOL vám přes zabezpečený kanál (např. email, Jira) předá jednorázový registrační klíč (platný 24 hodin).
Endpoint:
POST {QHUB_REGISTRATION_URL}/api/v1/Registration/Validate Content-Type: application/json
Request body:
{
"ParticipantSystem": "MYERP",
"ParticipantTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"RegistrationKey": "REG-KEY-ODOSLANY-ADMINOM",
"RoutingKey": "MYERP.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
RoutingKey– pro systémy mimo ASOL Cloud (3ST) má formát{SystemCode}.{TenantId}. Tento klíč identifikuje vaši delivery queue.
Úspěšná odpověď (HTTP 200):
{
"ParticipantSystem": "MYERP",
"ParticipantTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"RabbitMqUrl": "amqps://rmq.qcloud.assecosoldevsk.com:5671",
"RabbitMqName": "myerp_tenant_abc123",
"RabbitMqPass": "••••••••••••",
"ErrorCode": null,
"Success": true,
"RegisteredEntities": [
{
"EntityType": 100,
"OwnerSystem": "APEX",
"OwnerTenantId": "...",
"RoutingKey": "MYERP.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"Operations": 1,
"Direction": 0,
"SyncMode": 0,
"AllowedFrom": "2026-04-16T00:00:00Z",
"AllowedTo": "2027-04-16T00:00:00Z",
"DeliverySystem": "MYERP",
"DeliveryTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
]
}⚠️ Bezpečnost:
RabbitMqPassje citlivý údaj. Uložte ho do Kubernetes Secret, HashiCorp Vault, nebo jiného bezpečného úložiště. Nikdy ho nevkládejte přímo do zdrojového kódu ani do konfiguračních souborů v repozitáři.
Chybové kódy:
| Kód | Popis |
|---|---|
RegCodeNotFound | Klíč neexistuje nebo bylo nalezeno více záznamů |
RegCodeExpired | Klíč vypršel (platný 24h) |
SystemMismatch | ParticipantSystem se neshoduje s registrací |
TenantMismatch | ParticipantTenantId se neshoduje s registrací |
HubInternalError | Interní chyba QHUB |
4. Nastavení RabbitMQ připojení
Po úspěšné registraci získáte přihlašovací údaje k RabbitMQ. Připojení probíhá přes AMQPS (TLS, port 5671).
4.1 Queues a exchanges
Musíte pracovat s těmito frontami a exchanges:
| Název | Typ | Účel |
|---|---|---|
clouderp-delivery-queue.MYERP.{TenantId} | Direct queue | Příjem zpráv z QHUB do vašeho systému |
clouderp.sync | Fanout exchange | Odesílání zpráv z vašeho systému do QHUB |
clouderp.sync.response | Fanout exchange | Odesílání odpovědi o výsledku zpracování |
clouderp.delivery | Direct exchange | Zdrojová exchange, ze které QHUB plní delivery queue |
4.2 Oprávnění vašeho RabbitMQ účtu
Váš účet má oprávnění:
| Operace | Zdroj |
|---|---|
| Configure + Read | clouderp-delivery-queue.MYERP.{TenantId} |
| Write | clouderp.sync (pro odesílání dokumentů do Peppolu) |
| Write | clouderp.sync.response (pro odesílání response) |
4.3 Deklarace vaší delivery queue
Vaši delivery queue musí deklarovat váš systém (ne QHUB). Deklarujte ji před prvním consume:
Meno: clouderp-delivery-queue.MYERP.{TenantId}
Durable: true
Auto-delete: false
Arguments:
x-dead-letter-exchange: clouderp.dlq (odporúčané)Binding na exchange:
Exchange: clouderp.delivery
Type: direct
Routing key: MYERP.{TenantId}5. Příjem dokumentů z Peppolu
Když APEX přijme Peppol dokument adresovaný vašemu klientovi, QHUB ho doručí do vaší delivery queue jako zprávu SynchronizationMessage.
5.1 Tok příjmu
Peppol odosielateľ
→ AS4 → APEX
→ QHUB (SynchronizationMessage, EntityType=PeppolDocument)
→ clouderp-delivery-queue.MYERP.{TenantId}
→ VÁŠ KONZUMER
→ spracujete UBL XML (importujete faktúru)
→ SynchronizationResponseMessage → clouderp.sync.response5.2 Co obdržíte – SynchronizationMessage
Zpráva přichází jako JSON objekt (viz sekce 7):
{
"TransactionId": 123456,
"EventType": 1,
"EntityType": 100,
"RoutingKey": "MYERP.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"OwnerTenantId": "apex-tenant-uuid",
"OwnerSystem": "APEX",
"DeliverySystem": "MYERP",
"DeliveryTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"SyncKey": "550e8400-e29b-41d4-a716-446655440000",
"SchemaVersion": "1.0.0",
"Environment": "prod",
"DomainSyncVersion": 1,
"OccurredAt": "2026-04-16T10:30:00+02:00",
"Data": "{\"SenderParticipantId\":\"0245:2024000001\",\"ReceiverParticipantId\":\"0245:9999999999\",\"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\\\"...>\",\"As4MessageId\":\"uuid@peppol\",\"ReceivedAt\":\"2026-04-16T10:29:58+02:00\"}",
"IgnoredProperties": null
}Pole Data je JSON string (serializovaný ještě jednou) obsahující entitu PeppolDocument. Je nutné ho deserializovat zvlášť.
5.3 Co musíte udělat po přijetí
- Deserializujte
DatanaPeppolDocument. - Zkontrolujte
DeliverySystemaDeliveryTenantId– měly by odpovídat vašemu systému. - Importujte
UblDocument(UBL 2.1 XML) do vašeho systému. - Odešlete odpověď
SynchronizationResponseMessagena exchangeclouderp.sync.response.
5.4 Odeslání odpovědi (povinné)
Po každém zpracování zprávy musíte odeslat odpověď zpět do QHUB. Bez odpovědi QHUB neví, zda byla zpráva zpracována.
{
"TransactionId": 123456,
"Result": {
"Success": true,
"ErrorCode": null,
"ErrorParams": null
}
}Při chybě:
{
"TransactionId": 123456,
"Result": {
"Success": false,
"ErrorCode": "ImportFailed",
"ErrorParams": ["Duplicitná faktúra INV-2026-001"]
}
}Důležité: Odpověď odešlete vždy – i při chybě. Pokud odpověď neodešlete, QHUB může zprávu po čase znovu doručit (retry).
6. Odesílání dokumentů do Peppolu
Pokud chcete odeslat dokument (např. fakturu) přes Peppol, odešlete SynchronizationMessage s entitou PeppolDocument do exchange clouderp.sync. QHUB zprávu doručí APEX-u, který provede AS4 odeslání.
6.1 Tok odesílání
VÁŠ SYSTÉM
→ SynchronizationMessage (EntityType=PeppolDocument)
→ clouderp.sync (fanout exchange)
→ QHUB Synchronization Service
→ APEX (clouderp-delivery-queue.APEX)
→ AS4 / HTTPS
→ Peppol príjemca
QHUB → SynchronizationResponseMessage → clouderp.sync.response
→ VÁŠ konzumer odpovede (voliteľné, ale odporúčané)6.2 Co musíte odeslat
Odešlete SynchronizationMessage na exchange clouderp.sync (bez routing key – jde o fanout):
{
"TransactionId": 0,
"EventType": 1,
"EntityType": 100,
"RoutingKey": "MYERP.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"OwnerTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"OwnerSystem": "MYERP",
"DeliverySystem": null,
"DeliveryTenantId": null,
"SyncKey": "novy-uuid-pre-tuto-spravu",
"SchemaVersion": "1.0.0",
"Environment": "prod",
"DomainSyncVersion": 1,
"OccurredAt": "2026-04-16T10:30:00+02:00",
"Data": "{\"SenderParticipantId\":\"0245:2024000001\",\"ReceiverParticipantId\":\"0245:9999999999\",\"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\\\"...>\"}",
"IgnoredProperties": null
}
TransactionId: 0– při odesílání ho nastavte na 0; QHUB přidělí vlastní TransactionId při zpracování.
SyncKey– vygenerujte nové UUID pro každou zprávu. Je to trvalý identifikátor entity v QHUB synchronizaci.
DomainSyncVersion– začněte od 1. Při každé další změně téže entity zvyšte o 1 (Lamport timestamp).
6.3 Sledování výsledku odeslání (volitelné)
QHUB vám odpoví výsledek AS4 odeslání přes clouderp.sync.response. Pokud chcete sledovat úspěch/neúspěch doručení, musíte konzumovat odpovědi.
Poznámka: Odpověď od QHUB dostanete dvakrát – jednou když APEX zařadí dokument do fronty (optimistický úspěch), a podruhé po skutečném AS4 odeslání. Záleží na tom, jak APEX implementuje callback.
7. Struktury zpráv
Všechny zprávy jsou JSON s PascalCase názvy vlastností (kompatibilní s .NET serializací).
7.1 SynchronizationMessage
Zpráva používaná v obou směrech (odesílání i příjem).
| Pole | Typ | Popis | Povinné |
|---|---|---|---|
TransactionId | long | Identifikátor záznamu v QHUB transakční tabulce | áno |
EventType | int | Typ události: 1=CREATE, 2=UPDATE, 4=DELETE | áno |
EntityType | int | Typ entity: 100=PeppolDocument (TBD) | áno |
RoutingKey | string | Routing key příjemce (MYERP.{TenantId}) | nie |
OwnerTenantId | string (UUID) | Tenant zdrojového systému | áno |
OwnerSystem | string | Kód zdrojového systému (např. APEX, MYERP) | áno |
DeliverySystem | string | Kód cílového systému (vyplňuje QHUB) | nie |
DeliveryTenantId | string (UUID) | Tenant cílového systému (vyplňuje QHUB) | nie |
SyncKey | string (UUID) | Unikátní trvalý identifikátor entity | áno |
SchemaVersion | string | Verze schématu entity (SemVer, např. "1.0.0") | áno |
Environment | string | Prostředí: "prod", "test" | áno |
DomainSyncVersion | int | Verze změny entity (Lamport timestamp, ≥1) | áno |
OccurredAt | ISO 8601 datetime | Čas vzniku události ve zdrojovém systému | áno |
Data | string (JSON) | Serializovaná entita PeppolDocument | áno |
IgnoredProperties | string[] | Vlastnosti, které cíl nemá ignorovat (vyplňuje QHUB) | nie |
7.2 SynchronizationResponseMessage
Odpověď, kterou váš systém odešle po zpracování přijaté zprávy.
| Pole | Typ | Popis |
|---|---|---|
TransactionId | long | Zkopírováno z přijaté SynchronizationMessage |
Result.Success | boolean | true = zpracování proběhlo úspěšně |
Result.ErrorCode | string | Kód chyby (pokud Success=false) |
Result.ErrorParams | string[] | Doplňující parametry chyby |
8. Entita PeppolDocument
Entita, která se přenáší v poli Data zprávy SynchronizationMessage.
8.1 Schéma (verze 1.0.0)
| Pole | Typ | Povinné | Business Key | Popis |
|---|---|---|---|---|
SenderParticipantId | string | áno | nie | Peppol ID odesílatele. Formát: {schemeId}:{id} |
ReceiverParticipantId | string | áno | nie | Peppol ID příjemce. Formát: {schemeId}:{id} |
DocumentTypeId | string | áno | nie | Peppol Document Type URN (viz příloha A) |
ProcessId | string | áno | nie | Peppol Process URN (viz příloha A) |
UblDocument | string | áno | nie | Kompletní UBL 2.1 XML jako string |
As4MessageId | string | nie | áno | AS4 MessageId (přítomné u inbound zpráv) |
ReceivedAt | ISO 8601 | nie | nie | Čas přijetí z Peppol sítě (inbound) |
8.2 Příklad entity
{
"SenderParticipantId": "0245:2024000001",
"ReceiverParticipantId": "0245:9999999999",
"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\"?>\n<Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\">...</Invoice>",
"As4MessageId": "abc123@receiver.peppol.ap",
"ReceivedAt": "2026-04-16T10:29:58+02:00"
}8.3 Peppol Participant ID formáty (nejčastější)
| Země | Schéma | Příklad |
|---|---|---|
| Slovensko (DIČ) | 0245 | 0245:2024000001 |
| Česká republika | 0060 | 0060:12345678 |
| GLN | 0088 | 0088:1234567890123 |
9. Chybové kódy a zpracování chyb
9.1 Chyby registrace
| Kód | Příčina | Řešení |
|---|---|---|
RegCodeNotFound | Neplatný nebo neexistující registrační klíč | Vyžádejte nový klíč od ASOL admina |
RegCodeExpired | Klíč vypršel (>24h) | Vyžádejte nový klíč |
SystemMismatch | ParticipantSystem nesedí | Zkontrolujte SystemCode |
TenantMismatch | ParticipantTenantId nesedí | Zkontrolujte TenantId |
9.2 Chyby synchronizace (QHUB)
Pokud QHUB nedokáže zpracovat vaši zprávu, nastaví stav na Failed a zaznamená:
| Kód | Příčina |
|---|---|
EventTypeMissing / EventTypeWrong | Neplatný EventType |
EntityDataMissing | Chybí pole Data při CREATE/UPDATE |
TenantIdMissing | Chybí OwnerTenantId |
SchemaVersionMissing | Chybí SchemaVersion |
DomainSyncVersionWrong | DomainSyncVersion < 1 |
DeliverySystemWrong | DeliverySystem neodpovídá registraci |
9.3 Vaše chybové kódy (odesílané v odpovědi)
V SynchronizationResponseMessage.Result.ErrorCode můžete použít libovolný string kód (doporučujeme angličtinu, bez mezer). Příklady:
| Kód | Kdy použít |
|---|---|
ImportFailed | Nepodařilo se importovat dokument do vašeho systému |
DuplicateDocument | Dokument s tímto ID již existuje |
ValidationFailed | UBL XML selhalo ve vaší business validaci |
UnsupportedDocumentType | Typ dokumentu váš systém nepodporuje |
UnexpectedError | Neočekávaná chyba |
9.4 Dead-Letter Queue
Pokud zprávu nedokážete zpracovat ani po opakovaných pokusech, QHUB ji přesune do Dead-Letter Queue. ASOL admin obdrží notifikaci. Kontaktujte ASOL support pro řešení.
10. Příklady implementace
10.1 Java (Spring AMQP)
Konfigurace
@Configuration public class QhubRabbitConfig { @Value("${qhub.rabbitmq.host}") private String host; @Value("${qhub.rabbitmq.port:5671}") private int port; @Value("${qhub.rabbitmq.username}") private String username; @Value("${qhub.rabbitmq.password}") private String password; @Value("${myapp.tenant-id}") private String tenantId; @Bean public ConnectionFactory qhubConnectionFactory() { CachingConnectionFactory cf = new CachingConnectionFactory(host, port); cf.setUsername(username); cf.setPassword(password); cf.setVirtualHost("/"); cf.getRabbitConnectionFactory().useSslProtocol(); // TLS povinné return cf; } @Bean public RabbitTemplate qhubRabbitTemplate(ConnectionFactory qhubConnectionFactory) { RabbitTemplate rt = new RabbitTemplate(qhubConnectionFactory); rt.setMessageConverter(new Jackson2JsonMessageConverter()); return rt; } @Bean public Queue myDeliveryQueue() { return QueueBuilder .durable("clouderp-delivery-queue.MYERP." + tenantId) .withArgument("x-dead-letter-exchange", "clouderp.dlq") .build(); } @Bean public DirectExchange deliveryExchange() { return new DirectExchange("clouderp.delivery", true, false); } @Bean public Binding myDeliveryBinding(Queue myDeliveryQueue, DirectExchange deliveryExchange) { return BindingBuilder.bind(myDeliveryQueue) .to(deliveryExchange) .with("MYERP." + tenantId); } }
Konzumer (příjem)
@Slf4j @Component @RequiredArgsConstructor public class PeppolDocumentConsumer { private static final int PEPPOL_DOCUMENT_ENTITY_TYPE = 100; // dohodnuté s ASOL private final ObjectMapper objectMapper; private final RabbitTemplate qhubRabbitTemplate; private final MyInvoiceImportService importService; // vaša biznis logika @RabbitListener(queues = "#{myDeliveryQueue.name}") public void onMessage(SynchronizationMessage message) { if (message.getEntityType() != PEPPOL_DOCUMENT_ENTITY_TYPE) { return; // neznáma entita, ignorovať } SyncResult result; try { PeppolDocument doc = objectMapper.readValue( message.getData(), PeppolDocument.class); // Importujte UBL XML do vášho systému importService.importInvoice(doc.getUblDocument(), doc.getSenderParticipantId()); result = new SyncResult(true, null, null); } catch (DuplicateDocumentException e) { log.warn("Duplicate document, transactionId={}", message.getTransactionId()); result = new SyncResult(false, "DuplicateDocument", new String[]{e.getMessage()}); } catch (Exception e) { log.error("Failed to process PeppolDocument, transactionId={}", message.getTransactionId(), e); result = new SyncResult(false, "UnexpectedError", new String[]{e.getMessage()}); } // Vždy odošlite odpoveď sendResponse(message.getTransactionId(), result); } private void sendResponse(long transactionId, SyncResult result) { SynchronizationResponseMessage response = new SynchronizationResponseMessage(transactionId, result); qhubRabbitTemplate.convertAndSend("clouderp.sync.response", "", response); } }
Odesílání dokumentu
@Slf4j @Component @RequiredArgsConstructor public class PeppolDocumentSender { private static final int PEPPOL_DOCUMENT_ENTITY_TYPE = 100; private static final int EVENT_TYPE_CREATE = 1; private static final String SCHEMA_VERSION = "1.0.0"; private final RabbitTemplate qhubRabbitTemplate; private final ObjectMapper objectMapper; @Value("${myapp.system-code:MYERP}") private String systemCode; @Value("${myapp.tenant-id}") private String tenantId; /** * Odošle UBL XML dokument do QHUB → APEX → Peppol. */ public void send(String senderParticipantId, String receiverParticipantId, String documentTypeId, String processId, String ublXml) throws Exception { PeppolDocument doc = new PeppolDocument(); doc.setSenderParticipantId(senderParticipantId); doc.setReceiverParticipantId(receiverParticipantId); doc.setDocumentTypeId(documentTypeId); doc.setProcessId(processId); doc.setUblDocument(ublXml); SynchronizationMessage msg = new SynchronizationMessage(); msg.setEventType(EVENT_TYPE_CREATE); msg.setEntityType(PEPPOL_DOCUMENT_ENTITY_TYPE); msg.setRoutingKey(systemCode + "." + tenantId); msg.setOwnerSystem(systemCode); msg.setOwnerTenantId(tenantId); msg.setSyncKey(UUID.randomUUID().toString()); msg.setSchemaVersion(SCHEMA_VERSION); msg.setEnvironment("prod"); msg.setDomainSyncVersion(1); msg.setOccurredAt(OffsetDateTime.now()); msg.setData(objectMapper.writeValueAsString(doc)); qhubRabbitTemplate.convertAndSend("clouderp.sync", "", msg); log.info("Document sent to QHUB: sender={}, receiver={}", senderParticipantId, receiverParticipantId); } }
10.2 .NET (MassTransit / RabbitMQ.Client)
Konfigurace MassTransit
services.AddMassTransit(config => { config.AddConsumer<PeppolDocumentConsumer>(); config.UsingRabbitMq((ctx, cfg) => { cfg.Host(configuration["Qhub:RabbitMq:Host"], "/", h => { h.Username(configuration["Qhub:RabbitMq:Username"]); h.Password(configuration["Qhub:RabbitMq:Password"]); h.UseSsl(s => s.Protocol = SslProtocols.Tls12); }); // Konfigurácia odosielania (sync queue) cfg.Message<SynchronizationMessage>(p => p.SetEntityName("clouderp.sync")); // Konfigurácia odposielania odpovede cfg.Message<SynchronizationResponseMessage>(p => p.SetEntityName("clouderp.sync.response")); // Delivery queue s binding na exchange cfg.ReceiveEndpoint( $"clouderp-delivery-queue.MYERP.{tenantId}", e => { e.ConfigureConsumeTopology = false; e.UseMessageRetry(r => r.Immediate(1)); e.Bind("clouderp.delivery", x => { x.RoutingKey = $"MYERP.{tenantId}"; x.ExchangeType = ExchangeType.Direct; }); e.ConfigureConsumer<PeppolDocumentConsumer>(ctx); }); }); });
Konzumer
public class PeppolDocumentConsumer : IConsumer<SynchronizationMessage> { private const int PeppolDocumentEntityType = 100; private readonly IInvoiceImportService _importService; private readonly IPublishEndpoint _publishEndpoint; public PeppolDocumentConsumer(IInvoiceImportService importService, IPublishEndpoint publishEndpoint) { _importService = importService; _publishEndpoint = publishEndpoint; } public async Task Consume(ConsumeContext<SynchronizationMessage> context) { var message = context.Message; if (message.EntityType != PeppolDocumentEntityType) return; SyncResult result; try { var doc = JsonSerializer.Deserialize<PeppolDocument>(message.Data!); await _importService.ImportAsync(doc.UblDocument, doc.SenderParticipantId); result = SyncResult.Ok(); } catch (DuplicateDocumentException ex) { result = SyncResult.Fail("DuplicateDocument", new[] { ex.Message }); } catch (Exception ex) { result = SyncResult.Fail("UnexpectedError", new[] { ex.Message }); } await _publishEndpoint.Publish( new SynchronizationResponseMessage(message.TransactionId, result)); } }
Odesílání dokumentu
public class PeppolDocumentSender { private const int PeppolDocumentEntityType = 100; private const string SchemaVersion = "1.0.0"; private readonly IPublishEndpoint _bus; private readonly string _systemCode; private readonly string _tenantId; public PeppolDocumentSender(IPublishEndpoint bus, IConfiguration config) { _bus = bus; _systemCode = config["MyApp:SystemCode"]!; _tenantId = config["MyApp:TenantId"]!; } public async Task SendAsync(string senderParticipantId, string receiverParticipantId, string documentTypeId, string processId, string ublXml, CancellationToken ct = default) { var doc = new PeppolDocument { SenderParticipantId = senderParticipantId, ReceiverParticipantId = receiverParticipantId, DocumentTypeId = documentTypeId, ProcessId = processId, UblDocument = ublXml }; var message = new SynchronizationMessage { EventType = 1, // CREATE EntityType = PeppolDocumentEntityType, RoutingKey = $"{_systemCode}.{_tenantId}", OwnerSystem = _systemCode, OwnerTenantId = _tenantId, SyncKey = Guid.NewGuid().ToString(), SchemaVersion = SchemaVersion, Environment = "prod", DomainSyncVersion = 1, OccurredAt = DateTimeOffset.UtcNow, Data = JsonSerializer.Serialize(doc) }; await _bus.Publish(message, ct); } }
10.3 Python (pika)
import json import ssl import uuid from datetime import datetime, timezone import pika QHUB_HOST = "rmq.qcloud.assecosoldevsk.com" QHUB_PORT = 5671 QHUB_USER = "myerp_tenant_abc123" QHUB_PASS = "••••••••" MY_SYSTEM_CODE = "MYERP" MY_TENANT_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ENTITY_TYPE = 100 # PeppolDocument DELIVERY_QUEUE = f"clouderp-delivery-queue.{MY_SYSTEM_CODE}.{MY_TENANT_ID}" SYNC_EXCHANGE = "clouderp.sync" RESPONSE_EXCHANGE = "clouderp.sync.response" def connect(): ssl_ctx = ssl.create_default_context() credentials = pika.PlainCredentials(QHUB_USER, QHUB_PASS) params = pika.ConnectionParameters( host=QHUB_HOST, port=QHUB_PORT, credentials=credentials, ssl_options=pika.SSLOptions(ssl_ctx, QHUB_HOST) ) return pika.BlockingConnection(params) # ── Prijímanie dokumentov ────────────────────────────────────────────────── def on_message(channel, method, properties, body): msg = json.loads(body) if msg.get("EntityType") != ENTITY_TYPE: channel.basic_ack(delivery_tag=method.delivery_tag) return transaction_id = msg["TransactionId"] try: doc = json.loads(msg["Data"]) ubl_xml = doc["UblDocument"] # --- vaša biznis logika --- import_invoice(ubl_xml, doc["SenderParticipantId"]) result = {"Success": True, "ErrorCode": None, "ErrorParams": None} except Exception as e: result = {"Success": False, "ErrorCode": "UnexpectedError", "ErrorParams": [str(e)]} response = {"TransactionId": transaction_id, "Result": result} channel.basic_publish( exchange=RESPONSE_EXCHANGE, routing_key="", body=json.dumps(response), properties=pika.BasicProperties(content_type="application/json") ) channel.basic_ack(delivery_tag=method.delivery_tag) def start_consuming(): conn = connect() channel = conn.channel() channel.queue_declare(queue=DELIVERY_QUEUE, durable=True, arguments={"x-dead-letter-exchange": "clouderp.dlq"}) channel.queue_bind(queue=DELIVERY_QUEUE, exchange="clouderp.delivery", routing_key=f"{MY_SYSTEM_CODE}.{MY_TENANT_ID}") channel.basic_consume(queue=DELIVERY_QUEUE, on_message_callback=on_message) channel.start_consuming() # ── Odosielanie dokumentov ────────────────────────────────────────────────── def send_document(sender_id, receiver_id, document_type_id, process_id, ubl_xml): doc = { "SenderParticipantId": sender_id, "ReceiverParticipantId": receiver_id, "DocumentTypeId": document_type_id, "ProcessId": process_id, "UblDocument": ubl_xml, } message = { "TransactionId": 0, "EventType": 1, "EntityType": ENTITY_TYPE, "RoutingKey": f"{MY_SYSTEM_CODE}.{MY_TENANT_ID}", "OwnerSystem": MY_SYSTEM_CODE, "OwnerTenantId": MY_TENANT_ID, "SyncKey": str(uuid.uuid4()), "SchemaVersion": "1.0.0", "Environment": "prod", "DomainSyncVersion": 1, "OccurredAt": datetime.now(timezone.utc).isoformat(), "Data": json.dumps(doc), } conn = connect() channel = conn.channel() channel.basic_publish( exchange=SYNC_EXCHANGE, routing_key="", body=json.dumps(message), properties=pika.BasicProperties(content_type="application/json", delivery_mode=2) # persistent ) conn.close() def import_invoice(ubl_xml: str, sender_id: str): # Vaša implementácia importu pass
11. Časté otázky
Jak zjistím, že mi přišla faktura?
Nastavte konzumera na delivery queue clouderp-delivery-queue.MYERP.{TenantId}. Zpráva s EntityType=100 a OwnerSystem=APEX je dokument z Peppol sítě.
Mohu odeslat i jiné typy dokumentů než fakturu?
Ano. UblDocument může obsahovat jakýkoliv Peppol BIS 3.0 dokument (kreditní nótu, objednávku, …). Stačí správně nastavit DocumentTypeId a ProcessId (viz příloha A).
Co když odeslání přes Peppol selže?
APEX automaticky zopakuje odeslání (exponenciální backoff, max 5 pokusů). Výsledek dostanete přes SynchronizationResponseMessage se Success=false a příslušným ErrorCode.
Je UBL XML validováno?
Ano, APEX validuje každý dokument před odesláním (XSD schéma, EN 16931 Schematron, Peppol BIS 3.0 pravidla). Pokud dokument není validní, obdržíte Success=false.
Jaký je Peppol kód schématu pro Slovensko?
Slovenské IČO v Peppol síti používá schéma 0245, tedy ID má formát 0245:{DIČ}, např. 0245:2024000001.
Jak ověřím, zda je příjemce registrován v Peppol síti?
APEX to ověřuje automaticky (SMP lookup) před odesláním. Pokud příjemce není registrován, obdržíte chybu PARTICIPANT_NOT_FOUND.
Musím zachovávat pořadí zpráv?
Pro samostatné dokumenty (každá faktura je nová zpráva) pořadí není kritické. Pokud posíláte více verzí téhož dokumentu (UPDATE operace), použijte DomainSyncVersion pro řešení konfliktů.
Příloha A: Peppol Document Type identifikátory
Nejčastější typy dokumentů
| Typ dokumentu | DocumentTypeId | ProcessId |
|---|---|---|
| Faktura (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 |
| MLR (Message Level Response) | urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:fdc:peppol.eu:2017:pracc:t111:ver1.0::2.1 | urn:fdc:peppol.eu:2017:pracc:messagelevels:01:1.0 |
| IMR (Invoice Message Response) | urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:fdc:peppol.eu:2017:poacc:billing:08:1.0::2.1 | urn:fdc:peppol.eu:2017:poacc:billing:08:1.0 |
Příloha B: Kontrolní seznam integrace
- Získán
SystemCodeaTenantIdod ASOL - Získán URL a API klíč k QHUB Registration API
- Odeslaný popis entity
PeppolDocumentna Registration API - Od ASOL admina přijat jednorázový registrační klíč
- Zavolán endpoint
POST /api/v1/Registration/Validate - RabbitMQ credentials bezpečně uloženy
- Deklarována delivery queue s bindingem
- Implementován konzumer (příjem)
- Konzumer odešle
SynchronizationResponseMessagepo každém zpracování - Implementováno odesílání (pokud je potřebné)
- Otestován inbound tok: testovací AS4 zpráva → vaše aplikace
- Otestován outbound tok: vaše aplikace → APEX → Peppol test prostředí
Příloha C: Kontakty a podpora
| Typ požadavku | Kontakt |
|---|---|
| Onboarding (SystemCode, TenantId, API klíč) | ASOL integrační tým |
| Technická podpora QHUB | ASOL DevOps / Cloud tým |
| Technická podpora Peppol (certifikáty, SMP registrace) | ASOL Peppol tým / váš SMP provider |
| Nahlášení chyby | Customer Portal |