"""报销支付按未结清余额分次冲抵、分次发放,两次操作互不影响。""" from datetime import date, datetime import pytest from fastapi import HTTPException from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.core.bx_auth import BxUserContext from app.models.bx_models import BxAdvanceApply, BxReimburseApply, BxReimburseOffset, BxReimbursePayment from app.services.bx.reimburse import ReimburseService TENANT = "shanxia2.0" NOW = datetime(2026, 11, 5, 10, 0, 0) def _session(): engine = create_engine("sqlite://") for model in (BxReimburseApply, BxReimburseOffset, BxReimbursePayment, BxAdvanceApply): model.__table__.create(engine) return sessionmaker(bind=engine)() def _user() -> BxUserContext: return BxUserContext(id="finance-1", name="财务", dept_id="dept", role="super_admin", tenant_no=TENANT) def _apply(amount: float = 50000) -> BxReimburseApply: return BxReimburseApply( id="BX", apply_code="BX", title="报销BX", reimburse_date=date(2026, 11, 5), applicant_id="applicant", applicant_name="汪璇", expense_id="58", expense_name="车辆用品费", amount=amount, status="approved", tenant_no=TENANT, create_dept_id="dept", create_time=NOW, update_time=NOW, del_flag=0, ) def _advance(advance_id: str, amount: float) -> BxAdvanceApply: return BxAdvanceApply( id=advance_id, apply_code=advance_id, applicant_id="applicant", applicant_name="汪璇", reason="备用金", amount=amount, status="issued", issue_status="issued", actual_amount=amount, tenant_no=TENANT, create_dept_id="dept", create_time=NOW, update_time=NOW, del_flag=0, ) def test_offset_and_pay_are_separate_and_leave_balance_open(): db = _session() db.add(_apply()) db.add_all([_advance("ADV-A", 20000), _advance("ADV-B", 15000)]) db.commit() ReimburseService.batch_offset( db, "BX", [{"advance_id": "ADV-A", "offset_amount": 20000}], 0, _user() ) row = db.query(BxReimburseApply).one() assert row.status == "approved" assert row.pay_status is None assert row.pay_time is None assert float(row.offset_amount) == 20000 assert row.actual_amount is None ReimburseService.pay(db, "BX", _user(), actual_amount=10000) db.refresh(row) assert row.status == "approved" assert row.pay_status is None assert float(row.actual_amount) == 10000 assert float(row.offset_amount) == 20000 assert db.query(BxReimbursePayment).count() == 1 ReimburseService.batch_offset( db, "BX", [{"advance_id": "ADV-B", "offset_amount": 10000}], 0, _user() ) db.refresh(row) assert row.status == "approved" assert float(row.offset_amount) == 30000 assert db.query(BxReimburseOffset).count() == 2 ReimburseService.pay(db, "BX", _user(), actual_amount=10000) db.refresh(row) assert row.status == "processed" assert row.pay_status == "paid" assert float(row.actual_amount) == 20000 assert db.query(BxReimbursePayment).count() == 2 ReimburseService.pay(db, "BX", _user(), actual_amount=1) assert db.query(BxReimbursePayment).count() == 2 def test_same_request_records_offset_and_pay_without_taking_the_remainder(): db = _session() db.add(_apply()) db.add(_advance("ADV-A", 20000)) db.commit() ReimburseService.batch_offset( db, "BX", [{"advance_id": "ADV-A", "offset_amount": 20000}], 10000, _user() ) row = db.query(BxReimburseApply).one() assert row.status == "approved" assert float(row.offset_amount) == 20000 assert float(row.actual_amount) == 10000 assert db.query(BxReimburseOffset).count() == 1 assert db.query(BxReimbursePayment).count() == 1 def test_rejects_amount_above_balance_or_advance_remaining(): db = _session() db.add(_apply()) db.add(_advance("ADV-A", 20000)) db.commit() with pytest.raises(HTTPException) as pay_exc: ReimburseService.pay(db, "BX", _user(), actual_amount=50001) assert pay_exc.value.status_code == 400 with pytest.raises(HTTPException) as offset_exc: ReimburseService.batch_offset( db, "BX", [{"advance_id": "ADV-A", "offset_amount": 20001}], 0, _user() ) assert offset_exc.value.status_code == 400 row = db.query(BxReimburseApply).one() assert row.status == "approved" assert row.offset_amount is None assert db.query(BxReimbursePayment).count() == 0