Просмотр исходного кода

fix: make mobile presign transitions concurrency-safe.

zlwu 3 недель назад
Родитель
Сommit
34bf9aa0dc

+ 150 - 46
backend/app/crud/presign_order.py

@@ -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(

+ 20 - 0
backend/app/schemas/market_mobile.py

@@ -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):
     """预签单可写字段;租户与财务字段不接受客户端输入。"""

+ 18 - 85
backend/app/services/market_presign.py

@@ -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

+ 212 - 0
backend/tests/test_market_presign_api.py

@@ -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

+ 10 - 0
backend/tests/test_presign_order.py

@@ -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