Source code for ccbenefits.schemas

from datetime import date, datetime
from typing import Literal

from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator

# --- Card Template schemas ---


[docs] class BenefitTemplateOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str description: str | None max_value: float period_type: str redemption_type: str category: str
[docs] class CardTemplateOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str issuer: str annual_fee: float image_url: str | None benefits: list[BenefitTemplateOut] total_annual_value: float = 0.0
[docs] class CardTemplateListItem(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str issuer: str annual_fee: float image_url: str | None benefit_count: int = 0 total_annual_value: float = 0.0
# --- User Card schemas ---
[docs] class UserCardCreate(BaseModel): card_template_id: int nickname: str | None = None member_since_date: date | None = None
[docs] class UserCardOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int card_template_id: int card_name: str issuer: str annual_fee: float nickname: str | None member_since_date: date | None renewal_date: date | None = None closed_date: date | None = None is_active: bool created_at: datetime
[docs] class UserCardUpdate(BaseModel): renewal_date: date | None = None
[docs] class CardCloseRequest(BaseModel): closed_date: date
# --- Benefit Usage schemas ---
[docs] class BenefitUsageCreate(BaseModel): benefit_template_id: int amount_used: float = Field(ge=0) notes: str | None = None target_date: date | None = None
[docs] class BenefitUsageUpdate(BaseModel): amount_used: float | None = Field(default=None, ge=0) notes: str | None = None
[docs] class BenefitUsageOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int benefit_template_id: int benefit_name: str period_start_date: date period_end_date: date amount_used: float notes: str | None created_at: datetime
# --- Benefit Setting schemas ---
[docs] class BenefitSettingUpdate(BaseModel): perceived_max_value: float = Field(ge=0)
# --- Benefit Status (for card detail) ---
[docs] class PeriodSegment(BaseModel): label: str period_start_date: date period_end_date: date amount_used: float usage_id: int | None = None is_used: bool is_current: bool is_future: bool
[docs] class BenefitStatusOut(BaseModel): benefit_template_id: int usage_id: int | None = None name: str description: str | None max_value: float period_type: str redemption_type: str category: str period_start_date: date period_end_date: date days_remaining: int amount_used: float remaining: float perceived_max_value: float utilized_perceived_value: float is_used: bool periods: list[PeriodSegment] = []
[docs] class UserCardDetailOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int card_template_id: int card_name: str issuer: str annual_fee: float nickname: str | None member_since_date: date | None renewal_date: date | None = None closed_date: date | None = None is_active: bool available_years: list[int] = [] ytd_actual_used: float = 0 utilization_pct: float = 0 benefits_status: list[BenefitStatusOut]
# --- Summary / ROI ---
[docs] class UserCardSummaryOut(BaseModel): id: int card_name: str issuer: str nickname: str | None = None annual_fee: float available_years: list[int] = [] total_max_annual_value: float total_perceived_annual_value: float ytd_actual_used: float ytd_perceived_value: float net_actual: float net_perceived: float utilization_pct: float benefit_count: int benefits_used_count: int
# --- Auth schemas ---
[docs] class UserRegister(BaseModel): email: EmailStr password: str = Field(min_length=8, max_length=72) # bcrypt truncates at 72 bytes display_name: str = Field(min_length=1)
[docs] class UserLogin(BaseModel): email: EmailStr password: str
[docs] class TokenResponse(BaseModel): access_token: str refresh_token: str token_type: str = "bearer"
[docs] class RefreshRequest(BaseModel): refresh_token: str
[docs] class PasswordResetRequest(BaseModel): email: str # Intentionally not EmailStr — endpoint always returns 200 to prevent email enumeration
[docs] class PasswordReset(BaseModel): token: str new_password: str = Field(min_length=8, max_length=72)
[docs] class PasswordChange(BaseModel): current_password: str new_password: str = Field(min_length=8, max_length=72)
[docs] class ChannelPreferences(BaseModel): expiring_credits: bool = True period_start: bool = True utilization_summary: bool = False unused_recap: bool = True fee_approaching: bool = False
[docs] class NotificationPreferences(BaseModel): email: ChannelPreferences = ChannelPreferences() push: ChannelPreferences = ChannelPreferences() notification_hour: int = 9
[docs] @field_validator("notification_hour") @classmethod def validate_hour(cls, v): if not 0 <= v <= 23: raise ValueError("notification_hour must be 0-23") return v
[docs] class UserOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int email: str display_name: str preferred_currency: str timezone: str notification_preferences: NotificationPreferences | None is_active: bool is_admin: bool = False is_verified: bool = False created_at: datetime
[docs] class VerifyEmailRequest(BaseModel): token: str
[docs] class UserUpdate(BaseModel): display_name: str | None = None preferred_currency: str | None = None timezone: str | None = None notification_preferences: NotificationPreferences | None = None
[docs] class AuthResponse(TokenResponse): user: UserOut
[docs] class OAuthRequest(BaseModel): provider: Literal["google", "apple"] id_token: str display_name: str | None = None # Apple sends name only on first sign-in
[docs] class OAuthProviderOut(BaseModel): provider: str provider_email: str created_at: datetime
[docs] class OAuthLinkRequest(BaseModel): provider: Literal["google", "apple"] id_token: str display_name: str | None = None
# --- Push Token schemas ---
[docs] class PushTokenBase(BaseModel): token: str
[docs] @field_validator("token") @classmethod def validate_token_format(cls, v): if not v.startswith("ExponentPushToken[") or not v.endswith("]"): raise ValueError("Invalid Expo push token format") return v
[docs] class PushTokenCreate(PushTokenBase): device_name: str | None = None
[docs] class PushTokenUnregister(PushTokenBase): pass
# --- Notification schemas ---
[docs] class NotificationOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int notification_type: str title: str body: str data: dict | None = None is_read: bool created_at: datetime
[docs] class FeedbackCreate(BaseModel): category: str = Field(pattern=r"^(bug_report|feature_request|general)$") message: str = Field(min_length=1, max_length=1000)
[docs] class FeedbackOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int user_email: str # Populated manually from user relationship, not from ORM attribute category: str message: str created_at: datetime