From 468a7c5e12fa90ce65b7ff1031af3e55ec9947c8 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Thu, 8 Oct 2026 07:48:41 +0800 Subject: [PATCH] feat(backend): expose sample freight APIs and wire protocol saves Register admin sample-freight routes, enrich protocol sample details with freight, and stop writing legacy transport rows on save. Co-authored-by: Cursor --- backend/app/api/__init__.py | 2 + backend/app/api/sample_freight.py | 210 ++++++++++++++++++++++++ backend/app/crud/collection_protocol.py | 136 ++++++++++++++- backend/app/crud/research_donation.py | 49 +++++- backend/app/main.py | 3 +- 5 files changed, 385 insertions(+), 15 deletions(-) create mode 100644 backend/app/api/sample_freight.py diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index 3fea9dbd..bd2313e1 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -8,6 +8,7 @@ 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.presign_order import router as presign_order_router +from app.api.sample_freight import router as sample_freight_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 @@ -50,6 +51,7 @@ __all__ = [ "protocol_sample_payment_router", "protocol_refund_record_router", "presign_order_router", + "sample_freight_router", "leave_router", "operations_router", "ethnic_router", diff --git a/backend/app/api/sample_freight.py b/backend/app/api/sample_freight.py new file mode 100644 index 00000000..063f62fd --- /dev/null +++ b/backend/app/api/sample_freight.py @@ -0,0 +1,210 @@ +"""样本运费批次 API""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from fastapi import APIRouter, Depends, Query +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 sample_freight as freight_svc +from app.models.models import 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) + + +class SampleFreightItemBody(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + sample_source: str = Field(..., alias="sampleSource") + sample_id: str = Field(..., alias="sampleId") + protocol_id: Optional[str] = Field(None, alias="protocolId") + sample_code: Optional[str] = Field(None, alias="sampleCode") + + +class SampleFreightCreateBody(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + transport_mode: str = Field(..., alias="transportMode") + freight_amount: float = Field(..., alias="freightAmount") + items: Optional[List[SampleFreightItemBody]] = None + + +class SampleFreightUpdateBody(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + transport_mode: Optional[str] = Field(None, alias="transportMode") + freight_amount: Optional[float] = Field(None, alias="freightAmount") + + +class SampleFreightItemsBody(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + items: List[SampleFreightItemBody] = Field(default_factory=list) + + +@router.get("/sample-freights") +async def list_sample_freights( + page: int = Query(1, ge=1), + pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), + keyword: Optional[str] = None, + user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + try: + tenant_no = _get_tenant_no(db, user) + items, total = freight_svc.list_freights( + db, + tenant_no=tenant_no, + page=page, + page_size=pageSize, + keyword=keyword, + ) + return success_response( + {"list": items, "total": total, "page": page, "pageSize": pageSize} + ) + except PermissionError as e: + return error_response(403, str(e)) + + +@router.get("/sample-freights/{freight_id}") +async def get_sample_freight( + freight_id: str, + user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + try: + tenant_no = _get_tenant_no(db, user) + data = freight_svc.get_freight_detail(db, freight_id, tenant_no) + return success_response(data) + except PermissionError as e: + return error_response(403, str(e)) + except ValueError as e: + return error_response(404, str(e)) + + +@router.post("/sample-freights") +async def create_sample_freight( + body: SampleFreightCreateBody, + 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) + items = [it.model_dump(by_alias=True) for it in (body.items or [])] + data = freight_svc.create_freight( + db, + tenant_no=tenant_no, + user_id=user_id, + transport_mode_raw=body.transport_mode, + freight_amount=body.freight_amount, + items=items, + ) + return success_response(data, message="创建成功") + except PermissionError as e: + return error_response(403, str(e)) + except ValueError as e: + return error_response(400, str(e)) + + +@router.put("/sample-freights/{freight_id}") +async def update_sample_freight( + freight_id: str, + body: SampleFreightUpdateBody, + 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) + data = freight_svc.update_freight( + db, + freight_id, + tenant_no=tenant_no, + user_id=user_id, + transport_mode_raw=body.transport_mode, + freight_amount=body.freight_amount, + ) + return success_response(data, message="更新成功") + except PermissionError as e: + return error_response(403, str(e)) + except ValueError as e: + msg = str(e) + code = 404 if "不存在" in msg else 400 + return error_response(code, msg) + + +@router.post("/sample-freights/{freight_id}/link") +async def link_sample_freight( + freight_id: str, + body: SampleFreightItemsBody, + 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) + items = [it.model_dump(by_alias=True) for it in (body.items or [])] + data = freight_svc.link_samples( + db, + freight_id, + tenant_no=tenant_no, + user_id=user_id, + items=items, + ) + return success_response(data, message="关联成功") + except PermissionError as e: + return error_response(403, str(e)) + except ValueError as e: + msg = str(e) + code = 404 if "不存在" in msg else 400 + return error_response(code, msg) + + +@router.post("/sample-freights/{freight_id}/unlink") +async def unlink_sample_freight( + freight_id: str, + body: SampleFreightItemsBody, + 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) + items = [it.model_dump(by_alias=True) for it in (body.items or [])] + data = freight_svc.unlink_samples( + db, + freight_id, + tenant_no=tenant_no, + user_id=user_id, + items=items, + ) + return success_response(data, message="已取消关联") + except PermissionError as e: + return error_response(403, str(e)) + except ValueError as e: + msg = str(e) + code = 404 if "不存在" in msg else 400 + return error_response(code, msg) diff --git a/backend/app/crud/collection_protocol.py b/backend/app/crud/collection_protocol.py index 5232b8a6..6fbf0ed7 100644 --- a/backend/app/crud/collection_protocol.py +++ b/backend/app/crud/collection_protocol.py @@ -607,12 +607,17 @@ def _transport_to_dict(t: BizProtocolSampleTransport) -> Dict[str, Any]: def _storage_cert_to_dict(s: BizProtocolStorageCert) -> Dict[str, Any]: + from app.services.sample_info_progress import storage_cert_handled + return { "id": s.id, "collectionProtocolId": s.collection_protocol_id, "makeStatus": s.make_status or "0", "certImageUrls": s.cert_image_urls or "", "makeDate": (s.make_date or "").strip() if s.make_date else "", + "handleTime": _fmt_dt(s.handle_time) if getattr(s, "handle_time", None) else "", + "handlerUserId": getattr(s, "handler_user_id", None) or "", + "handled": storage_cert_handled(s), "tenantNo": s.tenant_no, "orderNum": s.order_num if s.order_num is not None else 0, "remark": s.remark or "", @@ -1348,12 +1353,21 @@ def get_protocol_detail_bundle( ) unit_name_map = _load_unit_name_map(db, s_rows) type_name_map = _load_sample_type_name_map(db, s_rows) + from app.crud import sample_freight as sample_freight_svc + + freight_by_sample = sample_freight_svc.freight_map_for_samples( + db, + tenant_no=tenant_no, + sample_source=sample_freight_svc.SAMPLE_SOURCE_CELL, + sample_ids=[s.id for s in s_rows], + ) samples_out: List[Dict[str, Any]] = [] for s in s_rows: item = _sample_to_dict(s) item["sampleTypeName"] = type_name_map.get(s.sample_type_id or "", "") item["unitName"] = unit_name_map.get(s.unit_id or "", "") item["transport"] = transport_by_sample.get(s.id) + item["freight"] = freight_by_sample.get(s.id) item["sampleReception"] = reception_by_sample.get(s.id) item["productionReception"] = (reception_by_sample.get(s.id) or {}).get( "productionReception" @@ -1365,9 +1379,22 @@ def get_protocol_detail_bundle( samples_out.append(item) out["samples"] = samples_out out["storageCert"] = _storage_cert_to_dict(sc) if sc else None + if out["storageCert"]: + hid = (out["storageCert"].get("handlerUserId") or "").strip() + if hid: + hu = db.query(SysUser).filter(SysUser.id == hid).first() + out["storageCert"]["handlerUserName"] = (hu.nick_name or "") if hu else "" + else: + out["storageCert"]["handlerUserName"] = "" ft = get_fullmoon_todo(db, protocol_id, tenant_no) delivery_dt = pc.newborn_delivery_time if pc else None out["fullmoonTodo"] = _fullmoon_todo_detail_dict(ft, newborn_delivery_time=delivery_dt) + hid2 = (out["fullmoonTodo"].get("handlerUserId") or "").strip() + if hid2: + hu2 = db.query(SysUser).filter(SysUser.id == hid2).first() + out["fullmoonTodo"]["handlerUserName"] = (hu2.nick_name or "") if hu2 else "" + else: + out["fullmoonTodo"]["handlerUserName"] = "" out["attachments"] = urls_to_attachment_list(p.protocol_attachments, db, tenant_no) out["consentAttachmentFiles"] = urls_to_attachment_list(p.consent_form_urls, db, tenant_no) out["healthSurveyAttachmentFiles"] = urls_to_attachment_list( @@ -1969,7 +1996,7 @@ def _apply_payment_detail_draft_fields( from app.crud.dict_data import normalize_transfer_type if d.pay_status != "dfj": - raise ValueError("仅待缴费状态可编辑缴费明细") + raise ValueError("仅已缴费状态可编辑缴费明细") if "transfer_type" in fields: d.transfer_type = normalize_transfer_type( db, value=str(fields.get("transfer_type") or ""), tenant_no=tenant_no @@ -2024,7 +2051,7 @@ def _apply_submit_receipt_finance( user_id: Optional[str], ) -> None: if d.pay_status != "dfj": - raise ValueError("仅待缴费状态可提交财务确认") + raise ValueError("仅已缴费状态可提交财务确认") d.pay_status = "jfqrz" d.update_by = user_id d.update_time = datetime.now() @@ -2797,7 +2824,19 @@ def save_samples( db.commit() for o in result: db.refresh(o) - save_sample_transports(db, collection_protocol_id, tenant_no, user_id, result, samples) + # 本阶段停写旧运输明细,改挂运费批次 + from app.crud import sample_freight as sample_freight_svc + + sample_freight_svc.apply_freight_on_protocol_save( + db, + tenant_no=tenant_no, + user_id=user_id, + sample_source=sample_freight_svc.SAMPLE_SOURCE_CELL, + protocol_id=collection_protocol_id, + saved_samples=result, + input_samples=samples, + ) + db.commit() return result @@ -3357,11 +3396,13 @@ def upsert_storage_cert( ) -> BizProtocolStorageCert: fields = _normalize_fields(fields) obj = get_storage_cert(db, collection_protocol_id, tenant_no) - _STR_FIELDS = ["make_status", "cert_image_urls", "make_date"] + _STR_FIELDS = ["make_status", "cert_image_urls", "make_date", "handler_user_id"] if obj: for k in _STR_FIELDS: if k in fields: - setattr(obj, k, (fields[k] or "").strip() or None) + setattr(obj, k, (fields[k] or "").strip() or None if isinstance(fields[k], str) or fields[k] is None else fields[k]) + if "handle_time" in fields: + obj.handle_time = fields.get("handle_time") if "remark" in fields: obj.remark = (fields["remark"] or "").strip() or None obj.update_by = user_id @@ -3385,13 +3426,49 @@ def upsert_storage_cert( ) for k in _STR_FIELDS: if k in fields: - setattr(obj, k, (fields[k] or "").strip() or None) + val = fields[k] + setattr(obj, k, (val or "").strip() or None if isinstance(val, str) or val is None else val) + if "handle_time" in fields: + obj.handle_time = fields.get("handle_time") db.add(obj) db.commit() db.refresh(obj) return obj +def upsert_storage_cert_handle( + db: Session, + collection_protocol_id: str, + tenant_no: str, + user_id: Optional[str], + *, + handle_time: Optional[datetime] = None, + handler_user_id: Optional[str] = None, + cert_image_urls: Optional[str] = None, + remark: Optional[str] = None, +) -> BizProtocolStorageCert: + from app.services.sample_info_progress import apply_handle_payload + + payload = apply_handle_payload( + handle_time=handle_time, + handler_user_id=handler_user_id, + attachments=cert_image_urls or "", + remark=remark or "", + ) + return upsert_storage_cert( + db, + collection_protocol_id, + tenant_no, + user_id, + handle_time=payload["handle_time"], + handler_user_id=payload["handler_user_id"], + make_status=payload["make_or_todo_status"], + make_date=payload["legacy_date"] or None, + cert_image_urls=payload["attachments"] or None, + remark=payload["remark"] or None, + ) + + # --------------------------------------------------------------------------- # 子表:满月待办 # --------------------------------------------------------------------------- @@ -3401,6 +3478,9 @@ def _fullmoon_todo_empty_dict() -> Dict[str, Any]: return { "todoStatus": "0", "handleDate": "", + "handleTime": "", + "handlerUserId": "", + "handled": False, "handleRemark": "", "insuranceAttachments": "", "insuranceCompanyId": "", @@ -3435,11 +3515,16 @@ def _fullmoon_todo_detail_dict( def _fullmoon_todo_to_dict(ft: BizProtocolFullmoonTodo) -> Dict[str, Any]: + from app.services.sample_info_progress import insurance_handled + return { "id": ft.id, "collectionProtocolId": ft.collection_protocol_id, "todoStatus": ft.todo_status or "0", "handleDate": (ft.handle_date or "").strip() if ft.handle_date else "", + "handleTime": _fmt_dt(ft.handle_time) if getattr(ft, "handle_time", None) else "", + "handlerUserId": getattr(ft, "handler_user_id", None) or "", + "handled": insurance_handled(ft), "handleRemark": ft.handle_remark or "", "insuranceAttachments": ft.insurance_attachments or "", "insuranceCompanyId": ft.insurance_company_id or "", @@ -3486,7 +3571,7 @@ def upsert_fullmoon_todo( fields = _normalize_fields(fields) obj = get_fullmoon_todo(db, collection_protocol_id, tenant_no) _STR_FIELDS = [ - "todo_status", "handle_date", "handle_remark", + "todo_status", "handle_date", "handle_remark", "handler_user_id", "insurance_attachments", "insurance_company_id", "insurance_company_name", "insurance_type", "insurance_policy_no", "insurance_effective_date", @@ -3497,6 +3582,8 @@ def upsert_fullmoon_todo( for k in _STR_FIELDS: if k in fields: setattr(obj, k, (fields[k] or "").strip() or None) + if "handle_time" in fields: + obj.handle_time = fields.get("handle_time") for k in _DEC_FIELDS: if k in fields and fields[k] is not None: setattr(obj, k, Decimal(str(fields[k]))) @@ -3524,6 +3611,8 @@ def upsert_fullmoon_todo( for k in _STR_FIELDS: if k in fields: setattr(obj, k, (fields[k] or "").strip() or None) + if "handle_time" in fields: + obj.handle_time = fields.get("handle_time") for k in _DEC_FIELDS: if k in fields and fields[k] is not None: setattr(obj, k, Decimal(str(fields[k]))) @@ -3533,6 +3622,39 @@ def upsert_fullmoon_todo( return obj +def upsert_fullmoon_todo_handle( + db: Session, + collection_protocol_id: str, + tenant_no: str, + user_id: Optional[str], + *, + handle_time: Optional[datetime] = None, + handler_user_id: Optional[str] = None, + insurance_attachments: Optional[str] = None, + handle_remark: Optional[str] = None, +) -> BizProtocolFullmoonTodo: + from app.services.sample_info_progress import apply_handle_payload + + payload = apply_handle_payload( + handle_time=handle_time, + handler_user_id=handler_user_id, + attachments=insurance_attachments or "", + remark=handle_remark or "", + ) + return upsert_fullmoon_todo( + db, + collection_protocol_id, + tenant_no, + user_id, + handle_time=payload["handle_time"], + handler_user_id=payload["handler_user_id"], + todo_status=payload["make_or_todo_status"], + handle_date=payload["legacy_date"] or None, + insurance_attachments=payload["attachments"] or None, + handle_remark=payload["remark"] or None, + ) + + # --------------------------------------------------------------------------- # 开票申请(主表 + 明细子表) # --------------------------------------------------------------------------- diff --git a/backend/app/crud/research_donation.py b/backend/app/crud/research_donation.py index 53052f5a..ccf09506 100644 --- a/backend/app/crud/research_donation.py +++ b/backend/app/crud/research_donation.py @@ -872,10 +872,19 @@ def get_donation_detail( ) samples_out: List[Dict[str, Any]] = [] unit_name_map = _load_unit_name_map(db, s_rows) + from app.crud import sample_freight as sample_freight_svc + + freight_by_sample = sample_freight_svc.freight_map_for_samples( + db, + tenant_no=tenant_no, + sample_source=sample_freight_svc.SAMPLE_SOURCE_DONATION, + sample_ids=sample_ids, + ) for s in s_rows: item = _sample_to_dict(s) item["unitName"] = unit_name_map.get(s.unit_id or "", "") item["transport"] = transport_by_sample.get(s.id) + item["freight"] = freight_by_sample.get(s.id) item["sampleReception"] = reception_by_sample.get(s.id) item["productionReception"] = (reception_by_sample.get(s.id) or {}).get( "productionReception" @@ -967,12 +976,24 @@ def create_donation( coll_raw = _normalize_fields(raw.get("collection") or {}) _create_collection(db, donation_id, tenant_no, user_id, coll_raw, now) - # samples + transports + # samples(运费批次在 commit 后挂钩;停写旧运输依赖) samples_raw = raw.get("samples") or [] - _create_samples(db, donation_id, tenant_no, user_id, samples_raw, now) + new_samples = _create_samples(db, donation_id, tenant_no, user_id, samples_raw, now) db.commit() db.refresh(obj) + from app.crud import sample_freight as sample_freight_svc + + sample_freight_svc.apply_freight_on_protocol_save( + db, + tenant_no=tenant_no, + user_id=user_id, + sample_source=sample_freight_svc.SAMPLE_SOURCE_DONATION, + protocol_id=donation_id, + saved_samples=new_samples, + input_samples=samples_raw, + ) + db.commit() return obj @@ -1067,7 +1088,8 @@ def update_donation( } _soft_delete_samples(db, donation_id, tenant_no, user_id) _soft_delete_transports(db, donation_id, tenant_no, user_id) - new_samples = _create_samples(db, donation_id, tenant_no, user_id, raw["samples"] or [], now) + samples_raw = raw["samples"] or [] + new_samples = _create_samples(db, donation_id, tenant_no, user_id, samples_raw, now) new_code_to_id = { (s.sample_code or "").strip(): s.id for s in new_samples @@ -1080,9 +1102,25 @@ def update_donation( new_code_to_id=new_code_to_id, user_id=user_id, ) + else: + samples_raw = None + new_samples = None db.commit() db.refresh(obj) + if new_samples is not None and samples_raw is not None: + from app.crud import sample_freight as sample_freight_svc + + sample_freight_svc.apply_freight_on_protocol_save( + db, + tenant_no=tenant_no, + user_id=user_id, + sample_source=sample_freight_svc.SAMPLE_SOURCE_DONATION, + protocol_id=donation_id, + saved_samples=new_samples, + input_samples=samples_raw, + ) + db.commit() return obj @@ -2101,10 +2139,7 @@ def _create_samples( db.flush() result.append(obj) - # transport - transport_raw = raw.get("transport") - if isinstance(transport_raw, dict): - _create_transport(db, donation_id, obj.id, tenant_no, user_id, transport_raw, idx, now, sample_code=obj.sample_code) + # 本阶段不再写入旧运输明细;运费由 apply_freight_on_protocol_save 挂钩 return result diff --git a/backend/app/main.py b/backend/app/main.py index 509079bc..149b8a81 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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, protocol_refund_record_router, presign_order_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, presign_order_router, sample_freight_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 @@ -120,6 +120,7 @@ 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(presign_order_router, prefix=settings.API_V1_PREFIX) +app.include_router(sample_freight_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)