95 lines
2.9 KiB
Python
95 lines
2.9 KiB
Python
|
|
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")
|