from __future__ import annotations from datetime import UTC, datetime from decimal import Decimal import pytest from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import StaticPool from app.db.base import Base from app.models.financial_record import ExpenseClaim from app.services.document_numbering import ( build_document_number, generate_unique_expense_claim_no, is_application_claim_no, ) def build_session() -> Session: engine = create_engine( "sqlite+pysqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) Base.metadata.create_all(bind=engine) factory = sessionmaker(bind=engine, autoflush=False, autocommit=False) return factory() def test_build_document_number_uses_kind_prefix_timestamp_and_token() -> None: timestamp = datetime(2026, 5, 25, 10, 30, 45, tzinfo=UTC) assert ( build_document_number("application", timestamp=timestamp, token="ABCDEFGH") == "AP-20260525103045-ABCDEFGH" ) assert ( build_document_number("reimbursement", timestamp=timestamp, token="ABCDEFGH") == "RE-20260525103045-ABCDEFGH" ) assert ( build_document_number("audit", timestamp=timestamp, token="ABCDEFGH") == "AD-20260525103045-ABCDEFGH" ) def test_build_document_number_rejects_ambiguous_token_chars() -> None: timestamp = datetime(2026, 5, 25, 10, 30, 45, tzinfo=UTC) with pytest.raises(ValueError): build_document_number("application", timestamp=timestamp, token="ABCDEF10") def test_generate_unique_expense_claim_no_retries_existing_candidate() -> None: timestamp = datetime(2026, 5, 25, 10, 30, 45, tzinfo=UTC) with build_session() as db: db.add( ExpenseClaim( claim_no="RE-20260525103045-ABCDEFGH", employee_name="张三", department_name="市场部", project_code=None, expense_type="transport", reason="交通报销", location="深圳", amount=Decimal("10.00"), currency="CNY", invoice_count=1, occurred_at=timestamp, status="draft", approval_stage="待提交", risk_flags_json=[], ) ) db.commit() tokens = iter(["ABCDEFGH", "HGFEDCBA"]) assert ( generate_unique_expense_claim_no( db, "reimbursement", timestamp=timestamp, token_factory=lambda: next(tokens), ) == "RE-20260525103045-HGFEDCBA" ) def test_is_application_claim_no_supports_new_and_legacy_prefixes() -> None: assert is_application_claim_no("AP-20260525103045-ABCDEFGH") assert is_application_claim_no("APP-20260525-ABC123") assert not is_application_claim_no("RE-20260525103045-ABCDEFGH")