REST-API in das eigenständige Projekt "PDF Service" ausgelagert
InvoiceSigningController entfernt; die ZUGFeRD-Services bleiben für die Weboberfläche erhalten. Multipart-Konfiguration entfällt (kein Upload mehr). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# PDF Tool — ZUGFeRD E-Rechnungs-Konverter
|
# PDF Tool — ZUGFeRD E-Rechnungs-Konverter
|
||||||
|
|
||||||
Spring Boot + Vaadin Flow Anwendung, die ein herkömmliches B2B-Rechnungs-PDF in eine
|
Spring Boot + Vaadin Flow Anwendung, die aus gespeicherten Rechnungstemplates
|
||||||
**rechtskonforme ZUGFeRD-E-Rechnung** umwandelt (PDF/A-3 mit eingebettetem EN16931-XML,
|
**rechtskonforme ZUGFeRD-E-Rechnungen** erzeugt (PDF/A-3 mit eingebettetem EN16931-XML,
|
||||||
Profil EN 16931 / Factur-X), das Ergebnis mit dem **Mustang-Validator** prüft und als
|
Profil EN 16931 / Factur-X), das Ergebnis mit dem **Mustang-Validator** prüft und als
|
||||||
**ZIP-Datei** bereitstellt.
|
**ZIP-Datei** bereitstellt.
|
||||||
|
|
||||||
@@ -26,7 +26,8 @@ mvn spring-boot:run
|
|||||||
```
|
```
|
||||||
|
|
||||||
Die Weboberfläche ist dann unter <http://localhost:8083> erreichbar:
|
Die Weboberfläche ist dann unter <http://localhost:8083> erreichbar:
|
||||||
PDF hochladen, Rechnungsdaten ausfüllen, "E-Rechnung erzeugen" klicken, ZIP herunterladen.
|
Rechnungstemplate auswählen, Rechnungsdaten ausfüllen, "E-Rechnung erzeugen" klicken —
|
||||||
|
das ZIP wird automatisch heruntergeladen.
|
||||||
|
|
||||||
Produktions-Build:
|
Produktions-Build:
|
||||||
|
|
||||||
@@ -37,83 +38,10 @@ java -jar target/pdf-tool-1.0.0-SNAPSHOT.jar
|
|||||||
|
|
||||||
## REST-API
|
## REST-API
|
||||||
|
|
||||||
### E-Rechnung erzeugen
|
Die REST-API (`/api/invoices/sign` und `/api/invoices/validate`) ist in das
|
||||||
|
eigenständige Projekt **PDF Service** ausgelagert
|
||||||
`POST /api/invoices/sign` (multipart/form-data) mit zwei Parts:
|
(`../PDF Service`, Port 8084). Dieses Tool enthält nur noch die Weboberfläche;
|
||||||
|
Rechnungen entstehen hier ausschließlich aus gespeicherten Templates.
|
||||||
| Part | Inhalt |
|
|
||||||
|------------|-------------------------------------------|
|
|
||||||
| `file` | das Rechnungs-PDF |
|
|
||||||
| `metadata` | Rechnungsdaten als JSON (siehe unten) |
|
|
||||||
|
|
||||||
Antwort: `application/zip` mit `<Rechnungsnummer>-zugferd.pdf`, `factur-x.xml` und
|
|
||||||
`validation-report.xml` (Mustang-Prüfbericht). Der Header `X-Zugferd-Valid: true|false`
|
|
||||||
meldet das Validierungsergebnis. Bei Fehlern: HTTP 400 mit `{"error": "..."}`.
|
|
||||||
|
|
||||||
> Häufigste Ursache für `X-Zugferd-Valid: false`: Das Quell-PDF bettet seine
|
|
||||||
> Schriften nicht ein. Das kann die Konvertierung nicht reparieren — das PDF muss
|
|
||||||
> dann mit eingebetteten Schriften neu erzeugt werden (Standard bei den meisten
|
|
||||||
> ERP-/Reporting-Systemen konfigurierbar).
|
|
||||||
|
|
||||||
### Bestehende E-Rechnung prüfen
|
|
||||||
|
|
||||||
`POST /api/invoices/validate` (multipart/form-data, Part `file` = ZUGFeRD-PDF oder
|
|
||||||
Factur-X-XML). Antwort: der Mustang-Prüfbericht als XML — HTTP 200 bei gültiger,
|
|
||||||
HTTP 422 bei ungültiger Rechnung, ebenfalls mit `X-Zugferd-Valid`-Header.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8083/api/invoices/validate \
|
|
||||||
-F "file=@rechnung-zugferd.pdf"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Beispiel
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8083/api/invoices/sign \
|
|
||||||
-F "file=@rechnung.pdf" \
|
|
||||||
-F "metadata=@metadata.json;type=application/json" \
|
|
||||||
-o rechnung-zugferd.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
`metadata.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"invoiceNumber": "RE-2026-0815",
|
|
||||||
"issueDate": "2026-07-08",
|
|
||||||
"deliveryDate": "2026-07-01",
|
|
||||||
"dueDate": "2026-07-22",
|
|
||||||
"currency": "EUR",
|
|
||||||
"paymentTerms": "Zahlbar innerhalb von 14 Tagen ohne Abzug.",
|
|
||||||
"sender": {
|
|
||||||
"name": "Assecutor GmbH",
|
|
||||||
"street": "Musterstrasse 1",
|
|
||||||
"zip": "20095",
|
|
||||||
"city": "Hamburg",
|
|
||||||
"countryCode": "DE",
|
|
||||||
"vatId": "DE123456789"
|
|
||||||
},
|
|
||||||
"recipient": {
|
|
||||||
"name": "Kunde AG",
|
|
||||||
"street": "Beispielweg 2",
|
|
||||||
"zip": "10115",
|
|
||||||
"city": "Berlin",
|
|
||||||
"countryCode": "DE"
|
|
||||||
},
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"description": "Beratungsleistung",
|
|
||||||
"quantity": 1,
|
|
||||||
"unitPriceNet": 1500.00,
|
|
||||||
"vatPercent": 19
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Pflichtfelder: `invoiceNumber`, `issueDate`, `sender` (inkl. Adresse), `recipient`,
|
|
||||||
mindestens ein Eintrag in `items`. `deliveryDate` fällt auf das Rechnungsdatum zurück,
|
|
||||||
`dueDate` auf Rechnungsdatum + 14 Tage, `currency` auf `EUR`.
|
|
||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
|
|
||||||
@@ -122,7 +50,6 @@ mindestens ein Eintrag in `items`. `deliveryDate` fällt auf das Rechnungsdatum
|
|||||||
Nicht-PDF/A-Eingaben (der Normalfall) werden tolerant konvertiert.
|
Nicht-PDF/A-Eingaben (der Normalfall) werden tolerant konvertiert.
|
||||||
- [ZugferdValidationService](src/main/java/de/assecutor/pdftool/zugferd/ZugferdValidationService.java) — Mustang-Validator
|
- [ZugferdValidationService](src/main/java/de/assecutor/pdftool/zugferd/ZugferdValidationService.java) — Mustang-Validator
|
||||||
(XSD + Schematron + veraPDF), liefert Status und XML-Prüfbericht
|
(XSD + Schematron + veraPDF), liefert Status und XML-Prüfbericht
|
||||||
- [InvoiceSigningController](src/main/java/de/assecutor/pdftool/api/InvoiceSigningController.java) — REST-Endpunkte `/sign` und `/validate`
|
|
||||||
- [MainView](src/main/java/de/assecutor/pdftool/ui/MainView.java) — Vaadin-Weboberfläche
|
- [MainView](src/main/java/de/assecutor/pdftool/ui/MainView.java) — Vaadin-Weboberfläche
|
||||||
- [InvoiceMetadata](src/main/java/de/assecutor/pdftool/invoice/InvoiceMetadata.java) — DTO für die Rechnungsdaten
|
- [InvoiceMetadata](src/main/java/de/assecutor/pdftool/invoice/InvoiceMetadata.java) — DTO für die Rechnungsdaten
|
||||||
|
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
package de.assecutor.pdftool.api;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import de.assecutor.pdftool.invoice.InvoiceMetadata;
|
|
||||||
import de.assecutor.pdftool.zugferd.ZugferdConversionException;
|
|
||||||
import de.assecutor.pdftool.zugferd.ZugferdResult;
|
|
||||||
import de.assecutor.pdftool.zugferd.ZugferdService;
|
|
||||||
import de.assecutor.pdftool.zugferd.ZugferdValidationService;
|
|
||||||
import jakarta.validation.ConstraintViolation;
|
|
||||||
import jakarta.validation.Validator;
|
|
||||||
import org.springframework.http.ContentDisposition;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestPart;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST-API zur Erzeugung und Prüfung rechtskonformer ZUGFeRD-E-Rechnungen.
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* curl -X POST http://localhost:8083/api/invoices/sign \
|
|
||||||
* -F "file=@rechnung.pdf" \
|
|
||||||
* -F "metadata=@metadata.json;type=application/json" \
|
|
||||||
* -o rechnung-zugferd.zip
|
|
||||||
*
|
|
||||||
* curl -X POST http://localhost:8083/api/invoices/validate \
|
|
||||||
* -F "file=@rechnung-zugferd.pdf"
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/invoices")
|
|
||||||
public class InvoiceSigningController {
|
|
||||||
|
|
||||||
/** Response-Header mit dem Ergebnis der Mustang-Validierung (true/false). */
|
|
||||||
public static final String VALIDATION_HEADER = "X-Zugferd-Valid";
|
|
||||||
|
|
||||||
private final ZugferdService zugferdService;
|
|
||||||
private final ZugferdValidationService validationService;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
private final Validator validator;
|
|
||||||
|
|
||||||
public InvoiceSigningController(ZugferdService zugferdService,
|
|
||||||
ZugferdValidationService validationService,
|
|
||||||
ObjectMapper objectMapper,
|
|
||||||
Validator validator) {
|
|
||||||
this.zugferdService = zugferdService;
|
|
||||||
this.validationService = validationService;
|
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
this.validator = validator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erzeugt aus PDF und Metadaten eine ZUGFeRD-Rechnung als ZIP (PDF + Prüfbericht).
|
|
||||||
* HTTP 200 bei gültiger, HTTP 422 bei ungültiger Rechnung; das ZIP wird in
|
|
||||||
* beiden Fällen geliefert.
|
|
||||||
*/
|
|
||||||
@PostMapping(value = "/sign", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
|
||||||
public ResponseEntity<byte[]> sign(@RequestPart("file") MultipartFile file,
|
|
||||||
@RequestPart("metadata") String metadataJson) throws IOException {
|
|
||||||
InvoiceMetadata metadata = parseAndValidate(metadataJson);
|
|
||||||
ZugferdResult result = zugferdService.createZugferdZip(file.getBytes(), metadata);
|
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.parseMediaType("application/zip"));
|
|
||||||
headers.setContentDisposition(ContentDisposition.attachment().filename(result.zipFileName()).build());
|
|
||||||
headers.set(VALIDATION_HEADER, String.valueOf(result.valid()));
|
|
||||||
return new ResponseEntity<>(result.zip(), headers,
|
|
||||||
result.valid() ? HttpStatus.OK : HttpStatus.UNPROCESSABLE_ENTITY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validiert eine bestehende ZUGFeRD-Rechnung (PDF oder Factur-X-XML).
|
|
||||||
* Antwort ist der Mustang-Prüfbericht als XML; HTTP 200 bei gültiger,
|
|
||||||
* HTTP 422 bei ungültiger Rechnung.
|
|
||||||
*/
|
|
||||||
@PostMapping(value = "/validate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
|
||||||
produces = MediaType.APPLICATION_XML_VALUE)
|
|
||||||
public ResponseEntity<String> validate(@RequestPart("file") MultipartFile file) throws IOException {
|
|
||||||
ZugferdValidationService.ValidationResult result =
|
|
||||||
validationService.validate(file.getBytes(), file.getOriginalFilename());
|
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.APPLICATION_XML);
|
|
||||||
headers.set(VALIDATION_HEADER, String.valueOf(result.valid()));
|
|
||||||
return new ResponseEntity<>(result.reportXml(), headers,
|
|
||||||
result.valid() ? HttpStatus.OK : HttpStatus.UNPROCESSABLE_ENTITY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvoiceMetadata parseAndValidate(String metadataJson) {
|
|
||||||
InvoiceMetadata metadata;
|
|
||||||
try {
|
|
||||||
metadata = objectMapper.readValue(metadataJson, InvoiceMetadata.class);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new ZugferdConversionException("Metadaten sind kein gültiges JSON: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
Set<ConstraintViolation<InvoiceMetadata>> violations = validator.validate(metadata);
|
|
||||||
if (!violations.isEmpty()) {
|
|
||||||
String details = violations.stream()
|
|
||||||
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
|
|
||||||
.sorted()
|
|
||||||
.collect(Collectors.joining("; "));
|
|
||||||
throw new ZugferdConversionException("Metadaten unvollständig: " + details);
|
|
||||||
}
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(ZugferdConversionException.class)
|
|
||||||
public ResponseEntity<Map<String, String>> handleConversionError(ZugferdConversionException e) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,9 +15,6 @@ server.servlet.session.cookie.name=PDFTOOL_SESSION
|
|||||||
# Wird beim Build durch die Version aus der pom.xml ersetzt (Resource-Filtering)
|
# Wird beim Build durch die Version aus der pom.xml ersetzt (Resource-Filtering)
|
||||||
pdftool.version=@project.version@
|
pdftool.version=@project.version@
|
||||||
|
|
||||||
spring.servlet.multipart.max-file-size=25MB
|
|
||||||
spring.servlet.multipart.max-request-size=30MB
|
|
||||||
|
|
||||||
# Auch normale (Nicht-Multipart-)Requests bis 30 MB zulassen: Canvas-Daten mit
|
# Auch normale (Nicht-Multipart-)Requests bis 30 MB zulassen: Canvas-Daten mit
|
||||||
# eingebetteten Bild-Elementen können das Tomcat-Standardlimit von 2 MB
|
# eingebetteten Bild-Elementen können das Tomcat-Standardlimit von 2 MB
|
||||||
# überschreiten; ein abgelehnter Request führt im Browser sonst zu einem
|
# überschreiten; ein abgelehnter Request führt im Browser sonst zu einem
|
||||||
|
|||||||
Reference in New Issue
Block a user