Files
TimeTracker/app/integrations/peppol_identifiers.py
T
Dries Peeters b4486a627f fix: CI tests, code quality, and duplicate DB indexes
- Webhook models: remove duplicate index definitions so db.create_all()
  no longer raises 'index already exists' (columns already have index=True)
- ImportService: fix circular import by late-importing ClientService,
  ProjectService, TimeTrackingService in __init__
- reports: fix F823 by renaming unpack variable _ to _entry_count to avoid
  shadowing gettext _ in export_task_excel()
- Code quality: add .flake8 with extend-ignore so flake8 CI passes;
  simplify pyproject.toml isort config (drop unsupported options)
- Format: run black and isort on app/
- tests: restore minimal app fixture in test_import_export_models
2026-03-15 10:51:52 +01:00

244 lines
5.4 KiB
Python

"""
PEPPOL participant identifier validation (scheme + endpoint ID).
Validates and normalizes sender/recipient identifiers before submission
to avoid malformed IDs and improve validator compliance.
"""
from __future__ import annotations
import re
from typing import Optional, Tuple
# Common PEPPOL participant identifier schemes (ISO 6523)
# See PEPPOL IC and country-specific scheme lists
KNOWN_SCHEMES = frozenset(
{
"0007",
"0088",
"0060",
"0130",
"0184",
"0190",
"0191",
"0192",
"0193",
"0195",
"0196",
"0198",
"0199",
"0200",
"0201",
"0202",
"0204",
"0208",
"0209",
"0210",
"0211",
"0212",
"0213",
"0215",
"0216",
"0218",
"0219",
"0220",
"0221",
"0222",
"0223",
"0224",
"0225",
"0226",
"0227",
"0228",
"0229",
"0230",
"0231",
"0232",
"0233",
"0234",
"0235",
"0236",
"0237",
"0238",
"0239",
"0240",
"0241",
"0242",
"0243",
"0244",
"0245",
"0246",
"0247",
"0248",
"0249",
"0250",
"0251",
"0252",
"0253",
"0254",
"0255",
"0256",
"0257",
"0258",
"0259",
"0260",
"0261",
"0262",
"0263",
"0264",
"0265",
"0266",
"0267",
"0268",
"0269",
"0270",
"0271",
"0272",
"0273",
"0274",
"0275",
"0276",
"0277",
"0278",
"0279",
"0280",
"0281",
"0282",
"0283",
"0284",
"0285",
"0286",
"0287",
"0288",
"0289",
"0290",
"0291",
"0292",
"0293",
"0294",
"0295",
"0296",
"0297",
"0298",
"0299",
"0300",
"9915",
"9925",
"9933",
"9944",
"9950",
"9952",
"9954",
"9955",
"9956",
"9957",
"9958",
"9959",
"9960",
"9961",
"9962",
"9963",
"9964",
"9965",
"9966",
"9967",
"9968",
"9969",
"9970",
"9971",
"9972",
"9973",
"9974",
"9975",
"9976",
"9977",
"9978",
"9979",
"9980",
"9981",
"9982",
"9983",
"9984",
"9985",
"9986",
"9987",
"9988",
"9989",
"9990",
"9991",
"9992",
"9993",
"9994",
"9995",
"9996",
"9997",
"9998",
"9999",
}
)
# Endpoint ID: alphanumeric, some schemes allow colon/dash (e.g. 0088:1234567890123)
_ENDPOINT_ID_PATTERN = re.compile(r"^[A-Za-z0-9_\-.:]+$")
class PeppolIdentifierError(ValueError):
"""Raised when a PEPPOL participant identifier is invalid."""
def __init__(self, message: str, field: Optional[str] = None):
super().__init__(message)
self.field = field
def validate_scheme_id(scheme_id: Optional[str], field: str = "scheme_id") -> str:
"""
Validate and return normalized scheme ID.
Raises PeppolIdentifierError if invalid.
"""
if not scheme_id or not str(scheme_id).strip():
raise PeppolIdentifierError("Participant scheme ID is required", field=field)
s = str(scheme_id).strip()
if not s.isdigit() and s not in KNOWN_SCHEMES:
# Allow unknown numeric schemes (4 digits typical)
if not (len(s) <= 10 and all(c.isdigit() for c in s)):
raise PeppolIdentifierError(
f"Invalid participant scheme ID: must be numeric or known scheme (e.g. 0088, 9915)",
field=field,
)
return s
def validate_endpoint_id(endpoint_id: Optional[str], field: str = "endpoint_id") -> str:
"""
Validate and return normalized endpoint ID.
Raises PeppolIdentifierError if invalid.
"""
if not endpoint_id or not str(endpoint_id).strip():
raise PeppolIdentifierError("Participant endpoint ID is required", field=field)
e = str(endpoint_id).strip()
if len(e) > 200:
raise PeppolIdentifierError("Endpoint ID must be at most 200 characters", field=field)
if not _ENDPOINT_ID_PATTERN.match(e):
raise PeppolIdentifierError(
"Endpoint ID may only contain letters, digits, and _ - . :",
field=field,
)
return e
def validate_participant_identifiers(
sender_endpoint_id: str,
sender_scheme_id: str,
recipient_endpoint_id: str,
recipient_scheme_id: str,
) -> Tuple[Tuple[str, str], Tuple[str, str]]:
"""
Validate sender and recipient identifiers.
Returns ((sender_endpoint_id, sender_scheme_id), (recipient_endpoint_id, recipient_scheme_id)).
Raises PeppolIdentifierError if any identifier is invalid.
"""
s_ep = validate_endpoint_id(sender_endpoint_id, "sender_endpoint_id")
s_sch = validate_scheme_id(sender_scheme_id, "sender_scheme_id")
r_ep = validate_endpoint_id(recipient_endpoint_id, "recipient_endpoint_id")
r_sch = validate_scheme_id(recipient_scheme_id, "recipient_scheme_id")
return (s_ep, s_sch), (r_ep, r_sch)