fix: restrict application linking for reimbursement drafts
This commit is contained in:
@@ -69,6 +69,36 @@ def build_claim(*, expense_type: str, location: str) -> ExpenseClaim:
|
||||
return claim
|
||||
|
||||
|
||||
def build_application_claim(
|
||||
*,
|
||||
id: str,
|
||||
claim_no: str,
|
||||
employee: Employee,
|
||||
status: str = "approved",
|
||||
amount: Decimal = Decimal("3000.00"),
|
||||
) -> ExpenseClaim:
|
||||
return ExpenseClaim(
|
||||
id=id,
|
||||
claim_no=claim_no,
|
||||
employee_id=employee.id,
|
||||
employee_name=employee.name,
|
||||
department_id=employee.organization_unit_id,
|
||||
department_name="Tech",
|
||||
project_code=None,
|
||||
expense_type="travel_application",
|
||||
reason="support deployment",
|
||||
location="Shanghai",
|
||||
amount=amount,
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 2, 20, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 2, 18, tzinfo=UTC),
|
||||
status=status,
|
||||
approval_stage=APPROVAL_DONE_STAGE if status in {"approved", "completed"} else "Pending",
|
||||
risk_flags_json=[],
|
||||
)
|
||||
|
||||
|
||||
def build_session() -> Session:
|
||||
engine = create_engine(
|
||||
"sqlite+pysqlite:///:memory:",
|
||||
@@ -322,6 +352,12 @@ def test_upsert_draft_from_ontology_persists_linked_application_context() -> Non
|
||||
email=user_id,
|
||||
)
|
||||
db.add(employee)
|
||||
db.flush()
|
||||
db.add(build_application_claim(
|
||||
id="application-linked-1",
|
||||
claim_no="AP-202605-001",
|
||||
employee=employee,
|
||||
))
|
||||
db.commit()
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
@@ -384,6 +420,12 @@ def test_upsert_linked_application_draft_without_receipts_has_no_placeholder_ite
|
||||
grade="P5",
|
||||
)
|
||||
db.add(employee)
|
||||
db.flush()
|
||||
db.add(build_application_claim(
|
||||
id="application-linked-no-receipt",
|
||||
claim_no="AP-202606-001",
|
||||
employee=employee,
|
||||
))
|
||||
db.commit()
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
@@ -474,6 +516,11 @@ def test_upsert_linked_application_draft_clears_existing_placeholder_item() -> N
|
||||
)
|
||||
db.add(employee)
|
||||
db.flush()
|
||||
db.add(build_application_claim(
|
||||
id="application-linked-existing-placeholder",
|
||||
claim_no="AP-202606-002",
|
||||
employee=employee,
|
||||
))
|
||||
existing_claim = ExpenseClaim(
|
||||
claim_no="RE-202606020001-PLACEHOLDER",
|
||||
employee_id=employee.id,
|
||||
@@ -550,6 +597,123 @@ def test_upsert_linked_application_draft_clears_existing_placeholder_item() -> N
|
||||
assert claim.items == []
|
||||
|
||||
|
||||
def test_upsert_linked_application_requires_approved_application() -> None:
|
||||
user_id = "linked-application-status-block@example.com"
|
||||
message = "save reimbursement draft from linked travel application"
|
||||
|
||||
with build_session() as db:
|
||||
employee = Employee(employee_no="E5108", name="Linked Employee", email=user_id)
|
||||
db.add(employee)
|
||||
db.flush()
|
||||
db.add(build_application_claim(
|
||||
id="application-returned-blocked",
|
||||
claim_no="AP-202606-STATUS",
|
||||
employee=employee,
|
||||
status="returned",
|
||||
))
|
||||
db.commit()
|
||||
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(query=message, user_id=user_id)
|
||||
)
|
||||
result = ExpenseClaimService(db).upsert_draft_from_ontology(
|
||||
run_id=ontology.run_id,
|
||||
user_id=user_id,
|
||||
message=message,
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"name": "Linked Employee",
|
||||
"user_input_text": message,
|
||||
"review_action": "save_draft",
|
||||
"review_form_values": {
|
||||
"expense_type": "travel",
|
||||
"application_claim_id": "application-returned-blocked",
|
||||
"application_claim_no": "AP-202606-STATUS",
|
||||
},
|
||||
"expense_scene_selection": {
|
||||
"expense_type": "travel",
|
||||
"application_claim_id": "application-returned-blocked",
|
||||
"application_claim_no": "AP-202606-STATUS",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert result["application_link_blocked"] is True
|
||||
assert result["application_claim_no"] == "AP-202606-STATUS"
|
||||
assert _count_claims(db) == 1
|
||||
|
||||
|
||||
def test_upsert_linked_application_rejects_duplicate_reimbursement_draft() -> None:
|
||||
user_id = "linked-application-duplicate-block@example.com"
|
||||
message = "save another reimbursement draft from linked travel application"
|
||||
|
||||
with build_session() as db:
|
||||
employee = Employee(employee_no="E5109", name="Linked Employee", email=user_id)
|
||||
db.add(employee)
|
||||
db.flush()
|
||||
db.add(build_application_claim(
|
||||
id="application-duplicate-blocked",
|
||||
claim_no="AP-202606-DUP",
|
||||
employee=employee,
|
||||
))
|
||||
existing_claim = ExpenseClaim(
|
||||
claim_no="RE-202606-DUP-DRAFT",
|
||||
employee_id=employee.id,
|
||||
employee_name=employee.name,
|
||||
department_name="Tech",
|
||||
project_code=None,
|
||||
expense_type="travel",
|
||||
reason="support deployment",
|
||||
location="Shanghai",
|
||||
amount=Decimal("0.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 2, 20, tzinfo=UTC),
|
||||
status="draft",
|
||||
approval_stage="Pending",
|
||||
risk_flags_json=[
|
||||
{
|
||||
"source": "application_link",
|
||||
"application_claim_id": "application-duplicate-blocked",
|
||||
"application_claim_no": "AP-202606-DUP",
|
||||
}
|
||||
],
|
||||
)
|
||||
db.add(existing_claim)
|
||||
db.commit()
|
||||
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(query=message, user_id=user_id)
|
||||
)
|
||||
result = ExpenseClaimService(db).upsert_draft_from_ontology(
|
||||
run_id=ontology.run_id,
|
||||
user_id=user_id,
|
||||
message=message,
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"name": "Linked Employee",
|
||||
"user_input_text": message,
|
||||
"review_action": "save_draft",
|
||||
"review_form_values": {
|
||||
"expense_type": "travel",
|
||||
"application_claim_id": "application-duplicate-blocked",
|
||||
"application_claim_no": "AP-202606-DUP",
|
||||
},
|
||||
"expense_scene_selection": {
|
||||
"expense_type": "travel",
|
||||
"application_claim_id": "application-duplicate-blocked",
|
||||
"application_claim_no": "AP-202606-DUP",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert result["application_link_blocked"] is True
|
||||
assert result["existing_claim_no"] == "RE-202606-DUP-DRAFT"
|
||||
assert _count_claims(db) == 2
|
||||
|
||||
|
||||
def test_sync_travel_allowance_uses_linked_application_range_days() -> None:
|
||||
with build_session() as db:
|
||||
employee = Employee(
|
||||
|
||||
Reference in New Issue
Block a user