from pydantic import BaseModel, field_validator from typing import Optional from datetime import datetime, date from decimal import Decimal # Customer schemas class CustomerBase(BaseModel): auth_net_profile_id: Optional[str] = None account_number: str customer_last_name: str customer_first_name: str customer_town: str customer_state: int customer_zip: str customer_first_call: Optional[datetime] = None customer_email: str customer_automatic: int customer_phone_number: str customer_home_type: int customer_apt: str customer_address: str company_id: int customer_latitude: str customer_longitude: str correct_address: bool class CustomerCreate(BaseModel): customer_first_name: str customer_last_name: str customer_phone_number: str customer_email: str customer_address: str customer_apt: Optional[str] = None customer_town: str customer_zip: str customer_home_type: int = 0 personal_notes: Optional[str] = None house_description: Optional[str] = None class CustomerCreateStep1(BaseModel): customer_first_name: str customer_last_name: str customer_phone_number: str # Now accepts formatted: (123) 456-7890 customer_address: str customer_apt: Optional[str] = None customer_town: str customer_zip: str customer_home_type: int = 0 house_description: Optional[str] = None class CustomerAccountCreate(BaseModel): account_number: str password: str confirm_password: str customer_email: str class NewCustomerCreate(CustomerCreate): password: str confirm_password: str class CustomerUpdate(BaseModel): auth_net_profile_id: Optional[str] = None account_number: Optional[str] = None customer_last_name: Optional[str] = None customer_first_name: Optional[str] = None customer_town: Optional[str] = None customer_state: Optional[int] = None customer_zip: Optional[str] = None customer_first_call: Optional[datetime] = None customer_email: Optional[str] = None customer_automatic: Optional[int] = None customer_phone_number: Optional[str] = None customer_home_type: Optional[int] = None customer_apt: Optional[str] = None customer_address: Optional[str] = None company_id: Optional[int] = None customer_latitude: Optional[str] = None customer_longitude: Optional[str] = None correct_address: Optional[bool] = None class CustomerResponse(CustomerBase): id: int # Order schemas (Delivery_Delivery) class OrderBase(BaseModel): customer_id: int customer_name: str customer_address: str customer_town: str customer_state: str customer_zip: int gallons_ordered: int customer_asked_for_fill: int gallons_delivered: Decimal customer_filled: int delivery_status: int when_ordered: Optional[date] = None when_delivered: Optional[date] = None expected_delivery_date: Optional[date] = None automatic: int automatic_id: int oil_id: int supplier_price: Decimal customer_price: Decimal customer_temperature: Decimal dispatcher_notes: str prime: int same_day: int emergency: int payment_type: int payment_card_id: int cash_recieved: Decimal driver_employee_id: int driver_first_name: str driver_last_name: str pre_charge_amount: Decimal total_price: Decimal final_price: Decimal check_number: str promo_id: int promo_money_discount: Decimal class OrderCreate(OrderBase): pass class OrderUpdate(BaseModel): customer_id: Optional[int] = None customer_name: Optional[str] = None customer_address: Optional[str] = None customer_town: Optional[str] = None customer_state: Optional[str] = None customer_zip: Optional[int] = None gallons_ordered: Optional[int] = None customer_asked_for_fill: Optional[int] = None gallons_delivered: Optional[Decimal] = None customer_filled: Optional[int] = None delivery_status: Optional[int] = None when_ordered: Optional[date] = None when_delivered: Optional[date] = None expected_delivery_date: Optional[date] = None automatic: Optional[int] = None automatic_id: Optional[int] = None oil_id: Optional[int] = None supplier_price: Optional[Decimal] = None customer_price: Optional[Decimal] = None customer_temperature: Optional[Decimal] = None dispatcher_notes: Optional[str] = None prime: Optional[int] = None same_day: Optional[int] = None emergency: Optional[int] = None payment_type: Optional[int] = None payment_card_id: Optional[int] = None cash_recieved: Optional[Decimal] = None driver_employee_id: Optional[int] = None driver_first_name: Optional[str] = None driver_last_name: Optional[str] = None pre_charge_amount: Optional[Decimal] = None total_price: Optional[Decimal] = None final_price: Optional[Decimal] = None check_number: Optional[str] = None promo_id: Optional[int] = None promo_money_discount: Optional[Decimal] = None class OrderResponse(OrderBase): id: int class DeliveryCreate(BaseModel): gallons_ordered: int expected_delivery_date: date dispatcher_notes: Optional[str] = None payment_type: int # 0=cash, 1=credit prime: Optional[int] = 0 same_day: Optional[int] = 0 emergency: Optional[int] = 0 pre_charge_amount: Optional[float] = None # Card schemas class CardBase(BaseModel): auth_net_payment_profile_id: Optional[str] = None card_number: Optional[str] = None last_four_digits: int name_on_card: Optional[str] = None expiration_month: str expiration_year: str type_of_card: Optional[str] = None security_number: Optional[str] = None accepted_or_declined: Optional[int] = None main_card: Optional[bool] = None zip_code: Optional[str] = None class CardCreate(BaseModel): card_number: str name_on_card: str expiration_month: str expiration_year: str security_number: str zip_code: str save_card: bool = False class CardResponse(CardBase): id: int date_added: datetime user_id: int class CardUpdate(BaseModel): name_on_card: Optional[str] = None expiration_month: Optional[str] = None expiration_year: Optional[str] = None zip_code: Optional[str] = None # Payment schemas class PaymentCreate(BaseModel): amount: Decimal card_id: Optional[int] = None # For saved cards card_details: Optional[CardCreate] = None # For new cards delivery_id: Optional[int] = None class PaymentResponse(BaseModel): transaction_id: str status: str amount: Decimal auth_net_transaction_id: str # Auth schemas class UserCreate(BaseModel): account_number: str house_number: str email: str password: str confirm_password: str @field_validator('house_number') @classmethod def validate_house_number(cls, v: str) -> str: """Validate house_number contains only safe characters. Allows alphanumeric characters, spaces, hyphens, and forward slashes (for addresses like "123-A" or "45 1/2"). """ import re if not v or not v.strip(): raise ValueError('House number cannot be empty') # Allow alphanumeric, spaces, hyphens, forward slashes if not re.match(r'^[a-zA-Z0-9\s\-/]+$', v): raise ValueError('House number contains invalid characters') if len(v) > 20: raise ValueError('House number is too long') return v.strip() class UserLogin(BaseModel): email: str password: str class Token(BaseModel): access_token: str token_type: str class TokenData(BaseModel): username: Optional[str] = None class UserResponse(BaseModel): id: int username: str email: str member_since: datetime last_seen: datetime admin: int admin_role: int confirmed: int active: int class ForgotPasswordRequest(BaseModel): email: str class ResetPasswordRequest(BaseModel): token: str password: str confirm_password: str