office-alexander-logistics/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java
2025-11-17 00:04:40 +01:00

229 lines
8.9 KiB
Java

package com.alexanderlogistics.ksef.api;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
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 javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.imixs.workflow.FileData;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.exceptions.PluginException;
import jakarta.annotation.security.DeclareRoles;
import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.RunAs;
import jakarta.ejb.Singleton;
import jakarta.inject.Inject;
import jakarta.json.JsonObject;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
/**
* THe KSeFAPIService provides methods to open an interactive session and upload
* an invoice.
*
*/
@DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
@RolesAllowed({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
@Singleton
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
public class KSeFAPIService {
private static Logger logger = Logger.getLogger(KSeFAPIService.class.getName());
public static final String ERROR_API = "API_ERROR";
@Inject
KSeFAuthManager kseFAuthManager;
@Inject
DocumentService documentService;
/**
* This method uploads a KSeF Invoice document (XML).
* The response of the upload is a 'referenceNumber' which is stored into the
* item ksef.referenceNumber
*
* @param workitem workitem containing the invoice
* @param fileName name of the XML file to upload
* @return referenceNumber from KSeF
* @throws PluginException
*/
public String uploadInvoice(ItemCollection workitem, String fileName) throws PluginException {
// First open an interactive session (reuses existing if valid)
kseFAuthManager.openSession();
logger.info("├── 📤 Upload Invoice...");
// Validate session
if (kseFAuthManager.getAccessToken() == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - missing AccessToken!");
}
if (kseFAuthManager.getSessionRefNumber() == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - missing SessionRefNumber!");
}
if (kseFAuthManager.getSessionEncryption() == null) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"API Error - missing Session Encryption!");
}
// Load invoice XML
FileData fileData = workitem.getFileData(fileName);
byte[] invoiceXml = fileData.getContent();
logger.info("│ ├── XML Invoice loaded - " + invoiceXml.length + " bytes");
try {
// Get the session encryption keys (already sent to KSeF during session
// creation!)
SessionEncryption encryption = kseFAuthManager.getSessionEncryption();
// Encrypt invoice with session keys
byte[] encryptedInvoice = encryptInvoiceWithSessionKeys(invoiceXml, encryption);
// Calculate hashes
String invoiceHash = calculateSHA256Hash(invoiceXml);
String encryptedInvoiceHash = calculateSHA256Hash(encryptedInvoice);
// Build JSON request body
String jsonBody = String.format(
"{" +
"\"invoiceHash\": \"%s\"," +
"\"invoiceSize\": %d," +
"\"encryptedInvoiceHash\": \"%s\"," +
"\"encryptedInvoiceSize\": %d," +
"\"encryptedInvoiceContent\": \"%s\"," +
"\"offlineMode\": false" +
"}",
invoiceHash,
invoiceXml.length,
encryptedInvoiceHash,
encryptedInvoice.length,
Base64.getEncoder().encodeToString(encryptedInvoice));
// Send upload request
String uri = kseFAuthManager.getBaseURI() + "/sessions/online/"
+ kseFAuthManager.getSessionRefNumber() + "/invoices";
logger.info("│ ├── POST: " + uri);
if (kseFAuthManager.isDebug()) {
logger.info("│ ├── Request Body: " + jsonBody);
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + kseFAuthManager.getAccessToken())
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = kseFAuthManager.getHttpClient().send(
request, HttpResponse.BodyHandlers.ofString());
logger.info("│ ├── HTTP Response: " + response.statusCode());
// if (kseFAuthManager.isDebug()) {
logger.info("│ ├── Response Body: " + response.body());
// }
if (response.statusCode() != 202) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Invoice upload failed with status: " + response.statusCode() +
" - " + response.body());
}
// Parse response and get reference number
String referenceNumber = null;
try (Jsonb jsonb = JsonbBuilder.create()) {
JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class);
referenceNumber = jsonObject.getString("referenceNumber");
logger.info("│ ├── Reference Number: " + referenceNumber);
} catch (Exception e) {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Error parsing upload response: " + e.getMessage());
}
// Store reference number in file metadata
List<Object> textlist = new ArrayList<>();
textlist.add(referenceNumber);
fileData.setAttribute("ksef.referenceNumber", textlist);
logger.info("├── ✅ Upload successful - Reference: " + referenceNumber);
return referenceNumber;
} catch (Exception e) {
logger.severe("├── ⚠️ Invoice upload failed: " + e.getMessage());
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
"Invoice upload failed: " + e.getMessage());
} finally {
// finally we close the session
kseFAuthManager.closeInteractiveSession();
kseFAuthManager.deleteCurrentAuthSession();
}
}
/**
* 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
*/
private byte[] encryptInvoiceWithSessionKeys(byte[] invoiceXml, SessionEncryption encryption)
throws Exception {
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());
// Encrypt with AES-256-CBC
Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv);
byte[] encryptedData = aesCipher.doFinal(invoiceXml);
logger.info("│ ├── Invoice encrypted - " + encryptedData.length + " bytes");
return encryptedData;
}
/**
* Calculate SHA-256 hash of data
*
* @param data byte array to hash
* @return Base64-encoded hash
* @throws NoSuchAlgorithmException if SHA-256 is not available
*/
private String calculateSHA256Hash(byte[] data) throws NoSuchAlgorithmException {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = sha256.digest(data);
return Base64.getEncoder().encodeToString(hashBytes);
}
}