diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAPIAdapter.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAPIAdapter.java index c4e1ccf..833f56e 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAPIAdapter.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/einvoice/KSeFAPIAdapter.java @@ -65,27 +65,14 @@ public class KSeFAPIAdapter implements SignalAdapter { try { List ksefExportDefinitions = workflowService.evalWorkflowResultXML(event, "ksef", "EXPORT", workitem, false); - // List 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); } diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java index 59d957a..b62d321 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java @@ -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 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()); + + } } /** diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java index f9ddaeb..5f4bb7a 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java @@ -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 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; } } diff --git a/office-alexander-logistics-app/src/test/resources/ksef/result.json b/office-alexander-logistics-app/src/test/resources/ksef/result.json index e591466..8857583 100644 --- a/office-alexander-logistics-app/src/test/resources/ksef/result.json +++ b/office-alexander-logistics-app/src/test/resources/ksef/result.json @@ -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" ] } }