refactoring KSeF invoice upload
This commit is contained in:
parent
9024ee2e38
commit
459f7808cf
4 changed files with 171 additions and 117 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
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,35 +217,119 @@ 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...");
|
||||
|
||||
// Use the SAME keys that were sent during session creation!
|
||||
|
|
@ -247,7 +337,10 @@ public class KSeFAPIService {
|
|||
IvParameterSpec iv = new IvParameterSpec(encryption.getInitializationVector());
|
||||
|
||||
// Encrypt with AES-256-CBC
|
||||
Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
Cipher aesCipher;
|
||||
|
||||
aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
|
||||
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv);
|
||||
|
||||
byte[] encryptedData = aesCipher.doFinal(invoiceXml);
|
||||
|
|
@ -255,6 +348,14 @@ public class KSeFAPIService {
|
|||
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());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -185,10 +185,12 @@ 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();
|
||||
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
|
||||
|
|
@ -206,9 +208,6 @@ public class KSeFAuthManager {
|
|||
|
||||
this.waitForSessionStatus(workitem);
|
||||
|
||||
} else {
|
||||
logger.warning("│ ├── ! Session already exists - reuse existing KSeF Session...");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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 jsonPayload = String.format(
|
||||
"{" +
|
||||
"\"formCode\": {" +
|
||||
String formCode = "{" +
|
||||
" \"systemCode\": \"FA (3)\"," +
|
||||
" \"schemaVersion\": \"1-0E\"," +
|
||||
" \"value\": \"FA\"" +
|
||||
"}," +
|
||||
"}";
|
||||
logger.info("│ ├── formCode: " + formCode);
|
||||
String jsonPayload = String.format(
|
||||
"{" +
|
||||
"\"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());
|
||||
if (debug) {
|
||||
logger.info("Response: " + response.body());
|
||||
}
|
||||
|
||||
// sessionRefNumber = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue