neue impl

This commit is contained in:
Ralph Soika 2025-11-14 13:43:58 +01:00
parent 7fe009c23c
commit 9f6d86d328
5 changed files with 514 additions and 29 deletions

View file

@ -1,13 +1,21 @@
# KSeF - Converter
Die Idee ist es auf Basis der KSeF FA(2) XSD-Spezifikation einen Java-Konverter zu entwickeln.
## Github
Die Idee ist es Rechnungen nach Erhalt aus cargosoft über die KSeF FA(2) API an die Behörde zu senden.
# API
Dokumentation: https://ksef-test.mf.gov.pl/docs/v2/index.html
Die Basis url ist: "https://ksef-test.mf.gov.pl/api/v2";
Der Ablauf für die Übermittlung ist folgender:
- vom Polnischen Goverment (bzw. AGL/Taxman) haben wir ein Secret bekommen (KSeFToken)
- wir machen den call `/security/public-key-certificates` um Security Zertifikate zu erhalten
- wir machen den call `/auth/challenge` um eine Challenge ID zu erhalten
- wir machen den call `/auth/ksef-token` um eine ReferenceID und einen AccessToken zu erhalten
- wir senden die Rechnung - mit dem öffentlichen Schlüssel verschlüsselt zusammen mit unserer ReferenceID und dem AccessToken an das Goverment.
## Generieren einer Referenznummer.
Man benötigt eine Referenznummer (Session ID). Dise kann man wie folgt generieren:
@ -24,6 +32,8 @@ Details:
https://github.com/CIRFMF/ksef-docs/blob/main/sesja-interaktywna.md#2-wys%C5%82anie-faktury
Man muss die XML Datei mit dem öffentlichen Schlüssel der Behörde vor dem Versand verschlüsseln
### Das Rechnungsformat:
XML Datei

View file

@ -5,11 +5,16 @@ import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Base64;
import java.util.Optional;
import java.util.logging.Logger;
import javax.crypto.Cipher;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.imixs.workflow.exceptions.InvalidAccessException;
import org.imixs.workflow.exceptions.PluginException;
@ -65,8 +70,19 @@ public class KSeFAuthManager {
private String challenge = "";
private String refNumber = null;
private String authToken = null;
private String accessToken = null;
private String refreshToken = null;
private String symmetricCertBase64; // der Zertifikat-String aus der API
private PublicKey symmetricPublicKey; // der extrahierte RSA Public Key
private String sessionRefNumber = null;
private String sessionValidUntil = null;
private String ksefTokenEncryptionCertBase64 = null;
private PublicKey ksefTokenEncryptionPublicKey;
private String challengeTimestamp = null;
private byte[] sessionAesKey;
private byte[] sessionIv;
@PostConstruct
void init() {
@ -94,6 +110,10 @@ public class KSeFAuthManager {
// this.jsonb = JsonbBuilder.create();
}
public HttpClient getHttpClient() {
return httpClient;
}
public void setKsefToken(String token) {
this.ksefToken = Optional.ofNullable(token);
}
@ -115,12 +135,13 @@ public class KSeFAuthManager {
*
* @return
*/
public String getEncodedToken() {
// public String getEncodedToken() {
String result = java.util.Base64.getEncoder().encodeToString(ksefToken.orElse("").getBytes());
// String result =
// java.util.Base64.getEncoder().encodeToString(ksefToken.orElse("").getBytes());
return result;
}
// return result;
// }
public String getBaseURI() {
return baseURI;
@ -182,9 +203,12 @@ public class KSeFAuthManager {
// Annahme: response.body() ist ein String mit JSON-Inhalt
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
challenge = jsonObject.getString("challenge");
challengeTimestamp = jsonObject.getString("timestamp");
if (debug) {
logger.info("│ ├── challenge: " + challenge);
logger.info("│ ├── timestamp: " + challengeTimestamp);
}
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
@ -210,6 +234,26 @@ public class KSeFAuthManager {
logger.info("├── KSeF API auth ksef-token...");
String uri = baseURI + "/auth/ksef-token";
logger.info("│ ├── Endpoint: " + uri);
if (ksefTokenEncryptionPublicKey == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
"Missing public key for KSeF token encryption (KsefTokenEncryption).");
}
// token|timestamp (aus challenge)
String token = ksefToken.orElseThrow(() -> new PluginException(
KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG, "Missing KSeF API token"));
String tokenWithTimestamp = token + "|" + challengeTimestamp;
logger.info("│ ├── token|timestamp = " + tokenWithTimestamp);
// Verschlüsseln mit RSA-OAEP SHA-256
String encryptedToken = encryptWithPublicKey(tokenWithTimestamp, ksefTokenEncryptionPublicKey);
logger.info("│ ├── Encrypted token (Base64): " + encryptedToken);
// JSON payload zusammenbauen
String jsonPayload = String.format(
"{" +
"\"challenge\": \"%s\"," +
@ -219,7 +263,9 @@ public class KSeFAuthManager {
"}," +
"\"encryptedToken\": \"%s\"" +
"}",
challenge, ksefNip.get(), getEncodedToken());
challenge,
ksefNip.get(),
encryptedToken);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
@ -229,55 +275,102 @@ public class KSeFAuthManager {
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
// Prüfen, dass wir eine API-Antwort bekommen
if (response.statusCode() >= 500) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
"Unable to access KSeF API: response=" + response.statusCode());
}
// JSON parsen und Werte speichern
if (response.statusCode() == 200 || response.statusCode() == 202) {
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
// Reference Number speichern
refNumber = jsonObject.getString("referenceNumber");
logger.info("Extracted referenceNumber: " + refNumber);
// Authentication Token speichern
JsonObject authTokenObject = jsonObject.getJsonObject("authenticationToken");
authToken = authTokenObject.getString("token");
logger.info("Extracted authenticationToken: " + authToken);
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing JSON response: " + e.getMessage());
}
}
}
@Lock(LockType.WRITE)
public void redeemAuthToken() throws Exception {
logger.info("├── KSeF API redeem authentication token...");
String uri = baseURI + "/auth/token/redeem";
logger.info("│ ├── Endpoint: " + uri);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + authToken) // authToken aus generateAuthToken()
.POST(HttpRequest.BodyPublishers.noBody()) // Payload leer, API erwartet nur Bearer im Header
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
// Prüfen auf Fehler
if (response.statusCode() >= 500) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
"Unable to access KSeF API: response=" + response.statusCode());
}
// JSON parsen und AccessToken + RefreshToken speichern
if (response.statusCode() == 200 || response.statusCode() == 201) {
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken");
accessToken = accessTokenObj.getString("token"); // finaler Bearer für alle weiteren API-Calls
logger.info("Extracted accessToken: " + accessToken);
JsonObject refreshTokenObj = jsonObject.getJsonObject("refreshToken");
refreshToken = refreshTokenObj.getString("token"); // optional zum späteren Refresh
if (debug) {
logger.info("Extracted refreshToken: " + refreshToken);
}
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing JSON response: " + e.getMessage());
}
} else {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Failed to redeem authentication token: " + response.statusCode());
}
}
/**
* Helper Method to read the certificates
*
* @throws Exception
*/
@Lock(LockType.WRITE)
public void getKsefPublicKeys() throws Exception {
public void loadKsefPublicKeys() throws Exception {
logger.info("├── KSeF API public-key-certificates...");
logger.info("├── KSeF API load public-key-certificates...");
String uri = baseURI + "/security/public-key-certificates";
logger.info("│ ├── Endpoint: " + uri);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Authorization", "Bearer " + authToken)
.GET()
.build();
@ -290,26 +383,34 @@ public class KSeFAuthManager {
if (response.statusCode() != 200) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Unexpected response from KSeF certificates endpoint: " + response.statusCode());
"Unexpected response: " + response.statusCode());
}
// JSON parsen
try (Jsonb jsonb = JsonbBuilder.create()) {
var certArray = jsonb.fromJson(response.body(), JsonObject[].class);
for (JsonObject entry : certArray) {
// usage = ["KsefTokenEncryption"] oder ["SymmetricKeyEncryption"]
// WICHTIG: JsonArray lesen NICHT JsonObject[]
jakarta.json.JsonArray certArray = jsonb.fromJson(response.body(), jakarta.json.JsonArray.class);
for (var entryValue : certArray) {
JsonObject entry = entryValue.asJsonObject();
var usages = entry.getJsonArray("usage");
if (usages.stream().anyMatch(v -> v.toString().contains("SymmetricKeyEncryption"))) {
symmetricCertBase64 = entry.getString("certificate");
logger.info("│ ├── SymmetricKeyEncryption certificate found.");
// Public Key extrahieren
symmetricPublicKey = extractPublicKeyFromCertificate(symmetricCertBase64);
logger.info("│ ├── ✓ RSA PublicKey successfully extracted.");
return; // gefunden wir sind fertig
if (usages != null &&
usages.stream().anyMatch(v -> v.toString().contains("KsefTokenEncryption"))) {
ksefTokenEncryptionCertBase64 = entry.getString("certificate");
logger.info("│ ├── Found certificate with usage = KsefTokenEncryption");
ksefTokenEncryptionPublicKey = extractPublicKeyFromCertificate(ksefTokenEncryptionCertBase64);
logger.info("│ ├── ✓ RSA PublicKey for KSeF Token Encryption extracted.");
return;
}
}
logger.warning("│ ├── ⚠️ No certificate with usage 'SymmetricKeyEncryption' found!");
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"No certificate with usage = KsefTokenEncryption found");
} catch (Exception e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
@ -317,6 +418,193 @@ public class KSeFAuthManager {
}
}
@Lock(LockType.WRITE)
public void loadSymmetricKeyPublicKey() throws Exception {
logger.info("├── KSeF API load symmetric public key...");
String uri = baseURI + "/security/public-key";
logger.info("│ ├── Endpoint: " + uri);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
if (response.statusCode() != 200) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Unexpected response: " + response.statusCode());
}
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject json = jsonb.fromJson(response.body(), JsonObject.class);
symmetricCertBase64 = json.getString("certificate");
logger.info("│ ├── Symmetric key certificate loaded");
symmetricPublicKey = extractPublicKeyFromCertificate(symmetricCertBase64);
logger.info("│ ├── ✓ RSA PublicKey for session encryption extracted.");
} catch (Exception e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing public key JSON: " + e.getMessage());
}
}
/**
* Helper Method to open an interactive session
*
* @throws Exception
*/
@Lock(LockType.WRITE)
public void openInteractiveSession() throws Exception {
if (accessToken == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error - missing access token!");
}
logger.info("├── KSeF API open interactive session...");
String uri = baseURI + "/sessions/online";
logger.info("│ ├── Endpoint: " + uri);
// --- AES Schlüssel erzeugen ---
SecureRandom rnd = new SecureRandom();
byte[] aesKeyBytes = new byte[32]; // 256-bit AES key
rnd.nextBytes(aesKeyBytes);
byte[] ivBytes = new byte[16]; // 128-bit IV
rnd.nextBytes(ivBytes);
// --- AES Key mit RSA encrypten ---
// Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
rsa.init(Cipher.ENCRYPT_MODE, this.symmetricPublicKey);
byte[] encryptedAesKey = rsa.doFinal(aesKeyBytes);
String jsonPayload = String.format(
"{" +
"\"formCode\": {" +
" \"systemCode\": \"FA (3)\"," +
" \"schemaVersion\": \"1-0E\"," +
" \"value\": \"FA\"" +
"}," +
"\"encryption\": {" +
" \"encryptedSymmetricKey\": \"%s\"," +
" \"initializationVector\": \"%s\"" +
"}" +
"}",
Base64.getEncoder().encodeToString(encryptedAesKey),
Base64.getEncoder().encodeToString(ivBytes));
if (debug) {
logger.info("│ ├── Payload: " + jsonPayload);
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
// .header("Accept-Charset", "UTF-8")
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + accessToken)
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
if (response.statusCode() != 201) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Failed to open interactive session: " + response.statusCode());
}
// JSON parsen
// Session RefNumber aus Response extrahieren und speichern
try (Jsonb jsonb = JsonbBuilder.create()) {
// Annahme: response.body() ist ein String mit JSON-Inhalt
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
sessionRefNumber = jsonObject.getString("referenceNumber");
sessionValidUntil = jsonObject.getString("validUntil");
if (debug) {
logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber);
logger.info("│ └── Session ValidUntil = " + sessionValidUntil);
}
// --- WICHTIG: AES Schlüssel speichern, wir brauchen ihn beim Upload ---
this.sessionAesKey = aesKeyBytes;
this.sessionIv = ivBytes;
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing JSON response: " + e.getMessage());
}
}
/**
* Helper Method to analyze the auth status
*
* @return
* @throws Exception
*/
public JsonObject getAuthStatus() throws Exception {
if (refNumber == null) {
throw new IllegalStateException("Reference number is missing. Call generateAuthToken() first.");
}
logger.info("├── KSeF API check authentication status...");
logger.info("Using public key algorithm: " + symmetricPublicKey.getAlgorithm());
logger.info("Key format: " + symmetricPublicKey.getFormat());
String uri = baseURI + "/auth/" + refNumber;
logger.info("│ ├── Endpoint: " + uri);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Authorization", "Bearer " + authToken)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── Response Code = " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
if (response.statusCode() >= 500) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_CONFIG,
"Unable to access KSeF API: response=" + response.statusCode());
}
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
Boolean isRedeemed = jsonObject.containsKey("isTokenRedeemed") ? jsonObject.getBoolean("isTokenRedeemed")
: null;
logger.info("│ ├── isTokenRedeemed: " + isRedeemed);
return jsonObject;
} catch (Exception e) {
logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing JSON response: " + e.getMessage());
}
}
private PublicKey extractPublicKeyFromCertificate(String base64Cert) throws Exception {
byte[] der = java.util.Base64.getDecoder().decode(base64Cert);
@ -329,4 +617,12 @@ public class KSeFAuthManager {
return certificate.getPublicKey();
}
private String encryptWithPublicKey(String data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
}

View file

@ -92,4 +92,34 @@ public class KSeFAuthManagerTest {
System.out.println("Received KSeF Token OK");
}
@Test
@Order(4)
@DisplayName("Upload Invoice XML (AES-256 + RSA)")
public void testOpenInteractiveSession() throws Exception {
// ---------- 1) Vorbereitung ----------
// API initialisieren
manager.loadKsefPublicKeys();
manager.getChallenge();
manager.generateAuthToken(); // erzeugt authToken + refNumber
manager.getAuthStatus();
manager.redeemAuthToken();
assertNotNull(manager.getAuthToken(), "AccessToken fehlt");
assertNotNull(manager.getRefNumber(), "ReferenceNumber fehlt");
manager.openInteractiveSession();
}
@Test
@Order(5)
@DisplayName("Upload Invoice XML (AES-256 + RSA)")
public void testUploadInvoice() throws Exception {
// ---------- 1) Vorbereitung ----------
// API initialisieren
}
}

View file

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<Faktura
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:etd="http://crd.gov.pl/xml/schematy/dziedzinowe/mf/2022/01/05/eD/DefinicjeTypy/"
xmlns="http://crd.gov.pl/wzor/2025/06/25/13775/">
<Naglowek>
<KodFormularza kodSystemowy="FA (3)" wersjaSchemy="1-0E">FA</KodFormularza>
<WariantFormularza>3</WariantFormularza>
<DataWytworzeniaFa>2026-06-05T02:45:33.422866Z</DataWytworzeniaFa>
<SystemInfo>Generator danych</SystemInfo>
</Naglowek>
<Podmiot1>
<DaneIdentyfikacyjne>
<NIP>9552521552</NIP>
<Nazwa>Alexander Global Logistics GmbH</Nazwa>
</DaneIdentyfikacyjne>
<Adres>
<KodKraju>PL</KodKraju>
<AdresL1>Museumstr. 2-6</AdresL1>
<AdresL2>28195 Bremen</AdresL2>
</Adres>
<DaneKontaktowe>
<Email>Lucja17@example.net</Email>
<Telefon>86-588-03-53</Telefon>
</DaneKontaktowe>
</Podmiot1>
<Podmiot2>
<DaneIdentyfikacyjne>
<NIP>3861610227</NIP>
<Nazwa>Błaszczak - Lenart</Nazwa>
</DaneIdentyfikacyjne>
<Adres>
<KodKraju>PL</KodKraju>
<AdresL1>al. Szewczyk 7247</AdresL1>
<AdresL2>84-642 Żelechów</AdresL2>
</Adres>
<DaneKontaktowe>
<Email>Jakubina82@gmail.com</Email>
<Telefon>75-461-31-12</Telefon>
</DaneKontaktowe>
<NrKlienta>KL-2936</NrKlienta>
<JST>2</JST>
<GV>2</GV>
</Podmiot2>
<Fa>
<KodWaluty>PLN</KodWaluty>
<P_1>#invoicing_date#</P_1>
<P_1M>Lidzbark</P_1M>
<P_2>FA/GRQMB-#invoice_number#/05/2025</P_2>
<P_6>2025-07-11</P_6>
<P_13_1>10652.38</P_13_1>
<P_14_1>2450.05</P_14_1>
<P_15>13102.43</P_15>
<Adnotacje>
<P_16>2</P_16>
<P_17>2</P_17>
<P_18>2</P_18>
<P_18A>2</P_18A>
<Zwolnienie>
<P_19N>1</P_19N>
</Zwolnienie>
<NoweSrodkiTransportu>
<P_22N>1</P_22N>
</NoweSrodkiTransportu>
<P_23>2</P_23>
<PMarzy>
<P_PMarzyN>1</P_PMarzyN>
</PMarzy>
</Adnotacje>
<RodzajFaktury>VAT</RodzajFaktury>
<FaWiersz>
<NrWierszaFa>1</NrWierszaFa>
<UU_ID>66465935-48e0-b7d9-ea88-d9d7ff4c6023</UU_ID>
<P_7>Refined Concrete Pants</P_7>
<P_8A>szt.</P_8A>
<P_8B>8</P_8B>
<P_9A>295.81</P_9A>
<P_11>2366.48</P_11>
<P_12>23</P_12>
</FaWiersz>
<FaWiersz>
<NrWierszaFa>2</NrWierszaFa>
<UU_ID>16d35141-c43f-98fc-5249-27d7b2845cf7</UU_ID>
<P_7>Sleek Steel Pants</P_7>
<P_8A>szt.</P_8A>
<P_8B>10</P_8B>
<P_9A>402.96</P_9A>
<P_11>4029.60</P_11>
<P_12>23</P_12>
</FaWiersz>
<FaWiersz>
<NrWierszaFa>3</NrWierszaFa>
<UU_ID>8647a6b5-f12a-3817-869e-010e4353686a</UU_ID>
<P_7>Incredible Steel Salad</P_7>
<P_8A>szt.</P_8A>
<P_8B>8</P_8B>
<P_9A>11.13</P_9A>
<P_11>89.04</P_11>
<P_12>23</P_12>
</FaWiersz>
<FaWiersz>
<NrWierszaFa>4</NrWierszaFa>
<UU_ID>6a9b993d-33c1-eba2-f06c-7d8f22e7430b</UU_ID>
<P_7>Ergonomic Soft Fish</P_7>
<P_8A>szt.</P_8A>
<P_8B>8</P_8B>
<P_9A>34.03</P_9A>
<P_11>272.24</P_11>
<P_12>23</P_12>
</FaWiersz>
<FaWiersz>
<NrWierszaFa>5</NrWierszaFa>
<UU_ID>05e42590-9bfb-10c2-a946-fd1641f705e7</UU_ID>
<P_7>Intelligent Frozen Cheese</P_7>
<P_8A>szt.</P_8A>
<P_8B>3</P_8B>
<P_9A>501.86</P_9A>
<P_11>1505.58</P_11>
<P_12>23</P_12>
</FaWiersz>
<FaWiersz>
<NrWierszaFa>6</NrWierszaFa>
<UU_ID>dbb7aed5-fedb-96d7-0afd-c3d0c23740a7</UU_ID>
<P_7>Ergonomic Metal Bike</P_7>
<P_8A>szt.</P_8A>
<P_8B>6</P_8B>
<P_9A>398.24</P_9A>
<P_11>2389.44</P_11>
<P_12>23</P_12>
</FaWiersz>
<Platnosc>
<Zaplacono>1</Zaplacono>
<DataZaplaty>2025-07-17</DataZaplaty>
<FormaPlatnosci>7</FormaPlatnosci>
</Platnosc>
</Fa>
</Faktura>

View file

@ -0,0 +1,11 @@
{
"formCode": {
"systemCode": "FA (3)",
"schemaVersion": "1-0E",
"value": "FA"
},
"encryption": {
"encryptedSymmetricKey": "bmrsRHK0uyMRB31FPLOocExKRbQMyiaqN0fzUVnwqiWWAnIySB/89iqHpGWOAiOkixrHIHRFQPHu8aUV33rv+SlCSfv/UIAuLO6FN3VgzVnXITUXFvWUE7Exi1GwJOHOPHp2+tk81Ca5WbPF3Hg3vQQoRo4YSYT7GRV63PVBSNrhHayXDRTT40dJEfLJ25/QzX7EuhV0PQU8sz/4j+kHFVEGTG6KkMN+sqctSRItauoUq7oNjoCw6zqPISvAu/FKxVmq5DxKMj2yFIJQfv9S+xIIpwQDYEPdI1s5ZD3vWtoApWUq4nH9Kbon1ma8yId5aV6PvlDG9oqKN+xvbpodzQ==",
"initializationVector": "81LUdc7vjDZtG239K+1WKg=="
}
}