refactoring KSeF invoice upload

This commit is contained in:
Ralph Soika 2025-11-27 11:07:21 +01:00
parent 9024ee2e38
commit 459f7808cf
4 changed files with 171 additions and 117 deletions

View file

@ -65,27 +65,14 @@ public class KSeFAPIAdapter implements SignalAdapter {
try {
List<ItemCollection> ksefExportDefinitions = workflowService.evalWorkflowResultXML(event, "ksef",
"EXPORT", workitem, false);
// List<ItemCollection> ksefStatusDefinitions =
// workflowService.evalWorkflowResultXML(event, "ksef",
// "STATUS", workitem, false);
// EXPORT
if (ksefExportDefinitions != null && ksefExportDefinitions.size() > 0) {
ItemCollection ksefDefinition = ksefExportDefinitions.get(0);
debug = ksefDefinition.getItemValueBoolean("debug");
kSeFAPIService.uploadInvoice(workitem, "ksef.xml");
}
// // STATUS
// if (ksefStatusDefinitions != null && ksefStatusDefinitions.size() > 0) {
// ItemCollection ksefDefinition = ksefStatusDefinitions.get(0);
// debug = ksefDefinition.getItemValueBoolean("debug");
// kSeFAPIService.getInvoiceStatus(workitem);
// }
} catch (PluginException e) {
throw new AdapterException(e);
}

View file

@ -1,16 +1,23 @@
package com.alexanderlogistics.ksef.api;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
@ -25,7 +32,9 @@ import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.RunAs;
import jakarta.ejb.Singleton;
import jakarta.inject.Inject;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
@ -134,9 +143,7 @@ public class KSeFAPIService {
// Send upload request
String uri = kseFAuthManager.getBaseURI() + "/sessions/online/"
+ kseFAuthManager.getSessionRefNumber() + "/invoices";
logger.info("│ ├── POST: " + uri);
if (kseFAuthManager.isDebug()) {
logger.info("│ ├── Request Body: " + jsonBody);
}
@ -152,10 +159,9 @@ public class KSeFAPIService {
request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── HTTP Response: " + response.statusCode());
// if (kseFAuthManager.isDebug()) {
logger.info("│ ├── Response Body: " + response.body());
// }
if (kseFAuthManager.isDebug()) {
logger.info("│ ├── Response Body: " + response.body());
}
if (response.statusCode() != 202) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
@ -192,7 +198,7 @@ public class KSeFAPIService {
// Now we need to wait until auth/{AuthRef} will return 200
int code = -1;
long start = System.currentTimeMillis();
long timeout = 20_000; // 20 Sekunden
long timeout = 60_000; // 60 Sekunden
boolean timeoutStatus = true;
while (System.currentTimeMillis() - start < timeout) {
code = kseFAuthManager.checkSessionStatus(workitem);
@ -201,7 +207,7 @@ public class KSeFAPIService {
break; // ok or broken
}
// kleine Pause, um CPU zu schonen
kseFAuthManager.waitSomeTime(500);
kseFAuthManager.waitSomeTime(1000);
}
@ -211,50 +217,145 @@ public class KSeFAPIService {
// In case we have an error we print out the reason...
if (code > 200) {
kseFAuthManager.checkSessionStatusInvoices();
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Failed to process invoice: " + response.statusCode());
checkSessionStatusInvoices(workitem);
// convert KSeF Error status into a PluginException
String errorCode = workitem.getItemValueString("ksef.status.code");
String errorMessage = workitem.getItemValueString("ksef.status.description");
throw new PluginException(KSeFAuthManager.class.getSimpleName(), errorCode,
errorMessage);
}
} catch (IOException | NoSuchAlgorithmException |
} catch (Exception e) {
InterruptedException e) {
logger.severe("├── ⚠️ Invoice upload failed: " + e.getMessage());
kseFAuthManager.closeInteractiveSession();
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Invoice upload failed: " + e.getMessage());
} finally {
// finally we close the session
kseFAuthManager.deleteCurrentAuthSession();
}
}
/**
* This method verifies the actual status of the status of all invoices
*
* The method returns the aut status code (not the HTTP Response code!)
*
* https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Status-wysylki-i-UPO/paths/~1api~1v2~1sessions~1%7BreferenceNumber%7D/get
*
* @return Session Status Code
* @throws PluginException
*/
public void checkSessionStatusInvoices(ItemCollection workitem) throws PluginException {
logger.info("├── 🚸 KSeF API check session invoice status...");
String uri = kseFAuthManager.getBaseURI() + "/sessions/" + kseFAuthManager.getSessionRefNumber() + "/invoices";
int httpResponse = -1;
int statusCode = -1;
String statusDescription;
String statusDetails;
logger.info("│ ├── GET: " + uri);
HttpResponse<String> response = null;
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + kseFAuthManager.getAccessToken())
.GET()
.build();
response = kseFAuthManager.getHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error: Unable to request auth status: " + e.getMessage());
}
httpResponse = response.statusCode();
logger.info("│ ├── HTTP Response: " + httpResponse);
logger.info("│ ├── " + response.body());
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
// Extract status from the first invoice in the invoices array
if (jsonObject.containsKey("invoices")) {
JsonArray invoicesArray = jsonObject.getJsonArray("invoices");
if (invoicesArray != null && !invoicesArray.isEmpty()) {
JsonObject firstInvoice = invoicesArray.getJsonObject(0);
// Status Code aus dem status-Objekt extrahieren
if (firstInvoice.containsKey("status")) {
JsonObject statusObject = firstInvoice.getJsonObject("status");
if (statusObject.containsKey("code")) {
statusCode = statusObject.getInt("code");
statusDescription = statusObject.getString("description");
// Convert details array to a single string
statusDetails = "";
if (statusObject.containsKey("details")) {
JsonArray detailsArray = statusObject.getJsonArray("details");
statusDetails = detailsArray.stream()
.map(v -> ((JsonString) v).getString())
.collect(Collectors.joining("; "));
}
logger.info("│ ├── Status Code: " + statusCode);
logger.info("│ ├── Status details: " + statusDetails);
workitem.setItemValue("ksef.status.code", statusCode);
workitem.setItemValue("ksef.status.description", statusDescription + ": "
+ statusDetails);
}
}
} else {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Unable to read invoice status from interactive session!");
}
}
} 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());
}
}
/**
* Encrypt invoice XML with the session encryption keys
*
* @param invoiceXml the invoice XML content
* @param encryption the session encryption containing AES key and IV
* @return encrypted invoice bytes
* @throws Exception if encryption fails
* @throws PluginException if encryption fails
*/
private byte[] encryptInvoiceWithSessionKeys(byte[] invoiceXml, SessionEncryption encryption)
throws Exception {
throws PluginException {
try {
logger.info("│ ├── Encrypting invoice with session keys...");
logger.info("│ ├── Encrypting invoice with session keys...");
// Use the SAME keys that were sent during session creation!
SecretKeySpec aesKey = new SecretKeySpec(encryption.getAesKeyBytes(), "AES");
IvParameterSpec iv = new IvParameterSpec(encryption.getInitializationVector());
// Use the SAME keys that were sent during session creation!
SecretKeySpec aesKey = new SecretKeySpec(encryption.getAesKeyBytes(), "AES");
IvParameterSpec iv = new IvParameterSpec(encryption.getInitializationVector());
// Encrypt with AES-256-CBC
Cipher aesCipher;
// Encrypt with AES-256-CBC
Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv);
aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] encryptedData = aesCipher.doFinal(invoiceXml);
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv);
logger.info("│ ├── Invoice encrypted - " + encryptedData.length + " bytes");
byte[] encryptedData = aesCipher.doFinal(invoiceXml);
return encryptedData;
logger.info("│ ├── Invoice encrypted - " + encryptedData.length + " bytes");
return encryptedData;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
e.printStackTrace();
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error encrypt Invoice: " + e.getMessage());
}
}
/**

View file

@ -185,30 +185,29 @@ public class KSeFAuthManager {
*/
public void openSession(ItemCollection workitem) throws PluginException {
logger.info("├── open Session...");
if (!hasValidSession()) {
// first we make sure that we do not have any old sessions!
// this.deleteAllAuthSessions();
logger.info("│ ├── open new interactive KSeF Session...");
// open new session
this.loadPublicKeyCertificates();
this.authChallenge();
this.authKSeFToken();
// Jetzt Status abwarten
this.waitForAuthStatus();
// waitSomeTime(5000);
this.redeemToken();
this.openInteractiveSession();
this.waitForSessionStatus(workitem);
} else {
logger.warning("│ ├── ! Session already exists - reuse existing KSeF Session...");
if (hasValidSession()) {
// we need to destroy and rebuild the session here
logger.info("│ ├── destroy deprecated KSeF Session...");
deleteCurrentAuthSession();
sessionRefNumber = null;
}
logger.info("│ ├── open new interactive KSeF Session...");
// open new session
this.loadPublicKeyCertificates();
this.authChallenge();
this.authKSeFToken();
// Jetzt Status abwarten
this.waitForAuthStatus();
// waitSomeTime(5000);
this.redeemToken();
this.openInteractiveSession();
this.waitForSessionStatus(workitem);
}
/**
@ -282,7 +281,6 @@ public class KSeFAuthManager {
}
logger.info("│ ├── HTTP Response: " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
@ -355,7 +353,6 @@ public class KSeFAuthManager {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── HTTP Response: " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
// Prüfen, dass wir eine API-Antwort bekommen, kein HTML
@ -636,13 +633,15 @@ public class KSeFAuthManager {
.createSessionEncryption(this.sessionEncryptionPublicKey);
// Build JSON payload
String formCode = "{" +
" \"systemCode\": \"FA (3)\"," +
" \"schemaVersion\": \"1-0E\"," +
" \"value\": \"FA\"" +
"}";
logger.info("│ ├── formCode: " + formCode);
String jsonPayload = String.format(
"{" +
"\"formCode\": {" +
" \"systemCode\": \"FA (3)\"," +
" \"schemaVersion\": \"1-0E\"," +
" \"value\": \"FA\"" +
"}," +
"\"formCode\": " + formCode + "," +
"\"encryption\": {" +
" \"encryptedSymmetricKey\": \"%s\"," +
" \"initializationVector\": \"%s\"" +
@ -913,44 +912,6 @@ public class KSeFAuthManager {
return statusCode;
}
/**
* This method verifies the actual status of the status of all invoices
*
* The method returns the aut status code (not the HTTP Response code!)
*
* https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Status-wysylki-i-UPO/paths/~1api~1v2~1sessions~1%7BreferenceNumber%7D/get
*
* @return Session Status Code
* @throws PluginException
*/
public void checkSessionStatusInvoices() throws PluginException {
logger.info("├── ↩️ KSeF API check session status invoices...");
String uri = baseURI + "/sessions/" + this.sessionRefNumber + "/invoices";
int httpResponse = -1;
int statusCode = -1;
logger.info("│ ├── GET: " + uri);
HttpResponse<String> response = null;
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + this.accessToken)
.GET()
.build();
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error: Unable to request auth status: " + e.getMessage());
}
httpResponse = response.statusCode();
logger.info("│ ├── HTTP Response: " + httpResponse);
logger.info("│ ├── " + response.body());
}
/**
* This method returns all active auth sessions
*
@ -1026,6 +987,8 @@ public class KSeFAuthManager {
logger.info("│ ├── HTTP Response: " + response.statusCode());
sessionRefNumber = null;
}
/**
@ -1116,6 +1079,10 @@ public class KSeFAuthManager {
* @throws PluginException
*/
public void closeInteractiveSession() throws PluginException {
if (sessionRefNumber == null || sessionRefNumber.isEmpty()) {
// no op
return;
}
logger.info("├── 📦 KSeF API close session...");
// https://ksef-test.mf.gov.pl/api/v2/sessions/online/{referenceNumber}/close
String uri = baseURI + "/sessions/online/" + sessionRefNumber + "/close";
@ -1141,12 +1108,10 @@ public class KSeFAuthManager {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error: Unable to request auth status: " + e.getMessage());
}
if (true) {
logger.info("│ ├── HTTP Response: " + response.statusCode());
logger.info("│ ├── HTTP Response: " + response.statusCode());
if (debug) {
logger.info("Response: " + response.body());
}
// sessionRefNumber = null;
}
}

View file

@ -2,14 +2,15 @@
"invoices": [
{
"ordinalNumber": 1,
"referenceNumber": "20251126-EE-481F863000-7F510C2298-8A",
"invoiceHash": "tR5z/LEg2accg3AKWoibxJF9Xoht15Pf8T1lT6S3WLc=",
"invoicingDate": "2025-11-26T21:00:26.5956823+00:00",
"invoiceNumber": "266",
"referenceNumber": "20251127-EE-1870507000-F4367AAF39-8E",
"invoiceHash": "ex+T+FKIJJH81XAzQnHtE2CzaBpkHjKEG5GkmcURDWw=",
"invoicingDate": "2025-11-27T07:07:05.8630372+00:00",
"status": {
"code": 450,
"description": "Błąd weryfikacji semantyki dokumentu faktury",
"code": 440,
"description": "Duplikat faktury",
"details": [
"Could not find schema information for the element 'http://crd.gov.pl/wzor/2025/06/25/13775/:Faktura'."
"Duplikat faktury. Faktura o numerze KSeF: 9552521552-20251126-01000054CA98-3E została już prawidłowo przesłana do systemu w sesji: 20251126-SO-4C65114000-8008C5CA92-88"
]
}
}