feat: add protocol-level refund registration API
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
aed7a29c1f
commit
2ff5bc09f9
6 files changed
+429
-1
No files matched your search
@@ -6,6 +6,7 @@ from app.api.workflow import router as workflow_router
|
||||
from app.api.basic_info import router as basic_info_router
|
||||
from app.api.collection_protocol import router as collection_protocol_router
|
||||
from app.api.protocol_sample_payment import router as protocol_sample_payment_router
|
||||
from app.api.protocol_refund_record import router as protocol_refund_record_router
|
||||
from app.api.leave import router as leave_router
|
||||
from app.api.operations import router as operations_router
|
||||
from app.api.ethnic import router as ethnic_router
|
||||
@@ -46,6 +47,7 @@ __all__ = [
|
||||
"basic_info_router",
|
||||
"collection_protocol_router",
|
||||
"protocol_sample_payment_router",
|
||||
"protocol_refund_record_router",
|
||||
"leave_router",
|
||||
"operations_router",
|
||||
"ethnic_router",
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
"""协议级退费登记 API"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.menu_guard import require_admin_or_super_admin
|
||||
from app.core.response import error_response, success_response
|
||||
from app.crud import protocol_refund_record as rr_svc
|
||||
from app.models.models import (
|
||||
BizCollectionProtocol,
|
||||
BizProtocolRefundRecord,
|
||||
SysUser,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/web/admin", tags=["细胞存储-退费登记"])
|
||||
|
||||
|
||||
def _get_tenant_no(db: Session, user: dict) -> str:
|
||||
user_id = user.get("user_id")
|
||||
if not user_id:
|
||||
raise PermissionError("用户身份信息缺失,无法确定租户")
|
||||
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
||||
if not sys_user or not sys_user.tenant_no:
|
||||
raise PermissionError("用户租户信息缺失")
|
||||
return sys_user.tenant_no
|
||||
|
||||
|
||||
def _get_current_user_id(user: dict) -> str:
|
||||
user_id = user.get("user_id")
|
||||
if not user_id:
|
||||
raise PermissionError("用户身份信息缺失")
|
||||
return str(user_id)
|
||||
|
||||
|
||||
def _protocol_accessible(db: Session, protocol_id: str, tenant_no: str, user: dict) -> bool:
|
||||
from app.core.data_scope import apply_data_scope
|
||||
|
||||
q = db.query(BizCollectionProtocol).filter(
|
||||
BizCollectionProtocol.id == protocol_id,
|
||||
BizCollectionProtocol.del_flag == 0,
|
||||
BizCollectionProtocol.tenant_no == tenant_no,
|
||||
)
|
||||
q = apply_data_scope(q, BizCollectionProtocol, user, db, tenant_no)
|
||||
return q.first() is not None
|
||||
|
||||
|
||||
class RefundRecordBody(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
refund_amount: float = Field(..., alias="refundAmount")
|
||||
refund_time: Optional[str] = Field(None, alias="refundTime")
|
||||
refund_remark: Optional[str] = Field(None, alias="refundRemark")
|
||||
refund_attachments: Optional[str] = Field(None, alias="refundAttachments")
|
||||
workflow_instance_id: Optional[str] = Field(None, alias="workflowInstanceId")
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class RefundRecordUpdateBody(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
refund_amount: Optional[float] = Field(None, alias="refundAmount")
|
||||
refund_time: Optional[str] = Field(None, alias="refundTime")
|
||||
refund_remark: Optional[str] = Field(None, alias="refundRemark")
|
||||
refund_attachments: Optional[str] = Field(None, alias="refundAttachments")
|
||||
workflow_instance_id: Optional[str] = Field(None, alias="workflowInstanceId")
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/collection-protocols/{protocol_id}/refund-records")
|
||||
async def list_protocol_refund_records(
|
||||
protocol_id: str,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||
return error_response(404, "协议单不存在或无权查看")
|
||||
data = rr_svc.list_refund_records(
|
||||
db, collection_protocol_id=protocol_id, tenant_no=tenant_no
|
||||
)
|
||||
return success_response(data)
|
||||
except PermissionError as e:
|
||||
return error_response(403, str(e))
|
||||
|
||||
|
||||
@router.post("/collection-protocols/{protocol_id}/refund-records")
|
||||
async def create_protocol_refund_record(
|
||||
protocol_id: str,
|
||||
body: RefundRecordBody,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
user_id = _get_current_user_id(user)
|
||||
if not _protocol_accessible(db, protocol_id, tenant_no, user):
|
||||
return error_response(404, "协议单不存在或无权操作")
|
||||
obj = rr_svc.create_refund_record(
|
||||
db,
|
||||
collection_protocol_id=protocol_id,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user_id,
|
||||
refund_amount=body.refund_amount,
|
||||
refund_time=body.refund_time,
|
||||
refund_remark=body.refund_remark,
|
||||
refund_attachments=body.refund_attachments,
|
||||
workflow_instance_id=body.workflow_instance_id,
|
||||
remark=body.remark,
|
||||
)
|
||||
return success_response(
|
||||
rr_svc._record_to_dict(db, obj, tenant_no), message="登记成功"
|
||||
)
|
||||
except PermissionError as e:
|
||||
return error_response(403, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
|
||||
|
||||
@router.put("/protocol-refund-records/{record_id}")
|
||||
async def update_protocol_refund_record(
|
||||
record_id: str,
|
||||
body: RefundRecordUpdateBody,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
user_id = _get_current_user_id(user)
|
||||
existing = (
|
||||
db.query(BizProtocolRefundRecord)
|
||||
.filter(
|
||||
BizProtocolRefundRecord.id == record_id,
|
||||
BizProtocolRefundRecord.tenant_no == tenant_no,
|
||||
BizProtocolRefundRecord.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not existing:
|
||||
return error_response(404, "退费登记不存在")
|
||||
if not _protocol_accessible(
|
||||
db, existing.collection_protocol_id, tenant_no, user
|
||||
):
|
||||
return error_response(403, "无权操作该协议")
|
||||
dumped = body.model_dump(by_alias=False, exclude_unset=True)
|
||||
obj = rr_svc.update_refund_record(
|
||||
db,
|
||||
record_id=record_id,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user_id,
|
||||
refund_amount=dumped.get("refund_amount"),
|
||||
refund_time=dumped.get("refund_time"),
|
||||
refund_remark=dumped.get("refund_remark"),
|
||||
refund_attachments=dumped.get("refund_attachments"),
|
||||
workflow_instance_id=dumped.get("workflow_instance_id"),
|
||||
remark=dumped.get("remark"),
|
||||
fields_set=set(dumped.keys()),
|
||||
)
|
||||
return success_response(
|
||||
rr_svc._record_to_dict(db, obj, tenant_no), message="保存成功"
|
||||
)
|
||||
except PermissionError as e:
|
||||
return error_response(403, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
|
||||
|
||||
@router.delete("/protocol-refund-records/{record_id}")
|
||||
async def delete_protocol_refund_record(
|
||||
record_id: str,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
user_id = _get_current_user_id(user)
|
||||
existing = (
|
||||
db.query(BizProtocolRefundRecord)
|
||||
.filter(
|
||||
BizProtocolRefundRecord.id == record_id,
|
||||
BizProtocolRefundRecord.tenant_no == tenant_no,
|
||||
BizProtocolRefundRecord.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not existing:
|
||||
return error_response(404, "退费登记不存在")
|
||||
if not _protocol_accessible(
|
||||
db, existing.collection_protocol_id, tenant_no, user
|
||||
):
|
||||
return error_response(403, "无权操作该协议")
|
||||
rr_svc.soft_delete_refund_record(
|
||||
db, record_id=record_id, tenant_no=tenant_no, user_id=user_id
|
||||
)
|
||||
return success_response(None, message="删除成功")
|
||||
except PermissionError as e:
|
||||
return error_response(403, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
@@ -0,0 +1,182 @@
|
||||
"""采集协议单 — 协议级退费登记 CRUD(不改转账笔 pay_status)"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
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 BizProtocolRefundRecord
|
||||
from app.services.protocol_refund_record import validate_refund_amount
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def _record_to_dict(
|
||||
db: Session, r: BizProtocolRefundRecord, tenant_no: str
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": r.id,
|
||||
"collectionProtocolId": r.collection_protocol_id,
|
||||
"refundAmount": float(r.refund_amount) if r.refund_amount is not None else None,
|
||||
"refundTime": r.refund_time.strftime("%Y-%m-%d %H:%M:%S") if r.refund_time else "",
|
||||
"refundRemark": r.refund_remark or "",
|
||||
"refundAttachments": r.refund_attachments or "",
|
||||
"refundAttachmentFiles": urls_to_attachment_list(
|
||||
r.refund_attachments, db, tenant_no
|
||||
),
|
||||
"workflowInstanceId": r.workflow_instance_id or "",
|
||||
"remark": r.remark or "",
|
||||
"createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
|
||||
"updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
|
||||
}
|
||||
|
||||
|
||||
def list_refund_records(
|
||||
db: Session, *, collection_protocol_id: str, tenant_no: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
rows = (
|
||||
db.query(BizProtocolRefundRecord)
|
||||
.filter(
|
||||
BizProtocolRefundRecord.collection_protocol_id == collection_protocol_id,
|
||||
BizProtocolRefundRecord.tenant_no == tenant_no,
|
||||
BizProtocolRefundRecord.del_flag == 0,
|
||||
)
|
||||
.order_by(BizProtocolRefundRecord.create_time.desc())
|
||||
.all()
|
||||
)
|
||||
return [_record_to_dict(db, r, tenant_no) for r in rows]
|
||||
|
||||
|
||||
def create_refund_record(
|
||||
db: Session,
|
||||
*,
|
||||
collection_protocol_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
refund_amount: Any,
|
||||
refund_time: Any = None,
|
||||
refund_remark: Optional[str] = None,
|
||||
refund_attachments: Optional[str] = None,
|
||||
workflow_instance_id: Optional[str] = None,
|
||||
remark: Optional[str] = None,
|
||||
) -> BizProtocolRefundRecord:
|
||||
amt = validate_refund_amount(refund_amount)
|
||||
now = _now()
|
||||
rt = None
|
||||
if refund_time not in (None, ""):
|
||||
rt = (
|
||||
refund_time
|
||||
if isinstance(refund_time, datetime)
|
||||
else parse_datetime(str(refund_time))
|
||||
)
|
||||
obj = BizProtocolRefundRecord(
|
||||
id=generate_uuid(),
|
||||
collection_protocol_id=collection_protocol_id,
|
||||
refund_amount=amt,
|
||||
refund_time=rt or now,
|
||||
refund_remark=(refund_remark or "").strip() or None,
|
||||
refund_attachments=(refund_attachments or "").strip() or None,
|
||||
workflow_instance_id=(workflow_instance_id 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.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def update_refund_record(
|
||||
db: Session,
|
||||
*,
|
||||
record_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
refund_amount: Any = None,
|
||||
refund_time: Any = None,
|
||||
refund_remark: Optional[str] = None,
|
||||
refund_attachments: Optional[str] = None,
|
||||
workflow_instance_id: Optional[str] = None,
|
||||
remark: Optional[str] = None,
|
||||
fields_set: Optional[set] = None,
|
||||
) -> BizProtocolRefundRecord:
|
||||
obj = (
|
||||
db.query(BizProtocolRefundRecord)
|
||||
.filter(
|
||||
BizProtocolRefundRecord.id == record_id,
|
||||
BizProtocolRefundRecord.tenant_no == tenant_no,
|
||||
BizProtocolRefundRecord.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not obj:
|
||||
raise ValueError("退费登记不存在")
|
||||
fs = fields_set or {
|
||||
"refund_amount",
|
||||
"refund_time",
|
||||
"refund_remark",
|
||||
"refund_attachments",
|
||||
"workflow_instance_id",
|
||||
"remark",
|
||||
}
|
||||
if "refund_amount" in fs and refund_amount is not None:
|
||||
obj.refund_amount = validate_refund_amount(refund_amount)
|
||||
if "refund_time" in fs:
|
||||
if refund_time in (None, ""):
|
||||
obj.refund_time = None
|
||||
elif isinstance(refund_time, datetime):
|
||||
obj.refund_time = refund_time
|
||||
else:
|
||||
obj.refund_time = parse_datetime(str(refund_time))
|
||||
if "refund_remark" in fs:
|
||||
obj.refund_remark = (refund_remark or "").strip() or None
|
||||
if "refund_attachments" in fs:
|
||||
obj.refund_attachments = (refund_attachments or "").strip() or None
|
||||
if "workflow_instance_id" in fs:
|
||||
obj.workflow_instance_id = (workflow_instance_id or "").strip() or None
|
||||
if "remark" in fs:
|
||||
obj.remark = (remark or "").strip() or None
|
||||
obj.update_by = user_id
|
||||
obj.update_time = _now()
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def soft_delete_refund_record(
|
||||
db: Session,
|
||||
*,
|
||||
record_id: str,
|
||||
tenant_no: str,
|
||||
user_id: Optional[str],
|
||||
) -> None:
|
||||
obj = (
|
||||
db.query(BizProtocolRefundRecord)
|
||||
.filter(
|
||||
BizProtocolRefundRecord.id == record_id,
|
||||
BizProtocolRefundRecord.tenant_no == tenant_no,
|
||||
BizProtocolRefundRecord.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not obj:
|
||||
raise ValueError("退费登记不存在")
|
||||
obj.del_flag = 1
|
||||
obj.update_by = user_id
|
||||
obj.update_time = _now()
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
+2
-1
@@ -13,7 +13,7 @@ from app.core.database import SessionLocal
|
||||
from app.core.api_request_log_middleware import ApiRequestLogMiddleware
|
||||
from app.core.api_crypto_middleware import ApiCryptoMiddleware
|
||||
from app.models import models # noqa: F401
|
||||
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router
|
||||
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -118,6 +118,7 @@ app.include_router(workflow_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(basic_info_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(collection_protocol_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(protocol_sample_payment_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(protocol_refund_record_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(leave_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(operations_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(ethnic_router, prefix=settings.API_V1_PREFIX)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"""协议级退费登记金额校验"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
|
||||
def validate_refund_amount(amount: Any) -> Decimal:
|
||||
if amount is None or amount == "":
|
||||
raise ValueError("请填写退费金额")
|
||||
try:
|
||||
dec = Decimal(str(amount))
|
||||
except Exception as e:
|
||||
raise ValueError("退费金额无效") from e
|
||||
if dec <= 0:
|
||||
raise ValueError("退费金额必须大于 0")
|
||||
return dec
|
||||
@@ -0,0 +1,20 @@
|
||||
"""协议级退费登记校验单测"""
|
||||
|
||||
import unittest
|
||||
|
||||
from app.services.protocol_refund_record import validate_refund_amount
|
||||
|
||||
|
||||
class TestValidateRefundAmount(unittest.TestCase):
|
||||
def test_refund_amount_must_be_positive(self):
|
||||
with self.assertRaises(ValueError):
|
||||
validate_refund_amount(0)
|
||||
with self.assertRaises(ValueError):
|
||||
validate_refund_amount(-1)
|
||||
with self.assertRaises(ValueError):
|
||||
validate_refund_amount(None)
|
||||
self.assertEqual(validate_refund_amount(0.01), validate_refund_amount("0.01"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user