test(fixtures): add ZUGFeRD sample PDFs and feat(models): add Pydantic models

- Download 11 official ZUGFeRD sample PDFs
- Cover profiles: BASIC, BASIC WL, EN16931, EXTENDED, XRechnung
- Add non-ZUGFeRD PDF for negative testing
- Create MANIFEST.md documenting all samples
- Implement all Pydantic models from spec
- Add 28 TDD tests for models
- All tests pass
This commit is contained in:
m3tm3re
2026-02-04 19:26:01 +01:00
parent 0db2482bf2
commit 29bd8453ec
16 changed files with 805 additions and 3 deletions

View File

@@ -42,3 +42,77 @@ Initial session for ZUGFeRD-Service implementation.
- Module-level docstrings: minimal, one line, describe purpose
- Entry point function docstrings: Args/Returns style for CLI documentation
- Both necessary for scaffolding clarity
## [2026-02-04T19:23:00.000Z] Task 2: Download ZUGFeRD Sample PDFs
### Sample PDF Sources
- **Best source**: Mustang project (https://github.com/ZUGFeRD/mustangproject)
- Contains 20+ authentic ZUGFeRD samples across multiple directories
- Library test resources: `library/src/test/resources/` (15 PDFs)
- Validator test resources: `validator/src/test/resources/` (14 PDFs)
- CLI test resources: `Mustang-CLI/src/test/resources/` (2 PDFs)
- **FeRD official site**: https://www.ferd-net.de/download/testrechnungen
- Returns 404 - URL may have moved
- Mustang project likely mirrors these samples
- **factur-x library tests**: https://github.com/akretion/factur-x/tree/master/tests
- No PDF files found in repository (only code tests)
### ZUGFeRD Profile Coverage
- **Available samples**: BASIC, BASIC WL, EN16931, EXTENDED, XRechnung
- **Missing**: MINIMUM profile (future addition needed)
- **Versions covered**: ZUGFeRD 1.0, 2.0, 2.1, XRechnung
- **Related formats**: ORDER-X (for orders, not invoices)
### Negative Testing
- `EmptyPDFA1.pdf`: Valid PDF/A-1 with no ZUGFeRD XML data
- Useful for testing error handling and graceful degradation
### PDF Verification Pattern
- When `file` command unavailable, verify PDF magic bytes
- Magic bytes: `25 50 44 46` (hex) = "%PDF" (ASCII)
- Command: `head -c 4 "$f" | od -A n -t x1`
- All valid PDFs start with these 4 bytes
### Sample Selection Strategy
- Prioritize coverage: multiple profiles, versions, edge cases
- Keep focused: 8-10 samples max (11 selected with good variety)
- Include historical samples for backward compatibility testing
- Document thoroughly: MANIFEST.md with profile, description, source
### File Naming Conventions
- Mustang uses descriptive names: `EN16931_1_Teilrechnung.pdf`
- Include profile and feature description in filename
- Date-based names for temporal versions: `MustangBeispiel20221026.pdf`
- Test prefixes: `ZTESTZUGFERD_1_...` for ZUGFeRD v1 test samples
## [2026-02-04T19:45:00.000Z] Task 3: Pydantic Models
### Pydantic v2+ Syntax Patterns
- Use `type | None = None` for optional fields (not `Optional[type]`)
- Use `Field(description=...)` for field documentation (appears in OpenAPI docs)
- Use `Field(default_factory=list)` for list defaults to avoid mutable default issues
- Use `Field(default=None)` for None defaults on optional fields
- Model docstrings serve as public API documentation for FastAPI's OpenAPI schema
### JSON Serialization
- Use `model.model_dump_json()` to serialize to JSON string
- Use `model.model_validate_json(json_str)` to deserialize from JSON
- Pydantic handles datetime, nested models, and type conversion automatically
### Test-First Development Pattern
- Write tests before implementing models (RED-GREEN-REFACTOR)
- Tests should cover: minimal data, full data, edge cases
- Test JSON roundtrip: `model.model_dump_json()``Model.model_validate_json()`
- Verify imports: `python -c "from src.models import ModelName"`
### Nested Models with Dict Input
- Pydantic v2 accepts dict for nested models: `supplier={"name": "ACME"}`
- Use for test convenience and API requests
- Internally converts to proper model instances
### Field Required vs Optional
- Required fields: No default value in Field
- Optional fields: `type | None = Field(default=None, ...)`
- Empty list defaults: `list[Type] = Field(default_factory=list)`