From fa1bc1a615a08b888872dc2bb51edd81344f85a8 Mon Sep 17 00:00:00 2001 From: ponzischeme89 Date: Mon, 4 May 2026 20:32:24 +1200 Subject: [PATCH] General enquries feature --- DEPLOYMENT.md | 3 +- Dockerfile | 2 +- deploy.env.template | 3 +- docker-compose.prod.yml | 10 +- docker-compose.yml | 10 +- mail-api/Dockerfile | 2 +- mail-api/__pycache__/main.cpython-314.pyc | Bin 0 -> 49743 bytes mail-api/main.py | 230 ++++++++++++++----- package-lock.json | 4 +- package.json | 2 +- src/lib/components/BookingPage.svelte | 13 +- src/lib/components/BookingSection.svelte | 245 +++++++++++++++------ src/lib/components/BookingSection.test.ts | 72 +++++- src/lib/components/ErrorModal.svelte | 8 +- src/lib/components/SuccessModal.svelte | 36 ++- src/lib/content/homepage.ts | 6 +- src/lib/content/static-pages.ts | 2 +- src/lib/server/feature-flags.ts | 21 ++ src/lib/styles/forms.css | 69 ++++++ src/lib/styles/responsive.css | 17 ++ src/lib/types.ts | 2 + src/routes/[slug]/+page.server.ts | 15 +- src/routes/[slug]/+page.svelte | 2 +- src/routes/[slug]/slug-page.server.test.ts | 28 +++ src/routes/[slug]/slug-page.test.ts | 17 ++ src/routes/home-page.test.ts | 1 + src/test/fixtures.ts | 1 + 27 files changed, 657 insertions(+), 164 deletions(-) create mode 100644 mail-api/__pycache__/main.cpython-314.pyc create mode 100644 src/lib/server/feature-flags.ts diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 160300b..3228ece 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -70,7 +70,8 @@ mkdir -p /docker/goodwalk-svelte It is created from [deploy.env.template](deploy.env.template). Current template contents: ```env -APP_VERSION=4.0.1 +APP_VERSION=4.0.2 +ENABLE_GENERAL_ENQUIRIES=false TZ=Pacific/Auckland POSTGRES_DB=goodwalk diff --git a/Dockerfile b/Dockerfile index 4092e83..e42df7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG APP_VERSION=4.0.1 +ARG APP_VERSION=4.0.2 FROM node:22-alpine AS builder ARG APP_VERSION diff --git a/deploy.env.template b/deploy.env.template index c886e1c..eaf9bf9 100644 --- a/deploy.env.template +++ b/deploy.env.template @@ -1,4 +1,4 @@ -APP_VERSION=4.0.1 +APP_VERSION=4.0.2 TZ=Pacific/Auckland POSTGRES_DB=goodwalk @@ -10,6 +10,7 @@ RESEND_API_KEY=re_hcDByLp8_HEBW93wDirr7o9g16FgCeYNF OWNER_EMAIL=mattcohen0@gmail.com FROM_EMAIL=GoodWalk REPLY_TO=info@goodwalk.co.nz +ENABLE_GENERAL_ENQUIRIES=false FORM_MIN_SECONDS=4 FORM_MAX_SECONDS=7200 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 3b80592..1bb821c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -3,13 +3,14 @@ services: build: context: . args: - APP_VERSION: ${APP_VERSION:-4.0.1} + APP_VERSION: ${APP_VERSION:-4.0.2} container_name: goodwalk_svelte_app environment: - APP_VERSION: ${APP_VERSION:-4.0.1} + APP_VERSION: ${APP_VERSION:-4.0.2} DATABASE_URL: postgresql://${POSTGRES_USER:-goodwalk}:${POSTGRES_PASSWORD_URLENCODED:-goodwalk}@db:5432/${POSTGRES_DB:-goodwalk} NODE_ENV: production PORT: 3000 + ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false} TZ: ${TZ:-Pacific/Auckland} depends_on: - db @@ -24,14 +25,15 @@ services: build: context: ./mail-api args: - APP_VERSION: ${APP_VERSION:-4.0.1} + APP_VERSION: ${APP_VERSION:-4.0.2} container_name: goodwalk_svelte_mail_api environment: - APP_VERSION: ${APP_VERSION:-4.0.1} + APP_VERSION: ${APP_VERSION:-4.0.2} RESEND_API_KEY: ${RESEND_API_KEY} OWNER_EMAIL: ${OWNER_EMAIL} FROM_EMAIL: ${FROM_EMAIL:-GoodWalk } REPLY_TO: ${REPLY_TO:-aless@goodwalk.co.nz} + ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false} FORM_MIN_SECONDS: ${FORM_MIN_SECONDS:-4} FORM_MAX_SECONDS: ${FORM_MAX_SECONDS:-7200} RATE_LIMIT_WINDOW_SECONDS: ${RATE_LIMIT_WINDOW_SECONDS:-900} diff --git a/docker-compose.yml b/docker-compose.yml index 9a763d6..140b075 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,12 +3,13 @@ services: build: context: . args: - APP_VERSION: ${APP_VERSION:-4.0.1} + APP_VERSION: ${APP_VERSION:-4.0.2} environment: - APP_VERSION: ${APP_VERSION:-4.0.1} + APP_VERSION: ${APP_VERSION:-4.0.2} DATABASE_URL: postgresql://${POSTGRES_USER:-goodwalk}:${POSTGRES_PASSWORD:-goodwalk}@db:5432/${POSTGRES_DB:-goodwalk} NODE_ENV: production PORT: ${APP_PORT:-3000} + ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false} TZ: ${TZ:-Pacific/Auckland} depends_on: - db @@ -18,13 +19,14 @@ services: build: context: ./mail-api args: - APP_VERSION: ${APP_VERSION:-4.0.1} + APP_VERSION: ${APP_VERSION:-4.0.2} environment: - APP_VERSION: ${APP_VERSION:-4.0.1} + APP_VERSION: ${APP_VERSION:-4.0.2} RESEND_API_KEY: ${RESEND_API_KEY} OWNER_EMAIL: ${OWNER_EMAIL} FROM_EMAIL: ${FROM_EMAIL:-GoodWalk } REPLY_TO: ${REPLY_TO:-aless@goodwalk.co.nz} + ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false} TZ: ${TZ:-Pacific/Auckland} restart: unless-stopped diff --git a/mail-api/Dockerfile b/mail-api/Dockerfile index d94b1d4..37187cf 100644 --- a/mail-api/Dockerfile +++ b/mail-api/Dockerfile @@ -1,4 +1,4 @@ -ARG APP_VERSION=4.0.1 +ARG APP_VERSION=4.0.2 FROM python:3.12-slim ARG APP_VERSION diff --git a/mail-api/__pycache__/main.cpython-314.pyc b/mail-api/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..33503fc7a0808f2c34980e2519b44b4ffbf9a8fd GIT binary patch literal 49743 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 dict: "owner_email": os.environ["OWNER_EMAIL"], "from_email": os.environ.get("FROM_EMAIL", "GoodWalk "), "reply_to": os.environ.get("REPLY_TO", "aless@goodwalk.co.nz"), + "enable_general_enquiries": os.environ.get("ENABLE_GENERAL_ENQUIRIES", "false").strip().lower() in {"1", "true", "yes", "on", "enabled"}, "max_attempts": max(1, int(os.environ.get("MAIL_MAX_ATTEMPTS", "3"))), "form_min_seconds": max(1, int(os.environ.get("FORM_MIN_SECONDS", "4"))), "form_max_seconds": max(60, int(os.environ.get("FORM_MAX_SECONDS", "7200"))), @@ -105,6 +106,7 @@ resend.api_key = _config["resend_api_key"] OWNER_EMAIL = _config["owner_email"] FROM_EMAIL = _config["from_email"] REPLY_TO = _config["reply_to"] +ENABLE_GENERAL_ENQUIRIES = _config["enable_general_enquiries"] MAX_SEND_ATTEMPTS = _config["max_attempts"] FORM_MIN_SECONDS = _config["form_min_seconds"] FORM_MAX_SECONDS = _config["form_max_seconds"] @@ -116,12 +118,13 @@ RATE_LIMIT_MIN_INTERVAL_SECONDS = _config["rate_limit_min_interval_seconds"] LOGO_URL = "https://www.goodwalk.co.nz/images/goodwalk-auckland-dog-walking-logo.png" logger.info( - "Mail API config: version=%r timezone=%r from=%r reply_to=%r owner=%r max_attempts=%d form_min=%ss form_max=%ss rate_window=%ss per_ip=%d per_email=%d min_interval=%ss", + "Mail API config: version=%r timezone=%r from=%r reply_to=%r owner=%r general_enquiries=%r max_attempts=%d form_min=%ss form_max=%ss rate_window=%ss per_ip=%d per_email=%d min_interval=%ss", APP_VERSION, os.environ.get("TZ", "system-default"), FROM_EMAIL, REPLY_TO, OWNER_EMAIL, + ENABLE_GENERAL_ENQUIRIES, MAX_SEND_ATTEMPTS, FORM_MIN_SECONDS, FORM_MAX_SECONDS, @@ -165,11 +168,12 @@ async def _request_logging_middleware(request: Request, call_next): class BookingSubmission(BaseModel): + enquiryType: str = "booking" fullName: str email: EmailStr phone: str - petName: str - location: str + petName: str = "" + location: str = "" message: str = "" services: list[str] = [] website: str = "" @@ -300,6 +304,85 @@ def _is_honeypot_triggered(data: BookingSubmission) -> bool: return bool(_trimmed(data.website)) +def _is_general_enquiry(data: BookingSubmission) -> bool: + return _trimmed(data.enquiryType).lower() == "general" + + +def _enquiry_type_label(data: BookingSubmission) -> str: + return "General enquiry" if _is_general_enquiry(data) else "Booking enquiry" + + +def _validate_submission(request_id: str, data: BookingSubmission) -> None: + enquiry_type = _trimmed(data.enquiryType).lower() + + if enquiry_type not in {"booking", "general"}: + logger.warning("[%s] rejected: invalid enquiryType=%r", request_id, data.enquiryType) + raise HTTPException( + status_code=400, + detail="Please choose a valid enquiry type and try again.", + ) + + if not _trimmed(data.fullName): + logger.warning("[%s] rejected: missing full name", request_id) + raise HTTPException( + status_code=400, + detail="Please enter your full name.", + ) + + if not _trimmed(data.phone): + logger.warning("[%s] rejected: missing phone number", request_id) + raise HTTPException( + status_code=400, + detail="Please enter your contact number.", + ) + + if _is_general_enquiry(data): + if not ENABLE_GENERAL_ENQUIRIES: + logger.warning("[%s] rejected: general enquiries are disabled", request_id) + raise HTTPException( + status_code=403, + detail="General enquiries are currently unavailable through this form.", + ) + if not _trimmed(data.message): + logger.warning("[%s] rejected: missing general enquiry message", request_id) + raise HTTPException( + status_code=400, + detail="Please tell us how we can help.", + ) + return + + if not _trimmed(data.petName): + logger.warning("[%s] rejected: missing pet name", request_id) + raise HTTPException( + status_code=400, + detail="Please enter your dog's name.", + ) + + if not _trimmed(data.location): + logger.warning("[%s] rejected: missing location", request_id) + raise HTTPException( + status_code=400, + detail="Please enter your location.", + ) + + +def _normalize_submission(data: BookingSubmission) -> None: + data.enquiryType = "general" if _is_general_enquiry(data) else "booking" + data.fullName = _trimmed(data.fullName) + data.phone = _trimmed(data.phone) + data.petName = _trimmed(data.petName) + data.location = _trimmed(data.location) + data.message = _trimmed(data.message) + data.referrer = _trimmed(data.referrer) + data.page = _trimmed(data.page) + data.services = [_trimmed(service) for service in data.services if _trimmed(service)] + + if _is_general_enquiry(data): + data.petName = "" + data.location = "" + data.services = [] + + def _parse_ua(ua: str) -> str: if not ua: return "Unknown" @@ -360,8 +443,46 @@ def _logo_header(badge_html: str = "", subtitle: str = "") -> str: def client_email(data: BookingSubmission) -> str: + is_general = _is_general_enquiry(data) services_text = ", ".join(data.services) if data.services else "Not specified" - message_row = _detail_row("About the dog", data.message) if data.message else "" + enquiry_summary_rows = [ + _detail_row("Your name", data.fullName), + _detail_row("Email", str(data.email)), + _detail_row("Phone", data.phone), + _detail_row("Type", _enquiry_type_label(data)), + ] + + if is_general: + if data.message: + enquiry_summary_rows.append(_detail_row("Message", data.message)) + intro_html = ( + "We’ve received your message and we will be in touch shortly." + ) + next_steps_html = ( + "We will review your message and reply within 1 business day." + ) + logo_subtitle = "General enquiries and dog walking support" + else: + enquiry_summary_rows.extend( + [ + _detail_row("Dog’s name", data.petName), + _detail_row("Location", data.location), + _detail_row("Services", services_text), + ] + ) + if data.message: + enquiry_summary_rows.append(_detail_row("About the dog", data.message)) + intro_html = ( + "We’ve received your enquiry and we will be in touch shortly to arrange " + "a Meet & Greet with you and " + f"{data.petName}." + ) + next_steps_html = ( + "We will review your details and reach out within 1 business day " + "to schedule a free Meet & Greet. No commitment required — just a " + f"chance for {data.petName} to make a new best friend." + ) + logo_subtitle = "Professional dog walking services" return f""" @@ -380,7 +501,7 @@ def client_email(data: BookingSubmission) -> str: style="max-width:600px;width:100%;border-radius:16px;overflow:hidden; box-shadow:0 4px 24px rgba(0,0,0,0.08);"> - {_logo_header(subtitle="Professional dog walking services")} + {_logo_header(subtitle=logo_subtitle)} @@ -392,9 +513,7 @@ def client_email(data: BookingSubmission) -> str:

- We’ve received your enquiry and we will be in touch shortly to arrange - a Meet & Greet with you and - {data.petName}. + {intro_html}

@@ -408,13 +527,7 @@ def client_email(data: BookingSubmission) -> str: Your enquiry summary - {_detail_row("Your name", data.fullName)} - {_detail_row("Email", data.email)} - {_detail_row("Phone", data.phone)} - {_detail_row("Dog’s name", data.petName)} - {_detail_row("Location", data.location)} - {_detail_row("Services", services_text)} - {message_row} + {"".join(enquiry_summary_rows)}
@@ -431,9 +544,7 @@ def client_email(data: BookingSubmission) -> str:
- We will review your details and reach out within 1 business days - to schedule a free Meet & Greet. No commitment required — just a - chance for {data.petName} to make a new best friend. + {next_steps_html}
@@ -469,17 +580,20 @@ def client_email(data: BookingSubmission) -> str: def owner_email(data: BookingSubmission, ip: str, browser: str) -> str: + is_general = _is_general_enquiry(data) services_text = ", ".join(data.services) if data.services else "—" now = datetime.now() submitted_at = now.strftime("%d %b %Y at %I:%M %p").lstrip("0") first_name = data.fullName.split()[0] if data.fullName.strip() else "them" + email_title = "New GoodWalk Enquiry" if is_general else "New GoodWalk Lead" + message_label = "Message" if is_general else "About the dog" message_block = f"""
About the dog
+ text-transform:uppercase;margin-bottom:8px;">{message_label}
{data.message}
@@ -490,7 +604,7 @@ def owner_email(data: BookingSubmission, ip: str, browser: str) -> str: padding:10px 28px;"> - 📩  New lead! + 📩  New enquiry!
- New GoodWalk Lead + {email_title} @@ -597,36 +727,13 @@ def owner_email(data: BookingSubmission, ip: str, browser: str) -> str:
Dog & services
+ text-transform:uppercase;margin-bottom:16px;">{detail_heading}
@@ -681,7 +788,7 @@ def owner_email(data: BookingSubmission, ip: str, browser: str) -> str: border-top:1px solid #e8e8e4;">
- Sent automatically by GoodWalk booking form + Sent automatically by GoodWalk enquiry form
@@ -753,15 +860,6 @@ async def submit_booking(data: BookingSubmission, request: Request): ip = _get_ip(request) browser = _parse_ua(request.headers.get("user-agent", "")) - name_parts = data.fullName.strip().split() - first_name = name_parts[0] if name_parts else "there" - - logger.info( - "[%s] /submit: email=%s ip=%s browser=%r dog=%s services=%s page=%r", - request_id, data.email, ip, browser, data.petName, data.services, data.page, - ) - logger.debug("[%s] full payload: %s", request_id, data.model_dump()) - await _enforce_submit_rate_limits(request_id, ip, str(data.email)) _enforce_form_timing(request_id, data) @@ -779,6 +877,18 @@ async def submit_booking(data: BookingSubmission, request: Request): "ignored": True, } + _validate_submission(request_id, data) + _normalize_submission(data) + + name_parts = data.fullName.split() + first_name = name_parts[0] if name_parts else "there" + + logger.info( + "[%s] /submit: type=%s email=%s ip=%s browser=%r dog=%s services=%s page=%r", + request_id, data.enquiryType, data.email, ip, browser, data.petName, data.services, data.page, + ) + logger.debug("[%s] full payload: %s", request_id, data.model_dump()) + failures: list[dict] = [] try: @@ -787,7 +897,7 @@ async def submit_booking(data: BookingSubmission, request: Request): "from": FROM_EMAIL, "to": [data.email], "reply_to": REPLY_TO, - "subject": f"We received your enquiry, {first_name}! 🐾", + "subject": f"We received your {'general enquiry' if _is_general_enquiry(data) else 'enquiry'}, {first_name}! 🐾", "html": client_email(data), }, label="client_email", @@ -807,7 +917,11 @@ async def submit_booking(data: BookingSubmission, request: Request): "from": FROM_EMAIL, "to": [OWNER_EMAIL], "reply_to": data.email, - "subject": f"New GoodWalk lead — {data.fullName} ({data.petName})", + "subject": ( + f"New GoodWalk general enquiry — {data.fullName}" + if _is_general_enquiry(data) + else f"New GoodWalk lead — {data.fullName} ({data.petName})" + ), "html": owner_email(data, ip, browser), }, label="owner_email", diff --git a/package-lock.json b/package-lock.json index 208bcfa..fa64b70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "goodwalk-svelte-port", - "version": "4.0.1", + "version": "4.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "goodwalk-svelte-port", - "version": "4.0.1", + "version": "4.0.2", "dependencies": { "canvas-confetti": "^1.9.4", "pg": "^8.13.1" diff --git a/package.json b/package.json index 2116bf5..d67f385 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "goodwalk-svelte-port", - "version": "4.0.1", + "version": "4.0.2", "private": true, "type": "module", "scripts": { diff --git a/src/lib/components/BookingPage.svelte b/src/lib/components/BookingPage.svelte index 2d2bf20..bf5480e 100644 --- a/src/lib/components/BookingPage.svelte +++ b/src/lib/components/BookingPage.svelte @@ -4,6 +4,7 @@ import type { BookingContent } from '$lib/types'; export let booking: BookingContent; + export let allowGeneralEnquiry = false; const email = 'info@goodwalk.co.nz'; const phone = '(022) 642 1011'; @@ -12,8 +13,14 @@
-

Book a Meet & Greet

-

Fill in the form below and we'll be in touch to arrange a free introduction.

+

Contact Us

+

+ {#if allowGeneralEnquiry} + Fill in the form below to book a Meet & Greet or send a general enquiry. + {:else} + Fill in the form below and we'll be in touch to arrange a free introduction. + {/if} +

- +
- - - - - - - - - - - - + {"".join(detail_rows)} {message_block}
Dog{data.petName}
Location{data.location}
Services{services_text}