| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855 |
- from datetime import datetime
- from decimal import Decimal
- import pytest
- from sqlalchemy import create_engine, update
- from sqlalchemy.orm import Query, sessionmaker
- from app.api.collection_protocol import _persist_protocol_sub_resources
- from app.crud.collection_protocol import save_payment_details, save_payment_with_details
- from app.models.base import Base
- from app.models.models import BizProtocolPayment, BizProtocolPaymentDetail
- def _session(*, allow_null_pay_status=False):
- engine = create_engine("sqlite:///:memory:")
- pay_status_column = BizProtocolPaymentDetail.__table__.c.pay_status
- original_nullable = pay_status_column.nullable
- try:
- if allow_null_pay_status:
- pay_status_column.nullable = True
- Base.metadata.create_all(
- engine,
- tables=[BizProtocolPayment.__table__, BizProtocolPaymentDetail.__table__],
- )
- finally:
- pay_status_column.nullable = original_nullable
- return sessionmaker(bind=engine)()
- def _payment(
- db,
- *,
- payment_id="payment-a",
- tenant_no="tenant-a",
- pay_mode="dj",
- contract_amount="1000",
- installment_count=None,
- ):
- row = BizProtocolPayment(
- id=payment_id,
- collection_protocol_id="protocol-a",
- pay_mode=pay_mode,
- contract_amount=Decimal(contract_amount),
- installment_count=installment_count,
- installment_ratios="[1000, 1000]" if installment_count == 2 else None,
- tenant_no=tenant_no,
- 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 _detail(
- db,
- *,
- detail_id="detail-a",
- payment_id="payment-a",
- tenant_no="tenant-a",
- pay_status="ytf",
- due_amount="1000",
- installment_no=None,
- installment_due_date=None,
- ):
- row = BizProtocolPaymentDetail(
- id=detail_id,
- payment_id=payment_id,
- transfer_type="bank",
- due_amount=Decimal(due_amount),
- transfer_amount=Decimal(due_amount),
- transfer_time=datetime(2026, 10, 1, 10, 0, 0),
- transfer_attachments="receipt.pdf",
- finance_confirm_attachments="finance.pdf",
- finance_remark="confirmed",
- refund_finance_confirm_attachments="refund-confirm.pdf",
- refund_finance_remark="refund confirmed",
- pay_status=pay_status,
- installment_no=installment_no,
- installment_due_date=installment_due_date,
- refund_amount=Decimal("100"),
- refund_remark="refund requested",
- refund_attachments="refund.pdf",
- tenant_no=tenant_no,
- create_dept_id="",
- create_time=datetime(2026, 10, 14),
- update_time=datetime(2026, 10, 14),
- del_flag=0,
- )
- db.add(row)
- db.commit()
- if pay_status is None:
- db.execute(
- update(BizProtocolPaymentDetail)
- .where(BizProtocolPaymentDetail.id == detail_id)
- .values(pay_status=None)
- )
- db.commit()
- db.refresh(row)
- return row
- @pytest.mark.parametrize(
- "pay_status", ["jfqrz", "yjf", "tfqrz", "ytf", "future_locked", None, "", " ", " dfj "]
- )
- def test_locked_detail_is_preserved_when_omitted(pay_status):
- db = _session(allow_null_pay_status=pay_status is None)
- _payment(db)
- _detail(db, pay_status=pay_status)
- rows = save_payment_details(
- db,
- payment_id="payment-a",
- tenant_no="tenant-a",
- user_id="market-user",
- details=[],
- )
- assert len(rows) == 1
- row = rows[0]
- assert row.id == "detail-a"
- assert row.del_flag == 0
- assert row.pay_status == pay_status
- @pytest.mark.parametrize(
- "pay_status", ["jfqrz", "yjf", "tfqrz", "ytf", "future_locked", None, "", " ", " dfj "]
- )
- def test_locked_detail_is_immutable_when_submitted_by_id(pay_status):
- db = _session(allow_null_pay_status=pay_status is None)
- _payment(db)
- _detail(db, pay_status=pay_status)
- rows = save_payment_details(
- db,
- payment_id="payment-a",
- tenant_no="tenant-a",
- user_id="market-user",
- details=[
- {
- "id": "detail-a",
- "transferType": "cash",
- "dueAmount": 1,
- "transferAmount": 2,
- "transferTime": "2026-12-31 23:59:59",
- "installmentNo": 9,
- "installmentDueDate": "20261231",
- "remark": "tampered",
- "attachments": [{"fileUrl": "tampered.pdf"}],
- }
- ],
- )
- assert len(rows) == 1
- row = rows[0]
- assert row.id == "detail-a"
- assert row.pay_status == pay_status
- assert row.transfer_type == "bank"
- assert row.due_amount == Decimal("1000")
- assert row.transfer_amount == Decimal("1000")
- assert row.transfer_time == datetime(2026, 10, 1, 10, 0, 0)
- assert row.installment_no is None
- assert row.installment_due_date is None
- assert row.remark is None
- assert row.transfer_attachments == "receipt.pdf"
- assert row.finance_confirm_attachments == "finance.pdf"
- assert row.finance_remark == "confirmed"
- assert row.refund_amount == Decimal("100")
- assert row.refund_remark == "refund requested"
- assert row.refund_attachments == "refund.pdf"
- assert row.refund_finance_confirm_attachments == "refund-confirm.pdf"
- assert row.refund_finance_remark == "refund confirmed"
- def test_mixed_save_updates_and_deletes_only_drafts_and_creates_safe_row():
- db = _session()
- _payment(db)
- _detail(db, detail_id="locked", pay_status="yjf")
- _detail(db, detail_id="draft-update", pay_status="dfj", due_amount="200")
- _detail(db, detail_id="draft-delete", pay_status="dfj", due_amount="300")
- rows = save_payment_details(
- db,
- payment_id="payment-a",
- tenant_no="tenant-a",
- user_id="market-user",
- details=[
- {
- "id": "locked",
- "dueAmount": 1,
- "transferAmount": 1,
- "remark": "tampered",
- },
- {
- "id": "draft-update",
- "transferType": "cash",
- "dueAmount": 250,
- "transferAmount": 150,
- "transferTime": "2026-10-20 12:00:00",
- "remark": "updated draft",
- "attachments": [{"fileUrl": "draft-new.pdf"}],
- },
- {
- "dueAmount": 500,
- "transferAmount": 0,
- "remark": "new draft",
- },
- ],
- )
- by_id = {row.id: row for row in rows}
- assert by_id["locked"].due_amount == Decimal("1000")
- assert by_id["locked"].remark is None
- assert by_id["draft-update"].due_amount == Decimal("250")
- assert by_id["draft-update"].transfer_amount == Decimal("150")
- assert by_id["draft-update"].remark == "updated draft"
- new_rows = [row for row in rows if row.id not in {"locked", "draft-update"}]
- assert len(new_rows) == 1
- assert new_rows[0].pay_status == "dfj"
- deleted = db.query(BizProtocolPaymentDetail).filter_by(id="draft-delete").one()
- assert deleted.del_flag == 1
- def test_foreign_or_unknown_detail_id_is_rejected_without_mutation():
- db = _session()
- _payment(db)
- _detail(db)
- _payment(db, payment_id="payment-b", tenant_no="tenant-b")
- _detail(db, detail_id="detail-b", payment_id="payment-b", tenant_no="tenant-b")
- with pytest.raises(ValueError, match="缴费明细不存在或不属于当前缴费方案"):
- save_payment_details(
- db,
- payment_id="payment-a",
- tenant_no="tenant-a",
- user_id="market-user",
- details=[
- {
- "id": "detail-b",
- "dueAmount": 1000,
- "transferAmount": 1000,
- }
- ],
- )
- db.expire_all()
- own = db.query(BizProtocolPaymentDetail).filter_by(id="detail-a").one()
- foreign = db.query(BizProtocolPaymentDetail).filter_by(id="detail-b").one()
- assert own.del_flag == 0
- assert own.pay_status == "ytf"
- assert foreign.del_flag == 0
- assert foreign.pay_status == "ytf"
- def test_final_effective_rows_reject_omitted_locked_row_plus_full_new_plan():
- db = _session()
- _payment(db, pay_mode="fq", contract_amount="2000", installment_count=2)
- _detail(db, pay_status="yjf", installment_no=1)
- with pytest.raises(ValueError, match="明细条数|无法切换分期类型"):
- save_payment_details(
- db,
- payment_id="payment-a",
- tenant_no="tenant-a",
- user_id="market-user",
- details=[
- {"installmentNo": 2, "dueAmount": 1000},
- {"installmentNo": 3, "dueAmount": 1000},
- ],
- )
- db.expire_all()
- locked = db.query(BizProtocolPaymentDetail).filter_by(id="detail-a").one()
- assert locked.del_flag == 0
- assert locked.installment_no == 1
- assert locked.pay_status == "yjf"
- def test_final_effective_rows_use_locked_period_and_reject_duplicate_number():
- db = _session()
- _payment(db, pay_mode="fq", contract_amount="2000", installment_count=2)
- _detail(db, pay_status="yjf", installment_no=1)
- with pytest.raises(ValueError, match="期号.*重复"):
- save_payment_details(
- db,
- payment_id="payment-a",
- tenant_no="tenant-a",
- user_id="market-user",
- details=[
- {
- "id": "detail-a",
- "installmentNo": 2,
- "dueAmount": 1,
- },
- {"installmentNo": 1, "dueAmount": 1000},
- ],
- )
- db.expire_all()
- locked = db.query(BizProtocolPaymentDetail).filter_by(id="detail-a").one()
- assert locked.installment_no == 1
- assert locked.due_amount == Decimal("1000")
- def test_save_payment_details_locks_payment_and_detail_rows(monkeypatch):
- db = _session()
- _payment(db)
- _detail(db, pay_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)
- save_payment_details(
- db,
- payment_id="payment-a",
- tenant_no="tenant-a",
- user_id="market-user",
- details=[{"id": "detail-a", "dueAmount": 1000}],
- )
- assert len(calls) >= 2
- def test_v1_aggregate_save_rolls_back_parent_when_locked_detail_validation_fails():
- db = _session()
- payment = _payment(
- db,
- pay_mode="fq",
- contract_amount="2000",
- installment_count=2,
- )
- payment.installment_interval_months = Decimal("12")
- payment.installment_start_date = "20261001"
- payment.installment_advance_days = 30
- payment.installment_ratios = "[500, 500]"
- payment.remark = "original plan"
- db.commit()
- _detail(
- db,
- detail_id="locked-period-2",
- pay_status="yjf",
- due_amount="1000",
- installment_no=2,
- )
- with pytest.raises(ValueError, match="明细条数|无法切换分期类型"):
- _persist_protocol_sub_resources(
- db,
- "protocol-a",
- "tenant-a",
- "market-user",
- party_customer=None,
- collection_info=None,
- samples=None,
- payment={
- "payMode": "djfq",
- "contractAmount": 700,
- "installmentCount": 1,
- "installmentIntervalMonths": 9,
- "installmentStartDate": "20270101",
- "installmentAdvanceDays": 15,
- "installmentRatios": [1000],
- "remark": "tampered plan",
- },
- payment_details=[],
- storage_cert=None,
- payment_model_version=1,
- )
- db.rollback()
- db.expire_all()
- saved_payment = db.get(BizProtocolPayment, "payment-a")
- assert saved_payment.contract_amount == Decimal("2000")
- assert saved_payment.pay_mode == "fq"
- assert saved_payment.installment_count == 2
- assert saved_payment.installment_interval_months == Decimal("12")
- assert saved_payment.installment_start_date == "20261001"
- assert saved_payment.installment_advance_days == 30
- assert saved_payment.installment_ratios == "[500, 500]"
- assert saved_payment.remark == "original plan"
- locked = db.get(BizProtocolPaymentDetail, "locked-period-2")
- assert locked.del_flag == 0
- assert locked.pay_status == "yjf"
- assert locked.installment_no == 2
- assert locked.due_amount == Decimal("1000")
- def test_v1_aggregate_passes_exact_max_amount_installments_to_persistence(monkeypatch):
- from app.crud import collection_protocol as collection_crud
- db = _session()
- payment = _payment(
- db,
- pay_mode="fq",
- contract_amount="100.00",
- installment_count=2,
- )
- payment.installment_ratios = None
- payment.installment_interval_months = Decimal("12")
- payment.installment_start_date = "20261001"
- db.commit()
- captured = {}
- def capture_details(
- _db,
- _payment_id,
- _tenant_no,
- _user_id,
- details,
- **_kwargs,
- ):
- captured["details"] = details
- return []
- monkeypatch.setattr(collection_crud, "save_payment_details", capture_details)
- save_payment_with_details(
- db,
- "protocol-a",
- "tenant-a",
- "market-user",
- payment={
- "pay_mode": "fq",
- "contract_amount": "9999999999999999.99",
- "installment_count": 2,
- "installment_interval_months": "12",
- "installment_start_date": "20261001",
- "installment_ratios": None,
- },
- payment_details=[],
- )
- assert [row["due_amount"] for row in captured["details"]] == [
- "4999999999999999.99",
- "5000000000000000",
- ]
- assert sum(
- Decimal(row["due_amount"]) for row in captured["details"]
- ) == Decimal("9999999999999999.99")
- def test_v1_aggregate_validates_drafts_against_remaining_contract_amount():
- db = _session()
- payment = _payment(
- db, pay_mode="fq", contract_amount="3000", installment_count=3
- )
- payment.installment_interval_months = Decimal("12")
- payment.installment_start_date = "20261001"
- payment.installment_advance_days = 30
- payment.installment_ratios = None
- db.commit()
- _detail(
- db, detail_id="paid-1", pay_status="yjf", due_amount="500",
- installment_no=1, installment_due_date="20261001",
- )
- _detail(
- db, detail_id="draft-2", pay_status="dfj", due_amount="1000",
- installment_no=2, installment_due_date="20270901",
- )
- _detail(
- db, detail_id="draft-3", pay_status="dfj", due_amount="1500",
- installment_no=3, installment_due_date="20280901",
- )
- saved = save_payment_with_details(
- db,
- "protocol-a",
- "tenant-a",
- "market-user",
- payment={
- "pay_mode": "fq",
- "contract_amount": "3000.00",
- "installment_count": 3,
- "installment_interval_months": "24",
- "installment_start_date": "20261001",
- "installment_advance_days": 30,
- "installment_ratios": None,
- },
- payment_details=[
- {
- "id": "paid-1", "installment_no": 1,
- "installment_due_date": "20261001", "due_amount": "500.00",
- },
- {
- "id": "draft-2", "installment_no": 2,
- "installment_due_date": "20280901", "due_amount": "1250.00",
- },
- {
- "id": "draft-3", "installment_no": 3,
- "installment_due_date": "20300901", "due_amount": "1250.00",
- },
- ],
- )
- assert saved.installment_interval_months == Decimal("24")
- rows = {
- row.id: row
- for row in db.query(BizProtocolPaymentDetail)
- .filter(BizProtocolPaymentDetail.del_flag == 0)
- .all()
- }
- assert rows["paid-1"].due_amount == Decimal("500")
- assert rows["draft-2"].due_amount == Decimal("1250")
- assert rows["draft-3"].due_amount == Decimal("1250")
- def test_v1_aggregate_uses_remaining_period_ratio_weights():
- db = _session()
- payment = _payment(
- db, pay_mode="fq", contract_amount="3000", installment_count=3
- )
- payment.installment_interval_months = Decimal("12")
- payment.installment_start_date = "20261001"
- payment.installment_advance_days = 30
- payment.installment_ratios = "[10, 45, 45]"
- db.commit()
- _detail(
- db, detail_id="ratio-paid-1", pay_status="yjf", due_amount="500",
- installment_no=1, installment_due_date="20261001",
- )
- _detail(
- db, detail_id="ratio-draft-2", pay_status="dfj", due_amount="1250",
- installment_no=2, installment_due_date="20270901",
- )
- _detail(
- db, detail_id="ratio-draft-3", pay_status="dfj", due_amount="1250",
- installment_no=3, installment_due_date="20280901",
- )
- save_payment_with_details(
- db,
- "protocol-a",
- "tenant-a",
- "market-user",
- payment={
- "pay_mode": "fq",
- "contract_amount": "3000.00",
- "installment_count": 3,
- "installment_interval_months": "12",
- "installment_start_date": "20261001",
- "installment_advance_days": 30,
- "installment_ratios": [10, 30, 60],
- },
- payment_details=[
- {
- "id": "ratio-paid-1", "installment_no": 1,
- "installment_due_date": "20261001", "due_amount": "500.00",
- },
- {
- "id": "ratio-draft-2", "installment_no": 2,
- "installment_due_date": "20270901", "due_amount": "833.33",
- },
- {
- "id": "ratio-draft-3", "installment_no": 3,
- "installment_due_date": "20280901", "due_amount": "1666.67",
- },
- ],
- )
- db.expire_all()
- assert db.get(BizProtocolPaymentDetail, "ratio-draft-2").due_amount == Decimal("833.33")
- assert db.get(BizProtocolPaymentDetail, "ratio-draft-3").due_amount == Decimal("1666.67")
- def test_locked_remaining_distribution_keeps_max_amount_cent_tail():
- from app.services.protocol_payment_installment import (
- resolve_locked_remaining_due_amounts,
- )
- amounts = resolve_locked_remaining_due_amounts(
- Decimal("9999999999999999.99"),
- Decimal("0.02"),
- [],
- [1, 2],
- )
- assert amounts == [
- Decimal("4999999999999999.98"),
- Decimal("4999999999999999.99"),
- ]
- assert sum(amounts, Decimal("0.02")) == Decimal("9999999999999999.99")
- def test_v1_aggregate_rejects_contract_below_locked_due_and_rolls_back():
- db = _session()
- payment = _payment(
- db, pay_mode="fq", contract_amount="3000", installment_count=2
- )
- payment.installment_interval_months = Decimal("12")
- payment.installment_start_date = "20261001"
- payment.installment_advance_days = 30
- db.commit()
- _detail(
- db, detail_id="locked-over-contract", pay_status="yjf",
- due_amount="500", installment_no=1,
- installment_due_date="20261001",
- )
- with pytest.raises(ValueError, match="合同金额不能小于"):
- save_payment_with_details(
- db,
- "protocol-a",
- "tenant-a",
- "market-user",
- payment={
- "pay_mode": "fq", "contract_amount": "400.00",
- "installment_count": 2, "installment_interval_months": "24",
- "installment_start_date": "20261001",
- "installment_advance_days": 30,
- "installment_ratios": None,
- },
- payment_details=[
- {
- "id": "locked-over-contract", "installment_no": 1,
- "installment_due_date": "20261001", "due_amount": "500.00",
- },
- {
- "installment_no": 2, "installment_due_date": "20280901",
- "due_amount": "0.00",
- },
- ],
- )
- db.expire_all()
- assert db.get(BizProtocolPayment, "payment-a").contract_amount == Decimal("3000")
- @pytest.mark.parametrize(
- ("draft_no", "draft_date", "message"),
- [
- (4, "20280901", "期号"),
- (2, "20270901", "应缴时间"),
- ],
- )
- def test_v1_aggregate_rejects_remaining_period_or_date_mismatch(
- draft_no, draft_date, message
- ):
- db = _session()
- payment = _payment(
- db, pay_mode="fq", contract_amount="3000", installment_count=3
- )
- payment.installment_interval_months = Decimal("12")
- payment.installment_start_date = "20261001"
- payment.installment_advance_days = 30
- payment.installment_ratios = None
- db.commit()
- _detail(
- db, detail_id="guard-paid-1", pay_status="yjf", due_amount="500",
- installment_no=1, installment_due_date="20261001",
- )
- _detail(
- db, detail_id="guard-draft-2", pay_status="dfj", due_amount="1250",
- installment_no=2, installment_due_date="20270901",
- )
- _detail(
- db, detail_id="guard-draft-3", pay_status="dfj", due_amount="1250",
- installment_no=3, installment_due_date="20280901",
- )
- with pytest.raises(ValueError, match=message):
- save_payment_with_details(
- db,
- "protocol-a",
- "tenant-a",
- "market-user",
- payment={
- "pay_mode": "fq", "contract_amount": "3000.00",
- "installment_count": 3, "installment_interval_months": "24",
- "installment_start_date": "20261001",
- "installment_advance_days": 30,
- "installment_ratios": None,
- },
- payment_details=[
- {
- "id": "guard-paid-1", "installment_no": 1,
- "installment_due_date": "20261001", "due_amount": "500.00",
- },
- {
- "id": "guard-draft-2", "installment_no": draft_no,
- "installment_due_date": draft_date, "due_amount": "1250.00",
- },
- {
- "id": "guard-draft-3", "installment_no": 3,
- "installment_due_date": "20300901", "due_amount": "1250.00",
- },
- ],
- )
- db.expire_all()
- assert db.get(BizProtocolPayment, "payment-a").installment_interval_months == Decimal("12")
- def test_v1_aggregate_rejects_invalid_all_draft_plan_and_rolls_back():
- db = _session()
- payment = _payment(
- db, pay_mode="fq", contract_amount="100", installment_count=2
- )
- payment.installment_interval_months = Decimal("1")
- payment.installment_start_date = "20260131"
- payment.installment_advance_days = 0
- payment.installment_ratios = None
- payment.remark = "original"
- db.commit()
- with pytest.raises(ValueError, match="期号|应缴金额|应缴时间"):
- save_payment_with_details(
- db,
- "protocol-a",
- "tenant-a",
- "market-user",
- payment={
- "pay_mode": "fq", "contract_amount": "100.00",
- "installment_count": 2, "installment_interval_months": "2",
- "installment_start_date": "20260131",
- "installment_advance_days": 0,
- "installment_ratios": None, "remark": "tampered",
- },
- payment_details=[
- {
- "installment_no": 99, "installment_due_date": "20000101",
- "due_amount": "1.00",
- },
- {
- "installment_no": 100, "installment_due_date": "19990101",
- "due_amount": "2.00",
- },
- ],
- )
- db.expire_all()
- saved = db.get(BizProtocolPayment, "payment-a")
- assert saved.installment_interval_months == Decimal("1")
- assert saved.remark == "original"
- assert db.query(BizProtocolPaymentDetail).count() == 0
- def test_v1_aggregate_accepts_complete_all_draft_plan_with_clamped_dates():
- db = _session()
- payment = _payment(
- db, pay_mode="fq", contract_amount="100", installment_count=2
- )
- payment.installment_interval_months = Decimal("1")
- payment.installment_start_date = "20260131"
- payment.installment_advance_days = 0
- payment.installment_ratios = None
- db.commit()
- save_payment_with_details(
- db,
- "protocol-a",
- "tenant-a",
- "market-user",
- payment={
- "pay_mode": "fq", "contract_amount": "100.00",
- "installment_count": 2, "installment_interval_months": "1",
- "installment_start_date": "20260131",
- "installment_advance_days": 0,
- "installment_ratios": None,
- },
- payment_details=[
- {
- "installment_no": 1, "installment_due_date": "20260131",
- "due_amount": "50.00",
- },
- {
- "installment_no": 2, "installment_due_date": "20260228",
- "due_amount": "50.00",
- },
- ],
- )
- rows = db.query(BizProtocolPaymentDetail).order_by(
- BizProtocolPaymentDetail.installment_no
- ).all()
- assert [row.installment_no for row in rows] == [1, 2]
- assert [row.installment_due_date for row in rows] == ["20260131", "20260228"]
- assert [row.due_amount for row in rows] == [Decimal("50"), Decimal("50")]
- def test_v1_aggregate_rejects_unknown_pay_mode_and_rolls_back():
- db = _session()
- payment = _payment(db, pay_mode="dj", contract_amount="100")
- payment.remark = "original"
- db.commit()
- with pytest.raises(ValueError, match="缴费模式"):
- save_payment_with_details(
- db,
- "protocol-a",
- "tenant-a",
- "market-user",
- payment={
- "pay_mode": "evil",
- "contract_amount": "1.00",
- "remark": "tampered",
- },
- payment_details=[],
- )
- db.expire_all()
- saved = db.get(BizProtocolPayment, "payment-a")
- assert saved.pay_mode == "dj"
- assert saved.contract_amount == Decimal("100")
- assert saved.remark == "original"
- @pytest.mark.parametrize(
- ("payment_patch", "detail_patch"),
- [
- ({"lump_sum_deadline": "20261399"}, {}),
- ({}, {"installment_due_date": "20230229"}),
- ],
- )
- def test_v1_aggregate_rejects_invalid_calendar_dates_and_rolls_back(
- payment_patch, detail_patch
- ):
- db = _session()
- payment = _payment(db, pay_mode="dj", contract_amount="100")
- payment.lump_sum_deadline = "20261231"
- payment.remark = "original"
- db.commit()
- with pytest.raises(ValueError, match="日期"):
- save_payment_with_details(
- db,
- "protocol-a",
- "tenant-a",
- "market-user",
- payment={
- "pay_mode": "dj",
- "contract_amount": "100.00",
- "lump_sum_deadline": "20261231",
- "remark": "tampered",
- **payment_patch,
- },
- payment_details=[{
- "installment_due_date": "20261231",
- "due_amount": "100.00",
- **detail_patch,
- }],
- )
- db.expire_all()
- saved = db.get(BizProtocolPayment, "payment-a")
- assert saved.lump_sum_deadline == "20261231"
- assert saved.remark == "original"
- assert db.query(BizProtocolPaymentDetail).count() == 0
|