From e0dc7f4ceedb17e16fe785fab166addab23cda56 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sun, 11 Oct 2026 13:50:47 +0800 Subject: [PATCH] fix: preserve mobile presign decimal precision. --- backend/app/services/market_presign.py | 81 ++++++++++++-- backend/tests/test_market_presign_api.py | 134 ++++++++++++++++++++++- 2 files changed, 206 insertions(+), 9 deletions(-) diff --git a/backend/app/services/market_presign.py b/backend/app/services/market_presign.py index 4eac38cd..dce013c3 100644 --- a/backend/app/services/market_presign.py +++ b/backend/app/services/market_presign.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime +from decimal import Decimal from typing import List, Optional, Tuple from fastapi import HTTPException @@ -29,6 +30,70 @@ from app.services.market_field_scope import ( from app.services.market_support import validate_attachment_refs +_MONEY_QUANTUM = Decimal('0.01') + + +def _money_text(value: Optional[Decimal]) -> Optional[str]: + if value is None: + return None + return format(Decimal(str(value)).quantize(_MONEY_QUANTUM), 'f') + + +def _mobile_transfer_to_dict( + db: Session, row: BizPresignTransfer, tenant_no: str +) -> dict: + data = presign_crud._transfer_to_dict(db, row, tenant_no) + data['transferAmount'] = _money_text(row.transfer_amount) + return data + + +def _mobile_order_to_dict( + db: Session, + row: BizPresignOrder, + tenant_no: str, + *, + with_children: bool, + deposit_amount: Optional[Decimal] = None, + user_name_map: Optional[dict] = None, +) -> dict: + deposit = ( + deposit_amount + if deposit_amount is not None + else presign_crud.confirmed_deposit_amount( + db, presign_order_id=row.id, tenant_no=tenant_no + ) + ) + data = presign_crud.order_to_dict( + db, + row, + tenant_no, + with_children=False, + deposit_amount=deposit, + user_name_map=user_name_map, + ) + data['contractAmount'] = _money_text(row.contract_amount) + data['confirmedDepositAmount'] = _money_text(deposit) + if with_children: + data['sampleTypeIds'] = presign_crud.list_sample_type_ids( + db, presign_order_id=row.id, tenant_no=tenant_no + ) + transfer_rows = ( + db.query(BizPresignTransfer) + .filter( + BizPresignTransfer.presign_order_id == row.id, + BizPresignTransfer.tenant_no == tenant_no, + BizPresignTransfer.del_flag == 0, + ) + .order_by(BizPresignTransfer.create_time.asc()) + .all() + ) + data['transfers'] = [ + _mobile_transfer_to_dict(db, transfer, tenant_no) + for transfer in transfer_rows + ] + return data + + def _dump_body(body: MarketPresignOrderBody) -> dict: return body.model_dump(by_alias=False, exclude_unset=True) @@ -236,7 +301,7 @@ def list_presign_orders( db, person_ids, ctx.tenant_no ) return [ - presign_crud.order_to_dict( + _mobile_order_to_dict( db, row, ctx.tenant_no, @@ -258,7 +323,7 @@ def get_presign_order( row = assert_presign_access( db, order_id, ctx, user_info=user_info ) - return presign_crud.order_to_dict( + return _mobile_order_to_dict( db, row, ctx.tenant_no, with_children=True ) @@ -347,7 +412,7 @@ def create_presign_order( except Exception: db.rollback() raise - return presign_crud.order_to_dict( + return _mobile_order_to_dict( db, row, ctx.tenant_no, with_children=True ) @@ -414,7 +479,7 @@ def update_presign_order( row = assert_presign_access( db, row.id, ctx, user_info=user_info ) - result = presign_crud.order_to_dict( + result = _mobile_order_to_dict( db, row, ctx.tenant_no, with_children=True ) db.commit() @@ -538,7 +603,7 @@ def create_presign_transfer( create_dept_id=_current_dept_id(user_info), commit=False, ) - result = presign_crud._transfer_to_dict(db, row, ctx.tenant_no) + result = _mobile_transfer_to_dict(db, row, ctx.tenant_no) db.commit() return result except Exception: @@ -576,7 +641,7 @@ def update_presign_transfer( fields=fields, commit=False, ) - result = presign_crud._transfer_to_dict(db, row, ctx.tenant_no) + result = _mobile_transfer_to_dict(db, row, ctx.tenant_no) db.commit() return result except Exception: @@ -626,7 +691,7 @@ def submit_presign_transfer( user_id=ctx.user_id, commit=False, ) - result = presign_crud._transfer_to_dict(db, row, ctx.tenant_no) + result = _mobile_transfer_to_dict(db, row, ctx.tenant_no) db.commit() return result except Exception: @@ -649,7 +714,7 @@ def withdraw_presign_transfer( user_id=ctx.user_id, commit=False, ) - result = presign_crud._transfer_to_dict(db, row, ctx.tenant_no) + result = _mobile_transfer_to_dict(db, row, ctx.tenant_no) db.commit() return result except Exception: diff --git a/backend/tests/test_market_presign_api.py b/backend/tests/test_market_presign_api.py index ec1ca55d..0a0e3535 100644 --- a/backend/tests/test_market_presign_api.py +++ b/backend/tests/test_market_presign_api.py @@ -1437,7 +1437,7 @@ def test_dfj_presign_transfer_can_edit_submit_withdraw_and_soft_delete(): ctx=_ctx('specialist'), user_info=_user(), ) - assert updated['transferAmount'] == 200.25 + assert updated['transferAmount'] == '200.25' assert updated['remark'] == '更新' assert ( db.query(BizPresignTransfer).filter_by(id=transfer_id).one().create_dept_id @@ -2172,3 +2172,135 @@ def test_market_presign_routes_are_fixed_and_expose_scoped_transfer_actions(): } ) assert transfer_actual == transfer_expected + + +def test_mobile_presign_money_dto_preserves_decimal_strings_for_presenter( + monkeypatch, +): + import json + import subprocess + from decimal import Decimal + from pathlib import Path + + from app.crud import presign_order as presign_crud + from app.services import market_presign + + db = _session() + normal = _order('money-normal', sales_user_id='u1') + normal.contract_amount = Decimal('1234.50') + empty = _order('money-empty', sales_user_id='u1') + empty.contract_amount = None + paid = _transfer( + 'money-paid', order_id=normal.id, pay_status='yjf' + ) + paid.transfer_amount = Decimal('200.25') + draft = _transfer( + 'money-draft', order_id=normal.id, pay_status='dfj' + ) + draft.transfer_amount = Decimal('12.30') + db.add_all([normal, empty, paid, draft]) + db.commit() + + items, total = market_presign.list_presign_orders( + db, + ctx=_ctx('specialist'), + user_info=_user(), + page=1, + page_size=10, + ) + by_id = {item['id']: item for item in items} + assert total == 2 + assert by_id['money-normal']['contractAmount'] == '1234.50' + assert by_id['money-normal']['confirmedDepositAmount'] == '200.25' + assert by_id['money-empty']['contractAmount'] is None + assert by_id['money-empty']['confirmedDepositAmount'] == '0.00' + + normal_detail = market_presign.get_presign_order( + db, + order_id=normal.id, + ctx=_ctx('specialist'), + user_info=_user(), + ) + transfer_amounts = { + transfer['id']: transfer['transferAmount'] + for transfer in normal_detail['transfers'] + } + assert transfer_amounts == { + 'money-paid': '200.25', + 'money-draft': '12.30', + } + + maximum = Decimal('9999999999999999.99') + maximum_order = _order('money-maximum', sales_user_id='u1') + maximum_order.contract_amount = maximum + monkeypatch.setattr( + market_presign, + 'assert_presign_access', + lambda *_args, **_kwargs: maximum_order, + ) + monkeypatch.setattr( + presign_crud, + 'confirmed_deposit_amount', + lambda *_args, **_kwargs: maximum, + ) + maximum_detail = market_presign.get_presign_order( + db, + order_id=maximum_order.id, + ctx=_ctx('specialist'), + user_info=_user(), + ) + assert maximum_detail['contractAmount'] == str(maximum) + assert maximum_detail['confirmedDepositAmount'] == str(maximum) + maximum_transfer = _transfer( + 'money-maximum-transfer', order_id=maximum_order.id + ) + maximum_transfer.transfer_amount = maximum + maximum_transfer_dto = market_presign._mobile_transfer_to_dict( + db, maximum_transfer, 'tenant-a' + ) + assert maximum_transfer_dto['transferAmount'] == str(maximum) + + serialized = json.dumps( + { + 'maximum': maximum_detail, + 'maximumTransfer': maximum_transfer_dto, + 'normal': normal_detail, + 'empty': by_id['money-empty'], + }, + ensure_ascii=False, + ) + presenter_url = ( + Path(__file__).resolve().parents[2] + / 'mobile-oa-h5/src/market/presign/presign-presenter.js' + ).as_uri() + script = """ +import fs from 'node:fs' +const { formatPresignMoney } = await import(process.argv[1]) +const dto = JSON.parse(fs.readFileSync(0, 'utf8')) +const values = [ + dto.maximum.contractAmount, + dto.maximum.confirmedDepositAmount, + dto.maximumTransfer.transferAmount, + dto.normal.contractAmount, + dto.normal.transfers.find((item) => item.id === 'money-paid').transferAmount, + dto.empty.contractAmount, + dto.empty.confirmedDepositAmount +] +process.stdout.write(JSON.stringify(values.map(formatPresignMoney))) +""" + formatted = subprocess.run( + ['node', '--input-type=module', '-e', script, presenter_url], + input=serialized, + text=True, + capture_output=True, + check=True, + ) + assert json.loads(formatted.stdout) == [ + '¥9,999,999,999,999,999.99', + '¥9,999,999,999,999,999.99', + '¥9,999,999,999,999,999.99', + '¥1,234.50', + '¥200.25', + '-', + '¥0.00', + ]