Berechnung der KSeF Download URLs

This commit is contained in:
Ralph Soika 2025-11-28 14:01:18 +01:00
parent e4347046b4
commit a1621420e2
5 changed files with 11352 additions and 5 deletions

View file

@ -174,6 +174,7 @@ The implementation uses `PluginException` for error handling with two error type
- [XML Invoice Example](https://github.com/CIRFMF/ksef-docs/blob/main/faktury/weryfikacja-faktury.md) - [XML Invoice Example](https://github.com/CIRFMF/ksef-docs/blob/main/faktury/weryfikacja-faktury.md)
- [Github Discussions](https://github.com/CIRFMF/ksef-docs/issues/351#issuecomment-3538013805) - [Github Discussions](https://github.com/CIRFMF/ksef-docs/issues/351#issuecomment-3538013805)
- [Github Discussions](https://github.com/CIRFMF/ksef-docs/issues/399)
# Validate XML # Validate XML

11183
doc/KSeF/openapi.json Normal file

File diff suppressed because it is too large Load diff

75
doc/KSeF/test.xml Normal file
View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<Potwierdzenie xmlns="http://upo.schematy.mf.gov.pl/KSeF/v4-2">
<NazwaPodmiotuPrzyjmujacego>Ministerstwo Finansów</NazwaPodmiotuPrzyjmujacego>
<NumerReferencyjnySesji>20251128-SO-1DA83DC000-9430B11465-93</NumerReferencyjnySesji>
<Uwierzytelnienie>
<IdKontekstu>
<Nip>9552521552</Nip>
</IdKontekstu>
<NumerReferencyjnyTokenaKSeF>20251113-EC-2751AC3000-5C5466924B-62</NumerReferencyjnyTokenaKSeF>
</Uwierzytelnienie>
<NazwaStrukturyLogicznej>1-0E</NazwaStrukturyLogicznej>
<KodFormularza>FA (3)</KodFormularza>
<Dokument>
<NipSprzedawcy>9552521552</NipSprzedawcy>
<NumerKSeFDokumentu>9552521552-20251128-010000AA503B-FE</NumerKSeFDokumentu>
<NumerFaktury>216577</NumerFaktury>
<DataWystawieniaFaktury>2024-04-03</DataWystawieniaFaktury>
<DataPrzeslaniaDokumentu>2025-11-28T09:38:18.096+01:00</DataPrzeslaniaDokumentu>
<DataNadaniaNumeruKSeF>2025-11-28T09:38:18.192+01:00</DataNadaniaNumeruKSeF>
<SkrotDokumentu>wk2TorojBbyNYtDODL1l/KCxQi/rlb3Yss/OEmf5HpE=</SkrotDokumentu>
</Dokument>
<Signature Id="Signature" xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<DigestValue>N/EazXugPjJtobeCSm/ShxeSafdffzHFeRo/BLsngR4=</DigestValue>
</Reference>
<Reference URI="#SignedProperties" Type="http://uri.etsi.org/01903#SignedProperties">
<Transforms>
<Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<DigestValue>7OYxftjJC1QhSKcWd/oeu7jJaLiO1kFvDCLja8xmNqk=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>
c87dKdbK3Wf2QpeKEEC0H2LMG92YBH95EbKL6wJTqozOzPq6wqj+06CIPX6bIEz8Szup9xWJlJHX+/xzO4NyMxeiXMQOCAiQ4ZnhPLG2EChopNZYGDbVgOMvfSQBSkHODobG8HH+q4fqueya0V+41CnCSzF8f68fcfVTrV8DfAuw3faKYVg4ddfkvFT+AkBCAZ/JPdYzVVN28np33bd1lHNFJ5wNSpILy9h0FF9iJIiB5RMyvJwxx0HlutrhIMUoId5vUW2XLJKAOLJSSntTJf2S4qjMJcgZr/ZVP9GSaBZPBX09nPWrXOfOwIOuakVfq9N2n+ONZ29ivHpZelMWlA==</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>
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</X509Certificate>
</X509Data>
</KeyInfo>
<Object>
<xades:QualifyingProperties Target="#Signature"
xmlns:xades="http://uri.etsi.org/01903/v1.3.2#"
xmlns="http://www.w3.org/2000/09/xmldsig#">
<xades:SignedProperties Id="SignedProperties">
<xades:SignedSignatureProperties>
<xades:SigningTime>2025-11-28T08:38:18.2360956+00:00</xades:SigningTime>
<xades:SigningCertificate>
<xades:Cert>
<xades:CertDigest>
<DigestMethod
Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<DigestValue>IFnqTIVQHeVNK0sYY9kn/o3hT2FGIV6U7keiW0zj/Ig=</DigestValue>
</xades:CertDigest>
<xades:IssuerSerial>
<X509IssuerName>CN=Certum SMIME RSA CA, O=Asseco Data Systems
S.A., C=PL</X509IssuerName>
<X509SerialNumber>132015587733884965165641957463135993546</X509SerialNumber>
</xades:IssuerSerial>
</xades:Cert>
</xades:SigningCertificate>
</xades:SignedSignatureProperties>
</xades:SignedProperties>
</xades:QualifyingProperties>
</Object>
</Signature>
</Potwierdzenie>

View file

@ -2,12 +2,15 @@ package com.alexanderlogistics.ksef.api;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
@ -122,7 +125,9 @@ public class KSeFAPIService {
// Calculate hashes // Calculate hashes
String invoiceHash = calculateSHA256Hash(invoiceXml); String invoiceHash = calculateSHA256Hash(invoiceXml);
workitem.setItemValue("ksef.invoiceHash", invoiceHash);
String encryptedInvoiceHash = calculateSHA256Hash(encryptedInvoice); String encryptedInvoiceHash = calculateSHA256Hash(encryptedInvoice);
workitem.setItemValue("ksef.encryptedInvoiceHash", encryptedInvoiceHash);
// Build JSON request body // Build JSON request body
String jsonBody = String.format( String jsonBody = String.format(
@ -198,7 +203,7 @@ public class KSeFAPIService {
// Now we need to wait until auth/{AuthRef} will return 200 // Now we need to wait until auth/{AuthRef} will return 200
int code = -1; int code = -1;
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
long timeout = 60_000; // 60 Sekunden long timeout = 60_000; // 3 Minuten
boolean timeoutStatus = true; boolean timeoutStatus = true;
while (System.currentTimeMillis() - start < timeout) { while (System.currentTimeMillis() - start < timeout) {
code = kseFAuthManager.checkSessionStatus(workitem); code = kseFAuthManager.checkSessionStatus(workitem);
@ -207,12 +212,13 @@ public class KSeFAPIService {
break; // ok or broken break; // ok or broken
} }
// kleine Pause, um CPU zu schonen // kleine Pause, um CPU zu schonen
kseFAuthManager.waitSomeTime(1000); kseFAuthManager.waitSomeTime(3000);
} }
if (timeoutStatus) { if (timeoutStatus) {
logger.severe("├── ⚠️ Status timeout"); 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... // In case we have an error we print out the reason...
@ -224,9 +230,18 @@ public class KSeFAPIService {
throw new PluginException(KSeFAuthManager.class.getSimpleName(), errorCode, throw new PluginException(KSeFAuthManager.class.getSimpleName(), errorCode,
errorMessage); errorMessage);
} }
} catch (IOException | NoSuchAlgorithmException |
InterruptedException e) { // if status code 200 and if we have a ksef.upo.referencenumber we download the
// document
if (code == 200 && !referenceNumber.isEmpty()) {
FileData upoFileData = downloadUOPXML(workitem);
workitem.addFileData(upoFileData);
// store verification url
generateVerificationUrl(workitem);
}
} catch (IOException | NoSuchAlgorithmException | InterruptedException e) {
logger.severe("├── ⚠️ Invoice upload failed: " + e.getMessage()); logger.severe("├── ⚠️ Invoice upload failed: " + e.getMessage());
kseFAuthManager.closeInteractiveSession(); kseFAuthManager.closeInteractiveSession();
throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API,
@ -319,6 +334,75 @@ public class KSeFAPIService {
} }
/**
* 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);
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 {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
String invoiceDate = formatter.format(workitem.getItemValueLocalDate("invoice.date"));
String invoiceHash = workitem.getItemValueString("ksef.invoiceHash");
String urlEncodedHash = URLEncoder.encode(invoiceHash, StandardCharsets.UTF_8);
String baseURL = kseFAuthManager.getBaseURI();
// https://ksef-test.mf.gov.pl/api/v2 -> https://ksef-test.mf.gov.pl/client-app/
baseURL = baseURL.replace("/api/v2", "/client-app/invoice");
// Build verification URL
workitem.setItemValue("ksef.VerificationUrl",
baseURL + "/" + kseFAuthManager.getKsefNip() + "/" + invoiceDate + "/"
+ urlEncodedHash);
}
/** /**
* Encrypt invoice XML with the session encryption keys * Encrypt invoice XML with the session encryption keys
* *

View file

@ -149,6 +149,10 @@ public class KSeFAuthManager {
this.ksefNip = Optional.ofNullable(nip); this.ksefNip = Optional.ofNullable(nip);
} }
public String getKsefNip() {
return ksefNip.orElse(null);
}
public void setKsefEndpoint(String endpoint) { public void setKsefEndpoint(String endpoint) {
this.ksefEndpoint = Optional.ofNullable(endpoint); this.ksefEndpoint = Optional.ofNullable(endpoint);
} }