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()
This commit is contained in:
jigoong
2026-06-04 18:22:14 +07:00
parent ee473aca8f
commit e4d32b86cb
3 changed files with 66 additions and 4 deletions

View File

@@ -9,9 +9,9 @@ from fastapi import APIRouter, Depends
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.orm import Session 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.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.security.dependencies import get_db, require_permission
from app.utils.supabase_client import SupabaseAPIError, upsert_to_supabase_sync 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_CHECKPOINT_WRITE = "feed.checkpoint:write"
PERM_FEED_OLD_CHECKPOINT_WRITE = "feed.old-checkpoint:write" PERM_FEED_OLD_CHECKPOINT_WRITE = "feed.old-checkpoint:write"
PERM_FEED_PATIENT_APPOINTMENT_WRITE = "feed.patient-appointment:write" PERM_FEED_PATIENT_APPOINTMENT_WRITE = "feed.patient-appointment:write"
PERM_VOC_DATA_WRITE = "voc.data:write"
def _to_tz(dt): def _to_tz(dt):
@@ -304,3 +305,41 @@ def upsert_patient_appointment(
"error": supabase_error, "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,
},
}

View File

@@ -37,3 +37,12 @@ class PatientAppointmentIn(BaseModel):
doctor_code: str | None = None doctor_code: str | None = None
period: str | None = None period: str | None = None
appointment_type: 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

View File

@@ -1,8 +1,8 @@
from __future__ import annotations 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.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship 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()) 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): class ApiClient(Base):
__tablename__ = "api_client" __tablename__ = "api_client"
__table_args__ = {"schema": "fastapi"} __table_args__ = {"schema": "fastapi"}