| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- from datetime import datetime
- from decimal import Decimal
- import pytest
- from sqlalchemy import create_engine
- from sqlalchemy.orm import Query, sessionmaker
- from app.crud.protocol_sample_payment import (
- replace_schedules,
- save_sample_payments_for_protocol,
- sync_schedules_and_transfers,
- )
- from app.models.base import Base
- from app.models.models import (
- BizProtocolPaymentSchedule,
- BizProtocolPaymentTransfer,
- BizProtocolSamplePayment,
- )
- def _session():
- engine = create_engine("sqlite:///:memory:")
- Base.metadata.create_all(
- engine,
- tables=[
- BizProtocolSamplePayment.__table__,
- BizProtocolPaymentSchedule.__table__,
- BizProtocolPaymentTransfer.__table__,
- ],
- )
- return sessionmaker(bind=engine)()
- def _payment(db):
- row = BizProtocolSamplePayment(
- id="pay-1",
- collection_protocol_id="protocol-1",
- sample_code="S1",
- pay_mode="fq",
- contract_amount=Decimal("100"),
- tenant_no="tenant-a",
- create_dept_id="",
- create_time=datetime(2026, 10, 14),
- update_time=datetime(2026, 10, 14),
- del_flag=0,
- )
- db.add(row)
- db.commit()
- return row
- def _schedule(db, *, schedule_no=1, status="cleared"):
- row = BizProtocolPaymentSchedule(
- id=f"schedule-{schedule_no}",
- sample_payment_id="pay-1",
- collection_protocol_id="protocol-1",
- sample_code="S1",
- schedule_no=schedule_no,
- due_amount=Decimal("100"),
- due_date="20261031",
- schedule_status=status,
- tenant_no="tenant-a",
- create_dept_id="",
- create_time=datetime(2026, 10, 14),
- update_time=datetime(2026, 10, 14),
- del_flag=0,
- )
- db.add(row)
- db.commit()
- return row
- def _transfer(db, *, schedule_no=1, status="yjf"):
- row = BizProtocolPaymentTransfer(
- id=f"transfer-{schedule_no}",
- schedule_id=f"schedule-{schedule_no}",
- sample_payment_id="pay-1",
- collection_protocol_id="protocol-1",
- sample_code="S1",
- transfer_amount=Decimal("100"),
- transfer_type="bank",
- pay_status=status,
- tenant_no="tenant-a",
- create_dept_id="",
- create_time=datetime(2026, 10, 14),
- update_time=datetime(2026, 10, 14),
- del_flag=0,
- )
- db.add(row)
- db.commit()
- return row
- def test_existing_schedule_status_is_not_overwritten_by_payload():
- db = _session()
- _payment(db)
- _schedule(db, status="cleared")
- rows = sync_schedules_and_transfers(
- db,
- sample_payment_id="pay-1",
- tenant_no="tenant-a",
- user_id="u1",
- schedules=[
- {
- "scheduleNo": 1,
- "dueAmount": 100,
- "dueDate": "20261130",
- "scheduleStatus": "void",
- }
- ],
- )
- assert rows[0].schedule_status == "cleared"
- @pytest.mark.parametrize("status", [None, "", " "])
- def test_non_open_blank_schedule_status_is_preserved(status):
- db = _session()
- _payment(db)
- _schedule(db, status=status)
- rows = sync_schedules_and_transfers(
- db,
- sample_payment_id="pay-1",
- tenant_no="tenant-a",
- user_id="u1",
- schedules=[{"scheduleNo": 1, "dueAmount": 100}],
- )
- assert rows[0].schedule_status == status
- def test_new_schedule_ignores_payload_status_and_defaults_open():
- db = _session()
- _payment(db)
- rows = sync_schedules_and_transfers(
- db,
- sample_payment_id="pay-1",
- tenant_no="tenant-a",
- user_id="u1",
- schedules=[
- {
- "scheduleNo": 1,
- "dueAmount": 100,
- "dueDate": "20261031",
- "scheduleStatus": "cleared",
- }
- ],
- )
- assert rows[0].schedule_status == "open"
- def test_structure_change_rejects_renumbering_locked_schedule():
- db = _session()
- _payment(db)
- _schedule(db, schedule_no=1, status="cleared")
- with pytest.raises(ValueError, match="锁定批次"):
- sync_schedules_and_transfers(
- db,
- sample_payment_id="pay-1",
- tenant_no="tenant-a",
- user_id="u1",
- schedules=[
- {
- "scheduleNo": 2,
- "dueAmount": 100,
- "dueDate": "20261130",
- }
- ],
- )
- def test_locked_schedule_cannot_be_omitted():
- db = _session()
- _payment(db)
- _schedule(db, status="cleared")
- with pytest.raises(ValueError, match="锁定批次"):
- sync_schedules_and_transfers(
- db,
- sample_payment_id="pay-1",
- tenant_no="tenant-a",
- user_id="u1",
- schedules=[],
- )
- db.expire_all()
- assert db.get(BizProtocolPaymentSchedule, "schedule-1").del_flag == 0
- @pytest.mark.parametrize("pay_status", ["jfqrz", "yjf"])
- @pytest.mark.parametrize("payload", [[], [{"scheduleNo": 2, "dueAmount": 100}]])
- def test_locked_transfer_schedule_cannot_be_omitted_or_renumbered(pay_status, payload):
- db = _session()
- _payment(db)
- _schedule(db, status="open")
- _transfer(db, status=pay_status)
- with pytest.raises(ValueError, match="锁定批次"):
- sync_schedules_and_transfers(
- db,
- sample_payment_id="pay-1",
- tenant_no="tenant-a",
- user_id="u1",
- schedules=payload,
- )
- db.expire_all()
- transfer = db.get(BizProtocolPaymentTransfer, "transfer-1")
- assert transfer.schedule_id == "schedule-1"
- assert transfer.pay_status == pay_status
- assert transfer.del_flag == 0
- def test_replace_schedules_never_falls_back_locked_transfer_to_first_new_period():
- db = _session()
- _payment(db)
- _schedule(db, status="open")
- _transfer(db, status="yjf")
- with pytest.raises(ValueError, match="锁定批次"):
- replace_schedules(
- db,
- sample_payment_id="pay-1",
- tenant_no="tenant-a",
- user_id="u1",
- schedules=[{"scheduleNo": 2, "dueAmount": 100}],
- )
- db.expire_all()
- assert db.get(BizProtocolPaymentTransfer, "transfer-1").schedule_id == "schedule-1"
- def test_locked_transfer_id_cannot_be_submitted_under_another_schedule():
- db = _session()
- _payment(db)
- _schedule(db, schedule_no=1, status="open")
- _schedule(db, schedule_no=2, status="open")
- _transfer(db, schedule_no=1, status="yjf")
- with pytest.raises(ValueError, match="转账笔.*批次"):
- sync_schedules_and_transfers(
- db,
- sample_payment_id="pay-1",
- tenant_no="tenant-a",
- user_id="u1",
- schedules=[
- {"scheduleNo": 1, "dueAmount": 50, "transfers": []},
- {
- "scheduleNo": 2,
- "dueAmount": 50,
- "transfers": [{"id": "transfer-1", "transferAmount": 999}],
- },
- ],
- )
- db.expire_all()
- transfer = db.get(BizProtocolPaymentTransfer, "transfer-1")
- assert transfer.schedule_id == "schedule-1"
- assert transfer.transfer_amount == Decimal("100")
- def test_locked_sample_payment_cannot_be_omitted_from_protocol_save():
- db = _session()
- _payment(db)
- _schedule(db, status="cleared")
- with pytest.raises(ValueError, match="锁定缴费方案"):
- save_sample_payments_for_protocol(
- db,
- collection_protocol_id="protocol-1",
- tenant_no="tenant-a",
- user_id="u1",
- sample_payments=[],
- )
- db.expire_all()
- assert db.get(BizProtocolSamplePayment, "pay-1").del_flag == 0
- assert db.get(BizProtocolPaymentSchedule, "schedule-1").del_flag == 0
- def test_save_sample_payments_commit_false_defers_commit(monkeypatch):
- """commit=False 时成功路径不得自行 db.commit。"""
- db = _session()
- _payment(db)
- _schedule(db, status="open")
- _transfer(db, status="dfj")
- commits: list = []
- real_commit = db.commit
- def tracking_commit(*args, **kwargs):
- commits.append(1)
- return real_commit(*args, **kwargs)
- monkeypatch.setattr(db, "commit", tracking_commit)
- save_sample_payments_for_protocol(
- db,
- collection_protocol_id="protocol-1",
- tenant_no="tenant-a",
- user_id="u1",
- sample_payments=[
- {
- "id": "pay-1",
- "sampleCode": "S1",
- "payMode": "fq",
- "contractAmount": 100,
- "schedules": [
- {"scheduleNo": 1, "dueAmount": 100, "dueDate": "20261031"}
- ],
- }
- ],
- commit=False,
- )
- assert commits == []
- db.expire_all()
- assert db.get(BizProtocolSamplePayment, "pay-1").update_by == "u1"
- def test_all_draft_structure_can_still_be_rebuilt():
- db = _session()
- _payment(db)
- _schedule(db, status="open")
- _transfer(db, status="dfj")
- rows = sync_schedules_and_transfers(
- db,
- sample_payment_id="pay-1",
- tenant_no="tenant-a",
- user_id="u1",
- schedules=[{"scheduleNo": 2, "dueAmount": 100}],
- )
- assert [row.schedule_no for row in rows] == [2]
- db.expire_all()
- assert db.get(BizProtocolPaymentSchedule, "schedule-1").del_flag == 1
- assert db.get(BizProtocolPaymentTransfer, "transfer-1").del_flag == 1
- def test_schedule_sync_locks_payment_schedule_and_transfer_rows(monkeypatch):
- db = _session()
- _payment(db)
- _schedule(db, status="open")
- _transfer(db, status="dfj")
- calls = []
- original = Query.with_for_update
- def tracking_with_for_update(query, *args, **kwargs):
- calls.append(query)
- return original(query, *args, **kwargs)
- monkeypatch.setattr(Query, "with_for_update", tracking_with_for_update)
- sync_schedules_and_transfers(
- db,
- sample_payment_id="pay-1",
- tenant_no="tenant-a",
- user_id="u1",
- schedules=[{"scheduleNo": 1, "dueAmount": 100}],
- )
- assert len(calls) >= 3
|