File size: 4,748 Bytes
b84ea83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""
Assessment Model β€” ORM + Pydantic Schemas

ORM:     Assessment (SQLAlchemy table: assessments)
Schemas: AssessmentCreate, AssessmentResponse

Stores individual Ghost Shopper evaluation items for each visit.
Each visit has ~21 assessment items grouped into 5 categories (A–E).

Categories:
    A = Pelayanan Awal (Frontliner/Kasir)       β€” 5 items
    B = Kualitas Produk (Minuman & Makanan)      β€” 5 items
    C = Kebersihan dan Suasana Outlet            β€” 4 items
    D = Kepatuhan SOP & Keselamatan              β€” 2 items
    E = Keseluruhan Pengalaman Pelanggan         β€” 5 items

All ratings are normalized to a 1.0–5.0 float scale regardless of
the original input format (Ya/Tidak, Qualitative, or Numeric).
"""

import uuid
from datetime import datetime
from typing import Optional

from sqlalchemy import String, Text, Float, Integer, DateTime, ForeignKey, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship

from pydantic import BaseModel, Field, ConfigDict

from models.base import Base


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ORM Model
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

class Assessment(Base):
    """SQLAlchemy ORM model for the 'assessments' table."""

    __tablename__ = "assessments"

    id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
    )
    visit_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("visits.id", ondelete="CASCADE"),
        nullable=False,
    )
    category: Mapped[str] = mapped_column(
        String(1), nullable=False,
        comment="Assessment category: A, B, C, D, or E"
    )
    item_no: Mapped[int] = mapped_column(
        Integer, nullable=False,
        comment="Item sequence number within the category (1-based)"
    )
    criteria: Mapped[str] = mapped_column(
        String(500), nullable=False,
        comment="Assessment question/criteria text"
    )
    score: Mapped[float] = mapped_column(
        Float, nullable=False,
        comment="Normalized score 1.0 to 5.0"
    )
    raw_value: Mapped[str] = mapped_column(
        String(100), nullable=False,
        comment="Original value from document: 'Baik', 'Ya', '4', etc."
    )
    notes: Mapped[Optional[str]] = mapped_column(
        Text, nullable=True,
        comment="Catatan / notes from the Ghost Shopper"
    )
    description: Mapped[Optional[str]] = mapped_column(
        Text, nullable=True,
        comment="Detailed evaluator description / observation for this item"
    )
    created_at: Mapped[datetime] = mapped_column(
        DateTime, server_default=func.now()
    )

    # Relationships
    visit = relationship("Visit", back_populates="assessments")
    photos = relationship(
        "Photo", back_populates="assessment",
        cascade="all, delete-orphan",
        foreign_keys="Photo.assessment_id",
    )

    def __repr__(self) -> str:
        return (
            f"<Assessment(category={self.category}, item={self.item_no}, "
            f"score={self.score}, raw='{self.raw_value}')>"
        )


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Pydantic Schemas
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

class AssessmentCreate(BaseModel):
    """Schema for creating an assessment item."""
    category: str = Field(..., min_length=1, max_length=1, description="Category A-E")
    item_no: int = Field(..., ge=1, le=10, description="Item number within category")
    criteria: str = Field(..., min_length=1, max_length=500, description="Criteria text")
    score: float = Field(..., ge=1.0, le=5.0, description="Normalized score 1.0-5.0")
    raw_value: str = Field(..., max_length=100, description="Original raw value")
    notes: Optional[str] = Field(None, description="Catatan from evaluator")
    description: Optional[str] = Field(None, description="Detailed observation/description for this item")


class AssessmentResponse(BaseModel):
    """Schema for an assessment item in API responses."""
    model_config = ConfigDict(from_attributes=True)

    id: uuid.UUID
    visit_id: uuid.UUID
    category: str
    item_no: int
    criteria: str
    score: float
    raw_value: str
    notes: Optional[str]
    description: Optional[str]
    created_at: datetime