diff --git a/0 b/0 new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/package.json b/apps/api/package.json index 55c1c9d..14891a6 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -22,6 +22,7 @@ "hono": "^4.6.17", "node-cron": "^3.0.3", "nodemailer": "^6.9.16", + "telnyx": "^6.41.0", "zod": "^4.3.6" }, "devDependencies": { diff --git a/apps/api/src/services/sms.ts b/apps/api/src/services/sms.ts new file mode 100644 index 0000000..969667c --- /dev/null +++ b/apps/api/src/services/sms.ts @@ -0,0 +1,140 @@ +import { Telnyx } from "telnyx"; + +export interface SmsProvider { + sendSms(to: string, body: string, mediaUrls?: string[]): Promise<{ messageId: string; status: string }>; + validateWebhookSignature(req: Request): boolean; +} + +interface TelnyxSmsResult { + message_id: string; + status: string; +} + +function createTelnyxClient(): Telnyx | null { + const apiKey = process.env.TELNYX_API_KEY; + if (!apiKey) return null; + return new Telnyx(apiKey); +} + +let _client: Telnyx | null | undefined; + +function getClient(): Telnyx | null { + if (_client === undefined) _client = createTelnyxClient(); + return _client; +} + +function getFromNumber(): string | null { + return process.env.TELNYX_FROM_NUMBER ?? null; +} + +function isE164(phone: string): boolean { + return /^\+[1-9]\d{7,14}$/.test(phone); +} + +export async function sendSms( + to: string, + body: string, + mediaUrls?: string[] +): Promise<{ messageId: string; status: string }> { + const client = getClient(); + if (!client) throw new Error("Telnyx client not initialized. Set TELNYX_API_KEY."); + + const from = getFromNumber(); + if (!from) throw new Error("TELNYX_FROM_NUMBER is not set"); + + if (!isE164(to)) throw new Error(`Invalid recipient phone format: ${to}. Expected E.164.`); + if (!isE164(from)) throw new Error(`Invalid sender phone format: ${from}. Expected E.164.`); + + const payload: Record = { + from, + to, + body, + }; + + if (mediaUrls && mediaUrls.length > 0) { + payload.media_urls = mediaUrls; + } + + const result = await client.messages.create(payload as Record); + const smsResult = result.data as unknown as TelnyxSmsResult; + return { + messageId: smsResult.message_id, + status: smsResult.status, + }; +} + +export class TelnyxProvider implements SmsProvider { + async sendSms( + to: string, + body: string, + mediaUrls?: string[] + ): Promise<{ messageId: string; status: string }> { + return sendSms(to, body, mediaUrls); + } + + validateWebhookSignature(req: Request): boolean { + const secret = process.env.TELNYX_WEBHOOK_SECRET; + if (!secret) return false; + + const signature = req.headers.get("telnyx-signature"); + if (!signature) return false; + + const payload = JSON.stringify(req.body); + + try { + const { createHmac } = await import("crypto"); + const hmac = createHmac("sha256", secret); + const expected = `sha256=${hmac.update(payload).digest("hex")}`; + + const sigBuf = Buffer.from(signature); + const expBuf = Buffer.from(expected); + + if (sigBuf.length !== expBuf.length) return false; + + let diff = 0; + for (let i = 0; i < sigBuf.length; i++) { + diff |= sigBuf[i] ^ expBuf[i]; + } + return diff === 0; + } catch { + return false; + } + } +} + +let _provider: SmsProvider | null | undefined; + +export function createSmsProvider(): SmsProvider | null { + if (_provider === undefined) { + if (process.env.SMS_ENABLED !== "true") { + _provider = null; + return null; + } + switch (process.env.SMS_PROVIDER) { + case "telnyx": { + const client = getClient(); + if (!client) { + _provider = null; + return null; + } + _provider = new TelnyxProvider(); + break; + } + default: + _provider = null; + } + } + return _provider; +} + +export async function smsSend( + to: string, + body: string, + mediaUrls?: string[] +): Promise { + const provider = createSmsProvider(); + if (!provider) return false; + + await provider.sendSms(to, body, mediaUrls); + return true; +} \ No newline at end of file diff --git a/generate_dogs.sh b/generate_dogs.sh new file mode 100755 index 0000000..63c35ce --- /dev/null +++ b/generate_dogs.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +API_HOST="https://api.minimax.io" +API_KEY="$MINIMAX_API_KEY" +OUTPUT_DIR="minimax-output" + +mkdir -p "$OUTPUT_DIR" + +# Diverse dog image prompts +declare -a PROMPTS=( + "A beautiful red Irish Setter with long flowing silky coat, standing proudly in golden hour sunlight, professional pet portrait photography, warm tones" + "A fluffy white Pomeranian puppy with thick fluffy coat, sitting alert with bright expression, studio white background, cute grooming" + "A black Schnauzer with distinctive full beard and mustache, freshly groomed with neat styling, professional grooming salon setting" + "A cream and white Cavalier King Charles Spaniel with silky coat, gentle sad eyes, soft warm indoor lighting, elegant pose" + "A brown and white Basset Hound with long droopy ears, lying down in relaxed pose, natural outdoor setting, peaceful expression" + "A black and tan miniature Dachshund with glossy coat, alert standing pose, warm studio lighting, detailed paws visible" + "A white fluffy Bichon Frise after professional grooming with rounded topknot, happy bouncy expression, bright cheerful background" + "A muscular fawn Boxer dog, athletic build, standing confidently outdoors in park, energetic expression, natural lighting" + "A blue merle Shetland Sheepdog with alert ears and fluffy coat, running happily, green grass field background, vibrant" + "A buff colored Cocker Spaniel with beautiful silky coat, friendly gentle expression, warm natural window lighting, indoor" +) + +declare -a FILENAMES=( + "dog-setter-red-sunlit.png" + "dog-pomeranian-white-studio.png" + "dog-schnauzer-black-groomed.png" + "dog-cavalier-cream-gentle.png" + "dog-basset-brown-white.png" + "dog-dachshund-black-tan.png" + "dog-bichon-white-groomed.png" + "dog-boxer-fawn-athletic.png" + "dog-sheepdog-merle-running.png" + "dog-cocker-buff-friendly.png" +) + +echo "Generating ${#PROMPTS[@]} diverse dog images..." + +for i in "${!PROMPTS[@]}"; do + PROMPT="${PROMPTS[$i]}" + FILENAME="${FILENAMES[$i]}" + + echo -n "[$((i+1))/${#PROMPTS[@]}] $FILENAME... " + + RESPONSE=$(curl -s -X POST "${API_HOST}/v1/image_generation" \ + -H "Authorization: Bearer ${API_KEY}" \ + -H "Content-Type: application/json" \ + -d "{\"model\":\"image-01\",\"prompt\":\"${PROMPT}\",\"image_count\":1}") + + # Extract image URL from response + IMAGE_URL=$(echo "$RESPONSE" | grep -o '"image_urls":\["\([^"]*\)' | cut -d'"' -f4) + + if [ -n "$IMAGE_URL" ]; then + curl -s "$IMAGE_URL" -o "$OUTPUT_DIR/$FILENAME" 2>/dev/null + if [ -f "$OUTPUT_DIR/$FILENAME" ] && [ -s "$OUTPUT_DIR/$FILENAME" ]; then + echo "✓" + else + echo "✗ (download failed)" + fi + else + echo "✗ (no URL)" + fi +done + +echo "Done! Generated images in $OUTPUT_DIR/" +ls -lh "$OUTPUT_DIR"/dog-*.png 2>/dev/null | wc -l diff --git a/generate_pet_images.sh b/generate_pet_images.sh new file mode 100755 index 0000000..e734a4b --- /dev/null +++ b/generate_pet_images.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# Use the configured MiniMax API host +API_HOST="${MINIMAX_API_HOST:-https://api.minimax.io}" +API_KEY="$MINIMAX_API_KEY" + +# Test endpoint - check which one works +echo "Testing API endpoints..." +echo "API_HOST: $API_HOST" +echo "API_KEY: ${API_KEY:0:15}..." + +# Array of diverse dog images to generate +declare -a PROMPTS=( + "A beautiful red Irish Setter with flowing silky coat, standing proudly in a sunny garden, warm natural lighting, professional pet photography" + "A fluffy white Pomeranian with thick coat, sitting alert, bright studio background, cute expression" + "A black Schnauzer with distinctive beard, freshly groomed, professional salon setting, dignified pose" + "A cream-colored Cavalier King Charles Spaniel, silky coat, gentle expression, soft warm lighting" + "A brown and white Basset Hound, long ears, relaxed sitting pose, natural outdoor background" + "A black and tan Dachshund, elongated body, alert posture, warm studio lighting" + "A white Bichon Frise, fluffy groomed coat, happy expression, bright cheerful background" + "A fawn Boxer with muscular build, athletic posture, outdoor park setting, energetic expression" + "A merle Shetland Sheepdog, alert ears, running pose, green garden background" + "A buff-colored Cocker Spaniel, silky coat, friendly expression, warm natural light" +) + +declare -a FILENAMES=( + "dog-setter-red-sunny.png" + "dog-pomeranian-white-alert.png" + "dog-schnauzer-groomed.png" + "dog-cavalier-cream.png" + "dog-basset-hound-outdoor.png" + "dog-dachshund-alert.png" + "dog-bichon-frise-happy.png" + "dog-boxer-athletic.png" + "dog-sheepdog-merle.png" + "dog-cocker-spaniel-buff.png" +) + +mkdir -p minimax-output + +echo "Generating ${#PROMPTS[@]} diverse dog images..." + +for i in "${!PROMPTS[@]}"; do + PROMPT="${PROMPTS[$i]}" + FILENAME="${FILENAMES[$i]}" + + echo "[$((i+1))/${#PROMPTS[@]}] Generating: $FILENAME" + + # Make API request + RESPONSE=$(curl -s -X POST "${API_HOST}/v1/image_generation" \ + -H "Authorization: Bearer ${API_KEY}" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"image-01\", + \"prompt\": \"${PROMPT}\", + \"image_count\": 1 + }") + + # Check if response contains image data + if echo "$RESPONSE" | grep -q "data\|image_url\|file_content"; then + echo " ✓ Response received" + + # Try to extract and save image data + # Different APIs format responses differently + IMAGE_DATA=$(echo "$RESPONSE" | grep -o '"file_content":"[^"]*' | head -1 | cut -d'"' -f4) + + if [ -n "$IMAGE_DATA" ]; then + echo "$IMAGE_DATA" | base64 -d > "minimax-output/$FILENAME" + echo " ✓ Image saved to minimax-output/$FILENAME" + else + echo " ✗ Could not extract image data" + fi + else + echo " ✗ API response: ${RESPONSE:0:100}" + fi + + sleep 1 +done + +echo "Image generation complete!" diff --git a/generate_puggles.py b/generate_puggles.py new file mode 100644 index 0000000..bc51485 --- /dev/null +++ b/generate_puggles.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import base64 +import requests +import os +import json + +api_key = os.environ.get("MINIMAX_API_KEY") +if not api_key: + raise ValueError("MINIMAX_API_KEY environment variable not set") + +url = "https://api.minimax.io/v1/image_generation" +headers = {"Authorization": f"Bearer {api_key}"} + +# Ensure output directory exists +os.makedirs("minimax-output", exist_ok=True) + +prompts = [ + { + "filename": "dog-puggle-fawn-playful.png", + "prompt": "Adorable fawn Puggle puppy with playful expression, compact muscular build, professional pet photography, studio lighting, photorealistic" + }, + { + "filename": "dog-puggle-black-sitting.png", + "prompt": "Black and tan Puggle with alert sitting posture, pointed beagle-like ears, gentle eyes, professional studio lighting, photorealistic" + }, + { + "filename": "dog-puggle-cream-groomed.png", + "prompt": "Cream Puggle freshly groomed with fluffy coat, happy expression, lying down comfortably, natural daylight, photorealistic" + }, + { + "filename": "dog-puggle-tricolor-outdoor.png", + "prompt": "Tricolor Puggle in outdoor garden setting, alert playful pose, natural sunlight, professional pet photography, photorealistic" + }, + { + "filename": "dog-puggle-fawn-grooming.png", + "prompt": "Fawn Puggle at grooming salon, gentle expression, compact muscular build with beagle-like features, professional grooming setup, warm lighting, photorealistic" + } +] + +print(f"Generating {len(prompts)} Puggle images...") + +for item in prompts: + filename = item["filename"] + prompt = item["prompt"] + + print(f"\nGenerating {filename}...") + + payload = { + "model": "image-01", + "prompt": prompt, + "aspect_ratio": "1:1", + "response_format": "base64", + } + + try: + response = requests.post(url, headers=headers, json=payload, timeout=60) + response.raise_for_status() + + data = response.json() + if "data" in data and "image_base64" in data["data"]: + images = data["data"]["image_base64"] + + # Save the first (and usually only) image + output_path = f"minimax-output/{filename}" + with open(output_path, "wb") as f: + f.write(base64.b64decode(images[0])) + + file_size = os.path.getsize(output_path) + print(f"✓ Saved {filename} ({file_size} bytes)") + else: + print(f"✗ Unexpected response format: {json.dumps(data, indent=2)}") + except requests.exceptions.RequestException as e: + print(f"✗ Error generating {filename}: {e}") + except Exception as e: + print(f"✗ Unexpected error for {filename}: {e}") + +print("\n✓ Image generation complete!") +print("Files saved to minimax-output/") diff --git a/infra b/infra index d6c0d13..b667a3f 160000 --- a/infra +++ b/infra @@ -1 +1 @@ -Subproject commit d6c0d13d0256cb9209acb9445f0fdf44fa9801ad +Subproject commit b667a3f0054beef8bb40f502197e7aee10bd9977 diff --git a/packages/db/migrations/meta/0026_snapshot.json b/packages/db/migrations/meta/0026_snapshot.json new file mode 100644 index 0000000..8d6baad --- /dev/null +++ b/packages/db/migrations/meta/0026_snapshot.json @@ -0,0 +1,2271 @@ +{ + "id": "003abeb5-ee36-444f-92ec-9a87bb6675d6", + "prevId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appointment_groups": { + "name": "appointment_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "appointment_groups_client_id_clients_id_fk": { + "name": "appointment_groups_client_id_clients_id_fk", + "tableFrom": "appointment_groups", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appointments": { + "name": "appointments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pet_id": { + "name": "pet_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "service_id": { + "name": "service_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "staff_id": { + "name": "staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "bather_staff_id": { + "name": "bather_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "appointment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "start_time": { + "name": "start_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "series_id": { + "name": "series_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "series_index": { + "name": "series_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "confirmation_status": { + "name": "confirmation_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "confirmation_token": { + "name": "confirmation_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_notes": { + "name": "customer_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "appointments_client_id_clients_id_fk": { + "name": "appointments_client_id_clients_id_fk", + "tableFrom": "appointments", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "appointments_pet_id_pets_id_fk": { + "name": "appointments_pet_id_pets_id_fk", + "tableFrom": "appointments", + "tableTo": "pets", + "columnsFrom": [ + "pet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "appointments_service_id_services_id_fk": { + "name": "appointments_service_id_services_id_fk", + "tableFrom": "appointments", + "tableTo": "services", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "appointments_staff_id_staff_id_fk": { + "name": "appointments_staff_id_staff_id_fk", + "tableFrom": "appointments", + "tableTo": "staff", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "appointments_bather_staff_id_staff_id_fk": { + "name": "appointments_bather_staff_id_staff_id_fk", + "tableFrom": "appointments", + "tableTo": "staff", + "columnsFrom": [ + "bather_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "appointments_series_id_recurring_series_id_fk": { + "name": "appointments_series_id_recurring_series_id_fk", + "tableFrom": "appointments", + "tableTo": "recurring_series", + "columnsFrom": [ + "series_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "appointments_group_id_appointment_groups_id_fk": { + "name": "appointments_group_id_appointment_groups_id_fk", + "tableFrom": "appointments", + "tableTo": "appointment_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "appointments_confirmation_token_unique": { + "name": "appointments_confirmation_token_unique", + "nullsNotDistinct": false, + "columns": [ + "confirmation_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_provider_config": { + "name": "auth_provider_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_base_url": { + "name": "internal_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid profile email'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_provider_config_provider_id_unique": { + "name": "auth_provider_config_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.business_settings": { + "name": "business_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "business_name": { + "name": "business_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'GroomBook'" + }, + "logo_base64": { + "name": "logo_base64", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_mime_type": { + "name": "logo_mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_key": { + "name": "logo_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#4f8a6f'" + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#8b7355'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.clients": { + "name": "clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_opt_out": { + "name": "email_opt_out", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sms_opt_in": { + "name": "sms_opt_in", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sms_consent_date": { + "name": "sms_consent_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sms_opt_out_date": { + "name": "sms_opt_out_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sms_consent_text": { + "name": "sms_consent_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "client_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grooming_visit_logs": { + "name": "grooming_visit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pet_id": { + "name": "pet_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "appointment_id": { + "name": "appointment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "staff_id": { + "name": "staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cut_style": { + "name": "cut_style", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "products_used": { + "name": "products_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "groomed_at": { + "name": "groomed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "grooming_visit_logs_pet_id_pets_id_fk": { + "name": "grooming_visit_logs_pet_id_pets_id_fk", + "tableFrom": "grooming_visit_logs", + "tableTo": "pets", + "columnsFrom": [ + "pet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "grooming_visit_logs_appointment_id_appointments_id_fk": { + "name": "grooming_visit_logs_appointment_id_appointments_id_fk", + "tableFrom": "grooming_visit_logs", + "tableTo": "appointments", + "columnsFrom": [ + "appointment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "grooming_visit_logs_staff_id_staff_id_fk": { + "name": "grooming_visit_logs_staff_id_staff_id_fk", + "tableFrom": "grooming_visit_logs", + "tableTo": "staff", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.impersonation_audit_logs": { + "name": "impersonation_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_visited": { + "name": "page_visited", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "impersonation_audit_logs_session_id_idx": { + "name": "impersonation_audit_logs_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impersonation_audit_logs_session_id_impersonation_sessions_id_fk": { + "name": "impersonation_audit_logs_session_id_impersonation_sessions_id_fk", + "tableFrom": "impersonation_audit_logs", + "tableTo": "impersonation_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.impersonation_sessions": { + "name": "impersonation_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "staff_id": { + "name": "staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "impersonation_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "impersonation_sessions_staff_id_status_idx": { + "name": "impersonation_sessions_staff_id_status_idx", + "columns": [ + { + "expression": "staff_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "impersonation_sessions_client_id_idx": { + "name": "impersonation_sessions_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impersonation_sessions_staff_id_staff_id_fk": { + "name": "impersonation_sessions_staff_id_staff_id_fk", + "tableFrom": "impersonation_sessions", + "tableTo": "staff", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "impersonation_sessions_client_id_clients_id_fk": { + "name": "impersonation_sessions_client_id_clients_id_fk", + "tableFrom": "impersonation_sessions", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoice_line_items": { + "name": "invoice_line_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invoice_id": { + "name": "invoice_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "unit_price_cents": { + "name": "unit_price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_cents": { + "name": "total_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_invoice_line_items_invoice_id": { + "name": "idx_invoice_line_items_invoice_id", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoice_line_items_invoice_id_invoices_id_fk": { + "name": "invoice_line_items_invoice_id_invoices_id_fk", + "tableFrom": "invoice_line_items", + "tableTo": "invoices", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoice_tip_splits": { + "name": "invoice_tip_splits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invoice_id": { + "name": "invoice_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "staff_id": { + "name": "staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "staff_name": { + "name": "staff_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "share_pct": { + "name": "share_pct", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true + }, + "share_cents": { + "name": "share_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_invoice_tip_splits_invoice_id": { + "name": "idx_invoice_tip_splits_invoice_id", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoice_tip_splits_invoice_id_invoices_id_fk": { + "name": "invoice_tip_splits_invoice_id_invoices_id_fk", + "tableFrom": "invoice_tip_splits", + "tableTo": "invoices", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invoice_tip_splits_staff_id_staff_id_fk": { + "name": "invoice_tip_splits_staff_id_staff_id_fk", + "tableFrom": "invoice_tip_splits", + "tableTo": "staff", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoices": { + "name": "invoices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "appointment_id": { + "name": "appointment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subtotal_cents": { + "name": "subtotal_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tax_cents": { + "name": "tax_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tip_cents": { + "name": "tip_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cents": { + "name": "total_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invoice_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "payment_method": { + "name": "payment_method", + "type": "payment_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_invoices_client_id": { + "name": "idx_invoices_client_id", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invoices_status": { + "name": "idx_invoices_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invoices_created_at": { + "name": "idx_invoices_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoices_appointment_id_appointments_id_fk": { + "name": "invoices_appointment_id_appointments_id_fk", + "tableFrom": "invoices", + "tableTo": "appointments", + "columnsFrom": [ + "appointment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "invoices_client_id_clients_id_fk": { + "name": "invoices_client_id_clients_id_fk", + "tableFrom": "invoices", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pets": { + "name": "pets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "species": { + "name": "species", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "breed": { + "name": "breed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight_kg": { + "name": "weight_kg", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "date_of_birth": { + "name": "date_of_birth", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "health_alerts": { + "name": "health_alerts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grooming_notes": { + "name": "grooming_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cut_style": { + "name": "cut_style", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shampoo_preference": { + "name": "shampoo_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "special_care_notes": { + "name": "special_care_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "photo_key": { + "name": "photo_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photo_uploaded_at": { + "name": "photo_uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pets_client_id_clients_id_fk": { + "name": "pets_client_id_clients_id_fk", + "tableFrom": "pets", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recurring_series": { + "name": "recurring_series", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "frequency_weeks": { + "name": "frequency_weeks", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminder_logs": { + "name": "reminder_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "appointment_id": { + "name": "appointment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reminder_type": { + "name": "reminder_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'email'" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reminder_logs_appointment_id_appointments_id_fk": { + "name": "reminder_logs_appointment_id_appointments_id_fk", + "tableFrom": "reminder_logs", + "tableTo": "appointments", + "columnsFrom": [ + "appointment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reminder_logs_appointment_id_reminder_type_channel_unique": { + "name": "reminder_logs_appointment_id_reminder_type_channel_unique", + "nullsNotDistinct": false, + "columns": [ + "appointment_id", + "reminder_type", + "channel" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.services": { + "name": "services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_price_cents": { + "name": "base_price_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "services_name_unique": { + "name": "services_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff": { + "name": "staff", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "staff_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'groomer'" + }, + "is_super_user": { + "name": "is_super_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ical_token": { + "name": "ical_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "staff_user_id_user_id_fk": { + "name": "staff_user_id_user_id_fk", + "tableFrom": "staff", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "staff_email_unique": { + "name": "staff_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "staff_oidc_sub_unique": { + "name": "staff_oidc_sub_unique", + "nullsNotDistinct": false, + "columns": [ + "oidc_sub" + ] + }, + "staff_ical_token_unique": { + "name": "staff_ical_token_unique", + "nullsNotDistinct": false, + "columns": [ + "ical_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist_entries": { + "name": "waitlist_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pet_id": { + "name": "pet_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "service_id": { + "name": "service_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "preferred_date": { + "name": "preferred_date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preferred_time": { + "name": "preferred_time", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "waitlist_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "notified_at": { + "name": "notified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_waitlist_client_id": { + "name": "idx_waitlist_client_id", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_waitlist_preferred_date": { + "name": "idx_waitlist_preferred_date", + "columns": [ + { + "expression": "preferred_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_waitlist_status": { + "name": "idx_waitlist_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "waitlist_entries_client_id_clients_id_fk": { + "name": "waitlist_entries_client_id_clients_id_fk", + "tableFrom": "waitlist_entries", + "tableTo": "clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "waitlist_entries_pet_id_pets_id_fk": { + "name": "waitlist_entries_pet_id_pets_id_fk", + "tableFrom": "waitlist_entries", + "tableTo": "pets", + "columnsFrom": [ + "pet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "waitlist_entries_service_id_services_id_fk": { + "name": "waitlist_entries_service_id_services_id_fk", + "tableFrom": "waitlist_entries", + "tableTo": "services", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.appointment_status": { + "name": "appointment_status", + "schema": "public", + "values": [ + "scheduled", + "confirmed", + "in_progress", + "completed", + "cancelled", + "no_show" + ] + }, + "public.client_status": { + "name": "client_status", + "schema": "public", + "values": [ + "active", + "disabled" + ] + }, + "public.impersonation_session_status": { + "name": "impersonation_session_status", + "schema": "public", + "values": [ + "active", + "ended", + "expired" + ] + }, + "public.invoice_status": { + "name": "invoice_status", + "schema": "public", + "values": [ + "draft", + "pending", + "paid", + "void" + ] + }, + "public.payment_method": { + "name": "payment_method", + "schema": "public", + "values": [ + "cash", + "card", + "check", + "other" + ] + }, + "public.staff_role": { + "name": "staff_role", + "schema": "public", + "values": [ + "groomer", + "receptionist", + "manager" + ] + }, + "public.waitlist_status": { + "name": "waitlist_status", + "schema": "public", + "values": [ + "active", + "notified", + "expired", + "cancelled" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aee6dc2..08a6c60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ importers: nodemailer: specifier: ^6.9.16 version: 6.10.1 + telnyx: + specifier: ^6.41.0 + version: 6.41.0(ws@8.19.0) zod: specifier: ^4.3.6 version: 4.3.6 @@ -2109,6 +2112,9 @@ packages: resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} engines: {node: '>=18.0.0'} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -3090,6 +3096,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -4097,6 +4106,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -4176,6 +4188,14 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + telnyx@6.41.0: + resolution: {integrity: sha512-93eKksI6HnLYp8e4DGlpC3SkBAfagblE+uug0FNDLT/+mix3PP0RveoQ/YZeRdxDhjMcoXVgeusJsgFP6PvUdw==} + peerDependencies: + ws: ^8.18.0 + peerDependenciesMeta: + ws: + optional: true + temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -6698,6 +6718,8 @@ snapshots: dependencies: tslib: 2.8.1 + '@stablelib/base64@1.0.1': {} + '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -7743,6 +7765,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-sha256@1.3.0: {} + fast-uri@3.1.0: {} fast-xml-builder@1.1.4: @@ -8744,6 +8768,11 @@ snapshots: stackback@0.0.2: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + std-env@3.10.0: {} stop-iteration-iterator@1.1.0: @@ -8842,6 +8871,12 @@ snapshots: tapable@2.3.0: {} + telnyx@6.41.0(ws@8.19.0): + dependencies: + standardwebhooks: 1.0.0 + optionalDependencies: + ws: 8.19.0 + temp-dir@2.0.0: {} tempy@0.6.0: diff --git a/test_logo.png b/test_logo.png new file mode 100644 index 0000000..08cd6f2 Binary files /dev/null and b/test_logo.png differ diff --git a/uat-admin-403-error.png b/uat-admin-403-error.png new file mode 100644 index 0000000..787dd70 Binary files /dev/null and b/uat-admin-403-error.png differ diff --git a/uat-admin-appointments.png b/uat-admin-appointments.png new file mode 100644 index 0000000..c4ff1d3 Binary files /dev/null and b/uat-admin-appointments.png differ diff --git a/uat-admin-clients.png b/uat-admin-clients.png new file mode 100644 index 0000000..6f37874 Binary files /dev/null and b/uat-admin-clients.png differ diff --git a/uat-admin-page.png b/uat-admin-page.png new file mode 100644 index 0000000..3d80995 Binary files /dev/null and b/uat-admin-page.png differ diff --git a/uat-admin-portal-working.png b/uat-admin-portal-working.png new file mode 100644 index 0000000..ed963af Binary files /dev/null and b/uat-admin-portal-working.png differ diff --git a/uat-clients-page.png b/uat-clients-page.png new file mode 100644 index 0000000..6f37874 Binary files /dev/null and b/uat-clients-page.png differ diff --git a/uat-current-state.png b/uat-current-state.png new file mode 100644 index 0000000..76542f3 Binary files /dev/null and b/uat-current-state.png differ diff --git a/uat-gro480-403-error.png b/uat-gro480-403-error.png new file mode 100644 index 0000000..32f5b2a Binary files /dev/null and b/uat-gro480-403-error.png differ diff --git a/uat-gro480-after-login.png b/uat-gro480-after-login.png new file mode 100644 index 0000000..76542f3 Binary files /dev/null and b/uat-gro480-after-login.png differ diff --git a/uat-gro480-clients.png b/uat-gro480-clients.png new file mode 100644 index 0000000..6f37874 Binary files /dev/null and b/uat-gro480-clients.png differ diff --git a/uat-gro480-page.png b/uat-gro480-page.png new file mode 100644 index 0000000..76542f3 Binary files /dev/null and b/uat-gro480-page.png differ diff --git a/uat-gro494-initial-state.png b/uat-gro494-initial-state.png new file mode 100644 index 0000000..ac05877 Binary files /dev/null and b/uat-gro494-initial-state.png differ diff --git a/uat-gro494-still-on-old-image.png b/uat-gro494-still-on-old-image.png new file mode 100644 index 0000000..ac05877 Binary files /dev/null and b/uat-gro494-still-on-old-image.png differ diff --git a/uat-gro504-invoices-error.png b/uat-gro504-invoices-error.png new file mode 100644 index 0000000..1cbe16d Binary files /dev/null and b/uat-gro504-invoices-error.png differ diff --git a/uat-gro504-still-broken.png b/uat-gro504-still-broken.png new file mode 100644 index 0000000..1cbe16d Binary files /dev/null and b/uat-gro504-still-broken.png differ diff --git a/uat-gro505-error.png b/uat-gro505-error.png new file mode 100644 index 0000000..1cbe16d Binary files /dev/null and b/uat-gro505-error.png differ diff --git a/uat-gro505-invoices-loaded.png b/uat-gro505-invoices-loaded.png new file mode 100644 index 0000000..e4b431d Binary files /dev/null and b/uat-gro505-invoices-loaded.png differ diff --git a/uat-gro505-paid-filter.png b/uat-gro505-paid-filter.png new file mode 100644 index 0000000..383473f Binary files /dev/null and b/uat-gro505-paid-filter.png differ diff --git a/uat-gro545-admin-portal-working.png b/uat-gro545-admin-portal-working.png new file mode 100644 index 0000000..15f7859 Binary files /dev/null and b/uat-gro545-admin-portal-working.png differ diff --git a/uat-gro545-login-page.png b/uat-gro545-login-page.png new file mode 100644 index 0000000..2d655e6 Binary files /dev/null and b/uat-gro545-login-page.png differ diff --git a/uat-gro563-initial-state.png b/uat-gro563-initial-state.png new file mode 100644 index 0000000..2d655e6 Binary files /dev/null and b/uat-gro563-initial-state.png differ diff --git a/uat-gro564-admin-dashboard.png b/uat-gro564-admin-dashboard.png new file mode 100644 index 0000000..b81b28d Binary files /dev/null and b/uat-gro564-admin-dashboard.png differ diff --git a/uat-gro564-initial-state.png b/uat-gro564-initial-state.png new file mode 100644 index 0000000..5132f1a Binary files /dev/null and b/uat-gro564-initial-state.png differ diff --git a/uat-gro564-setup-wizard.png b/uat-gro564-setup-wizard.png new file mode 100644 index 0000000..5a4e6ab Binary files /dev/null and b/uat-gro564-setup-wizard.png differ diff --git a/uat-gro564-staff-page.png b/uat-gro564-staff-page.png new file mode 100644 index 0000000..d0dd3b2 Binary files /dev/null and b/uat-gro564-staff-page.png differ diff --git a/uat-gro572-auth-500.png b/uat-gro572-auth-500.png new file mode 100644 index 0000000..7b31832 Binary files /dev/null and b/uat-gro572-auth-500.png differ diff --git a/uat-gro572-auth-broken.png b/uat-gro572-auth-broken.png new file mode 100644 index 0000000..7b31832 Binary files /dev/null and b/uat-gro572-auth-broken.png differ diff --git a/uat-gro572-initial-state.png b/uat-gro572-initial-state.png new file mode 100644 index 0000000..2d655e6 Binary files /dev/null and b/uat-gro572-initial-state.png differ diff --git a/uat-gro574-initial-state.png b/uat-gro574-initial-state.png new file mode 100644 index 0000000..2d655e6 Binary files /dev/null and b/uat-gro574-initial-state.png differ diff --git a/uat-gro574-sso-error.png b/uat-gro574-sso-error.png new file mode 100644 index 0000000..ed698c9 Binary files /dev/null and b/uat-gro574-sso-error.png differ diff --git a/uat-initial-state.png b/uat-initial-state.png new file mode 100644 index 0000000..76542f3 Binary files /dev/null and b/uat-initial-state.png differ diff --git a/uat-login-page.png b/uat-login-page.png new file mode 100644 index 0000000..76542f3 Binary files /dev/null and b/uat-login-page.png differ diff --git a/uat-login-page2.png b/uat-login-page2.png new file mode 100644 index 0000000..76542f3 Binary files /dev/null and b/uat-login-page2.png differ diff --git a/uat-nginx-proxy-still-present.png b/uat-nginx-proxy-still-present.png new file mode 100644 index 0000000..0db7f67 Binary files /dev/null and b/uat-nginx-proxy-still-present.png differ diff --git a/uat-settings-page.png b/uat-settings-page.png new file mode 100644 index 0000000..f8279bb Binary files /dev/null and b/uat-settings-page.png differ diff --git a/uat-staff-page.png b/uat-staff-page.png new file mode 100644 index 0000000..6982f78 Binary files /dev/null and b/uat-staff-page.png differ