Berechnung der KSeF Download URLs
This commit is contained in:
parent
e4347046b4
commit
a1621420e2
5 changed files with 11352 additions and 5 deletions
|
|
@ -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)
|
||||
|
||||
- [Github Discussions](https://github.com/CIRFMF/ksef-docs/issues/351#issuecomment-3538013805)
|
||||
- [Github Discussions](https://github.com/CIRFMF/ksef-docs/issues/399)
|
||||
|
||||
# Validate XML
|
||||
|
||||
|
|
|
|||
11183
doc/KSeF/openapi.json
Normal file
11183
doc/KSeF/openapi.json
Normal file
File diff suppressed because it is too large
Load diff
75
doc/KSeF/test.xml
Normal file
75
doc/KSeF/test.xml
Normal 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>
|
||||
|
|
@ -2,12 +2,15 @@ package com.alexanderlogistics.ksef.api;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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.List;
|
||||
|
|
@ -122,7 +125,9 @@ public class KSeFAPIService {
|
|||
|
||||
// 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(
|
||||
|
|
@ -198,7 +203,7 @@ public class KSeFAPIService {
|
|||
// Now we need to wait until auth/{AuthRef} will return 200
|
||||
int code = -1;
|
||||
long start = System.currentTimeMillis();
|
||||
long timeout = 60_000; // 60 Sekunden
|
||||
long timeout = 60_000; // 3 Minuten
|
||||
boolean timeoutStatus = true;
|
||||
while (System.currentTimeMillis() - start < timeout) {
|
||||
code = kseFAuthManager.checkSessionStatus(workitem);
|
||||
|
|
@ -207,12 +212,13 @@ public class KSeFAPIService {
|
|||
break; // ok or broken
|
||||
}
|
||||
// kleine Pause, um CPU zu schonen
|
||||
kseFAuthManager.waitSomeTime(1000);
|
||||
|
||||
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...
|
||||
|
|
@ -224,9 +230,18 @@ public class KSeFAPIService {
|
|||
throw new PluginException(KSeFAuthManager.class.getSimpleName(), errorCode,
|
||||
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());
|
||||
kseFAuthManager.closeInteractiveSession();
|
||||
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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -149,6 +149,10 @@ public class KSeFAuthManager {
|
|||
this.ksefNip = Optional.ofNullable(nip);
|
||||
}
|
||||
|
||||
public String getKsefNip() {
|
||||
return ksefNip.orElse(null);
|
||||
}
|
||||
|
||||
public void setKsefEndpoint(String endpoint) {
|
||||
this.ksefEndpoint = Optional.ofNullable(endpoint);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue