from __future__ import annotations import re import secrets from datetime import datetime from typing import Callable, Literal from sqlalchemy import select from sqlalchemy.orm import Session from app.models.financial_record import ExpenseClaim DocumentNumberKind = Literal["application", "reimbursement", "audit"] DOCUMENT_NUMBER_PREFIXES: dict[DocumentNumberKind, str] = { "application": "A", "reimbursement": "R", "audit": "D", } DOCUMENT_NUMBER_TOKEN_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" DOCUMENT_NUMBER_TOKEN_LENGTH = 8 DOCUMENT_NUMBER_SHORT_BODY = ( rf"(?:A|R|D)[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}" ) DOCUMENT_NUMBER_LEGACY_BODY = ( rf"(?:AP|RE|AD)-\d{{14}}-[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}" ) DOCUMENT_NUMBER_PATTERN = re.compile( rf"^(?:{DOCUMENT_NUMBER_SHORT_BODY}|{DOCUMENT_NUMBER_LEGACY_BODY})$", flags=re.IGNORECASE, ) DOCUMENT_NUMBER_EXTRACT_PATTERN = re.compile( rf"(? str: return "".join( secrets.choice(DOCUMENT_NUMBER_TOKEN_ALPHABET) for _ in range(DOCUMENT_NUMBER_TOKEN_LENGTH) ) def build_document_number( kind: DocumentNumberKind, *, timestamp: datetime | None = None, token: str | None = None, ) -> str: prefix = DOCUMENT_NUMBER_PREFIXES[kind] normalized_token = (token or generate_document_token()).strip().upper() if not re.fullmatch( rf"[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}", normalized_token, ): raise ValueError("document number token must be 8 chars from the configured alphabet") return f"{prefix}{normalized_token}" def generate_unique_expense_claim_no( db: Session, kind: DocumentNumberKind, *, timestamp: datetime | None = None, token_factory: Callable[[], str] = generate_document_token, max_attempts: int = 8, ) -> str: for _ in range(max_attempts): candidate = build_document_number( kind, timestamp=timestamp, token=token_factory(), ) exists = db.scalar( select(ExpenseClaim.id) .where(ExpenseClaim.claim_no == candidate) .limit(1) ) if exists is None: return candidate raise RuntimeError(f"failed to generate a unique {kind} document number") def is_application_claim_no(value: object) -> bool: normalized = str(value or "").strip().upper() return bool( re.fullmatch( rf"A[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}", normalized, ) ) or normalized.startswith(("AP-", "APP-"))