"""Pydantic request/response models for the mail API.""" from __future__ import annotations from typing import Any, Literal from pydantic import BaseModel, EmailStr class BaseSubmission(BaseModel): fullName: str email: EmailStr phone: str website: str = "" formStartedAt: int | None = None visitStartedAt: int | None = None pageEnteredAt: int | None = None firstInteractionAt: int | None = None sendClickedAt: int | None = None referrer: str = "" page: str = "" class BookingSubmission(BaseSubmission): enquiryType: str = "booking" petName: str = "" location: str = "" message: str = "" services: list[str] = [] stepChanges: int = 0 journey: list[str] = [] class OnboardingSubmission(BaseSubmission): address: str dogName: str dogBreed: str dogAge: str = "" servicesNeeded: list[str] = [] temperament: str = "" medicalNotes: str = "" accessInstructions: str = "" vetName: str vetAddress: str vetPhone: str emergencyContactName: str emergencyContactPhone: str regularFleaTickTreatment: str = "" petInsurance: str = "" petInsuranceOwnerExpenseAccepted: bool = False councilRegistrationConfirmed: bool = False vaccinationsConfirmed: bool = False emergencyVetConsent: bool = False termsAccepted: bool = False signatureDataUrl: str submissionSnapshot: dict[str, Any] = {} class WelcomePackEmailRequest(BaseModel): email: EmailStr # Owner-set subject line. Defaults to "Goodwalk Onboarding - " when # blank (resolved server-side so scheduled sends keep a sensible subject too). subject: str = "" # Structured offer details rendered in the details table. Now fully optional — # the owner can hide the table entirely (showDetails=False) and write a free # email instead. Left blank, a row is simply omitted from the table. serviceType: str = "" priceDetails: str = "" startDate: str = "" # Free-text customisation. All blank => the email falls back to the original # default copy, so existing callers keep working unchanged. `{first_name}` # and `{dog_name}` tokens are substituted server-side. heading: str = "" intro: str = "" outro: str = "" tagLabel: str = "" # Customisable button block. Defaults to "Complete onboarding" pointing at the # client onboarding portal. Clearing either field hides the button. ctaLabel: str = "" ctaUrl: str = "" # Whether to render the Service / Price / Start date table at all. showDetails: bool = True # Rich-text (WYSIWYG) message body. When present it replaces the # heading/intro/outro copy above and is sanitised to an email-safe subset of # HTML server-side. `includeButton` toggles the onboarding button beneath it. bodyHtml: str = "" includeButton: bool = True preview: bool = False previewRecipients: list[EmailStr] = [] # When sending for real (not a preview), also copy the owner so they keep a # record of exactly what the client received. CC is visible to the client; # BCC is hidden. Independent. Ignored on previews and scheduled sends. ccOwner: bool = False bccOwner: bool = False # ISO 8601 local datetime (e.g. "2026-06-20T14:30"). When set on a non-preview # request, the welcome email is queued for reliable delivery at that time # instead of being sent immediately. scheduledFor: str | None = None class ScheduledEmailCancelRequest(BaseModel): id: str class ScheduledEmailRescheduleRequest(BaseModel): id: str scheduledFor: str class BirthdayEmailRequest(BaseModel): email: EmailStr dogId: str | None = None # Owner-editable subject line. Blank falls back to the default # "Happy birthday " subject. subject: str = "" preview: bool = False previewRecipients: list[EmailStr] = [] class BirthdayAutoSendRequest(BaseModel): email: EmailStr dogId: str | None = None enabled: bool # Client lifecycle status. Soft-delete only — every client stays on file so the # history is preserved for future newsletter / retention work. ClientLifecycleStatus = Literal["active", "paused", "cancelled", "archived"] class ClientStatusUpdate(BaseModel): email: EmailStr status: ClientLifecycleStatus reason: str = "" class ClientProfileUpdate(BaseModel): email: EmailStr nextEmail: EmailStr fullName: str phone: str = "" address: str = "" dogName: str dogBreed: str = "" dogAge: str = "" class ResetOnboardingRequest(BaseModel): """Owner-initiated reset: keep the client's saved details but mark their onboarding incomplete so they can sign in and complete the new form (used for clients imported from the legacy Gravity Forms data).""" email: EmailStr class DeleteClientRequest(BaseModel): """Owner-initiated permanent deletion of a client and their access.""" email: EmailStr class NewClientRequest(BaseModel): """Owner-initiated client creation (e.g. leads from Instagram / Facebook that never came through the public enquiry form). Registers the email so the client can access the onboarding form, and seeds a profile.""" email: EmailStr fullName: str phone: str = "" address: str = "" dogName: str = "" dogBreed: str = "" dogAge: str = "" # Where the client came from, e.g. "Instagram", "Facebook", "Referral". source: str = "" # The date the client joined Goodwalk (ISO yyyy-mm-dd). Owner-supplied, since # only the owner knows it. Used to surface joining anniversaries. joiningDate: str = "" # When true (and the MYOB integration is configured), also create the client # as a customer contact in MYOB. Non-fatal: a MYOB failure never blocks the # local client from being created. createInMyob: bool = False class ClientDogUpsertRequest(BaseModel): email: EmailStr dogId: str | None = None dogName: str dogBreed: str = "" dogAge: str = "" birthdayAutoSend: bool = False class ContractSubmission(BaseSubmission): address: str dogName: str dogBreed: str dogAge: str = "" serviceType: str startDate: str walkFrequency: str = "" additionalNotes: str = "" agreeServiceTerms: bool = False agreeCancellation: bool = False agreePayment: bool = False agreeEmergency: bool = False agreeLiability: bool = False agreeAccuracy: bool = False signatureDataUrl: str class RenderMessageRequest(BaseModel): templateId: str heading: str = "" body: str = "" ctaLabel: str = "" ctaUrl: str = "" subHeading: str = "" highlightText: str = "" signOff: str = "" footerNote: str = "" fontId: str = "system" class SendMessageRequest(BaseModel): templateId: str subject: str heading: str = "" body: str = "" ctaLabel: str = "" ctaUrl: str = "" subHeading: str = "" highlightText: str = "" signOff: str = "" footerNote: str = "" fontId: str = "system" recipients: list[EmailStr] = [] preview: bool = False previewRecipients: list[EmailStr] = []