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]
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