fix: make mobile presign transitions concurrency-safe.
This commit is contained in:
1 parent
c5c7e42feb
commit
34bf9aa0dc
5 files changed
+410
-131
No files matched your search
@@ -108,6 +108,51 @@ def _get_transfer_or_raise(
|
||||
return obj
|
||||
|
||||
|
||||
def _update_transfer_expected_status(
|
||||
db: Session,
|
||||
*,
|
||||
transfer_id: str,
|
||||
tenant_no: str,
|
||||
expected_status: str,
|
||||
values: Dict[Any, Any],
|
||||
extra_conditions: Tuple[Any, ...] = (),
|
||||
commit: bool = True,
|
||||
) -> BizPresignTransfer:
|
||||
"""以旧状态作为 CAS 条件更新,并返回事务内刷新后的稳定快照。"""
|
||||
changed = (
|
||||
db.query(BizPresignTransfer)
|
||||
.filter(
|
||||
BizPresignTransfer.id == transfer_id,
|
||||
BizPresignTransfer.tenant_no == tenant_no,
|
||||
BizPresignTransfer.del_flag == 0,
|
||||
BizPresignTransfer.pay_status == expected_status,
|
||||
*extra_conditions,
|
||||
)
|
||||
.update(values, synchronize_session=False)
|
||||
)
|
||||
if changed != 1:
|
||||
raise ValueError("转账状态已变化,请刷新后重试")
|
||||
obj = (
|
||||
db.query(BizPresignTransfer)
|
||||
.filter(
|
||||
BizPresignTransfer.id == transfer_id,
|
||||
BizPresignTransfer.tenant_no == tenant_no,
|
||||
BizPresignTransfer.del_flag == 0,
|
||||
)
|
||||
.populate_existing()
|
||||
.first()
|
||||
)
|
||||
if obj is None:
|
||||
raise ValueError("意向金转账笔不存在")
|
||||
db.flush()
|
||||
if commit:
|
||||
# 提交会 expire 会话内 ORM 对象;先分离,调用方序列化时只读取
|
||||
# 本次事务内已刷新的标量快照,不会在提交后重新读取并发新状态。
|
||||
db.expunge(obj)
|
||||
db.commit()
|
||||
return obj
|
||||
|
||||
|
||||
def _transfer_to_dict(
|
||||
db: Session, t: BizPresignTransfer, tenant_no: str
|
||||
) -> Dict[str, Any]:
|
||||
@@ -613,45 +658,62 @@ def update_transfer_draft(
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
fields: Dict[str, Any],
|
||||
commit: bool = True,
|
||||
) -> BizPresignTransfer:
|
||||
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
||||
if t.pay_status != "dfj":
|
||||
raise ValueError("仅已缴费状态可编辑意向金转账笔")
|
||||
values: Dict[Any, Any] = {
|
||||
BizPresignTransfer.update_by: user_id,
|
||||
BizPresignTransfer.update_time: _now(),
|
||||
}
|
||||
if "transfer_amount" in fields or "transferAmount" in fields:
|
||||
t.transfer_amount = _dec(
|
||||
values[BizPresignTransfer.transfer_amount] = _dec(
|
||||
fields.get("transfer_amount", fields.get("transferAmount"))
|
||||
)
|
||||
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
|
||||
values[BizPresignTransfer.transfer_time] = None
|
||||
elif isinstance(v, datetime):
|
||||
t.transfer_time = v
|
||||
values[BizPresignTransfer.transfer_time] = v
|
||||
else:
|
||||
t.transfer_time = parse_datetime(str(v))
|
||||
parsed = parse_datetime(str(v))
|
||||
if parsed is None:
|
||||
raise ValueError("转账时间格式无效")
|
||||
values[BizPresignTransfer.transfer_time] = parsed
|
||||
if "transfer_type" in fields or "transferType" in fields:
|
||||
t.transfer_type = (
|
||||
values[BizPresignTransfer.transfer_type] = (
|
||||
str(fields.get("transfer_type", fields.get("transferType")) or "")
|
||||
).strip() or None
|
||||
if "transfer_attachments" in fields or "transferAttachments" in fields:
|
||||
t.transfer_attachments = (
|
||||
values[BizPresignTransfer.transfer_attachments] = (
|
||||
str(
|
||||
fields.get("transfer_attachments", fields.get("transferAttachments"))
|
||||
or ""
|
||||
)
|
||||
).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
|
||||
values[BizPresignTransfer.remark] = (
|
||||
fields.get("remark") or ""
|
||||
).strip() or None
|
||||
return _update_transfer_expected_status(
|
||||
db,
|
||||
transfer_id=transfer_id,
|
||||
tenant_no=tenant_no,
|
||||
expected_status="dfj",
|
||||
values=values,
|
||||
commit=commit,
|
||||
)
|
||||
|
||||
|
||||
def submit_receipt_finance(
|
||||
db: Session, *, transfer_id: str, tenant_no: str, user_id: Optional[str]
|
||||
db: Session,
|
||||
*,
|
||||
transfer_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
commit: bool = True,
|
||||
) -> BizPresignTransfer:
|
||||
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
||||
if not can_submit_receipt(t.pay_status):
|
||||
@@ -660,29 +722,50 @@ def submit_receipt_finance(
|
||||
raise ValueError("请选择转账方式")
|
||||
if t.transfer_amount is None or Decimal(str(t.transfer_amount)) <= 0:
|
||||
raise ValueError("请填写转账金额")
|
||||
t.pay_status = "jfqrz"
|
||||
t.update_by = user_id
|
||||
t.update_time = _now()
|
||||
db.add(t)
|
||||
db.commit()
|
||||
db.refresh(t)
|
||||
return t
|
||||
return _update_transfer_expected_status(
|
||||
db,
|
||||
transfer_id=transfer_id,
|
||||
tenant_no=tenant_no,
|
||||
expected_status="dfj",
|
||||
values={
|
||||
BizPresignTransfer.pay_status: "jfqrz",
|
||||
BizPresignTransfer.update_by: user_id,
|
||||
BizPresignTransfer.update_time: _now(),
|
||||
},
|
||||
extra_conditions=(
|
||||
BizPresignTransfer.transfer_amount.isnot(None),
|
||||
BizPresignTransfer.transfer_amount > 0,
|
||||
BizPresignTransfer.transfer_type.isnot(None),
|
||||
func.length(func.trim(BizPresignTransfer.transfer_type)) > 0,
|
||||
),
|
||||
commit=commit,
|
||||
)
|
||||
|
||||
|
||||
def withdraw_receipt(
|
||||
db: Session, *, transfer_id: str, tenant_no: str, user_id: Optional[str]
|
||||
db: Session,
|
||||
*,
|
||||
transfer_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
commit: bool = True,
|
||||
) -> BizPresignTransfer:
|
||||
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
||||
if not can_withdraw_receipt(t.pay_status):
|
||||
raise ValueError("仅缴费确认中可撤回")
|
||||
t.pay_status = "dfj"
|
||||
# 故意不改 finance_remark / finance_confirm_attachments
|
||||
t.update_by = user_id
|
||||
t.update_time = _now()
|
||||
db.add(t)
|
||||
db.commit()
|
||||
db.refresh(t)
|
||||
return t
|
||||
return _update_transfer_expected_status(
|
||||
db,
|
||||
transfer_id=transfer_id,
|
||||
tenant_no=tenant_no,
|
||||
expected_status="jfqrz",
|
||||
values={
|
||||
BizPresignTransfer.pay_status: "dfj",
|
||||
BizPresignTransfer.update_by: user_id,
|
||||
BizPresignTransfer.update_time: _now(),
|
||||
},
|
||||
commit=commit,
|
||||
)
|
||||
|
||||
|
||||
def confirm_receipt(
|
||||
@@ -693,36 +776,57 @@ def confirm_receipt(
|
||||
user_id: Optional[str],
|
||||
finance_confirm_attachments: Optional[str] = None,
|
||||
finance_remark: Optional[str] = None,
|
||||
commit: bool = True,
|
||||
) -> BizPresignTransfer:
|
||||
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
||||
if not can_confirm_receipt(t.pay_status):
|
||||
raise ValueError("仅缴费确认中状态可确认收款")
|
||||
t.pay_status = "yjf"
|
||||
values: Dict[Any, Any] = {
|
||||
BizPresignTransfer.pay_status: "yjf",
|
||||
BizPresignTransfer.update_by: user_id,
|
||||
BizPresignTransfer.update_time: _now(),
|
||||
}
|
||||
if finance_confirm_attachments is not None:
|
||||
t.finance_confirm_attachments = (finance_confirm_attachments or "").strip() or None
|
||||
values[BizPresignTransfer.finance_confirm_attachments] = (
|
||||
finance_confirm_attachments or ""
|
||||
).strip() or None
|
||||
if finance_remark is not None:
|
||||
t.finance_remark = (finance_remark or "").strip() or None
|
||||
t.update_by = user_id
|
||||
t.update_time = _now()
|
||||
db.add(t)
|
||||
db.commit()
|
||||
db.refresh(t)
|
||||
return t
|
||||
values[BizPresignTransfer.finance_remark] = (
|
||||
finance_remark or ""
|
||||
).strip() or None
|
||||
return _update_transfer_expected_status(
|
||||
db,
|
||||
transfer_id=transfer_id,
|
||||
tenant_no=tenant_no,
|
||||
expected_status="jfqrz",
|
||||
values=values,
|
||||
commit=commit,
|
||||
)
|
||||
|
||||
|
||||
def reject_receipt(
|
||||
db: Session, *, transfer_id: str, tenant_no: str, user_id: Optional[str]
|
||||
db: Session,
|
||||
*,
|
||||
transfer_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
commit: bool = True,
|
||||
) -> BizPresignTransfer:
|
||||
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
||||
if not can_confirm_receipt(t.pay_status):
|
||||
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
|
||||
return _update_transfer_expected_status(
|
||||
db,
|
||||
transfer_id=transfer_id,
|
||||
tenant_no=tenant_no,
|
||||
expected_status="jfqrz",
|
||||
values={
|
||||
BizPresignTransfer.pay_status: "dfj",
|
||||
BizPresignTransfer.update_by: user_id,
|
||||
BizPresignTransfer.update_time: _now(),
|
||||
},
|
||||
commit=commit,
|
||||
)
|
||||
|
||||
|
||||
def list_presign_transfers_for_finance(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""移动市场接口的白名单请求 DTO。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import List, Literal, Optional
|
||||
@@ -45,6 +46,25 @@ class MarketPresignTransferBody(BaseModel):
|
||||
)
|
||||
remark: Optional[str] = Field(None, max_length=512)
|
||||
|
||||
@field_validator('transfer_time')
|
||||
@classmethod
|
||||
def validate_transfer_time(cls, value: Optional[str]) -> Optional[str]:
|
||||
if value in (None, ''):
|
||||
return value
|
||||
text = str(value).strip()
|
||||
if re.fullmatch(
|
||||
r'(?:\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
|
||||
r'|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}'
|
||||
r'(?::\d{2}(?:\.\d{1,6})?)?)',
|
||||
text,
|
||||
) is None:
|
||||
raise ValueError('转账时间格式无效')
|
||||
try:
|
||||
datetime.fromisoformat(text)
|
||||
except ValueError as exc:
|
||||
raise ValueError('转账时间格式无效') from exc
|
||||
return text
|
||||
|
||||
|
||||
class MarketPresignOrderBody(BaseModel):
|
||||
"""预签单可写字段;租户与财务字段不接受客户端输入。"""
|
||||
|
||||
@@ -8,7 +8,6 @@ from fastapi import HTTPException
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.utils import parse_datetime
|
||||
from app.crud import presign_order as presign_crud
|
||||
from app.models.models import (
|
||||
BizCellSampleType,
|
||||
@@ -498,24 +497,6 @@ def _update_transfer_if_status(
|
||||
raise ValueError('转账状态已变化,请刷新后重试')
|
||||
|
||||
|
||||
def _reload_transfer(
|
||||
db: Session, transfer_id: str, tenant_no: str
|
||||
) -> BizPresignTransfer:
|
||||
row = (
|
||||
db.query(BizPresignTransfer)
|
||||
.filter(
|
||||
BizPresignTransfer.id == transfer_id,
|
||||
BizPresignTransfer.tenant_no == tenant_no,
|
||||
BizPresignTransfer.del_flag == 0,
|
||||
)
|
||||
.populate_existing()
|
||||
.first()
|
||||
)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail='转账记录不存在')
|
||||
return row
|
||||
|
||||
|
||||
def create_presign_transfer(
|
||||
db: Session,
|
||||
order_id: str,
|
||||
@@ -577,43 +558,17 @@ def update_presign_transfer(
|
||||
existing_refs=current.transfer_attachments,
|
||||
business_id=current.presign_order_id,
|
||||
)
|
||||
values = {
|
||||
BizPresignTransfer.update_by: ctx.user_id,
|
||||
BizPresignTransfer.update_time: datetime.now(),
|
||||
}
|
||||
if 'transfer_amount' in fields:
|
||||
values[BizPresignTransfer.transfer_amount] = fields[
|
||||
'transfer_amount'
|
||||
]
|
||||
if 'transfer_time' in fields:
|
||||
raw_time = fields['transfer_time']
|
||||
values[BizPresignTransfer.transfer_time] = (
|
||||
None
|
||||
if raw_time in (None, '')
|
||||
else parse_datetime(str(raw_time))
|
||||
)
|
||||
if 'transfer_type' in fields:
|
||||
values[BizPresignTransfer.transfer_type] = (
|
||||
str(fields['transfer_type'] or '').strip() or None
|
||||
)
|
||||
if 'transfer_attachments' in fields:
|
||||
values[BizPresignTransfer.transfer_attachments] = (
|
||||
str(fields['transfer_attachments'] or '').strip() or None
|
||||
)
|
||||
if 'remark' in fields:
|
||||
values[BizPresignTransfer.remark] = (
|
||||
str(fields['remark'] or '').strip() or None
|
||||
)
|
||||
_update_transfer_if_status(
|
||||
row = presign_crud.update_transfer_draft(
|
||||
db,
|
||||
transfer_id=transfer_id,
|
||||
tenant_no=ctx.tenant_no,
|
||||
expected_status='dfj',
|
||||
values=values,
|
||||
user_id=ctx.user_id,
|
||||
fields=fields,
|
||||
commit=False,
|
||||
)
|
||||
result = presign_crud._transfer_to_dict(db, row, ctx.tenant_no)
|
||||
db.commit()
|
||||
row = _reload_transfer(db, transfer_id, ctx.tenant_no)
|
||||
return presign_crud._transfer_to_dict(db, row, ctx.tenant_no)
|
||||
return result
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
@@ -653,33 +608,17 @@ def submit_presign_transfer(
|
||||
user_info: dict,
|
||||
) -> dict:
|
||||
try:
|
||||
row = _scoped_transfer(db, transfer_id, ctx, user_info)
|
||||
if (row.pay_status or '').strip() != 'dfj':
|
||||
raise ValueError('仅待缴费状态可提交财务确认')
|
||||
if row.transfer_amount is None or row.transfer_amount <= 0:
|
||||
raise ValueError('请填写转账金额')
|
||||
if not (row.transfer_type or '').strip():
|
||||
raise ValueError('请选择转账方式')
|
||||
_update_transfer_if_status(
|
||||
_scoped_transfer(db, transfer_id, ctx, user_info)
|
||||
row = presign_crud.submit_receipt_finance(
|
||||
db,
|
||||
transfer_id=transfer_id,
|
||||
tenant_no=ctx.tenant_no,
|
||||
expected_status='dfj',
|
||||
values={
|
||||
BizPresignTransfer.pay_status: 'jfqrz',
|
||||
BizPresignTransfer.update_by: ctx.user_id,
|
||||
BizPresignTransfer.update_time: datetime.now(),
|
||||
},
|
||||
extra_conditions=(
|
||||
BizPresignTransfer.transfer_amount.isnot(None),
|
||||
BizPresignTransfer.transfer_amount > 0,
|
||||
BizPresignTransfer.transfer_type.isnot(None),
|
||||
func.length(func.trim(BizPresignTransfer.transfer_type)) > 0,
|
||||
),
|
||||
user_id=ctx.user_id,
|
||||
commit=False,
|
||||
)
|
||||
result = presign_crud._transfer_to_dict(db, row, ctx.tenant_no)
|
||||
db.commit()
|
||||
row = _reload_transfer(db, transfer_id, ctx.tenant_no)
|
||||
return presign_crud._transfer_to_dict(db, row, ctx.tenant_no)
|
||||
return result
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
@@ -692,23 +631,17 @@ def withdraw_presign_transfer(
|
||||
user_info: dict,
|
||||
) -> dict:
|
||||
try:
|
||||
row = _scoped_transfer(db, transfer_id, ctx, user_info)
|
||||
if (row.pay_status or '').strip() != 'jfqrz':
|
||||
raise ValueError('仅缴费确认中可撤回')
|
||||
_update_transfer_if_status(
|
||||
_scoped_transfer(db, transfer_id, ctx, user_info)
|
||||
row = presign_crud.withdraw_receipt(
|
||||
db,
|
||||
transfer_id=transfer_id,
|
||||
tenant_no=ctx.tenant_no,
|
||||
expected_status='jfqrz',
|
||||
values={
|
||||
BizPresignTransfer.pay_status: 'dfj',
|
||||
BizPresignTransfer.update_by: ctx.user_id,
|
||||
BizPresignTransfer.update_time: datetime.now(),
|
||||
},
|
||||
user_id=ctx.user_id,
|
||||
commit=False,
|
||||
)
|
||||
result = presign_crud._transfer_to_dict(db, row, ctx.tenant_no)
|
||||
db.commit()
|
||||
row = _reload_transfer(db, transfer_id, ctx.tenant_no)
|
||||
return presign_crud._transfer_to_dict(db, row, ctx.tenant_no)
|
||||
return result
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
@@ -1023,15 +1023,28 @@ def test_market_presign_bodies_forbid_unknown_fields_and_validate_transfer_limit
|
||||
valid = MarketPresignTransferBody.model_validate(
|
||||
{
|
||||
'transferAmount': '123.45',
|
||||
'transferTime': '2026-10-11 12:34:56',
|
||||
'transferType': 'bank',
|
||||
'transferAttachments': 'a' * 4000,
|
||||
'remark': 'r' * 512,
|
||||
}
|
||||
)
|
||||
assert str(valid.transfer_amount) == '123.45'
|
||||
assert valid.transfer_time == '2026-10-11 12:34:56'
|
||||
assert (
|
||||
MarketPresignTransferBody.model_validate(
|
||||
{'transferTime': '2026-10-11T12:34'}
|
||||
).transfer_time
|
||||
== '2026-10-11T12:34'
|
||||
)
|
||||
for payload in (
|
||||
{'unknown': 'value'},
|
||||
{'transferAmount': 0},
|
||||
{'transferTime': 'not-a-datetime'},
|
||||
{'transferTime': '2026-10-11'},
|
||||
{'transferTime': '2026-10-11 12:34'},
|
||||
{'transferTime': '2026-10-11T12'},
|
||||
{'transferTime': '2026-10-11T12:34+08:00'},
|
||||
{'transferType': 'x' * 17},
|
||||
{'transferAttachments': 'x' * 4001},
|
||||
{'remark': 'x' * 513},
|
||||
@@ -1673,6 +1686,205 @@ def test_stale_withdraw_does_not_overwrite_later_finance_confirmation(tmp_path):
|
||||
finance_db.close()
|
||||
|
||||
|
||||
def test_stale_finance_confirmation_does_not_overwrite_later_mobile_withdraw(
|
||||
tmp_path,
|
||||
):
|
||||
from app.crud import presign_order as presign_crud
|
||||
from app.services.market_presign import withdraw_presign_transfer
|
||||
|
||||
Session = _session_factory(
|
||||
f"sqlite:///{tmp_path / 'finance-confirm-after-withdraw-race.db'}"
|
||||
)
|
||||
finance_db = Session()
|
||||
market_db = Session()
|
||||
try:
|
||||
finance_db.add(_order('own', sales_user_id='u1'))
|
||||
finance_db.add(_transfer('transfer', pay_status='jfqrz'))
|
||||
finance_db.commit()
|
||||
stale = finance_db.query(BizPresignTransfer).filter_by(id='transfer').one()
|
||||
assert stale.pay_status == 'jfqrz'
|
||||
|
||||
withdrawn = withdraw_presign_transfer(
|
||||
market_db,
|
||||
transfer_id='transfer',
|
||||
ctx=_ctx('specialist'),
|
||||
user_info=_user(),
|
||||
)
|
||||
assert withdrawn['payStatus'] == 'dfj'
|
||||
|
||||
with pytest.raises(ValueError, match='状态'):
|
||||
presign_crud.confirm_receipt(
|
||||
finance_db,
|
||||
transfer_id='transfer',
|
||||
tenant_no='tenant-a',
|
||||
user_id='finance-user',
|
||||
)
|
||||
|
||||
market_db.expire_all()
|
||||
current = market_db.query(BizPresignTransfer).filter_by(id='transfer').one()
|
||||
assert current.pay_status == 'dfj'
|
||||
assert current.update_by == 'u1'
|
||||
finally:
|
||||
finance_db.close()
|
||||
market_db.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('action', ['update', 'submit'])
|
||||
def test_stale_web_draft_action_does_not_mutate_later_paid_transfer(
|
||||
tmp_path, action
|
||||
):
|
||||
from app.crud import presign_order as presign_crud
|
||||
|
||||
Session = _session_factory(
|
||||
f"sqlite:///{tmp_path / f'web-{action}-after-paid-race.db'}"
|
||||
)
|
||||
web_db = Session()
|
||||
finance_db = Session()
|
||||
try:
|
||||
web_db.add(_order('own', sales_user_id='u1'))
|
||||
transfer = _transfer('transfer', pay_status='dfj')
|
||||
transfer.remark = 'original'
|
||||
web_db.add(transfer)
|
||||
web_db.commit()
|
||||
stale = web_db.query(BizPresignTransfer).filter_by(id='transfer').one()
|
||||
assert stale.pay_status == 'dfj'
|
||||
|
||||
presign_crud.submit_receipt_finance(
|
||||
finance_db,
|
||||
transfer_id='transfer',
|
||||
tenant_no='tenant-a',
|
||||
user_id='finance-user',
|
||||
)
|
||||
presign_crud.confirm_receipt(
|
||||
finance_db,
|
||||
transfer_id='transfer',
|
||||
tenant_no='tenant-a',
|
||||
user_id='finance-user',
|
||||
)
|
||||
|
||||
calls = {
|
||||
'update': lambda: presign_crud.update_transfer_draft(
|
||||
web_db,
|
||||
transfer_id='transfer',
|
||||
tenant_no='tenant-a',
|
||||
user_id='web-user',
|
||||
fields={'remark': 'stale-write'},
|
||||
),
|
||||
'submit': lambda: presign_crud.submit_receipt_finance(
|
||||
web_db,
|
||||
transfer_id='transfer',
|
||||
tenant_no='tenant-a',
|
||||
user_id='web-user',
|
||||
),
|
||||
}
|
||||
with pytest.raises(ValueError, match='状态'):
|
||||
calls[action]()
|
||||
|
||||
finance_db.expire_all()
|
||||
current = finance_db.query(BizPresignTransfer).filter_by(id='transfer').one()
|
||||
assert current.pay_status == 'yjf'
|
||||
assert current.remark == 'original'
|
||||
assert current.update_by == 'finance-user'
|
||||
finally:
|
||||
web_db.close()
|
||||
finance_db.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('action', ['withdraw', 'confirm', 'reject'])
|
||||
def test_stale_shared_pending_action_does_not_mutate_later_draft_transfer(
|
||||
tmp_path, action
|
||||
):
|
||||
from app.crud import presign_order as presign_crud
|
||||
from app.services.market_presign import withdraw_presign_transfer
|
||||
|
||||
Session = _session_factory(
|
||||
f"sqlite:///{tmp_path / f'shared-{action}-after-withdraw-race.db'}"
|
||||
)
|
||||
stale_db = Session()
|
||||
market_db = Session()
|
||||
try:
|
||||
stale_db.add(_order('own', sales_user_id='u1'))
|
||||
stale_db.add(_transfer('transfer', pay_status='jfqrz'))
|
||||
stale_db.commit()
|
||||
stale = stale_db.query(BizPresignTransfer).filter_by(id='transfer').one()
|
||||
assert stale.pay_status == 'jfqrz'
|
||||
|
||||
withdraw_presign_transfer(
|
||||
market_db,
|
||||
transfer_id='transfer',
|
||||
ctx=_ctx('specialist'),
|
||||
user_info=_user(),
|
||||
)
|
||||
calls = {
|
||||
'withdraw': lambda: presign_crud.withdraw_receipt(
|
||||
stale_db,
|
||||
transfer_id='transfer',
|
||||
tenant_no='tenant-a',
|
||||
user_id='stale-user',
|
||||
),
|
||||
'confirm': lambda: presign_crud.confirm_receipt(
|
||||
stale_db,
|
||||
transfer_id='transfer',
|
||||
tenant_no='tenant-a',
|
||||
user_id='stale-user',
|
||||
),
|
||||
'reject': lambda: presign_crud.reject_receipt(
|
||||
stale_db,
|
||||
transfer_id='transfer',
|
||||
tenant_no='tenant-a',
|
||||
user_id='stale-user',
|
||||
),
|
||||
}
|
||||
with pytest.raises(ValueError, match='状态'):
|
||||
calls[action]()
|
||||
|
||||
market_db.expire_all()
|
||||
current = market_db.query(BizPresignTransfer).filter_by(id='transfer').one()
|
||||
assert current.pay_status == 'dfj'
|
||||
assert current.update_by == 'u1'
|
||||
finally:
|
||||
stale_db.close()
|
||||
market_db.close()
|
||||
|
||||
|
||||
def test_mobile_transfer_update_serializes_before_commit(monkeypatch):
|
||||
from app.crud import presign_order as presign_crud
|
||||
from app.schemas.market_mobile import MarketPresignTransferBody
|
||||
from app.services.market_presign import update_presign_transfer
|
||||
|
||||
db = _session()
|
||||
db.add(_order('own', sales_user_id='u1'))
|
||||
db.add(_transfer('transfer', pay_status='dfj'))
|
||||
db.commit()
|
||||
|
||||
committed = False
|
||||
original_commit = db.commit
|
||||
original_to_dict = presign_crud._transfer_to_dict
|
||||
|
||||
def tracked_commit():
|
||||
nonlocal committed
|
||||
committed = True
|
||||
original_commit()
|
||||
|
||||
def snapshot_before_commit(*args, **kwargs):
|
||||
assert not committed
|
||||
return original_to_dict(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(db, 'commit', tracked_commit)
|
||||
monkeypatch.setattr(presign_crud, '_transfer_to_dict', snapshot_before_commit)
|
||||
|
||||
result = update_presign_transfer(
|
||||
db,
|
||||
transfer_id='transfer',
|
||||
body=MarketPresignTransferBody(remark='updated'),
|
||||
ctx=_ctx('specialist'),
|
||||
user_info=_user(),
|
||||
)
|
||||
|
||||
assert committed
|
||||
assert result['remark'] == 'updated'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('action', ['update', 'delete', 'submit'])
|
||||
def test_stale_dfj_action_does_not_mutate_later_paid_transfer(tmp_path, action):
|
||||
from app.crud import presign_order as presign_crud
|
||||
|
||||
@@ -112,7 +112,16 @@ class TestPresignWithdrawReceipt(unittest.TestCase):
|
||||
)
|
||||
db = MagicMock()
|
||||
orig = po._get_transfer_or_raise
|
||||
orig_update = po._update_transfer_expected_status
|
||||
po._get_transfer_or_raise = lambda *a, **k: t
|
||||
|
||||
def atomic_update(*args, **kwargs):
|
||||
for column, value in kwargs["values"].items():
|
||||
setattr(t, column.key, value)
|
||||
db.commit()
|
||||
return t
|
||||
|
||||
po._update_transfer_expected_status = atomic_update
|
||||
try:
|
||||
out = po.withdraw_receipt(
|
||||
db, transfer_id="t1", tenant_no="zkxw", user_id="u1"
|
||||
@@ -123,6 +132,7 @@ class TestPresignWithdrawReceipt(unittest.TestCase):
|
||||
db.commit.assert_called()
|
||||
finally:
|
||||
po._get_transfer_or_raise = orig
|
||||
po._update_transfer_expected_status = orig_update
|
||||
|
||||
def test_withdraw_rejects_non_jfqrz(self):
|
||||
from types import SimpleNamespace
|
||||
|
||||
Reference in new issue
Block a user