730 lines
30 KiB
Java
730 lines
30 KiB
Java
package com.alexanderlogistics.ksef.api;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
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.time.format.DateTimeFormatter;
|
|
import java.util.ArrayList;
|
|
import java.util.Base64;
|
|
import java.util.Collection;
|
|
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;
|
|
|
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
import org.apache.pdfbox.pdmodel.PDPage;
|
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
|
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|
import org.imixs.archive.core.SnapshotService;
|
|
import org.imixs.workflow.FileData;
|
|
import org.imixs.workflow.ItemCollection;
|
|
import org.imixs.workflow.engine.DocumentService;
|
|
import org.imixs.workflow.engine.WorkflowService;
|
|
import org.imixs.workflow.exceptions.PluginException;
|
|
import org.imixs.workflow.exceptions.QueryException;
|
|
|
|
import com.google.zxing.BarcodeFormat;
|
|
import com.google.zxing.WriterException;
|
|
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
|
import com.google.zxing.common.BitMatrix;
|
|
import com.google.zxing.qrcode.QRCodeWriter;
|
|
|
|
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.JsonArray;
|
|
import jakarta.json.JsonObject;
|
|
import jakarta.json.JsonString;
|
|
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";
|
|
public static final String DOCUMENT_ERROR = "DOCUMENT_ERROR";
|
|
public static final String CONFIG_ERROR = "CONFIG_ERROR";
|
|
final String TYPE_TEXTBLOCK = "textblock";
|
|
|
|
@Inject
|
|
KSeFAuthManager kseFAuthManager;
|
|
|
|
@Inject
|
|
DocumentService documentService;
|
|
|
|
@Inject
|
|
WorkflowService workflowService;
|
|
|
|
@Inject
|
|
SnapshotService snapshotService;
|
|
|
|
/**
|
|
* 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 void uploadInvoice(ItemCollection workitem, String fileName) throws PluginException {
|
|
|
|
// First open an interactive session (reuses existing if valid)
|
|
kseFAuthManager.openSession(workitem);
|
|
|
|
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
|
|
byte[] invoiceXml = null;
|
|
FileData fileData = workitem.getFileData(fileName);
|
|
// test if we need to load the snapshot data...
|
|
if (fileData.getContent().length == 0) {
|
|
fileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), fileName);
|
|
if (fileData != null) {
|
|
invoiceXml = fileData.getContent();
|
|
}
|
|
} else {
|
|
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);
|
|
workitem.setItemValue("ksef.invoiceHash", invoiceHash);
|
|
String encryptedInvoiceHash = calculateSHA256Hash(encryptedInvoice);
|
|
workitem.setItemValue("ksef.encryptedInvoiceHash", encryptedInvoiceHash);
|
|
|
|
// 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);
|
|
|
|
workitem.setItemValue("ksef.referenceNumber", referenceNumber);
|
|
logger.info("├── ✅ Upload successful - Reference: " + referenceNumber);
|
|
|
|
// Close Session
|
|
kseFAuthManager.closeInteractiveSession();
|
|
|
|
// Status abfragen
|
|
// Now we need to wait until auth/{AuthRef} will return 200
|
|
int code = -1;
|
|
long start = System.currentTimeMillis();
|
|
long timeout = 150_000; // 2,5 Minuten
|
|
boolean timeoutStatus = true;
|
|
while (System.currentTimeMillis() - start < timeout) {
|
|
code = kseFAuthManager.checkSessionStatus(workitem);
|
|
if (code >= 200) {
|
|
timeoutStatus = false;
|
|
break; // ok or broken
|
|
}
|
|
// kleine Pause, um CPU zu schonen
|
|
kseFAuthManager.waitSomeTime(3000);
|
|
}
|
|
|
|
if (timeoutStatus) {
|
|
logger.severe("├── ⚠️ Status timeout");
|
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), "Session Status: " + code,
|
|
"Invoice was not processed within expected timeout " + timeout + " upload canceled");
|
|
}
|
|
|
|
// In case we have an error we print out the reason...
|
|
if (code > 200) {
|
|
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);
|
|
}
|
|
|
|
// if status code 200 and if we have a ksef.upo.referencenumber we download the
|
|
// document
|
|
if (code == 200 && !referenceNumber.isEmpty()) {
|
|
checkSessionStatusInvoices(workitem);
|
|
|
|
FileData upoFileData = downloadUOPXML(workitem);
|
|
workitem.addFileData(upoFileData);
|
|
|
|
// store verification url
|
|
generateVerificationUrl(workitem);
|
|
|
|
// store QR Code
|
|
generateQRCode(workitem);
|
|
|
|
// print QR Code to pdf
|
|
embedQRCode(workitem);
|
|
}
|
|
|
|
} catch (IOException | NoSuchAlgorithmException | 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;
|
|
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);
|
|
if (firstInvoice.containsKey("ksefNumber")) {
|
|
String ksefNumber = firstInvoice.getString("ksefNumber");
|
|
logger.info("│ ├── ksefNumber: " + ksefNumber);
|
|
workitem.setItemValue("ksef.number", ksefNumber);
|
|
}
|
|
// 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
|
|
if (statusObject.containsKey("details")) {
|
|
JsonArray detailsArray = statusObject.getJsonArray("details");
|
|
String details = detailsArray.stream()
|
|
.map(v -> ((JsonString) v).getString())
|
|
.collect(Collectors.joining("; "));
|
|
statusDescription = statusDescription + ": " + details;
|
|
}
|
|
|
|
logger.info("│ ├── Status Code: " + statusCode);
|
|
workitem.setItemValue("ksef.status.code", statusCode);
|
|
workitem.setItemValue("ksef.status.description", statusDescription);
|
|
}
|
|
}
|
|
} 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());
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* This method downloads the UPO XML document based on the KSeF reference
|
|
* number
|
|
*
|
|
* https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Status-wysylki-i-UPO/paths/~1api~1v2~1sessions~1%7BreferenceNumber%7D~1invoices~1%7BinvoiceReferenceNumber%7D~1upo/get
|
|
*
|
|
* @return Session Status Code
|
|
* @throws PluginException
|
|
*/
|
|
public FileData downloadUOPXML(ItemCollection workitem) throws PluginException {
|
|
|
|
String ksefRefNumber = workitem.getItemValueString("ksef.referenceNumber");
|
|
if (ksefRefNumber == null || ksefRefNumber.isEmpty()) {
|
|
logger.severe("├── ⚠️ Error parsing ksef.referenceNumber");
|
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
|
"Error parsing ksef.referenceNumber");
|
|
}
|
|
logger.info("├── 📥 KSeF API download UPO XML...");
|
|
String uri = kseFAuthManager.getBaseURI() + "/sessions/" + kseFAuthManager.getSessionRefNumber()
|
|
+ "/invoices/" + ksefRefNumber + "/upo";
|
|
|
|
int httpResponse = -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 " + 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);
|
|
if (kseFAuthManager.isDebug()) {
|
|
logger.info("│ ├── " + response.body());
|
|
}
|
|
FileData fileData = new FileData(ksefRefNumber + ".xml", response.body().getBytes(), "application/xml", null);
|
|
return fileData;
|
|
}
|
|
|
|
/**
|
|
* Generate KSeF verification URL for QR code (KOD I) and stores the URL into
|
|
* the item ksef.VerificationUrl
|
|
*
|
|
* @param workitem
|
|
*/
|
|
public void generateVerificationUrl(ItemCollection workitem) throws NoSuchAlgorithmException {
|
|
logger.info("├── 🔲 Generate QR-Code...");
|
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
String invoiceDate = formatter.format(workitem.getItemValueLocalDate("invoice.date"));
|
|
String baseURL = kseFAuthManager.getBaseURI();
|
|
|
|
String invoiceHash = workitem.getItemValueString("ksef.invoiceHash");
|
|
byte[] decoded = Base64.getDecoder().decode(invoiceHash);
|
|
// Base64URL ohne Padding
|
|
String base64Url = Base64.getUrlEncoder().withoutPadding().encodeToString(decoded);
|
|
|
|
// Die Client App URL muss angepasst werden. Das /v2/ element aus der Basis URL
|
|
// muss mit 'client-app/invoice' ausgetauscht werden.
|
|
// Es entsteht dann https://api-test.ksef.mf.gov.pl/client-app/invoice/
|
|
|
|
// BASE URL: https://api.ksef.mf.gov.pl/api/v2"
|
|
// /api/v2 muss ersetzt werden:
|
|
baseURL = baseURL.replace("/api/v2", "/client-app/invoice");
|
|
// remove resource /api/
|
|
// https://api.ksef.mf.gov.pl/api/client-app/invoice/9552521552/01-04-2026/88Yl-F_6vhLeMUONRz2LBcAHF21NpbLk7ikm4ZqV1bg
|
|
// =>
|
|
// https://api.ksef.mf.gov.pl/client-app/invoice/9552521552/01-04-2026/88Yl-F_6vhLeMUONRz2LBcAHF21NpbLk7ikm4ZqV1bg
|
|
|
|
// Build verification URL
|
|
workitem.setItemValue("ksef.VerificationUrl",
|
|
baseURL + "/" + kseFAuthManager.getKsefNip() + "/" + invoiceDate + "/"
|
|
+ base64Url);
|
|
}
|
|
|
|
/**
|
|
* Generates a QR code image.
|
|
*
|
|
* If the generation fails we simply print a warning but do not stop the
|
|
* complete process.
|
|
*
|
|
* @param text
|
|
* @param width
|
|
* @param height
|
|
* @param filePath
|
|
* @throws PluginException
|
|
* @throws WriterException
|
|
* @throws IOException
|
|
*/
|
|
public void generateQRCode(ItemCollection workitem) {
|
|
|
|
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
|
String text = workitem.getItemValueString("ksef.verificationurl");
|
|
BitMatrix bitMatrix;
|
|
try {
|
|
bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, 300, 300);
|
|
|
|
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
|
|
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
|
|
|
|
// store qr code
|
|
String ksefRefNumber = workitem.getItemValueString("ksef.referenceNumber");
|
|
FileData fileData = new FileData(ksefRefNumber + ".png", pngOutputStream.toByteArray(), "image/png", null);
|
|
|
|
workitem.addFileData(fileData);
|
|
} catch (WriterException | IOException e) {
|
|
logger.warning("├── ⚠️ Failed to generate QR Code:: " + e.getMessage());
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This helper method prints the QR Code on the AGL Invoice document
|
|
*
|
|
* @param workitem
|
|
* @throws PluginException
|
|
*/
|
|
public void embedQRCode(ItemCollection workitem) {
|
|
|
|
try {
|
|
FileData pdfFileData = this.loadPDF(workitem);
|
|
|
|
String ksefRefNumber = workitem.getItemValueString("ksef.referenceNumber");
|
|
FileData qrCodeFileData = workitem.getFileData(ksefRefNumber + ".png");
|
|
if (qrCodeFileData.getContent().length == 0) {
|
|
qrCodeFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), ksefRefNumber + ".png");
|
|
}
|
|
|
|
// now we have the QR Code (qrCodeFileData) and the PDF (pdfFileData)
|
|
PDDocument document = PDDocument.load(pdfFileData.getContent());
|
|
|
|
logger.info("pdf file loaded, embed qrcode...");
|
|
// place thee PNG on the first page 10x10 cm from left upper corner
|
|
// ....
|
|
PDPage page = document.getPage(0);
|
|
PDImageXObject pdImage = PDImageXObject.createFromByteArray(document, qrCodeFileData.getContent(),
|
|
pdfFileData.getName());
|
|
// PDPageContentStream contentStream = new PDPageContentStream(document, page);
|
|
|
|
PDPageContentStream contentStream = new PDPageContentStream(
|
|
document,
|
|
page,
|
|
PDPageContentStream.AppendMode.APPEND, // Append statt Overwrite
|
|
true, // compress
|
|
true // resetContext - wichtig für konsistente Grafik-Zustände
|
|
);
|
|
|
|
// Mit Größenangabe (Breite und Höhe in Points):
|
|
// QR-Code zeichnen
|
|
// Seitenhöhe auslesen
|
|
float pageHeight = page.getMediaBox().getHeight();
|
|
// Position von OBEN berechnen (z.B. 50 Points vom oberen Rand)
|
|
float marginFromTop = 230;
|
|
float imageWidth = 50;
|
|
float imageHeight = 50;
|
|
float imageX = 350;
|
|
float imageY = pageHeight - marginFromTop - imageHeight; // Von oben berechnet!
|
|
|
|
contentStream.drawImage(pdImage, imageX, imageY, imageWidth, imageHeight);
|
|
|
|
// Text darunter schreiben
|
|
String text = "KSeF-No.: " + workitem.getItemValueString("ksef.number");
|
|
float fontSize = 7;
|
|
PDType1Font font = PDType1Font.COURIER;
|
|
float textX = imageX + 5;
|
|
float textY = imageY - 5; // 10 Points unter dem Bild
|
|
contentStream.beginText();
|
|
contentStream.setFont(font, fontSize);
|
|
contentStream.newLineAtOffset(textX, textY);
|
|
contentStream.showText(text);
|
|
contentStream.endText();
|
|
|
|
contentStream.close();
|
|
|
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
document.save(baos);
|
|
byte[] eInvoiceData = baos.toByteArray();
|
|
document.close();
|
|
baos.close();
|
|
|
|
pdfFileData.setContent(eInvoiceData);
|
|
|
|
// finally update the pdf file
|
|
workitem.removeFile(pdfFileData.getName());
|
|
// set new pdf file...
|
|
workitem.addFileData(pdfFileData);
|
|
|
|
} catch (IOException | PluginException e) {
|
|
logger.warning("Failed to embed QR Code on PDF file: " + 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 PluginException if encryption fails
|
|
*/
|
|
private byte[] encryptInvoiceWithSessionKeys(byte[] invoiceXml, SessionEncryption encryption)
|
|
throws PluginException {
|
|
try {
|
|
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;
|
|
|
|
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;
|
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
|
|
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
|
e.printStackTrace();
|
|
|
|
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
|
|
"Error encrypt Invoice: " + e.getMessage());
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* This helper method tries to find the pdf file matching the item
|
|
* 'cargosoft.import.filename'. If not found the method returns the first .pdf
|
|
* file form the list of attachments. The method returns null if no pdf file
|
|
* exits
|
|
*
|
|
* @param workitem
|
|
* @return the pdf FileData object
|
|
* @throws PluginException if not pdf file was found
|
|
*/
|
|
public FileData loadPDF(ItemCollection workitem) throws PluginException {
|
|
FileData pdfFileData = null;
|
|
String cargosoftFileName = workitem.getItemValueString("cargosoft.import.filename");
|
|
if (!cargosoftFileName.isEmpty()) {
|
|
cargosoftFileName = cargosoftFileName.replace(".xml", ".pdf");
|
|
|
|
// try to load filedata....
|
|
pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), cargosoftFileName);
|
|
|
|
}
|
|
|
|
if (pdfFileData == null) {
|
|
// take first pdf file available
|
|
List<String> fileNames = workitem.getFileNames();
|
|
for (String filename : fileNames) {
|
|
|
|
if (filename.toLowerCase().endsWith(".pdf")) {
|
|
pdfFileData = snapshotService.getWorkItemFile(workitem.getUniqueID(), filename);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pdfFileData == null) {
|
|
throw new PluginException(KSeFAPIService.class.getSimpleName(),
|
|
DOCUMENT_ERROR,
|
|
"DOCUMENT_ERROR: no pdf file found!");
|
|
}
|
|
return pdfFileData;
|
|
|
|
}
|
|
|
|
/**
|
|
* This method returns a text-block ItemCollection for a specified name.
|
|
*
|
|
* @param name in attribute txtname
|
|
*
|
|
*
|
|
*/
|
|
public FileData loadTextBlockFileData(String name, String fileName) {
|
|
ItemCollection textBlockItemCollection = null;
|
|
|
|
// load text-block by name....
|
|
String sQuery = "(type:\"" + TYPE_TEXTBLOCK + "\" AND txtname:\"" + name + "\")";
|
|
Collection<ItemCollection> col;
|
|
try {
|
|
// find the textblock...
|
|
col = documentService.find(sQuery, 1, 0);
|
|
if (col.size() > 0) {
|
|
textBlockItemCollection = col.iterator().next();
|
|
// fetch the fileData...
|
|
return snapshotService.getWorkItemFile(textBlockItemCollection.getUniqueID(), fileName);
|
|
|
|
} else {
|
|
logger.warning("Missing text-block : '" + name + "'");
|
|
}
|
|
} catch (QueryException e) {
|
|
logger.warning("getTextBlock - invalid query: " + e.getMessage());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* This method loads a text-block for a specified ref and appends the named
|
|
* fileData object of this document.
|
|
*
|
|
* @param document
|
|
* @throws PluginException
|
|
*/
|
|
|
|
public FileData loadXMLTemplate(ItemCollection workitem, ItemCollection config)
|
|
throws PluginException {
|
|
|
|
String textblock = config.getItemValueString("textblock");
|
|
String template = config.getItemValueString("template");
|
|
String sourceName = config.getItemValueString("source");
|
|
|
|
String targetName = "factur-x.xml";
|
|
|
|
// adapt text....
|
|
sourceName = workflowService.adaptText(sourceName, workitem);
|
|
|
|
if ((template == null || template.isEmpty()) || (textblock == null || textblock.isEmpty())) {
|
|
throw new PluginException(KSeFAPIService.class.getSimpleName(),
|
|
KSeFAPIService.CONFIG_ERROR,
|
|
"invalid e-invoice configuration in model event - textblock/template reference not defined!");
|
|
}
|
|
|
|
// load the text block
|
|
FileData fileData = this.loadTextBlockFileData(textblock, template);
|
|
|
|
// do we found the document?
|
|
if (fileData == null) {
|
|
throw new PluginException(KSeFAPIService.class.getSimpleName(),
|
|
KSeFAPIService.CONFIG_ERROR,
|
|
"invalid e-invoice configuration in model event - textblock/template: " + textblock + "/" + template
|
|
+ " not found!");
|
|
}
|
|
fileData.setName(targetName);
|
|
|
|
return fileData;
|
|
}
|
|
}
|