Files
gw-svelte/mail-api/mail_api/models.py
T
adminandClaude Opus 4.8 0e88e7bdac v4.0.5
Component architecture
- Reorganise src/lib/components into pages/, sections/, ui/ subdirectories and
  update all imports across routes and tests.

Owner admin dashboard (+2k lines)
- Scheduled welcome-pack emails: durable queue with cancel/reschedule and a
  background sender loop (SCHEDULED_CHECK_INTERVAL_SECONDS, default 60s).
- Custom welcome-pack subject line, preview recipients, and client BCC.
- Add-client, edit client profile, and reset-onboarding flows.
- "View as client" onboarding preview (owner impersonation, dry-run submit).
- Tabbed owner welcome route (/owner/welcome/[[tab]]).

MYOB integration
- New mail_api/myob.py: create new clients as MYOB AccountRight customer
  contacts. Disabled until all MYOB_* env vars are set (no-op otherwise).

Onboarding
- New "Does your dog resource guard?" Yes/No question in the Behaviour step.
- Persist vetAddress, flea/tick, and pet-insurance fields server-side.

Misc
- vite config, new hooks.ts, responsive CSS tweaks, deploy/docker config,
  mail-api README and start-dev.ps1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:51:29 +12:00

188 lines
4.9 KiB
Python

"""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 - <Dog Name>" when
# blank (resolved server-side so scheduled sends keep a sensible subject too).
subject: str = ""
serviceType: str
priceDetails: str
startDate: str
preview: bool = False
previewRecipients: list[EmailStr] = []
# 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
preview: bool = False
previewRecipients: list[EmailStr] = []
class BirthdayAutoSendRequest(BaseModel):
email: EmailStr
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
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 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 = ""
# Where the client came from, e.g. "Instagram", "Facebook", "Referral".
source: 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 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] = []