feat: add sample payment schedule and transfer status rules

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-03 10:58:21 +08:00
1 parent ca3d464761
commit 8827ec3804
3 files changed
+770

No files matched your search

+701
View File
@@ -0,0 +1,701 @@
"""按样本缴费方案 / 应缴批次 / 转账笔 CRUD(payment_model_version=2)"""
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid, parse_datetime
from app.crud.collection_protocol import urls_to_attachment_list
from app.models.models import (
BizCollectionProtocol,
BizProtocolPartyCustomer,
BizProtocolPaymentSchedule,
BizProtocolPaymentTransfer,
BizProtocolSamplePayment,
)
from app.services.protocol_transfer_status import (
can_confirm_receipt,
can_submit_receipt,
schedule_paid_and_arrears,
)
def _dec(v: Any) -> Optional[Decimal]:
if v is None or v == "":
return None
return Decimal(str(v))
def _now() -> datetime:
return datetime.now()
def _get_sample_payment_or_raise(
db: Session, *, sample_payment_id: str, tenant_no: str
) -> BizProtocolSamplePayment:
obj = (
db.query(BizProtocolSamplePayment)
.filter(
BizProtocolSamplePayment.id == sample_payment_id,
BizProtocolSamplePayment.tenant_no == tenant_no,
BizProtocolSamplePayment.del_flag == 0,
)
.first()
)
if not obj:
raise ValueError("样本缴费方案不存在")
return obj
def _get_schedule_or_raise(
db: Session, *, schedule_id: str, tenant_no: str
) -> BizProtocolPaymentSchedule:
obj = (
db.query(BizProtocolPaymentSchedule)
.filter(
BizProtocolPaymentSchedule.id == schedule_id,
BizProtocolPaymentSchedule.tenant_no == tenant_no,
BizProtocolPaymentSchedule.del_flag == 0,
)
.first()
)
if not obj:
raise ValueError("应缴批次不存在")
return obj
def _get_transfer_or_raise(
db: Session, *, transfer_id: str, tenant_no: str
) -> BizProtocolPaymentTransfer:
obj = (
db.query(BizProtocolPaymentTransfer)
.filter(
BizProtocolPaymentTransfer.id == transfer_id,
BizProtocolPaymentTransfer.tenant_no == tenant_no,
BizProtocolPaymentTransfer.del_flag == 0,
)
.first()
)
if not obj:
raise ValueError("转账笔不存在")
return obj
def has_blocking_transfer_for_sample_payment(
db: Session, *, sample_payment_id: str, tenant_no: str
) -> bool:
row = (
db.query(BizProtocolPaymentTransfer.id)
.filter(
BizProtocolPaymentTransfer.sample_payment_id == sample_payment_id,
BizProtocolPaymentTransfer.tenant_no == tenant_no,
BizProtocolPaymentTransfer.del_flag == 0,
BizProtocolPaymentTransfer.pay_status == "jfqrz",
)
.first()
)
return row is not None
def assert_sample_payment_editable(
db: Session, *, sample_payment_id: str, tenant_no: str
) -> None:
if has_blocking_transfer_for_sample_payment(
db, sample_payment_id=sample_payment_id, tenant_no=tenant_no
):
raise ValueError("存在缴费确认中的转账笔,无法修改缴费方案或批次")
def upsert_sample_payment(
db: Session,
*,
collection_protocol_id: str,
tenant_no: str,
user_id: Optional[str],
sample_code: str,
pay_mode: str,
contract_amount: Any = None,
protocol_sample_id: Optional[str] = None,
installment_count: Optional[int] = None,
installment_interval_months: Any = None,
installment_start_date: Optional[str] = None,
installment_advance_days: Optional[int] = None,
installment_ratios: Optional[str] = None,
lump_sum_deadline: Optional[str] = None,
remark: Optional[str] = None,
sample_payment_id: Optional[str] = None,
) -> BizProtocolSamplePayment:
code = (sample_code or "").strip()
if not code:
raise ValueError("样本编码不能为空")
mode = (pay_mode or "").strip()
if mode not in ("dj", "fq", "djfq"):
raise ValueError("缴费模式无效")
obj: Optional[BizProtocolSamplePayment] = None
if sample_payment_id:
obj = _get_sample_payment_or_raise(
db, sample_payment_id=sample_payment_id, tenant_no=tenant_no
)
else:
obj = (
db.query(BizProtocolSamplePayment)
.filter(
BizProtocolSamplePayment.collection_protocol_id == collection_protocol_id,
BizProtocolSamplePayment.tenant_no == tenant_no,
BizProtocolSamplePayment.sample_code == code,
BizProtocolSamplePayment.del_flag == 0,
)
.first()
)
now = _now()
if obj:
assert_sample_payment_editable(db, sample_payment_id=obj.id, tenant_no=tenant_no)
if (obj.pay_mode or "") != mode:
assert_sample_payment_editable(db, sample_payment_id=obj.id, tenant_no=tenant_no)
obj.protocol_sample_id = (protocol_sample_id or "").strip() or None
obj.sample_code = code
obj.pay_mode = mode
obj.contract_amount = _dec(contract_amount)
obj.installment_count = installment_count
obj.installment_interval_months = _dec(installment_interval_months)
obj.installment_start_date = (installment_start_date or "").strip() or None
obj.installment_advance_days = installment_advance_days
obj.installment_ratios = (installment_ratios or "").strip() or None
obj.lump_sum_deadline = (lump_sum_deadline or "").strip() or None
if remark is not None:
obj.remark = (remark or "").strip() or None
obj.update_by = user_id
obj.update_time = now
db.add(obj)
db.flush()
return obj
obj = BizProtocolSamplePayment(
id=generate_uuid(),
collection_protocol_id=collection_protocol_id,
protocol_sample_id=(protocol_sample_id or "").strip() or None,
sample_code=code,
pay_mode=mode,
contract_amount=_dec(contract_amount),
installment_count=installment_count,
installment_interval_months=_dec(installment_interval_months),
installment_start_date=(installment_start_date or "").strip() or None,
installment_advance_days=installment_advance_days,
installment_ratios=(installment_ratios or "").strip() or None,
lump_sum_deadline=(lump_sum_deadline or "").strip() or None,
tenant_no=tenant_no,
order_num=0,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(obj)
db.flush()
return obj
def replace_schedules(
db: Session,
*,
sample_payment_id: str,
tenant_no: str,
user_id: Optional[str],
schedules: List[Dict[str, Any]],
) -> List[BizProtocolPaymentSchedule]:
"""软删原批次(及其下 dfj 转账),重建批次。存在 jfqrz 时禁止。"""
sp = _get_sample_payment_or_raise(
db, sample_payment_id=sample_payment_id, tenant_no=tenant_no
)
assert_sample_payment_editable(
db, sample_payment_id=sample_payment_id, tenant_no=tenant_no
)
now = _now()
old_schedules = (
db.query(BizProtocolPaymentSchedule)
.filter(
BizProtocolPaymentSchedule.sample_payment_id == sample_payment_id,
BizProtocolPaymentSchedule.tenant_no == tenant_no,
BizProtocolPaymentSchedule.del_flag == 0,
)
.all()
)
old_ids = [s.id for s in old_schedules]
if old_ids:
transfers = (
db.query(BizProtocolPaymentTransfer)
.filter(
BizProtocolPaymentTransfer.schedule_id.in_(old_ids),
BizProtocolPaymentTransfer.tenant_no == tenant_no,
BizProtocolPaymentTransfer.del_flag == 0,
)
.all()
)
for t in transfers:
if t.pay_status == "jfqrz":
raise ValueError("存在缴费确认中的转账笔,无法重建批次")
if t.pay_status == "dfj":
t.del_flag = 1
t.update_by = user_id
t.update_time = now
db.add(t)
# yjf 转账保留:所属旧批次软删后仍可历史追溯;新批次不含这些笔
for s in old_schedules:
s.del_flag = 1
s.update_by = user_id
s.update_time = now
db.add(s)
created: List[BizProtocolPaymentSchedule] = []
for idx, raw in enumerate(schedules or []):
due_date = (raw.get("due_date") or raw.get("dueDate") or "").strip() or None
schedule_status = (
raw.get("schedule_status") or raw.get("scheduleStatus") or "open"
)
obj = BizProtocolPaymentSchedule(
id=generate_uuid(),
sample_payment_id=sp.id,
collection_protocol_id=sp.collection_protocol_id,
sample_code=sp.sample_code,
schedule_no=raw.get("schedule_no")
if raw.get("schedule_no") is not None
else raw.get("scheduleNo"),
due_amount=_dec(raw.get("due_amount") if "due_amount" in raw else raw.get("dueAmount")),
due_date=due_date,
schedule_status=(str(schedule_status).strip() or "open"),
tenant_no=tenant_no,
order_num=idx,
remark=(raw.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(obj)
created.append(obj)
db.flush()
return created
def add_transfer(
db: Session,
*,
schedule_id: str,
tenant_no: str,
user_id: Optional[str],
transfer_amount: Any = None,
transfer_time: Any = None,
transfer_type: Optional[str] = None,
transfer_attachments: Optional[str] = None,
remark: Optional[str] = None,
) -> BizProtocolPaymentTransfer:
sch = _get_schedule_or_raise(db, schedule_id=schedule_id, tenant_no=tenant_no)
now = _now()
tt = None
if transfer_time not in (None, ""):
tt = (
transfer_time
if isinstance(transfer_time, datetime)
else parse_datetime(str(transfer_time))
)
obj = BizProtocolPaymentTransfer(
id=generate_uuid(),
schedule_id=sch.id,
sample_payment_id=sch.sample_payment_id,
collection_protocol_id=sch.collection_protocol_id,
sample_code=sch.sample_code,
transfer_amount=_dec(transfer_amount),
transfer_time=tt,
transfer_type=(transfer_type or "").strip() or None,
transfer_attachments=(transfer_attachments or "").strip() or None,
pay_status="dfj",
tenant_no=tenant_no,
order_num=0,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def update_transfer_draft(
db: Session,
*,
transfer_id: str,
tenant_no: str,
user_id: Optional[str],
fields: Dict[str, Any],
) -> BizProtocolPaymentTransfer:
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
if t.pay_status != "dfj":
raise ValueError("仅待缴费状态可编辑转账笔")
if "transfer_type" in fields or "transferType" in fields:
v = fields.get("transfer_type", fields.get("transferType"))
t.transfer_type = (str(v) if v is not None else "").strip() or None
if "transfer_amount" in fields or "transferAmount" in fields:
v = fields.get("transfer_amount", fields.get("transferAmount"))
amt = _dec(v)
if amt is not None and amt < 0:
raise ValueError("转账金额无效")
t.transfer_amount = amt
if "transfer_time" in fields or "transferTime" in fields:
v = fields.get("transfer_time", fields.get("transferTime"))
if v in (None, ""):
t.transfer_time = None
elif isinstance(v, datetime):
t.transfer_time = v
else:
t.transfer_time = parse_datetime(str(v))
if "transfer_attachments" in fields or "transferAttachments" in fields:
v = fields.get("transfer_attachments", fields.get("transferAttachments"))
t.transfer_attachments = (str(v) if v is not None else "").strip() or None
if "remark" in fields:
t.remark = (fields.get("remark") or "").strip() or None
t.update_by = user_id
t.update_time = _now()
db.add(t)
db.commit()
db.refresh(t)
return t
def _validate_transfer_submit_ready(t: BizProtocolPaymentTransfer) -> None:
if not (t.transfer_type or "").strip():
raise ValueError("请选择转账方式")
if t.transfer_amount is None or Decimal(str(t.transfer_amount)) <= 0:
raise ValueError("请填写转账金额")
def submit_receipt_finance(
db: Session,
*,
transfer_id: str,
tenant_no: str,
user_id: Optional[str],
draft_fields: Optional[Dict[str, Any]] = None,
) -> BizProtocolPaymentTransfer:
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
if draft_fields:
if t.pay_status != "dfj":
raise ValueError("仅待缴费状态可编辑转账笔")
# 就地改草稿字段(不单独 commit)
if "transfer_type" in draft_fields or "transferType" in draft_fields:
v = draft_fields.get("transfer_type", draft_fields.get("transferType"))
t.transfer_type = (str(v) if v is not None else "").strip() or None
if "transfer_amount" in draft_fields or "transferAmount" in draft_fields:
t.transfer_amount = _dec(
draft_fields.get("transfer_amount", draft_fields.get("transferAmount"))
)
if "transfer_time" in draft_fields or "transferTime" in draft_fields:
v = draft_fields.get("transfer_time", draft_fields.get("transferTime"))
if v in (None, ""):
t.transfer_time = None
elif isinstance(v, datetime):
t.transfer_time = v
else:
t.transfer_time = parse_datetime(str(v))
if "transfer_attachments" in draft_fields or "transferAttachments" in draft_fields:
v = draft_fields.get(
"transfer_attachments", draft_fields.get("transferAttachments")
)
t.transfer_attachments = (str(v) if v is not None else "").strip() or None
if not can_submit_receipt(t.pay_status):
raise ValueError("仅待缴费状态可提交财务确认")
_validate_transfer_submit_ready(t)
t.pay_status = "jfqrz"
t.update_by = user_id
t.update_time = _now()
db.add(t)
db.commit()
db.refresh(t)
return t
def confirm_receipt(
db: Session,
*,
transfer_id: str,
tenant_no: str,
user_id: Optional[str],
finance_confirm_attachments: Optional[str] = None,
finance_remark: Optional[str] = None,
) -> BizProtocolPaymentTransfer:
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
if t.pay_status == "yjf":
raise ValueError("该转账笔已确认收款")
if not can_confirm_receipt(t.pay_status):
raise ValueError("当前状态不可确认收款")
t.pay_status = "yjf"
if finance_confirm_attachments is not None:
t.finance_confirm_attachments = finance_confirm_attachments.strip() or None
if finance_remark is not None:
t.finance_remark = finance_remark.strip() or None
t.update_by = user_id
t.update_time = _now()
db.add(t)
db.commit()
db.refresh(t)
return t
def reject_receipt(
db: Session,
*,
transfer_id: str,
tenant_no: str,
user_id: Optional[str],
) -> BizProtocolPaymentTransfer:
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
if t.pay_status != "jfqrz":
raise ValueError("当前状态不可退回")
t.pay_status = "dfj"
t.update_by = user_id
t.update_time = _now()
db.add(t)
db.commit()
db.refresh(t)
return t
def _transfer_to_dict(
db: Session, t: BizProtocolPaymentTransfer, tenant_no: str
) -> Dict[str, Any]:
return {
"id": t.id,
"scheduleId": t.schedule_id,
"samplePaymentId": t.sample_payment_id,
"collectionProtocolId": t.collection_protocol_id,
"sampleCode": t.sample_code or "",
"transferAmount": float(t.transfer_amount) if t.transfer_amount is not None else None,
"transferTime": t.transfer_time.strftime("%Y-%m-%d %H:%M:%S") if t.transfer_time else "",
"transferType": t.transfer_type or "",
"transferAttachments": t.transfer_attachments or "",
"attachments": urls_to_attachment_list(t.transfer_attachments, db, tenant_no),
"payStatus": t.pay_status or "",
"financeConfirmAttachments": t.finance_confirm_attachments or "",
"financeConfirmAttachmentFiles": urls_to_attachment_list(
t.finance_confirm_attachments, db, tenant_no
),
"financeRemark": t.finance_remark or "",
"remark": t.remark or "",
"source": "formal",
"createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
"updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
}
def _schedule_to_dict(
db: Session,
s: BizProtocolPaymentSchedule,
transfers: List[BizProtocolPaymentTransfer],
tenant_no: str,
) -> Dict[str, Any]:
confirmed = [
t.transfer_amount
for t in transfers
if (t.pay_status or "") == "yjf"
]
paid, arrears = schedule_paid_and_arrears(
due=s.due_amount, confirmed_transfers=confirmed
)
return {
"id": s.id,
"samplePaymentId": s.sample_payment_id,
"collectionProtocolId": s.collection_protocol_id,
"sampleCode": s.sample_code or "",
"scheduleNo": s.schedule_no,
"dueAmount": float(s.due_amount) if s.due_amount is not None else None,
"dueDate": s.due_date or "",
"scheduleStatus": s.schedule_status or "",
"paidAmount": float(paid),
"arrearsAmount": float(arrears),
"remark": s.remark or "",
"transfers": [_transfer_to_dict(db, t, tenant_no) for t in transfers],
}
def _sample_payment_to_dict(
db: Session,
p: BizProtocolSamplePayment,
schedules: List[BizProtocolPaymentSchedule],
transfers_by_schedule: Dict[str, List[BizProtocolPaymentTransfer]],
tenant_no: str,
) -> Dict[str, Any]:
return {
"id": p.id,
"collectionProtocolId": p.collection_protocol_id,
"protocolSampleId": p.protocol_sample_id or "",
"sampleCode": p.sample_code or "",
"payMode": p.pay_mode or "",
"contractAmount": float(p.contract_amount) if p.contract_amount is not None else None,
"installmentCount": p.installment_count,
"installmentIntervalMonths": float(p.installment_interval_months)
if p.installment_interval_months is not None
else None,
"installmentStartDate": p.installment_start_date or "",
"installmentAdvanceDays": p.installment_advance_days,
"installmentRatios": p.installment_ratios or "",
"lumpSumDeadline": p.lump_sum_deadline or "",
"remark": p.remark or "",
"schedules": [
_schedule_to_dict(
db, s, transfers_by_schedule.get(s.id, []), tenant_no
)
for s in schedules
],
}
def get_sample_payments_bundle(
db: Session, collection_protocol_id: str, tenant_no: str
) -> List[Dict[str, Any]]:
payments = (
db.query(BizProtocolSamplePayment)
.filter(
BizProtocolSamplePayment.collection_protocol_id == collection_protocol_id,
BizProtocolSamplePayment.tenant_no == tenant_no,
BizProtocolSamplePayment.del_flag == 0,
)
.order_by(
BizProtocolSamplePayment.order_num.asc(),
BizProtocolSamplePayment.create_time.asc(),
)
.all()
)
if not payments:
return []
pids = [p.id for p in payments]
schedules = (
db.query(BizProtocolPaymentSchedule)
.filter(
BizProtocolPaymentSchedule.sample_payment_id.in_(pids),
BizProtocolPaymentSchedule.tenant_no == tenant_no,
BizProtocolPaymentSchedule.del_flag == 0,
)
.order_by(
BizProtocolPaymentSchedule.order_num.asc(),
BizProtocolPaymentSchedule.schedule_no.asc(),
)
.all()
)
sids = [s.id for s in schedules]
transfers: List[BizProtocolPaymentTransfer] = []
if sids:
transfers = (
db.query(BizProtocolPaymentTransfer)
.filter(
BizProtocolPaymentTransfer.schedule_id.in_(sids),
BizProtocolPaymentTransfer.tenant_no == tenant_no,
BizProtocolPaymentTransfer.del_flag == 0,
)
.order_by(BizProtocolPaymentTransfer.create_time.asc())
.all()
)
by_schedule: Dict[str, List[BizProtocolPaymentTransfer]] = {}
for t in transfers:
by_schedule.setdefault(t.schedule_id, []).append(t)
schedules_by_payment: Dict[str, List[BizProtocolPaymentSchedule]] = {}
for s in schedules:
schedules_by_payment.setdefault(s.sample_payment_id, []).append(s)
return [
_sample_payment_to_dict(
db,
p,
schedules_by_payment.get(p.id, []),
by_schedule,
tenant_no,
)
for p in payments
]
def list_transfers_for_finance(
db: Session,
*,
pay_status: str,
tenant_no: str,
keyword: Optional[str] = None,
page: int = 1,
page_size: int = 10,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
st = (pay_status or "").strip()
q = (
db.query(BizProtocolPaymentTransfer, BizCollectionProtocol, BizProtocolPartyCustomer)
.join(
BizCollectionProtocol,
and_(
BizCollectionProtocol.id
== BizProtocolPaymentTransfer.collection_protocol_id,
BizCollectionProtocol.del_flag == 0,
),
)
.outerjoin(
BizProtocolPartyCustomer,
and_(
BizProtocolPartyCustomer.collection_protocol_id
== BizProtocolPaymentTransfer.collection_protocol_id,
BizProtocolPartyCustomer.tenant_no == tenant_no,
BizProtocolPartyCustomer.del_flag == 0,
),
)
.filter(
BizProtocolPaymentTransfer.tenant_no == tenant_no,
BizProtocolPaymentTransfer.del_flag == 0,
BizProtocolPaymentTransfer.pay_status == st,
)
)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q = q.filter(
or_(
BizCollectionProtocol.protocol_no.like(like),
BizProtocolPaymentTransfer.sample_code.like(like),
BizProtocolPartyCustomer.mother_name.like(like),
BizProtocolPartyCustomer.mother_id_card.like(like),
)
)
if user_info:
from app.core.data_scope import apply_data_scope
q = apply_data_scope(
q, BizProtocolPaymentTransfer, user_info, db, tenant_no
)
total = q.order_by(None).with_entities(func.count(BizProtocolPaymentTransfer.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q.order_by(BizProtocolPaymentTransfer.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
out: List[Dict[str, Any]] = []
for t, proto, party in rows:
item = _transfer_to_dict(db, t, tenant_no)
item["protocolNo"] = (proto.protocol_no if proto else "") or ""
item["motherName"] = (party.mother_name if party else "") or ""
out.append(item)
return out, int(total)
@@ -0,0 +1,25 @@
"""转账笔缴费状态规则(正式样本缴费 / 预签约意向金共用)"""
from __future__ import annotations
from decimal import Decimal
from typing import Iterable, Optional, Tuple, Union
def can_submit_receipt(pay_status: Optional[str]) -> bool:
return (pay_status or "").strip() == "dfj"
def can_confirm_receipt(pay_status: Optional[str]) -> bool:
return (pay_status or "").strip() == "jfqrz"
def schedule_paid_and_arrears(
*,
due: Union[Decimal, float, int, str, None],
confirmed_transfers: Iterable[Union[Decimal, float, int, str, None]],
) -> Tuple[Decimal, Decimal]:
"""已缴 = Σ 已确认转账;欠缴 = 应缴 − 已缴(可为负表示超缴)。"""
due_amt = Decimal(str(due or 0))
paid = sum((Decimal(str(x or 0)) for x in confirmed_transfers), Decimal("0"))
return paid, due_amt - paid
@@ -0,0 +1,44 @@
"""按样本缴费:转账状态机与批次已缴/欠缴计算"""
from decimal import Decimal
import unittest
from app.services.protocol_transfer_status import (
can_confirm_receipt,
can_submit_receipt,
schedule_paid_and_arrears,
)
class TestTransferStatusRules(unittest.TestCase):
def test_submit_only_dfj(self):
self.assertTrue(can_submit_receipt("dfj"))
self.assertFalse(can_submit_receipt("jfqrz"))
self.assertFalse(can_submit_receipt("yjf"))
self.assertFalse(can_submit_receipt(""))
self.assertFalse(can_submit_receipt(None))
def test_confirm_only_jfqrz(self):
self.assertTrue(can_confirm_receipt("jfqrz"))
self.assertFalse(can_confirm_receipt("dfj"))
self.assertFalse(can_confirm_receipt("yjf"))
def test_schedule_arrears(self):
paid, arrears = schedule_paid_and_arrears(
due=Decimal("1000"),
confirmed_transfers=[Decimal("300"), Decimal("200")],
)
self.assertEqual(paid, Decimal("500"))
self.assertEqual(arrears, Decimal("500"))
def test_schedule_arrears_overpay(self):
paid, arrears = schedule_paid_and_arrears(
due=Decimal("100"),
confirmed_transfers=[Decimal("60"), Decimal("50")],
)
self.assertEqual(paid, Decimal("110"))
self.assertEqual(arrears, Decimal("-10"))
if __name__ == "__main__":
unittest.main()