From 9d06e1830e0f4c891bfcc877f6f9b5e52f257e86 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Mon, 14 Sep 2026 09:59:02 +0800 Subject: [PATCH] feat(bx): add advance apply withdraw API Co-authored-by: Cursor --- backend/app/api/bx/advance.py | 17 +++++++ backend/app/services/bx/advance.py | 16 +++++++ backend/tests/test_bx_advance_withdraw.py | 56 +++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 backend/tests/test_bx_advance_withdraw.py diff --git a/backend/app/api/bx/advance.py b/backend/app/api/bx/advance.py index 1c9a58408..f42356f7c 100644 --- a/backend/app/api/bx/advance.py +++ b/backend/app/api/bx/advance.py @@ -613,6 +613,23 @@ def reject_advance( return ApiResponse(data=AdvanceApplyOut.model_validate(advance)) +@router.post("/{advance_id}/withdraw", response_model=ApiResponse[AdvanceApplyOut]) +def withdraw_advance( + advance_id: str, + db: Session = Depends(get_db), + user: BxUserContext = Depends(get_bx_current_user), +): + advance = AdvanceService.withdraw(db, advance_id, user) + if not advance: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="备用金申请不存在、非本人申请或当前状态不可撤回", + ) + db.commit() + db.refresh(advance) + return ApiResponse(data=AdvanceApplyOut.model_validate(advance)) + + @router.delete("/{advance_id}", response_model=ApiResponse[None]) def delete_advance( advance_id: str, diff --git a/backend/app/services/bx/advance.py b/backend/app/services/bx/advance.py index 66645655c..ec771c069 100644 --- a/backend/app/services/bx/advance.py +++ b/backend/app/services/bx/advance.py @@ -347,6 +347,22 @@ class AdvanceService: db.flush() return apply_obj + @staticmethod + def withdraw(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxAdvanceApply]: + tenant_no = AdvanceService._tenant_no(db, user) + apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user) + if not apply_obj: + return None + if apply_obj.applicant_id != user.id: + return None + if apply_obj.status != "pending": + return None + apply_obj.status = "draft" + apply_obj.current_node = None + apply_obj.update_by = user.id + db.flush() + return apply_obj + @staticmethod def _approval_record_dict(approval: BxAdvanceApproval, apply_id: str) -> Dict[str, Any]: return { diff --git a/backend/tests/test_bx_advance_withdraw.py b/backend/tests/test_bx_advance_withdraw.py new file mode 100644 index 000000000..e9ec6824e --- /dev/null +++ b/backend/tests/test_bx_advance_withdraw.py @@ -0,0 +1,56 @@ +"""备用金撤回:仅申请人且审批中可回草稿。""" +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from app.services.bx.advance import AdvanceService + + +def _user(uid="u1"): + return SimpleNamespace(id=uid, name="张三", dept_id="d1") + + +def _apply(**kwargs): + base = { + "id": "a1", + "applicant_id": "u1", + "status": "pending", + "current_node": "一级审批", + "update_by": None, + } + base.update(kwargs) + return SimpleNamespace(**base) + + +@patch.object(AdvanceService, "_tenant_no", return_value="T1") +@patch.object(AdvanceService, "get_by_id") +def test_withdraw_pending_by_applicant(mock_get, _tenant): + apply_obj = _apply() + mock_get.return_value = apply_obj + db = MagicMock() + out = AdvanceService.withdraw(db, "a1", _user("u1")) + assert out is apply_obj + assert apply_obj.status == "draft" + assert apply_obj.current_node is None + assert apply_obj.update_by == "u1" + db.flush.assert_called_once() + + +@patch.object(AdvanceService, "_tenant_no", return_value="T1") +@patch.object(AdvanceService, "get_by_id") +def test_withdraw_rejects_non_applicant(mock_get, _tenant): + mock_get.return_value = _apply(applicant_id="u1") + assert AdvanceService.withdraw(MagicMock(), "a1", _user("u2")) is None + + +@patch.object(AdvanceService, "_tenant_no", return_value="T1") +@patch.object(AdvanceService, "get_by_id") +def test_withdraw_rejects_non_pending(mock_get, _tenant): + mock_get.return_value = _apply(status="draft") + assert AdvanceService.withdraw(MagicMock(), "a1", _user("u1")) is None + + +@patch.object(AdvanceService, "_tenant_no", return_value="T1") +@patch.object(AdvanceService, "get_by_id") +def test_withdraw_missing_returns_none(mock_get, _tenant): + mock_get.return_value = None + assert AdvanceService.withdraw(MagicMock(), "a1", _user("u1")) is None