From e4d32b86cbd2ca8bfbfa07d7f59a3176874ae285 Mon Sep 17 00:00:00 2001 From: jigoong Date: Thu, 4 Jun 2026 18:22:14 +0700 Subject: [PATCH] feat: add VOC data endpoint (POST /api/v1/voc-data) - Add VocDataIn schema (date, topic, sub_topic, level, depart_id, dep_name) - Add RawVocData SQLAlchemy model (rawdata.raw_voc_data, BIGSERIAL PK) - Add POST /api/v1/voc-data endpoint with voc.data:write permission - Dual-write to local PostgreSQL + Supabase - Table auto-created on startup via Base.metadata.create_all() --- 03-apiservice/app/api/v1/routes.py | 43 +++++++++++++++++++++++++++-- 03-apiservice/app/api/v1/schemas.py | 9 ++++++ 03-apiservice/app/db/models.py | 18 ++++++++++-- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/03-apiservice/app/api/v1/routes.py b/03-apiservice/app/api/v1/routes.py index 469ee79..da283c8 100644 --- a/03-apiservice/app/api/v1/routes.py +++ b/03-apiservice/app/api/v1/routes.py @@ -9,9 +9,9 @@ from fastapi import APIRouter, Depends from sqlalchemy.dialects.postgresql import insert from sqlalchemy.orm import Session -from app.api.v1.schemas import FeedCheckpointIn, FeedWaitingTimeIn, PatientAppointmentIn +from app.api.v1.schemas import FeedCheckpointIn, FeedWaitingTimeIn, PatientAppointmentIn, VocDataIn from app.core.config import settings -from app.db.models import RawOpdCheckpoint, RawWaitingTime, PatientAppointment +from app.db.models import RawOpdCheckpoint, RawWaitingTime, PatientAppointment, RawVocData from app.security.dependencies import get_db, require_permission from app.utils.supabase_client import SupabaseAPIError, upsert_to_supabase_sync @@ -22,6 +22,7 @@ router = APIRouter(prefix="/api/v1") PERM_FEED_CHECKPOINT_WRITE = "feed.checkpoint:write" PERM_FEED_OLD_CHECKPOINT_WRITE = "feed.old-checkpoint:write" PERM_FEED_PATIENT_APPOINTMENT_WRITE = "feed.patient-appointment:write" +PERM_VOC_DATA_WRITE = "voc.data:write" def _to_tz(dt): @@ -304,3 +305,41 @@ def upsert_patient_appointment( "error": supabase_error, }, } + + +@router.post("/voc-data") +def insert_voc_data( + payload: list[VocDataIn], + _: Annotated[object, Depends(require_permission(PERM_VOC_DATA_WRITE))], + db: Annotated[Session, Depends(get_db)], +): + rows = [r.model_dump() for r in payload] + + stmt = insert(RawVocData).values(rows) + result = db.execute(stmt) + db.commit() + + supabase_rows = [{**r, "date": r["date"].isoformat()} for r in rows] + supabase_result = None + supabase_error = None + + try: + logger.info(f"Sending {len(supabase_rows)} VOC records to Supabase API") + supabase_result = upsert_to_supabase_sync(table="raw_voc_data", data=supabase_rows) + logger.info(f"Successfully sent VOC data to Supabase: {supabase_result.get('status_code')}") + except SupabaseAPIError as e: + logger.error(f"Failed to send VOC data to Supabase: {str(e)}") + supabase_error = str(e) + except Exception as e: + logger.error(f"Unexpected error sending VOC data to Supabase: {str(e)}") + supabase_error = f"Unexpected error: {str(e)}" + + return { + "inserted": len(rows), + "rowcount": result.rowcount, + "supabase": { + "success": supabase_result is not None, + "result": supabase_result, + "error": supabase_error, + }, + } diff --git a/03-apiservice/app/api/v1/schemas.py b/03-apiservice/app/api/v1/schemas.py index 76db03f..f3b0708 100644 --- a/03-apiservice/app/api/v1/schemas.py +++ b/03-apiservice/app/api/v1/schemas.py @@ -37,3 +37,12 @@ class PatientAppointmentIn(BaseModel): doctor_code: str | None = None period: str | None = None appointment_type: str | None = None + + +class VocDataIn(BaseModel): + date: date + topic: str + sub_topic: str + level: str + depart_id: str + dep_name: str | None = None diff --git a/03-apiservice/app/db/models.py b/03-apiservice/app/db/models.py index 273068c..c8b8151 100644 --- a/03-apiservice/app/db/models.py +++ b/03-apiservice/app/db/models.py @@ -1,8 +1,8 @@ from __future__ import annotations -from datetime import datetime +from datetime import date, datetime -from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func +from sqlalchemy import BigInteger, Boolean, Date, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -65,6 +65,20 @@ class PatientAppointment(Base): updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) +class RawVocData(Base): + __tablename__ = "raw_voc_data" + __table_args__ = {"schema": "rawdata"} + + id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) + date: Mapped[date] = mapped_column(Date, nullable=False) + topic: Mapped[str] = mapped_column(String(200), nullable=False) + sub_topic: Mapped[str] = mapped_column(String(200), nullable=False) + level: Mapped[str] = mapped_column(String(50), nullable=False) + depart_id: Mapped[str] = mapped_column(String(50), nullable=False) + dep_name: Mapped[str | None] = mapped_column(String(200), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + class ApiClient(Base): __tablename__ = "api_client" __table_args__ = {"schema": "fastapi"}