Integrační příručka: Napojení třetí strany na QHUB a příjem/odeslání Peppol dokumentů přes APEX

Verze: 1.0  •  Datum: 2026-04-16
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

☁ ASOL CLOUD 🖥️ Váš systém ERP · Účetní systém · Portál konzumer & producent zpráv 🐇 RabbitMQ AMQPS · async zprávy QCloud HUB Synchronization Service Směrování · Retry & DLQ · Audit · Transakce HUB APEX Peppol Access Point (Java) SMP Lookup · AS4 · Validace UBL AS4 AS4 / HTTPS 🌐 Peppol Network Evropská eInvoicing síť response / delivery

Co QHUB dělá za vás

Co musíte udělat vy

SměrVaš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

  1. Peppol participant ID – váš klient musí být registrován v Peppol síti. Identifikátor má formát {schemeId}:{participantId}, například 0245:2024000001.

    Schéma 0245 = slovenské DIČ v Peppol. Pokud neznáte své Peppol ID, kontaktujte svého Peppol Service Provider (SMP).

  2. ASOL kontakt – požádejte ASOL tým o:
    • přidělení SystemCode vašeho systému (např. MYERP),
    • přidělení TenantId (UUID) vašeho tenanta v QHUB,
    • přístup k QHUB Registration API (URL + API klíč).
  3. 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)

postup
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áciu

3. 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:

http
POST {QHUB_REGISTRATION_URL}/api/v1/SyncEntityCatalogItem/StoreSyncEntityItems
Content-Type: application/json
X-Api-Key: {váš API kľúč}

Request body:

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

http
POST {QHUB_REGISTRATION_URL}/api/v1/Registration/Validate
Content-Type: application/json

Request body:

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

json
{
  "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: RabbitMqPass je 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ódPopis
RegCodeNotFoundKlíč neexistuje nebo bylo nalezeno více záznamů
RegCodeExpiredKlíč vypršel (platný 24h)
SystemMismatchParticipantSystem se neshoduje s registrací
TenantMismatchParticipantTenantId se neshoduje s registrací
HubInternalErrorInterní 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ázevTypÚčel
clouderp-delivery-queue.MYERP.{TenantId}Direct queuePříjem zpráv z QHUB do vašeho systému
clouderp.syncFanout exchangeOdesílání zpráv z vašeho systému do QHUB
clouderp.sync.responseFanout exchangeOdesílání odpovědi o výsledku zpracování
clouderp.deliveryDirect exchangeZdrojová exchange, ze které QHUB plní delivery queue

4.2 Oprávnění vašeho RabbitMQ účtu

Váš účet má oprávnění:

OperaceZdroj
Configure + Readclouderp-delivery-queue.MYERP.{TenantId}
Writeclouderp.sync (pro odesílání dokumentů do Peppolu)
Writeclouderp.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:

config
Meno:       clouderp-delivery-queue.MYERP.{TenantId}
Durable:    true
Auto-delete: false
Arguments:
  x-dead-letter-exchange: clouderp.dlq   (odporúčané)

Binding na exchange:

config
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

diagram
Peppol odosielateľ
    → AS4 → APEX
        → QHUB (SynchronizationMessage, EntityType=PeppolDocument)
            → clouderp-delivery-queue.MYERP.{TenantId}
                → VÁŠ KONZUMER
                    → spracujete UBL XML (importujete faktúru)
                        → SynchronizationResponseMessage → clouderp.sync.response

5.2 Co obdržíte – SynchronizationMessage

Zpráva přichází jako JSON objekt (viz sekce 7):

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

  1. Deserializujte Data na PeppolDocument.
  2. Zkontrolujte DeliverySystem a DeliveryTenantId – měly by odpovídat vašemu systému.
  3. Importujte UblDocument (UBL 2.1 XML) do vašeho systému.
  4. Odešlete odpověď SynchronizationResponseMessage na exchange clouderp.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.

json
{
  "TransactionId": 123456,
  "Result": {
    "Success": true,
    "ErrorCode": null,
    "ErrorParams": null
  }
}

Při chybě:

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

diagram
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):

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

PoleTypPopisPovinné
TransactionIdlongIdentifikátor záznamu v QHUB transakční tabulceáno
EventTypeintTyp události: 1=CREATE, 2=UPDATE, 4=DELETEáno
EntityTypeintTyp entity: 100=PeppolDocument (TBD)áno
RoutingKeystringRouting key příjemce (MYERP.{TenantId})nie
OwnerTenantIdstring (UUID)Tenant zdrojového systémuáno
OwnerSystemstringKód zdrojového systému (např. APEX, MYERP)áno
DeliverySystemstringKód cílového systému (vyplňuje QHUB)nie
DeliveryTenantIdstring (UUID)Tenant cílového systému (vyplňuje QHUB)nie
SyncKeystring (UUID)Unikátní trvalý identifikátor entityáno
SchemaVersionstringVerze schématu entity (SemVer, např. "1.0.0")áno
EnvironmentstringProstředí: "prod", "test"áno
DomainSyncVersionintVerze změny entity (Lamport timestamp, ≥1)áno
OccurredAtISO 8601 datetimeČas vzniku události ve zdrojovém systémuáno
Datastring (JSON)Serializovaná entita PeppolDocumentáno
IgnoredPropertiesstring[]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.

PoleTypPopis
TransactionIdlongZkopírováno z přijaté SynchronizationMessage
Result.Successbooleantrue = zpracování proběhlo úspěšně
Result.ErrorCodestringKód chyby (pokud Success=false)
Result.ErrorParamsstring[]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)

PoleTypPovinnéBusiness KeyPopis
SenderParticipantIdstringánoniePeppol ID odesílatele. Formát: {schemeId}:{id}
ReceiverParticipantIdstringánoniePeppol ID příjemce. Formát: {schemeId}:{id}
DocumentTypeIdstringánoniePeppol Document Type URN (viz příloha A)
ProcessIdstringánoniePeppol Process URN (viz příloha A)
UblDocumentstringánonieKompletní UBL 2.1 XML jako string
As4MessageIdstringnieánoAS4 MessageId (přítomné u inbound zpráv)
ReceivedAtISO 8601nienieČas přijetí z Peppol sítě (inbound)

8.2 Příklad entity

json
{
  "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émaPříklad
Slovensko (DIČ)02450245:2024000001
Česká republika00600060:12345678
GLN00880088:1234567890123

9. Chybové kódy a zpracování chyb

9.1 Chyby registrace

KódPříčinaŘešení
RegCodeNotFoundNeplatný nebo neexistující registrační klíčVyžádejte nový klíč od ASOL admina
RegCodeExpiredKlíč vypršel (>24h)Vyžádejte nový klíč
SystemMismatchParticipantSystem nesedíZkontrolujte SystemCode
TenantMismatchParticipantTenantId nesedíZkontrolujte TenantId

9.2 Chyby synchronizace (QHUB)

Pokud QHUB nedokáže zpracovat vaši zprávu, nastaví stav na Failed a zaznamená:

KódPříčina
EventTypeMissing / EventTypeWrongNeplatný EventType
EntityDataMissingChybí pole Data při CREATE/UPDATE
TenantIdMissingChybí OwnerTenantId
SchemaVersionMissingChybí SchemaVersion
DomainSyncVersionWrongDomainSyncVersion < 1
DeliverySystemWrongDeliverySystem 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ódKdy použít
ImportFailedNepodařilo se importovat dokument do vašeho systému
DuplicateDocumentDokument s tímto ID již existuje
ValidationFailedUBL XML selhalo ve vaší business validaci
UnsupportedDocumentTypeTyp dokumentu váš systém nepodporuje
UnexpectedErrorNeoč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

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

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

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

csharp
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

csharp
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

csharp
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)

python
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 dokumentuDocumentTypeIdProcessId
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.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
MLR (Message Level Response)urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:fdc:peppol.eu:2017:pracc:t111:ver1.0::2.1urn: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.1urn:fdc:peppol.eu:2017:poacc:billing:08:1.0

Příloha B: Kontrolní seznam integrace


Příloha C: Kontakty a podpora

Typ požadavkuKontakt
Onboarding (SystemCode, TenantId, API klíč)ASOL integrační tým
Technická podpora QHUBASOL DevOps / Cloud tým
Technická podpora Peppol (certifikáty, SMP registrace)ASOL Peppol tým / váš SMP provider
Nahlášení chybyCustomer Portal