The AI Trades
Call & Lead Handling

Missed Call Auto-Response and Dispatcher Alert

No-Code Flow 45 min
OpenPhoneRingCentralTwilioNextiva

The Problem

When a call goes unanswered, 60-80% of callers never leave a voicemail. They call your competitor instead. This automation texts the caller within seconds and sends a lead card to your dispatcher so the callback actually happens.

How It Works

Input

Missed call event from VoIP system (webhook)

Transformation

AI instantly texts caller with personalized response, simultaneously sends lead card to dispatcher via Slack/SMS

Output

Every missed call gets instant text. Lead card created. Callback promise tracked.

ReplacesAnswering service ($300-1,500/mo)

Importable Templates

Make.comWebhook to Twilio SMS
ZapierSend SMS via webhook

PRD

# Product Requirements Document

## Recipe 002 — Missed Call Command Center

### AI Trades Platform

---

Recipe Slug: `missed-call-auto-response`

Recipe Number: 002

Difficulty: Replit Build

Time Estimate: 4–6 hours

Category: Call & Lead Handling

Revenue Impact: $2,400–9,600/mo

Hours Saved: 8 hrs/wk

Replaces: Answering service ($300–1,500/mo)

Reusable Modules Referenced:

Integration Docs (include with build):

### Module Configuration: SMS AI Conversation

PlaceholderValue
`{{APP_NAME}}`Missed Call Command Center
`{{BUSINESS_TYPE}}`home service contractor
`{{SENDER_IDENTITY}}`Contractor's company name from profile
`{{CONTACT_NOUN}}`caller
`{{CONTACT_NOUN_PLURAL}}`callers
`{{CONVERSATION_GOAL}}`acknowledge the missed call, answer questions, and book appointments
`{{PRIMARY_COLOR}}``#0a85c2`
`{{DARK_COLOR}}``#003853`
`{{RECIPE_SLUG}}``missed-call-auto-response`

SMS Triggers:

### Module Configuration: CRM Integration

PlaceholderValue
`{{APP_NAME}}`Missed Call Command Center
`{{RECORD_TYPE}}`lead
`{{RECORD_TYPE_PLURAL}}`leads
`{{SYNC_DIRECTION}}`write
`{{TRIGGER_EVENT}}`missed call resolved (voice completed, text conversation ended, voicemail transcribed)
`{{RECIPE_SLUG}}``missed-call-auto-response`

---

## Table of Contents

1. Recipe Overview

2. Strategy Brief

3. Shared Architecture

4. Integration Safety Rules

5. Contractor Profile Data Model

6. Recipe-Specific Functionality (VTCR)

7. Recipe-Specific Setup

8. Database Migration Script

9. UX Expectations

10. Relevant Integration Best Practices

11. Testing Scenarios

---

1. Recipe Overview

The Missed Call Command Center is a comprehensive missed call recovery system for home service contractors. When a customer calls and nobody answers, the app automatically responds via AI voice agent, personalized text message, or both — based on the contractor's time-based routing rules.

Key Capabilities:

  • Multi-channel response: AI Voice (Vapi), Auto-Text (SMS), Both, or Voicemail
  • Time-based routing rules: different responses for business hours vs. after hours
  • AI Voice Agent answers live calls, responds to FAQs, and books appointments
  • Auto-Text sends personalized SMS with FAQ and calendar booking capability
  • Google Calendar integration for real-time availability and appointment booking
  • CRM integration pushes every interaction to ServiceTitan, Housecall Pro, Jobber, or GoHighLevel
  • Missed Call Dashboard shows all calls, outcomes, and recovery metrics
  • Works with any VoIP provider (Google Voice, Twilio, OpenPhone, RingCentral, Nextiva) via call forwarding

Core Flow:

1. Customer calls contractor's normal business number

2. Call rings through their existing phone system

3. Nobody answers → call forwards to the app's number

4. App checks routing rules for current day/time

5. Routes to: AI Voice → answers live, collects info, books appointments

OR: Auto-Text → sends personalized SMS, enables text conversation

OR: Both → AI voice answers AND text is sent

OR: Voicemail → records message, transcribes, notifies

6. Interaction logged to dashboard and synced to CRM

---

2. Strategy Brief

# Product Strategy Brief

Recipe 002: Missed Call Command Center

Slug: missed-call-auto-response

Category: Call & Lead Handling

---

1. The Business Villain

"The Silent Revenue Leak"

Every missed call is a customer who was ready to spend money — right now. They had a broken AC, a leaking pipe, a roof issue. They called. Nobody picked up. And in the 30–90 seconds that follow, they call the next contractor on Google.

The villain isn't the missed call itself — contractors know they miss calls. The villain is the dead air after the miss. The gap between "nobody answered" and "something happens." For most contractors, that gap is infinite. The caller gets nothing — no voice, no text, no acknowledgment. They're gone.

The compounding damage:

  • A single missed call in home services represents $250–$2,500 in lost revenue (service call + potential upsell)
  • The average contractor misses 15–30 calls/week (driving, on a roof, in a crawlspace, after hours)
  • That's $3,750–$75,000/month walking out the door
  • After-hours calls are the most valuable (emergency jobs, highest urgency, lowest competition) and the most likely to be missed
  • Answering services cost $300–$1,500/mo and deliver robotic, error-prone interactions that damage brand trust

The emotional toll: Contractors feel perpetually anxious about their phone. They answer during dinner, on ladders, in the middle of installs. They know every ring could be $1,000. This anxiety is corrosive — it degrades their work quality, their family time, and their mental health.

The villain is the absence of a system. Not a missed call app. Not a texting tool. A command center that guarantees: no call goes unanswered, no lead goes cold, no revenue disappears into silence.

---

2. The Behavioral Solution

"From Black Hole to Safety Net"

The behavioral shift we're engineering:

Before (Current State)After (With Command Center)
Missed call = lost customerMissed call = engaged lead
Anxiety about every ringConfidence that every call is covered
Manual callback attempts hours laterInstant AI engagement within seconds
No data on what was missedFull dashboard showing every call + outcome
After-hours = total darknessAfter-hours = AI working the phones
"I need to hire a receptionist""My AI handles it"

The core behavioral insight: Contractors don't need to answer more calls. They need to stop worrying about the calls they miss. The Command Center converts anxiety into confidence by making "missed" a misnomer — every call is caught, responded to, and tracked.

The trust ladder:

1. Day 1: Contractor sees their first missed call get auto-texted. Feels relief.

2. Week 1: They see the AI voice agent handle a call, collect info, book an appointment. Feels amazement.

3. Week 2: They check the dashboard and see 12 missed calls recovered, 4 appointments booked. Feels ROI.

4. Month 1: They stop flinching when a call goes to voicemail. Feels freedom.

---

3. Core Loops

This app has three parallel engagement loops that converge on a single dashboard. Each loop operates independently but shares the same FAQ knowledge base, calendar system, and CRM pipeline.

---

Loop A: AI Voice Agent (Vapi)

Trigger: Missed call forwards to app → routing rules say "Voice" or "Both"

Micro-step sequence:

StepWhat HappensUser Sees/DoesTech
A1Call forwards to Vapi numberCaller hears ringing → AI picks upVapi inbound call handling
A2AI greets caller with contractor's custom greetingCaller hears: "Hi, thanks for calling [Business Name]! I'm [AI Name], [Owner]'s assistant. How can I help?"Vapi TTS, pre-baked prompt
A3AI identifies caller intentNatural conversation — AI classifies: scheduling, emergency, pricing question, general inquiryVapi LLM with intent classification
A4AI checks FAQ knowledge baseIf question matches FAQ → delivers answer conversationallyFAQ module lookup
A5AI offers appointment booking"I can get you on [Owner]'s schedule. Let me check what's available..."Google Calendar API availability check
A6AI confirms appointment"Great, I've got you down for Tuesday at 2pm. You'll get a confirmation text shortly."Calendar event creation + Twilio SMS confirmation
A7AI collects contact infoName, phone (auto from caller ID), address, brief description of issueVapi data extraction
A8Call ends → data flows to dashboardTranscript, recording, outcome classification, appointment detailsVapi webhook → app database
A9CRM record created/updatedLead pushed to ServiceTitan/Jobber/HCP/GHL with full notesCRM Integration Module

Pre-baked prompt strategy (critical — contractor never writes prompts):

The system ships with a home-service-specific prompt template that includes:

  • Warm, professional tone calibrated for blue-collar trust (friendly, not corporate)
  • Emergency detection: "If the caller describes an active water leak, gas smell, electrical sparking, or no heat/AC in extreme temperatures, the AI flags as URGENT and offers to page the contractor immediately"
  • Service area awareness: AI knows the contractor's service area and can politely decline out-of-area requests
  • Price deflection: AI is trained to NOT quote prices (unless contractor enables it) — instead says "Pricing depends on the specifics, but [Owner] can give you an exact quote. Let's get you on the schedule."
  • Upsell awareness: AI asks qualifying questions that help the contractor prepare (e.g., "Do you know the age of your system?" "Is this your primary residence?")
  • Graceful handoff: If AI can't handle the request, it offers to take a message and have the contractor call back ASAP

Contractor configuration (setup, not prompt writing):

  • Business name, owner name, AI assistant name
  • Service types offered (checkboxes: AC repair, furnace install, duct cleaning, etc.)
  • Service area (zip codes or city/radius)
  • Hours of operation
  • Emergency paging preference (text contractor for emergencies? yes/no)
  • Voice selection (pick from 4–6 pre-curated voices that sound natural, not robotic)
  • Custom greeting (text field with pre-filled template they can tweak)

---

Loop B: Auto-Text (SMS AI)

Trigger: Missed call forwards to app → routing rules say "Text" or "Both"

Micro-step sequence:

StepWhat HappensUser Sees/DoesTech
B1Missed call detectedSystem identifies caller phone numberTwilio call event / forwarding detection
B2Auto-text sent within 10 secondsCaller receives: "Hey! This is [Business Name]. Sorry we missed your call — how can we help?"Twilio SMS, configurable template
B3Caller replies via text"My AC isn't working, can someone come out today?"Inbound SMS to app
B4SMS AI responds conversationallyAI checks FAQs, offers scheduling, asks qualifying questionsSMS AI Conversation Module
B5If booking requested → calendar checkAI offers available slots via textGoogle Calendar API
B6Appointment confirmed"You're all set for Tuesday at 2pm! We'll send a reminder."Calendar event + confirmation SMS
B7Conversation thread saved to dashboardFull text thread visible with outcome classificationApp database
B8CRM record created/updatedLead with conversation notes pushed to CRMCRM Integration Module

Auto-text template engine:

  • Pre-filled template: "Hey! This is [Business Name]. Sorry we missed your call — what can we help you with?"
  • After-hours variant: "Hey! This is [Business Name]. We're closed for the night but got your call. What's going on? We'll get back to you first thing in the morning!"
  • Emergency variant (if after-hours + emergency keywords detected in any response): "That sounds urgent. Let me get [Owner] on the line — sit tight."

---

Loop C: Voicemail + Transcription

Trigger: Missed call forwards to app → routing rules say "Voicemail"

Micro-step sequence:

StepWhat HappensUser Sees/DoesTech
C1Call forwards to voicemailCaller hears custom greetingTwilio voicemail / Vapi simple recording mode
C2Caller leaves messageStandard voicemail experienceAudio recording
C3Recording transcribedAI transcribes voicemail to textWhisper API or Vapi transcription
C4AI classifies voicemailIntent detection: scheduling request, emergency, question, sales call/spamLLM classification
C5Notification sent to contractorPush/SMS: "New voicemail from (555) 123-4567: 'My furnace stopped working, can someone come out?' — classified as SCHEDULING REQUEST"Twilio SMS to contractor
C6Voicemail appears on dashboardAudio player + transcript + classification + one-click callback buttonApp UI
C7Optional: auto-text sent to caller"Got your voicemail! We'll call you back shortly."Twilio SMS (configurable toggle)
C8CRM record createdVoicemail notes pushed to CRMCRM Integration Module

---

Loop Convergence: The Dashboard

All three loops feed into a unified missed call dashboard. This is the contractor's morning coffee screen — the place they check every day to see what happened while they were working/sleeping.

---

4. Integration Architecture

Primary Integrations

SystemPurposeConnection MethodSetup Complexity
VapiAI voice agentAPI key (contractor enters during setup)Medium — requires Vapi account + API key
TwilioSMS sending/receiving, voicemail recording, forwarding number provisioningAPI key (or platform-managed Twilio account)Medium — phone number provisioning
Google CalendarAvailability checking, appointment bookingOAuth 2.0Low — standard Google sign-in
Google VoiceCall forwarding sourceManual setup (guided instructions)Low — follow screenshot guide
OpenPhoneCall forwarding sourceManual setup (guided instructions)Low — follow screenshot guide
RingCentralCall forwarding sourceManual setup (guided instructions)Low — follow screenshot guide
NextivaCall forwarding sourceManual setup (guided instructions)Low — follow screenshot guide
ServiceTitanCRM syncAPI / webhookMedium — API key setup
Housecall ProCRM syncAPI / webhookMedium — API key setup
JobberCRM syncAPI / webhookMedium — API key setup
GoHighLevelCRM syncAPI / webhookMedium — API key setup
Webhook fallbackGeneric CRM/system syncWebhook URLLow — paste URL

Architecture Decision: Forwarding Number

The app provisions a dedicated phone number (via Twilio) for each contractor. This number is the forwarding destination they configure in their existing phone system.

Why this matters: The contractor keeps their existing phone number and phone system. Nothing changes about how they receive calls day-to-day. They simply add a "forward on no-answer" rule pointing to our number. This is zero-disruption adoption.

Number provisioning flow:

1. During setup, contractor selects their area code preference

2. App provisions a local Twilio number

3. App displays the number with copy-to-clipboard

4. App shows the setup guide for their specific VoIP provider

5. Contractor configures forwarding (one-time, 5-minute setup)

6. App has a "Test it" button — contractor calls their own number, lets it ring, and verifies the forwarding works

---

5. Profile Enrichment

Every missed call enriches the contractor's business intelligence:

Caller Profile Building

  • First call: Name, phone, rough location (area code), stated need → new lead created
  • Repeat caller: System recognizes the number → pulls up history → AI says "Welcome back, [Name]!" and references prior interactions
  • Cross-channel: If same caller hits voice AND text, threads are unified under one contact record

Business Intelligence Metrics

MetricWhat It RevealsStrategic Value
Total missed calls/weekCall volume vs. capacityHiring trigger — "You're missing 40 calls/week, you need a CSR"
Recovery rate% of missed calls that convert to engagementProves ROI of the tool
Appointments booked via AIDirect revenue attribution"Your AI booked 8 jobs this week worth ~$X"
Peak miss timesWhen calls are most frequently missedHelps contractor adjust staffing/availability
Top caller intentsWhat people are calling aboutInforms marketing, service offerings
After-hours volumeDemand outside business hoursJustifies extended hours or emergency service
Average response timeHow fast the system engagesCompetitive advantage data
Voicemail abandonmentCallers who hang up without leaving a messageShows value of AI voice over voicemail
SMS conversation lengthHow many messages to resolutionOptimizes AI conversation flow
Geographic distributionWhere callers are located (by area code/zip)Service area optimization

Weekly Digest

Auto-generated summary sent to contractor every Monday morning:

  • "Last week: 23 missed calls, 19 recovered (83%), 6 appointments booked, estimated revenue recovered: $4,200"
  • "Your busiest miss time was Tuesday 2–4pm — consider having someone on phones during that window"
  • "3 after-hours emergency calls were handled by your AI voice agent"

---

6. What This App Is NOT

It Is NOTWhy NotWhat It IS Instead
A phone system replacementContractors already have phone systems they're comfortable with. Ripping and replacing creates massive friction.A safety net that sits behind their existing phone system
A full call center / IVR"Press 1 for scheduling, press 2 for..." is corporate and alienating for home service customersA single, intelligent AI that has a natural conversation
A CRMWe push data to their CRM. We don't try to be one.A lead capture + routing engine that feeds their CRM
A marketing toolIt doesn't generate calls. It handles the ones that come in.A revenue recovery tool for existing inbound demand
A human answering service replacement (fully)Some calls genuinely need a human (complex negotiations, angry customers, insurance claims). The AI knows its limits.A first-responder that handles 80% of calls and escalates the rest
A scheduling platformWe check calendar availability and book. We don't manage the full job lifecycle.A scheduling interface that connects to their existing calendar
A prompt engineering toolContractors will never write prompts. Ever.A configuration tool with smart defaults — toggle switches, not text fields
A multi-line call management systemWe don't handle simultaneous inbound calls to a team of 20.A solo-contractor / small-team missed call recovery system
A robocaller or spam toolWe never initiate outbound calls. All engagement is in response to an inbound missed call.A reactive engagement system triggered only by real customer calls

---

7. User Experience Blueprint

First-Time Setup (Target: Under 15 minutes)

Step 1: Business Profile (2 min)

  • Business name, owner name, trade type (dropdown: HVAC, Plumbing, Roofing, Electrical, Landscaping, General)
  • Service area (zip codes or city + radius)
  • Business hours (visual time picker, pre-filled with M-F 8am-5pm)
  • Logo upload (optional, used in text messages if MMS supported)

Step 2: Choose Your Response Mode (1 min)

  • Visual card selection:
  • 🗣️ AI Voice Agent — "AI answers your missed calls live"
  • 💬 Auto-Text — "Sends a text when you miss a call"
  • 🗣️💬 Both — "AI answers AND sends a text" (recommended badge)
  • 📞 Voicemail — "Takes a message and transcribes it"
  • After-hours toggle: "Use a different mode after hours?" → If yes, select after-hours mode
  • This creates the basic routing rule. Advanced schedule editing available later.

Step 3: Connect Your Phone System (3-5 min)

  • "Which phone system do you use?" → Select from list (Google Voice, OpenPhone, RingCentral, Nextiva, Twilio, Other)
  • App provisions a forwarding number: "Your AI phone number: (555) 867-5309" [Copy]
  • Displays step-by-step guide with screenshots for the selected provider
  • "Forward your calls to this number when nobody answers"
  • Test button: "Call your business number now, let it ring, and we'll confirm it's working" → Success animation

Step 4: API Keys (2-3 min, conditional)

  • If Voice mode selected: "Enter your Vapi API key" → Link to Vapi signup + where to find the key
  • Twilio API key (if not platform-managed): Same guided flow
  • Visual indicator: ✅ Connected / ❌ Not connected / ⏳ Checking...

Step 5: Customize Your AI (2 min)

  • Voice selection (if voice mode): Preview 4-6 voices with a "Play sample" button
  • Greeting: Pre-filled text field: "Hi, thanks for calling [Business Name]! I'm your virtual assistant. How can I help you today?"
  • Auto-text template: Pre-filled: "Hey! This is [Business Name]. Sorry we missed your call — how can we help?"
  • Services offered: Checkboxes pre-populated based on trade type selected in Step 1
  • FAQ: Pre-loaded with 3–5 common questions for their trade. Contractor can edit/add.

Step 6: Connect Calendar (1 min, optional but encouraged)

  • "Connect Google Calendar so your AI can book appointments" → OAuth flow
  • Select which calendar to use
  • Set appointment duration default (30 min, 1 hr, 2 hr)
  • Set buffer time between appointments (0, 15, 30, 60 min)

Step 7: Connect CRM (1-2 min, optional)

  • Select CRM from list
  • Enter API key or webhook URL
  • Test connection

🎉 Setup complete. "Your missed call command center is live. Try it — call your number and let it ring!"

---

Daily Use: The Dashboard

Layout: Single-page command center

Top bar — Key Metrics (always visible):

  • Today's missed calls: 7
  • Recovered: 5 (71%)
  • Appointments booked: 2
  • Pending callbacks: 1

Main feed — Missed Call Timeline:

Each entry is a card showing:

```

📞 (555) 234-5678 — John M. Today, 2:34 PM

🗣️ AI Voice | Duration: 2:14

📝 "Needs AC repair, unit not cooling. Built in 2018. Booked for Thursday 10am."

🟢 APPOINTMENT BOOKED | ✅ Synced to ServiceTitan

[Play Recording] [View Transcript] [Call Back]

```

```

💬 (555) 345-6789 — Unknown Today, 1:15 PM

📱 Auto-Text | 4 messages exchanged

📝 "Asking about pricing for duct cleaning. Sent FAQ response. Customer said they'll think about it."

🟡 LEAD — NO BOOKING | ✅ Synced to Jobber

[View Conversation] [Send Follow-Up] [Call Back]

```

```

📞 (555) 456-7890 — Sarah K. Today, 11:02 AM

📞 Voicemail | Duration: 0:42

📝 "Requesting quote for new furnace install. Has a 15-year-old Carrier unit."

🔴 NEEDS CALLBACK | ⏳ CRM sync pending

[Play Voicemail] [View Transcript] [Call Back]

```

Filters sidebar:

  • Date range
  • Routing type (Voice / Text / Voicemail / Both)
  • Outcome (Booked / Lead / Question Answered / Callback Needed / Spam)
  • CRM sync status

---

Settings: Routing Rules

Visual Weekly Schedule Grid:

A calendar-style grid (Mon-Sun, 6am-10pm) where each time block is color-coded:

  • 🟢 Green = AI Voice
  • 🔵 Blue = Auto-Text
  • 🟣 Purple = Both
  • 🟠 Orange = Voicemail
  • ⚫ Gray = Off (no response — calls go to carrier voicemail)

Contractor clicks/drags to "paint" their preferred routing across the week. Pre-filled based on their business hours selection during setup.

Example default:

  • Mon-Fri 8am-6pm: 🟣 Both (AI Voice + Text)
  • Mon-Fri 6pm-9pm: 🔵 Auto-Text only
  • Sat 8am-2pm: 🟣 Both
  • All other times: 🟠 Voicemail

Emergency override toggle: "If a caller mentions an emergency keyword (leak, no heat, gas smell, sparking, flooding), always route to AI Voice regardless of schedule" → On/Off

---

8. Edge Cases & Guardrails

Edge CaseHow We Handle It
Spam/robocallsAI detects non-human callers or known spam patterns → auto-tags as spam, doesn't send text, doesn't create CRM record
Caller speaks Spanish (or other language)V1: AI responds in English with "I apologize, let me have [Owner] call you back." V2: Vapi multi-language support
Caller is angry/abusiveAI remains calm, offers to have the owner call back personally, flags as "urgent — unhappy customer"
Caller asks for pricing AI doesn't haveAI deflects gracefully: "I want to make sure you get the right price — let me have [Owner] give you a call with an exact quote."
Repeat caller same daySystem recognizes the number, doesn't send duplicate text, AI references prior call: "I see you called earlier today..."
Calendar has no availabilityAI says "We're fully booked this week, but I can put you on our earliest available slot next week, or I can have [Owner] call you to work something out."
Forwarding fails / number not configuredDashboard shows alert banner: "⚠️ No calls received in 48 hours. Check your call forwarding setup." with link to troubleshooting guide
Vapi API key invalid/expiredGraceful fallback to Auto-Text mode + notification to contractor: "Your voice AI is offline. Missed calls are being handled via text."
CRM sync failsRetries 3x, then marks as "sync failed" on dashboard with manual retry button. Data is never lost — stored locally.

---

9. Revenue Impact Model

Conservative scenario (solo HVAC contractor, summer):

  • Missed calls/week: 20
  • Recovery rate with Command Center: 70% → 14 calls recovered
  • Booking rate from recovered calls: 40% → ~6 jobs/week
  • Average job value: $400
  • Weekly revenue recovered: $2,400
  • Monthly revenue recovered: $9,600

Conservative scenario (slow season):

  • Missed calls/week: 10
  • Recovery rate: 60% → 6 recovered
  • Booking rate: 40% → ~2.5 jobs/week
  • Average job value: $400
  • Weekly revenue recovered: $1,000
  • Monthly revenue recovered: $4,000

What it replaces:

  • Answering service: $300–$1,500/mo
  • Missed call text service (Smith.ai, Ruby, etc.): $100–$500/mo
  • Part-time receptionist (shared): $800–$2,000/mo
  • Total replaced cost: $300–$1,500/mo
  • Net ROI: 3–10x cost of answering service, with better quality and 24/7 coverage

---

10. Technical Build Notes

Stack:

  • Frontend: React (or similar) — single-page dashboard app
  • Backend: Node.js/Python on Replit
  • Database: PostgreSQL for call records, contacts, settings
  • Vapi: Inbound call handling, voice AI, transcription
  • Twilio: SMS, voicemail recording, phone number provisioning
  • Google Calendar API: OAuth + read/write
  • CRM APIs: REST integrations per platform

Critical path for MVP (4-6 hour build estimate):

1. Twilio number provisioning + inbound call forwarding handling (1 hr)

2. Auto-text on missed call (0.5 hr)

3. Vapi integration — inbound voice agent with pre-baked prompt (1.5 hr)

4. Dashboard — missed call log with basic info (1 hr)

5. Settings — routing rules, business hours, greeting config (1 hr)

6. Google Calendar — availability check + booking (1 hr)

Post-MVP iterations:

  • CRM sync module
  • Weekly digest email
  • Visual schedule grid
  • Voicemail transcription
  • Repeat caller recognition
  • FAQ management UI
  • VoIP setup instruction library

---

11. The 10-Second Value Test

Scenario: Contractor finishes setup at 9am. At 9:47am, they're on a roof and miss a call.

Within 10 seconds: Their phone buzzes with a notification: "Missed call handled — AI Voice answered (555) 234-5678. Transcript: 'Customer needs AC repair, booked for tomorrow at 2pm.' ✅ Synced to Jobber."

The contractor's reaction: "Holy shit, it actually works." They smile, put their phone away, and keep working. They didn't lose the job. They didn't even have to think about it.

That moment — that is the product.

---

3–5. Shared Architecture, Integration Safety Rules, Contractor Profile

> These sections follow the standard Master PRD Template. See `recipe-prd-process.md` for the complete shared architecture, safety rules, and contractor profile data model that apply to all recipes.

Recipe-specific additions to Contractor Profile:

```ts

interface MissedCallContractorProfile extends BaseContractorProfile {

voip_provider: "google_voice" | "twilio" | "openphone" | "ringcentral" | "nextiva" | "other";

forwarding_number: string;

forwarding_verified: boolean;

vapi_api_key_encrypted: string;

vapi_voice_id: string;

vapi_greeting: string;

google_calendar_connected: boolean;

google_calendar_tokens_encrypted: string;

crm_provider: string;

crm_connected: boolean;

default_appointment_duration: 15 | 30 | 45 | 60;

business_hours: { day: number; start: string; end: string }[];

}

```

---

6. Recipe-Specific Functionality (VTCR)

6.1 VoIP Call Forwarding Setup

6.2 Routing Rules Engine

6.3 AI Voice Agent (Vapi)

# Missed Call Command Center — VTCR Specifications

---

Feature 1: VoIP Call Forwarding Setup

VTCR 1.1 — Provider Selection & Instructions Display

Visual:

The "Forwarding Setup" screen opens with a prominent heading: "Connect Your Phone in 3 Steps." Below, a horizontal row of provider logo tiles (Google Voice, Twilio, OpenPhone, RingCentral, Nextiva) sits above a large instruction panel. Each tile has the provider's logo, name, and a radio-style selection indicator. Below the provider row is a read-only field labeled "Your Forwarding Number" displaying a generated Twilio/Vapi number (e.g., `+1 (512) 555-0199`) with a copy-to-clipboard icon beside it. The instruction panel area below is initially empty with ghost text: "Select your provider above to see setup steps."

Trigger:

The contractor taps/clicks one of the five provider tiles (e.g., "Google Voice").

Condition:

  • The system has already generated and stored a forwarding number for this account (created on signup or first visit to this screen).
  • If no forwarding number exists yet, the system provisions one via the Twilio API before rendering the page (see VTCR 1.2 for error case).

Result:

  • The selected tile becomes highlighted with a blue border and checkmark badge; all others dim slightly.
  • The instruction panel populates with a numbered, provider-specific step-by-step guide. For Google Voice, this would read:

> Step 1: Open Google Voice settings → Open Google Voice Settings ↗

>

> Step 2: Scroll to "Calls" and locate "Forward calls to" or "No-answer call forwarding."

>

> Step 3: Click "Add a linked number" or "Enable forwarding" and enter your forwarding number: +1 (512) 555-0199

>

> Step 4: Google may send a verification code to the forwarding number. The code will appear in this app automatically — enter it in Google Voice to confirm.

>

> Step 5: Set the ring duration before forwarding to 15 seconds (recommended) so callers don't wait too long.

  • Each external link opens in a new tab/in-app browser.
  • A green "Test My Forwarding" button appears pinned at the bottom of the instruction panel (initially disabled; becomes enabled after the contractor confirms they've completed the steps via a checkbox: "I've completed the steps above").

---

VTCR 1.2 — Forwarding Number Generation (First Visit / Provisioning)

Visual:

On first navigation to the "Forwarding Setup" screen, before the provider tiles and instructions load, a full-width loading card appears with a spinning icon and the text: "Generating your dedicated forwarding number…" The provider tiles and instruction panel are visible but skeleton-loaded (greyed placeholder bars) behind a subtle overlay.

Trigger:

The contractor navigates to the "Forwarding Setup" screen for the first time, or a previous provisioning attempt failed and no forwarding number is stored in the account record.

Condition:

  • The account has no `forwarding_number` value in the database.
  • The Twilio (or Vapi) number provisioning API is reachable.

Result — Success:

  • The system calls the Twilio API to provision a local phone number in the contractor's area code (derived from the contractor's primary business phone number on file).
  • The loading card transitions to a success card with a green checkmark: "Your forwarding number is ready!" showing the number in large text with a copy button.
  • After 2 seconds, the success card auto-dismisses and the full provider selection screen renders normally with the forwarding number pre-populated.
  • The number is persisted to the `accounts.forwarding_number` field.

Result — Failure (API Error):

  • If the Twilio API returns an error (rate limit, insufficient funds on the platform Twilio sub-account, or network timeout after 10 seconds):
  • The loading card transitions to a red error card: "We couldn't generate your forwarding number. This is usually temporary."
  • A "Retry" button and a "Contact Support" link appear.
  • The provider tiles remain skeleton-loaded and non-interactive.
  • The error is logged with the Twilio error code, timestamp, and account ID for the ops team.

---

VTCR 1.3 — Test Call Flow: Initiation

Visual:

After selecting a provider and checking the "I've completed the steps above" checkbox, the green "Test My Forwarding" button is enabled and pulses gently to attract attention. Below it, helper text reads: "We'll call your business number. Let it ring — don't answer. If forwarding is set up correctly, our system will pick up and you'll see a success message here."

Trigger:

The contractor clicks "Test My Forwarding."

Condition:

  • The contractor has a primary business phone number stored in their profile.
  • A forwarding number has been generated and stored.
  • No test call is currently in progress for this account (debounce).

Result:

  • The button state changes to a disabled "Calling now…" state with a pulsing phone icon and a real-time status area appears below:

1. `✅ Initiating call to (512) 555-0100…` (the contractor's business number)

2. `⏳ Ringing… waiting for forwarding to activate` (appears after the call is placed)

  • The system initiates an outbound call via Twilio Programmable Voice to the contractor's business number.
  • A 45-second timeout timer starts (covers typical ring durations of 15–30 seconds plus forwarding connection time).
  • The system listens on the forwarding number for an inbound call leg arriving within the timeout window.

---

VTCR 1.4 — Test Call: Success

Visual:

The real-time status area on the test call card.

Trigger:

The system's forwarding number receives an incoming call within 45 seconds of the test call being placed, AND the originating caller ID matches the contractor's business number (or the Twilio outbound call SID is correlatable via call metadata).

Condition:

  • Inbound call to forwarding number is detected.
  • Caller ID or call SID correlation confirms this is the test call, not a real customer call.

Result:

  • The system answers the inbound call on the forwarding number, plays a short audio confirmation to the line ("Forwarding is working. You're all set!"), and hangs up after 3 seconds.
  • The status area updates with:

1. `✅ Initiating call to (512) 555-0100…`

2. `✅ Ringing…`

3. `✅ Forwarding detected! Your setup is complete.`

  • A green success banner replaces the test button: "✅ Forwarding is active. Missed calls will now reach your Command Center."
  • The account record is updated: `forwarding_verified: true`, `forwarding_verified_at: [timestamp]`, `forwarding_provider: google_voice`.
  • The "Forwarding Setup" section on the main dashboard now shows a green "Connected" badge.

---

VTCR 1.5 — Test Call: Failure

Visual:

The real-time status area on the test call card.

Trigger:

The 45-second timeout expires without the forwarding number receiving a correlated inbound call, OR the outbound test call is answered by the contractor (meaning forwarding didn't engage), OR the outbound call fails entirely (number unreachable).

Condition:

  • Timeout elapsed with no forwarding detected, OR
  • Outbound call was answered (call status `in-progress` on the contractor's number rather than `no-answer`), OR
  • Outbound call returned an error status (`busy`, `failed`, `no-answer` but no forwarding leg detected).

Result:

  • The status area updates with a failure state:

1. `✅ Initiating call to (512) 555-0100…`

2. `✅ Ringing…`

3. `❌ Forwarding not detected.`

  • A contextual troubleshooting card appears below with specific guidance based on the failure mode:
  • If call was answered: "It looks like you picked up the call. Please try again and let it ring without answering."
  • If timeout with no forwarding: "The call rang but wasn't forwarded to your Command Center number. Double-check that you entered the correct forwarding number in your [Google Voice] settings: +1 (512) 555-0199. Make sure 'No-answer forwarding' (not 'Always forward') is enabled."
  • If call failed: "We couldn't reach your business number. Please verify (512) 555-0100 is correct in your Profile settings."
  • A "Re-Test" button and a "Re-read Setup Steps" button (which scrolls/navigates back to the instruction panel) are shown.
  • The account record remains `forwarding_verified: false`. A failed test event is logged with the failure reason.

---

Feature 2: Routing Rules Engine

VTCR 2.1 — Schedule Grid Display (Default State)

Visual:

The "Routing Rules" screen displays a weekly schedule grid. The horizontal axis contains 7 columns (Mon–Sun). The vertical axis is divided into 48 rows representing 30-minute time blocks from 12:00 AM to 11:30 PM (row labels shown at every full hour on the left margin: 12 AM, 1 AM, … 11 PM). Each cell is colored to indicate the active routing method:

ColorRouting MethodCell Label
BlueAI Voice (Vapi)🤖 AI Voice
GreenAuto-Text (SMS)💬 Auto-Text
PurpleBoth (AI Voice + Auto-Text)🤖💬 Both
GrayVoicemail📩 Voicemail

By default (on first load / no saved config), Monday–Friday 8:00 AM–6:00 PM cells are blue (AI Voice) and all other cells are gray (Voicemail). A legend bar above the grid shows the four color-coded options. Above the legend, two quick-set buttons are displayed: `⚡ Business Hours Preset` and `🌙 After Hours Preset`.

A "Save Changes" button (disabled until a change is made) and a "Reset to Defaults" link sit in the top-right corner. A last-saved timestamp is shown: "Last saved: June 14, 2025 at 3:42 PM."

Trigger:

The contractor navigates to the "Routing Rules" tab/screen from the main navigation.

Condition:

  • If the contractor has a saved routing configuration, the grid renders from saved data.
  • If no saved configuration exists (first-time user), the grid renders with defaults (AI Voice 8am–6pm M–F, Voicemail everywhere else).

Result:

  • The grid renders fully with the correct color-coding per cell.
  • The grid is scrollable vertically on mobile; on desktop, the "core hours" (7 AM–9 PM) are visible by default with a scroll to access overnight hours.
  • Hovering over any cell (desktop) shows a tooltip: "Monday 8:00 AM – 8:30 AM: AI Voice".
  • No unsaved changes exist, so "Save Changes" button is disabled/dimmed.

---

VTCR 2.2 — Cell Selection & Routing Assignment

Visual:

The contractor interacts with the schedule grid. When a single cell is clicked, it receives a white pulsing border (selected state). When the contractor clicks and drags across multiple cells (or shift-clicks a second cell to define a range), all cells in the rectangular selection receive the pulsing border. Upon selection, a floating action popover appears near the selection with four routing option buttons stacked vertically:

```

┌─────────────────────────┐

│ Set routing for 6 cells│

│ │

│ 🤖 AI Voice │

│ 💬 Auto-Text │

│ 🤖💬 Both │

│ 📩 Voicemail │

└─────────────────────────┘

```

Trigger:

The contractor clicks a single cell, or clicks and drags to select a contiguous rectangular group of cells (e.g., Monday–Friday 6:00 PM–9:00 PM = 5 columns × 6 rows = 30 cells).

Condition:

  • The grid is in its normal (non-saving) state.
  • At least one cell is selected.

Result:

  • The selected cells are visually highlighted with a pulsing white border overlay.
  • The floating popover appears anchored to the center of the selection (on mobile, it docks to the bottom of the screen as a bottom sheet).
  • The popover title dynamically reflects the selection: "Set routing for 30 cells" or "Set routing for Mon 2:00 PM – 2:30 PM" (for a single cell).
  • Clicking one of the four routing options:
  • Immediately recolors all selected cells to the corresponding color.
  • Dismisses the popover.
  • Removes the selection borders.
  • Enables the "Save Changes" button (it becomes active with a subtle bounce animation).
  • An unsaved-changes indicator dot appears on the button.
  • Clicking outside the popover (or pressing Escape) cancels the selection without making changes.

---

VTCR 2.3 — Preset Buttons (Business Hours & After Hours)

Visual:

Two preset buttons above the grid legend:

  • `⚡ Business Hours Preset` — has a sub-label: "M–F, 8 AM – 6 PM"
  • `🌙 After Hours Preset` — has a sub-label: "Evenings, weekends, & holidays"

Trigger:

The contractor clicks `⚡ Business Hours Preset`.

Condition:

  • The grid may or may not have unsaved changes. If unsaved changes exist, a confirmation dialog appears first: "You have unsaved changes. Applying a preset will overwrite them. Continue?" with "Apply Preset" and "Cancel" buttons.

Result:

  • A modal appears: "Configure Business Hours Routing"
  • Time range selector: Start time (default 8:00 AM) and End time (default 6:00 PM) dropdowns in 30-min increments.
  • Day checkboxes: Mon ☑ Tue ☑ Wed ☑ Thu ☑ Fri ☑ Sat ☐ Sun ☐ (defaults).
  • Routing method for these hours: Four radio options (AI Voice selected by default).
  • An "Apply" button and "Cancel" link.
  • On clicking "Apply":
  • All cells matching the selected days and time range are updated to the chosen routing method's color.
  • All cells outside those days/times remain unchanged (the After Hours preset handles those separately).
  • The modal closes, the grid reflects the changes, and "Save Changes" becomes enabled.
  • Clicking `🌙 After Hours Preset` opens an analogous modal, but the time/day configuration is inverted (pre-populated to select all time blocks NOT covered by the current Business Hours configuration), and the default routing method is "Voicemail."

---

VTCR 2.4 — Saving Configuration

Visual:

The "Save Changes" button in the top-right corner of the Routing Rules screen is active (blue, with an unsaved-changes dot). The grid reflects the contractor's modifications.

Trigger:

The contractor clicks "Save Changes."

Condition:

  • At least one cell has been modified since the last save (or since the default was loaded).
  • The user's session is active and authenticated.

Result:

  • The button transitions to a loading state: spinner replaces text, button is disabled.
  • The system serializes the grid into a data structure: an array of 336 objects (48 half-hours × 7 days), each containing `{ day, start_time, routing_method }`, and sends a `PUT` request to the `/api/routing-rules` endpoint.
  • On success (200 OK):
  • The button transitions to a green checkmark with text "Saved ✓" for 2 seconds, then reverts to a disabled "Save Changes" state.
  • The "Last saved" timestamp updates to the current time.
  • A non-blocking toast notification appears at the top: "Routing rules updated. Changes take effect immediately."
  • The unsaved-changes dot disappears.
  • On failure (network error / 500):
  • The button reverts to its active "Save Changes" state.
  • A red toast notification appears: "Failed to save routing rules. Your changes are preserved — please try again."
  • The grid retains the unsaved modifications so the contractor doesn't lose their work.

---

VTCR 2.5 — Edge Case: No Routing Method Set / Incomplete Coverage

Visual:

During the save operation, the system validates the grid before submitting.

Trigger:

The contractor clicks "Save Changes," AND one or more cells have been explicitly set to "None" (e.g., through a bug, API manipulation, or if a future "Clear" option is added), OR if the routing method for any cell is null/undefined.

Condition:

  • Client-side validation detects that at least one cell in the 336-cell grid has no routing method assigned (value is `null`, `undefined`, or empty string).

Result:

  • The save is blocked. The "Save Changes" button shakes briefly (a horizontal shake animation indicating an error).
  • A warning banner appears above the grid:

> ⚠️ Every time slot must have a routing method. We found 4 time slots with no routing assigned. They've been highlighted below — click them to set a method, or use a preset to fill gaps.

  • The cells with no routing method are highlighted with a red dashed border and a pulsing red background.
  • The save does not proceed until all 336 cells have a valid routing method.
  • If the contractor navigates away with unsaved changes (whether valid or invalid), a browser/app-level confirmation dialog appears: "You have unsaved routing changes. Leave without saving?"

---

Feature 3: AI Voice Agent (Vapi Integration)

VTCR 3.1 — Vapi API Key Setup & Validation

Visual:

Within the Settings > AI Voice Agent screen, the top section is titled "Connect to Vapi" and contains:

  • A text input field labeled "Vapi API Key" with a placeholder: `sk-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. The field masks the key by default (dots), with an eye icon to toggle visibility.
  • A helper link below the field: "Don't have an API key? Get one from your Vapi dashboard ↗"
  • A "Validate & Save" button to the right of the field.
  • Below the connection section, the rest of the AI Voice Agent settings (voice selector, greeting editor, info checklist, FAQ, calendar) are visible but overlaid with a translucent disabled state and a lock icon with text: "Connect your Vapi account to configure these settings."

Trigger:

The contractor pastes or types an API key and clicks "Validate & Save."

Condition:

  • The key is non-empty and matches the expected format (basic regex: starts with `sk-`, 36+ characters).
  • The system makes a lightweight validation call to the Vapi API (e.g., `GET /account` with the provided key as a Bearer token).

Result — Success:

  • The Vapi API returns a 200 response with account metadata (account name, plan tier, available voices).
  • The API key field transitions to a confirmed state: green border, checkmark icon, and text: "✅ Connected as 'Mike's Plumbing' (Vapi Pro Plan)".
  • The API key is encrypted and stored in the database (`accounts.vapi_api_key_encrypted`).
  • The disabled overlay on the settings below is removed with a fade-up animation, revealing the fully interactive voice selector, greeting editor, info checklist, FAQ, and calendar sections.
  • A toast notification: "Vapi connected successfully. Configure your AI voice agent below."

Result — Failure (Invalid Key):

  • The Vapi API returns a 401 or 403 error.
  • The input field gains a red border. Error text appears below: "❌ Invalid API key. Please check that you copied the full key from your Vapi dashboard ↗."
  • The settings below remain locked.
  • The contractor can modify the key and retry without page reload.

---

VTCR 3.2 — Voice Selection with Audio Preview

Visual:

The "Agent Voice" section shows a dropdown select labeled "Choose a voice" populated with Vapi's available voices, organized by category (e.g., "Professional," "Friendly," "Casual"). Each option shows the voice name and a gender/accent descriptor (e.g., "Aria — Female, American English, Warm"). To the right of the dropdown is a `▶ Preview` button. Below the dropdown is a small waveform visualizer area (initially empty).

Trigger:

The contractor selects a voice from the dropdown (e.g., "Aria — Female, American English, Warm") and clicks the `▶ Preview` button.

Condition:

  • A valid Vapi API key is connected.
  • The selected voice ID is valid and available on the contractor's Vapi plan.

Result:

  • The `▶ Preview` button transitions to a `⏹ Stop` button.
  • The system calls the Vapi text-to-speech endpoint with a sample script: "Thanks for calling! I'm an AI assistant here to help you. How can I help you today?" using the selected voice ID.
  • The returned audio streams through the browser/device speaker. The waveform visualizer animates in sync with the audio playback.
  • When playback completes (approximately 4–6 seconds), the button reverts to `▶ Preview`.
  • The voice selection is not saved until the contractor clicks the master "Save Settings" button at the bottom of the page — but the selection persists in the form state.
  • If audio playback fails (Vapi TTS error or browser audio context blocked), a small inline message appears: "Couldn't play preview. Check your device volume or try a different browser."

---

VTCR 3.3 — Greeting Editor & Info Collection Checklist

Visual:

Two sections stacked vertically:

Opening Greeting Editor:

A text area labeled "Opening Greeting" with a smart default pre-filled based on the contractor's trade and business name (pulled from profile): "Thanks for calling Mike's Plumbing! I'm an AI assistant. I can help schedule an appointment or take a message. How can I help you today?". A small "Reset to default" link sits below the text area. A character counter shows `142 / 300 max`.

Information to Collect:

A checklist with toggle switches, labeled "What should the AI ask callers for?":

ToggleItemDefaultRequired?
Caller's NameONYes (locked on)
Callback Phone NumberONYes (locked on)
Description of Issue/Service NeededONNo
Urgency Level (Emergency / Routine / Flexible)ONNo
Service AddressOFFNo
Preferred Appointment Date/TimeOFFNo
How They Heard About UsOFFNo

Trigger:

The contractor modifies the greeting text and/or toggles one or more checklist items (e.g., turns on "Service Address" and "Preferred Appointment Date/Time").

Condition:

  • The Vapi API key is connected and validated.
  • The greeting text is between 10 and 300 characters.
  • At least the two required items (Name, Callback Number) are enabled (they cannot be toggled off — their switches are visually locked with a lock icon and tooltip: "Required for every call").

Result:

  • As the contractor types in the greeting field, the character counter updates in real time. If they exceed 300 characters, the counter turns red and the "Save Settings" button becomes disabled with a tooltip: "Greeting must be under 300 characters."
  • Toggling a checklist item on adds a subtle slide-in animation; toggling off shows a brief strikethrough animation.
  • Both the greeting and the checklist selections are held in the unsaved form state. The "Save Settings" button at the page bottom becomes enabled (if not already) and gains an unsaved-changes indicator dot.
  • On save, the greeting text and the list of enabled info-collection items are sent to the backend. The backend compiles them into the system prompt for the Vapi assistant (the contractor never sees or edits the system prompt directly). The system prompt is constructed from a template:

> "You are a friendly, professional AI phone assistant for {business_name}, a {trade} company. When you answer, say: '{greeting_text}'. During the conversation, collect the following from the caller: {enabled_items_list}. If the caller mentions an emergency (e.g., burst pipe, gas leak, no heat in winter), flag it as URGENT. Use the provided FAQ entries to answer common questions. {calendar_instructions_if_enabled}. Always be polite, concise, and reassure the caller that {business_name} will follow up promptly."

  • This compiled prompt is stored and passed to Vapi when creating/updating the assistant configuration.

---

VTCR 3.4 — Live Call Flow: Call Completed Successfully

Visual:

The "Recent Calls" section of the AI Voice Agent screen (or the main dashboard "Call Log" tab) shows a new call entry appearing in real time. The entry is a card/row with:

  • Caller's phone number (or name if matched to a contact)
  • Call duration (e.g., "2m 34s")
  • Status badge: `✅ Completed — Info Collected`
  • Urgency tag if flagged: `🔴 URGENT`
  • Expandable sections: "Transcript," "Recording," "Collected Info," "Appointment" (if booked)

Trigger:

A missed call is routed to the Vapi AI agent (per the Routing Rules engine), the Vapi assistant answers, completes the full conversation flow (greeting → info collection → appointment check → summary → goodbye), and the call ends normally (either party hangs up after the agent's closing statement).

Condition:

  • The routing rule for the current time block is set to "AI Voice" or "Both."
  • The Vapi API key is valid and the assistant is active.
  • The Vapi webhook fires a `call.ended` event with `status: completed` and includes the transcript, recording URL, and extracted data.

Result:

  • The system receives the Vapi `call.ended` webhook payload containing:
  • `transcript`: Full text transcript of the conversation.
  • `recording_url`: URL to the call recording audio file (MP3).
  • `extracted_data`: Structured JSON of the collected info fields (name, callback number, issue description, urgency, address, etc.).
  • `appointment`: If the caller requested an appointment and the calendar integration is enabled, this contains the booked slot details (`date`, `time`, `duration`, `google_calendar_event_id`).
  • The system creates a `call_record` in the database with all the above data.
  • The recording audio is downloaded from Vapi's URL and stored in the app's own cloud storage (S3/GCS) for persistence independent of Vapi's retention policy.
  • The call appears in the Recent Calls list with a push notification sent to the contractor's device:

> "📞 AI Agent handled a call from (512) 555-0177. Issue: Leaking water heater. Urgency: 🔴 Emergency. [View Details]"

  • If "Both" routing was active, the auto-text SMS is also sent to the caller (handled by the SMS module), and the call record is cross-linked to the SMS record.
  • Expanding the "Transcript" section shows the full conversation with speaker labels (`AI:` / `Caller:`), timestamps on each turn, and keyword highlights (urgency terms in red, service types in blue).
  • Expanding "Recording" shows an embedded audio player with playback controls (play, pause, scrub, speed 1x/1.5x/2x) and a "Download MP3" link.
  • Expanding "Collected Info" shows a clean summary card:
FieldValue
NameSarah Johnson
Callback(512) 555-0177
IssueWater heater leaking from the bottom, water on garage floor
Urgency🔴 Emergency
Address4210 Oak Hill Drive
  • If an appointment was booked, the "Appointment" section shows the slot with a link to the Google Calendar event and a "Reschedule" / "Cancel" button that syncs back to Google Calendar.

---

VTCR 3.5 — Edge Cases: Vapi API Error & Caller Hangs Up Early

#### VTCR 3.5a — Vapi API Error During Live Call

Visual:

The "Recent Calls" log shows a new entry with a red status badge: `❌ Agent Error — Fallback Activated`.

Trigger:

A missed call is routed to the Vapi AI agent, but the Vapi API returns an error when the system attempts to initiate or transfer the call to the Vapi assistant. This includes: HTTP 5xx from Vapi, Vapi assistant not found (deleted from Vapi dashboard), API key revoked/expired, Vapi rate limit exceeded, or network timeout (>5 seconds).

Condition:

  • The routing rule directs the call to "AI Voice" or "Both."
  • The Vapi API call fails for any reason before the assistant can answer the caller.
  • A fallback routing method is configured (default fallback: Voicemail).

Result:

  • The system immediately executes the fallback routing within 2 seconds of the Vapi failure so the caller experiences minimal dead air:
  • Fallback to Voicemail: The system plays a pre-recorded voicemail greeting (the contractor's custom greeting or a default: "You've reached {business_name}. We're unable to take your call right now. Please leave a message and we'll call you back promptly.") and records the caller's voicemail.
  • Fallback to Auto-Text: If the caller's number is available, an SMS is sent immediately.
  • A call record is created with:
  • `status: error_fallback`
  • `error_type: vapi_api_error`
  • `error_detail`: The specific error message/code from Vapi.
  • `fallback_method: voicemail` (or `sms`)
  • The voicemail recording (if applicable) is attached.
  • The contractor receives an elevated-priority push notification:

> "⚠️ AI Voice Agent failed on an incoming call from (512) 555-0177. Fallback to voicemail was activated. Error: Vapi API key expired. [Fix in Settings]"

  • If the error is an API key issue (401/403), the Settings > AI Voice Agent screen shows a persistent red warning banner: "⚠️ Your Vapi connection is broken. Calls are falling back to voicemail. [Re-enter API Key]"
  • The system logs the error and, if the same error occurs 3+ times in a 1-hour window, sends an email alert to the contractor in addition to push notifications.

#### VTCR 3.5b — Caller Hangs Up Early (Incomplete Call)

Visual:

The "Recent Calls" log shows a new entry with a yellow/amber status badge: `⚠️ Incomplete — Caller Hung Up`.

Trigger:

The Vapi AI agent answers the call and begins the conversation, but the caller disconnects before the agent has collected the minimum required information (Name and Callback Number). The Vapi `call.ended` webhook fires with `status: caller_hangup` or `ended_reason: customer-ended-call` and the `extracted_data` is partial or empty.

Condition:

  • The call connected to the Vapi assistant successfully.
  • The call duration was less than 15 seconds, OR the extracted data is missing one or both required fields (Name, Callback Number).

Result:

  • A call record is created with:
  • `status: incomplete`
  • `duration`: Actual call duration (e.g., "0m 08s")
  • `transcript`: Whatever partial conversation occurred (may be just the greeting and the caller saying nothing or "never mind")
  • `recording_url`: Saved even for short calls
  • `extracted_data`: Partial — whatever was captured (possibly only the caller's phone number from caller ID, if no name was given verbally)
  • The system automatically supplements the record:
  • The caller's phone number (from caller ID) is stored as the callback number even if the AI didn't verbally collect it.
  • If the caller said anything interpretable before hanging up, it's noted in the issue description (e.g., "Caller said 'I need a plumber' then disconnected.").
  • If the routing method is "Both," the auto-text SMS is still sent to the caller:

> "Hi, this is Mike's Plumbing. Sorry we missed you! Reply here to let us know how we can help, or we'll try calling you back shortly."

  • The contractor receives a push notification:

> "📞 Missed connection: Caller (512) 555-0133 hung up after 8 seconds. Partial info captured. [View Details]"

  • In the call log, the "Collected Info" section shows whatever was captured with missing fields marked: `Name: ❓ Not provided | Callback: (512) 555-0133 (from caller ID) | Issue: "I need a plumber" (partial)`.
  • A "Follow Up" quick-action button appears on the call record, which pre-populates an outbound call or text to the caller's number.

---

End of VTCR Specifications — Missed Call Command Center v1.0

6.4 Auto-Text SMS Response

6.5 Voicemail Handling

6.6 Calendar Integration

# Missed Call Command Center — VTCR Specifications

---

Feature 4: Auto-Text SMS Response

---

VTCR 4.1 — Auto-Text Triggered & Sent Successfully

Visual:

  • When a missed call is detected and routing rules resolve to "Text" or "Both," a system process indicator appears on the Missed Call Dashboard timeline for that call record: a small SMS icon with a spinning loader, then a green checkmark upon delivery confirmation.
  • The Lead Card for this call displays an "Auto-Text Sent" badge with a timestamp.
  • In the call detail view, a "Conversation" tab shows the outbound SMS as the first message in a threaded conversation view. The message bubble is right-aligned (outbound), displays the contractor's business name, a personalized greeting referencing the caller's situation (if context is available from caller ID or prior history), the contractor's trade, and a clear callback expectation (e.g., "We'll call you back within 30 minutes" during business hours).
  • A delivery status indicator beneath the message bubble shows: "Delivered ✓✓" with a timestamp.

Trigger:

  • A missed call event is logged AND the routing rules engine evaluates the call against the contractor's configured rules (time-of-day, caller source, trade category) AND the resolved action is "Text" or "Both."
  • The system confirms a valid phone number exists for the caller (not a blocked number, not a landline if SMS-incapable).

Condition:

  • The caller's phone number is a valid, SMS-capable mobile number.
  • The contractor has an active SMS AI Conversation Module subscription and a configured SMS sender number (10DLC registered or toll-free verified).
  • The contractor's business profile is complete (business name, trade/specialty, business hours are populated).
  • The routing rule that matched is active and not paused.
  • The caller's number is not on the contractor's block list or the system-wide opt-out/DNC list.
  • If the routing rule is time-based: the system correctly determines whether the current time falls within business hours or after hours and selects the appropriate message variant (see VTCR 4.4).

Result:

  • The SMS AI Conversation Module generates a personalized SMS using the contractor's business name, trade, and current availability context.
  • Business Hours Example: "Hi! This is [Business Name], your local [trade, e.g., 'licensed plumber']. Sorry we missed your call! We've got your number and will call you back within [configured SLA, e.g., '30 minutes']. In the meantime, feel free to reply here — I can answer questions or help you book an appointment. 😊"
  • The SMS is dispatched via the configured SMS gateway (Twilio / Vonage).
  • Upon delivery confirmation from the carrier, the Lead Card is updated with "Auto-Text Sent" status.
  • A `sms_auto_text_sent` event is logged with: `call_id`, `caller_phone`, `sms_body`, `sent_at`, `delivery_status`, `message_variant` (business_hours / after_hours), `routing_rule_id`.
  • The conversation thread is initialized in the Inbox module, ready to receive replies.
  • If the routing action was "Both," the voicemail or voice AI flow runs in parallel (does not block on SMS delivery).

---

VTCR 4.2 — Text Delivery Failed

Visual:

  • The Lead Card for the call displays a red "SMS Failed" badge with an error icon (⚠️) instead of the green "Auto-Text Sent" badge.
  • In the call detail → Conversation tab, the outbound message bubble shows a red "Not Delivered ✗" status with a tappable "Retry" button and a small "Why?" link that expands to show the failure reason in plain language.
  • On the Missed Call Dashboard, the row for this call shows a red SMS icon in the "Actions Taken" column.
  • If the contractor has notifications enabled for failures, a toast notification appears in-app: "Auto-text to (555) 867-5309 failed — [reason]. Tap to retry or call back manually."
  • In the Settings → SMS Logs section, the failed message appears with full error details (carrier error code, failure category).

Trigger:

  • The SMS gateway returns a delivery failure webhook/callback (status: `undelivered` or `failed`) for an auto-text message that was previously dispatched.
  • OR the SMS gateway rejects the send request synchronously (invalid number format, account suspended, etc.).
  • OR the send attempt times out after 30 seconds with no gateway acknowledgment.

Condition:

  • The failure reason is one of: invalid/disconnected number, carrier rejection (spam filter), landline detected, opt-out/STOP previously received, gateway account issue (insufficient funds, suspended), rate limit exceeded, or network timeout.
  • The system has not already retried this specific message more than 2 times (max retry limit).

Result:

  • The Lead Card status is updated to `sms_delivery_failed` with the failure reason code and human-readable description.
  • Automatic retry logic: If the failure is transient (network timeout, temporary carrier issue), the system retries once after 60 seconds and once more after 5 minutes. If the failure is permanent (invalid number, opt-out, landline), no retry is attempted.
  • A `sms_delivery_failed` event is logged with: `call_id`, `caller_phone`, `failure_reason`, `failure_code`, `retry_count`, `will_retry` (boolean).
  • A notification is sent to the contractor/dispatcher via their preferred channel (in-app push, Slack, or SMS to their personal number) informing them that the auto-text failed and manual follow-up is recommended.
  • The Lead Card's priority is automatically escalated by one tier (e.g., "Normal" → "High") to ensure manual follow-up is not missed.
  • The Missed Call Dashboard's "Needs Attention" filter count increments to reflect the unresolved item.

---

VTCR 4.3 — Caller Replies & FAQ Answered via Text

Visual:

  • When the caller replies to the auto-text, the Inbox module shows a new unread conversation thread. The thread header displays the caller's phone number (and name if matched to a contact), the originating missed call timestamp, and a "🤖 AI Active" indicator showing that the SMS AI is handling the conversation.
  • Each message in the thread is displayed as a chat-style conversation: caller messages left-aligned (inbound, gray bubbles), AI responses right-aligned (outbound, blue bubbles). Each AI response has a small "AI-generated" label and a "✏️ Edit before send" option (if the contractor has enabled human-in-the-loop review in settings).
  • When the AI successfully answers an FAQ, a small inline annotation appears below the AI message: "Answered from FAQ: [topic, e.g., 'Service Area']" with a link to the FAQ entry in settings.
  • The Lead Card in the Dashboard updates its status from "Auto-Text Sent" to "In Conversation" with a blue chat icon and the last message preview.
  • An unread badge appears on the Inbox tab in the main navigation.

Trigger:

  • An inbound SMS is received on the contractor's registered SMS number AND the sender's phone number matches an existing conversation thread that was initiated by an auto-text within the last 30 days.
  • The SMS AI Conversation Module parses the inbound message and identifies it as a question matching one or more entries in the contractor's FAQ knowledge base.

Condition:

  • The conversation thread is in "AI Active" mode (the contractor has not manually taken over the conversation).
  • The caller has not sent "STOP" or any opt-out keyword.
  • The FAQ knowledge base for this contractor has at least one entry, and the AI's confidence score for the FAQ match is ≥ 0.75 (configurable threshold).
  • If confidence is below threshold, the AI responds with a graceful fallback: "Great question! Let me have [contractor name] get back to you on that personally." and flags the conversation for human review.
  • The contractor's SMS sending limits have not been exceeded for the day.

Result:

  • The AI generates a contextual response drawing from the matched FAQ entry, personalized with the contractor's business name and relevant details. The response maintains the conversational tone established in the auto-text.
  • Example exchange:
  • Caller: "Do you guys work on weekends?"
  • AI: "Great question! [Business Name] is available Monday–Saturday, 8 AM–6 PM. Sundays we're closed, but you can book a Saturday appointment right here if you'd like. Want me to check availability?"
  • The outbound AI response is sent via the SMS gateway. Delivery status is tracked per VTCR 4.2.
  • A `faq_answered_via_sms` event is logged with: `conversation_id`, `caller_phone`, `inbound_message`, `matched_faq_topic`, `confidence_score`, `ai_response`, `timestamp`.
  • The conversation remains in "AI Active" mode, ready for follow-up messages.
  • If the caller's reply does NOT match an FAQ and instead expresses intent to book an appointment, the AI transitions to the booking flow (see VTCR 4.4 and Feature 6 integration).
  • The Lead Card's "Last Activity" timestamp updates in real time on the Dashboard.

---

VTCR 4.4 — Appointment Booked via Text & After-Hours Variant

Visual:

  • Booking Flow in Conversation: When the AI identifies booking intent, the conversation thread shows the AI presenting available time slots in a numbered list format within the SMS (e.g., "Here's what's open this week: 1️⃣ Tue 10:00 AM, 2️⃣ Tue 2:00 PM, 3️⃣ Wed 9:00 AM. Reply with a number to book!"). The caller replies with a number, and the AI confirms the booking.
  • Upon successful booking, the conversation thread shows a confirmation message bubble with a 📅 icon: "You're all set! [Service type] appointment with [Business Name] booked for [Day, Date at Time]. We'll send you a reminder beforehand. See you then!"
  • The Lead Card status updates to "Appointment Booked ✅" with the appointment date/time displayed as a subtitle. A calendar icon links directly to the Google Calendar event (Feature 6).
  • After-Hours Variant: If the auto-text is triggered outside of business hours, the initial SMS uses the after-hours template. The message bubble in the conversation thread has a 🌙 moon icon label indicating "After-Hours Auto-Text." The message tone and content differ from business hours:
  • "Hi! You've reached [Business Name] after hours. We're [trade] specialists and we'll call you back first thing when we reopen at [next business day/time]. Need help sooner? Reply here — I can answer common questions or help you book an appointment for [next available day]."
  • In Settings → Auto-Text Templates, both variants (Business Hours / After Hours) are displayed side-by-side in a tabbed editor with preview panes showing how the SMS will render on a phone mockup.

Trigger:

  • Booking: During an active SMS AI conversation, the caller expresses intent to schedule an appointment (detected via NLP intent classification: keywords like "book," "schedule," "appointment," "come out," "available," "when can you come," etc.) AND the AI's intent confidence score is ≥ 0.80.
  • After-Hours Variant: The auto-text trigger fires (per VTCR 4.1) AND the current time is outside the contractor's configured business hours (as defined in Settings → Business Hours).

Condition:

  • Booking Conditions:
  • Google Calendar is connected and authorized (Feature 6, VTCR 6.1).
  • At least one available slot exists within the contractor's configured booking window (e.g., next 14 days).
  • The max appointments per day limit has not been reached for the requested day (Feature 6, VTCR 6.5).
  • The caller provides required booking info: name and issue/service description (AI prompts for these if not already captured).
  • No double-booking conflict exists for the selected slot (Feature 6, VTCR 6.4).
  • After-Hours Conditions:
  • The contractor has configured business hours in Settings. If business hours are not configured, the system defaults to Mon–Fri 8:00 AM – 5:00 PM in the contractor's configured timezone.
  • The after-hours message template exists (default AI-generated template is used if contractor has not customized).

Result:

  • Booking Result:
  • The AI collects customer name, phone (already known), and issue description through the conversational flow (1–3 follow-up messages as needed).
  • Available slots are fetched from Google Calendar in real time (see Feature 6, VTCR 6.2).
  • Upon caller's slot selection, a Google Calendar event is created per Feature 6, VTCR 6.3 specifications (customer name, phone, issue, source: "Missed Call - Text").
  • A confirmation SMS is sent immediately (see Feature 6, VTCR 6.3).
  • Reminder texts are queued for 24 hours before and 1 hour before the appointment (see Feature 6, VTCR 6.3).
  • A `appointment_booked_via_sms` event is logged with: `conversation_id`, `caller_phone`, `customer_name`, `issue_description`, `appointment_datetime`, `calendar_event_id`, `booking_source: "text"`.
  • The Lead Card status transitions to "Appointment Booked" and the call is moved to the "Scheduled" section of the Dashboard.
  • A notification is sent to the dispatcher/contractor: "New appointment booked via text! [Customer Name] — [Service] on [Date/Time]."
  • After-Hours Result:
  • The after-hours message variant is sent instead of the business-hours variant.
  • The AI remains active for FAQ and booking conversations (it can book appointments for the next business day / next available slot).
  • The message variant used is recorded in the event log (`message_variant: "after_hours"`).
  • If the caller books an appointment during the after-hours conversation, the same booking flow applies — availability is pulled from the next available business day(s).

---

VTCR 4.5 — SMS Opt-Out Handling

Visual:

  • If the caller replies "STOP" (or any CTIA-mandated opt-out keyword: STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT), the conversation thread displays the inbound "STOP" message, followed by a system-generated (non-AI) compliance message: "You've been unsubscribed from [Business Name] messages. Reply START to re-subscribe. For help, reply HELP."
  • The conversation thread shows a 🚫 "Opted Out" banner across the top, and no further outbound messages can be composed (the reply input is disabled with a tooltip: "This contact has opted out of SMS").
  • The Lead Card shows an "Opted Out" badge in orange, and the contact record in the CRM/contacts section shows an "SMS: Opted Out" flag.
  • In Settings → SMS Compliance, an opt-out log table shows all opt-out events with timestamps, phone numbers, and the keyword used.

Trigger:

  • An inbound SMS is received containing an opt-out keyword (case-insensitive exact match: STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT) from a phone number associated with any conversation thread.

Condition:

  • The inbound message originates from a phone number that is currently opted IN to receive SMS from this contractor's sender number.
  • The SMS gateway / carrier-level opt-out processing has not already handled this (some gateways like Twilio auto-process STOP at the carrier level — the system must sync with gateway opt-out lists).

Result:

  • The system immediately ceases all outbound SMS to this phone number — including queued AI responses, appointment reminders, and any future auto-texts.
  • The mandatory opt-out confirmation message is sent (this single message is required by TCPA/CTIA even after opt-out).
  • The phone number is added to the contractor's opt-out list AND the system-wide suppression list.
  • All queued/scheduled messages to this number are cancelled (including any pending appointment reminders from Feature 6).
  • A `sms_opt_out` event is logged with: `phone_number`, `contractor_id`, `keyword_used`, `timestamp`, `conversation_id`.
  • The Lead Card is updated but NOT deleted — the call record and any prior conversation history are preserved for reference, but all SMS functionality is disabled for this contact until they re-subscribe (reply START).
  • The contractor/dispatcher is notified: "[Phone number] has opted out of text messages. Follow up by phone if needed."

---

Feature 5: Voicemail Handling

---

VTCR 5.1 — Voicemail Greeting Configuration

Visual:

  • In Settings → Voicemail, a "Greeting" section displays three options in a segmented control / radio card layout:

1. 🎙️ Record In-App — Tapping opens a recording interface with a large red "Record" button, a waveform visualizer during recording, a playback bar after recording, and "Re-record" / "Save" buttons. A timer shows recording duration (max 60 seconds). A quality indicator shows signal level.

2. 📁 Upload Audio File — Tapping opens a file picker supporting .mp3, .wav, .m4a (max 5 MB, max 60 seconds). After upload, a playback bar with play/pause and a waveform preview appear. File name, duration, and file size are displayed.

3. 🤖 Default AI Greeting — A preview card shows the auto-generated greeting text: "You've reached [Business Name]. We're unable to take your call right now. Please leave a message with your name, number, and a brief description of what you need, and we'll get back to you as soon as possible." A "Play Preview" button generates and plays the TTS audio. A "Customize Text" link opens an editable text field to modify the script before TTS generation. A voice selector dropdown allows choosing from 3–5 TTS voices (male/female, different tones).

  • Below the greeting selector, a "Test Greeting" button allows the contractor to call their own number and hear the greeting as a caller would.
  • A "Currently Active" label with a green dot indicates which greeting is in use.
  • Separate tabs or a toggle for "Business Hours Greeting" and "After-Hours Greeting" allow different greetings for each time period.

Trigger:

  • The contractor navigates to Settings → Voicemail → Greeting.
  • The contractor selects one of the three greeting options and completes the recording, upload, or AI customization flow, then taps "Save."

Condition:

  • For in-app recording: the browser/device has granted microphone permissions. Recording duration is between 3 and 60 seconds.
  • For file upload: the file is a supported format (.mp3, .wav, .m4a), under 5 MB, between 3 and 60 seconds in duration, and passes a basic audio quality check (not silent, not corrupted).
  • For AI greeting: the contractor's business name and trade are populated in their profile (required for personalization). If not, a warning appears: "Complete your business profile to generate a personalized greeting."
  • The contractor has at least one routing rule configured with a "Voicemail" action (otherwise, a helpful note appears: "This greeting will be used when you create a routing rule with the Voicemail action.").

Result:

  • The selected greeting audio is saved to cloud storage (S3/GCS) and associated with the contractor's account.
  • The greeting is registered with the telephony provider (Vapi / Twilio) as the voicemail prompt for the contractor's phone number.
  • If an AI greeting was generated, the TTS audio file is generated via the TTS engine (e.g., ElevenLabs, Google TTS) and stored alongside the text script.
  • A `voicemail_greeting_updated` event is logged with: `contractor_id`, `greeting_type` (recorded / uploaded / ai_generated), `duration_seconds`, `file_url`, `timestamp`.
  • The "Currently Active" indicator updates to reflect the new greeting.
  • Previous greetings are archived (not deleted) and accessible via a "Greeting History" accordion below the main selector (last 5 greetings saved).
  • A success toast appears: "Voicemail greeting saved! Callers will now hear your new greeting."

---

VTCR 5.2 — Voicemail Recorded & Transcription Generated

Visual:

  • On the Missed Call Dashboard, when a voicemail is left, the Lead Card for that call displays a "Voicemail" badge with a 🎤 icon and the voicemail duration (e.g., "Voicemail · 0:42").
  • Clicking the Lead Card opens the Call Detail view. A "Voicemail" section displays:
  • Audio Player: A waveform-style audio player with play/pause, scrub bar, current time / total duration, playback speed control (0.5x, 1x, 1.5x, 2x), and a download button.
  • Transcript: Below the audio player, a text block shows the full transcription with paragraph breaks. A "Transcribing..." skeleton loader with a pulsing animation appears while transcription is in progress (typically 5–15 seconds). Once complete, the text renders with a small "Transcribed by AI" label and a confidence indicator (High / Medium / Low based on Whisper's confidence scores). A "Copy Transcript" button is available.
  • Key Info Extracted: Below the transcript, an AI-extracted summary card highlights detected caller name, phone number (if verbally stated and different from caller ID), issue/request summary, and urgency level (routine / urgent / emergency — based on keywords like "flooding," "no heat," "emergency").
  • On the Dashboard list view, a preview of the first ~80 characters of the transcript appears as a subtitle beneath the voicemail badge.

Trigger:

  • A call is routed to voicemail (routing rule action = "Voicemail") AND the caller leaves a message (recording duration ≥ 3 seconds) AND the caller hangs up or the max recording time (120 seconds) is reached.
  • The voicemail audio file is received from the telephony provider (Vapi / Twilio webhook: `recording.completed`).

Condition:

  • The voicemail recording is at least 3 seconds long (recordings under 3 seconds are treated as "hang-up, no message" — see Result for handling).
  • The audio file is successfully retrieved from the telephony provider's storage.
  • The Whisper API / Vapi transcription service is available and responding.
  • The audio quality is sufficient for transcription (not pure silence or pure noise — if transcription returns empty or very low confidence, a fallback is triggered).

Result:

  • The voicemail audio file (.mp3 / .wav) is downloaded from the telephony provider and stored in the app's cloud storage, associated with the `call_id`.
  • The audio file is submitted to the Whisper API (or Vapi's built-in transcription) for speech-to-text processing.
  • Model: Whisper `large-v3` for accuracy (or `medium` for cost optimization, configurable per account tier).
  • Language: Auto-detected, with fallback to English.
  • Response includes: Full transcript text, word-level timestamps, overall confidence score.
  • The transcript is stored in the database linked to the `call_id` and `voicemail_id`.
  • AI entity extraction runs on the transcript to identify: caller name, callback number, issue description, urgency keywords. Results are stored as structured metadata.
  • The Lead Card on the Dashboard is updated in real time (via WebSocket) to show the voicemail badge, audio player, and transcript.
  • A `voicemail_received` event is logged with: `call_id`, `caller_phone`, `recording_duration`, `recording_url`, `transcript_text`, `transcript_confidence`, `extracted_name`, `extracted_issue`, `urgency_level`, `timestamp`.
  • If recording is < 3 seconds: The call record is updated with status "Voicemail - No Message" and no transcription is attempted. The Lead Card shows "Voicemail - No message left" in gray text.
  • If transcription fails: The Lead Card shows the audio player with a note: "Transcription unavailable — listen to recording." A retry is attempted once after 60 seconds. A `transcription_failed` event is logged.

---

VTCR 5.3 — Voicemail Notification Sent to Dispatcher

Visual:

  • Within 15 seconds of voicemail transcription completing, the contractor/dispatcher receives a notification on their configured channel(s):
  • Slack Notification: A rich Slack message in the configured channel with:
  • Header: "🎤 New Voicemail — [Business Name]"
  • Fields: Caller phone, timestamp, duration, urgency badge (color-coded).
  • Body: Full voicemail transcript text.
  • Audio link: "🔊 Listen to Recording" button linking to the in-app voicemail player.
  • Action buttons: "📞 Call Back" (opens phone dialer with the number), "💬 Send Text" (opens the SMS conversation), "📅 Book Appointment" (deep links to booking flow).
  • SMS Notification (to contractor's personal phone): A concise text:

"[Business Name] Voicemail from (555) 867-5309 at 3:42 PM: '[First 160 chars of transcript]...' Open: [deep link to dashboard]"

  • In-App Push Notification: Title: "New Voicemail", Body: "[Caller phone] left a [duration] voicemail: '[First 60 chars]...'", Tap action: opens the Call Detail view for that voicemail.
  • On the Dashboard, a red notification badge increments on the "Voicemails" filter tab.

Trigger:

  • A `voicemail_received` event fires (per VTCR 5.2) with a successful transcript AND the contractor has at least one notification channel configured and active in Settings → Notifications.

Condition:

  • The contractor's notification preferences include at least one active channel (Slack, SMS, in-app push). If no channels are configured, the voicemail is still saved but a warning appears in Settings: "You have unread voicemails but no notification channels configured."
  • For Slack: the Slack workspace is connected via OAuth, and the target channel is accessible by the bot.
  • For SMS: the contractor's personal notification phone number is valid and has not opted out of system notifications.
  • The contractor has not configured "Do Not Disturb" hours that include the current time (if DND is active, notifications are queued and delivered when DND ends, with a "While You Were Away" summary).
  • Duplicate notification suppression: if the same voicemail ID has already triggered a notification, do not re-send (prevents duplicates from retry logic).

Result:

  • Notifications are dispatched to all active channels simultaneously (fan-out).
  • Each notification includes sufficient context for the contractor to decide on immediate action without opening the app (caller number, transcript, urgency).
  • Deep links in notifications resolve correctly to the specific Call Detail view in the app (including for users not currently logged in — deep link triggers login flow first, then redirects).
  • A `voicemail_notification_sent` event is logged with: `voicemail_id`, `call_id`, `channels_notified` (array: ["slack", "sms", "push"]), `notification_timestamps`, `delivery_status_per_channel`.
  • If any notification channel fails (Slack API error, SMS delivery failure), the failure is logged but does not block other channels. A retry is attempted once for failed channels after 30 seconds.
  • The Lead Card's "Notified" checkbox is marked on the Dashboard, providing visual confirmation that the dispatcher has been alerted.
  • If urgency is "emergency" (based on transcript keyword extraction), the notification is upgraded: Slack message uses `@channel` mention, SMS is sent with HIGH priority flag, and push notification uses critical alert sound (iOS) / high-importance channel (Android).

---

VTCR 5.4 — Voicemail Greeting Playback & Preview in Settings

Visual:

  • In Settings → Voicemail → Greeting, the "Currently Active" greeting section displays:
  • A prominent audio player identical in style to the call detail voicemail player (waveform, play/pause, scrub, duration).
  • Below the player: greeting type label ("Recorded In-App" / "Uploaded File" / "AI-Generated"), date last updated, and duration.
  • A "Preview as Caller" button that initiates a test call to the contractor's number so they can hear exactly what callers experience (rings once, plays greeting, allows leaving a test voicemail).
  • An "Edit" button that re-opens the greeting configuration flow (VTCR 5.1).
  • A "Greeting History" expandable section showing the last 5 saved greetings with mini audio players and "Restore" buttons.
  • For the AI-generated greeting, an additional "View Script" expandable shows the text script used for TTS generation, with an "Edit Script & Regenerate" button.

Trigger:

  • The contractor navigates to Settings → Voicemail → Greeting at any time.
  • The contractor taps "Play" on the currently active greeting or any historical greeting.
  • The contractor taps "Preview as Caller."

Condition:

  • The greeting audio file exists in cloud storage and the URL is accessible (not expired, not deleted).
  • For "Preview as Caller": the contractor's phone number is on file and the telephony system is operational.

Result:

  • The audio player streams the greeting audio from cloud storage. Playback is near-instant (file is cached locally after first play).
  • If the audio file is unavailable (storage error, file corrupted), a fallback message appears: "Greeting audio unavailable. Please re-record or re-upload your greeting." The system falls back to the default AI greeting for live calls until the issue is resolved.
  • "Preview as Caller" initiates an outbound call via the telephony provider to the contractor's personal phone, playing the configured greeting. Any voicemail left during the test is tagged as "Test Voicemail" and does not trigger dispatcher notifications.
  • Greeting history is displayed in reverse chronological order. Tapping "Restore" on a historical greeting makes it the active greeting (with a confirmation dialog: "Replace current greeting with this one?").
  • A `greeting_previewed` event is logged (for analytics on greeting engagement/iteration).

---

VTCR 5.5 — Audio File Upload for Voicemail Greeting

Visual:

  • When the contractor selects "Upload Audio File" in the greeting configuration:
  • A drag-and-drop zone appears with a dashed border and upload icon: "Drag your audio file here, or tap to browse."
  • Supported formats listed below: .mp3, .wav, .m4a — Max 5 MB, 3–60 seconds.
  • During upload: a progress bar fills with percentage and a cancel button is available.
  • After upload: the file name, size, and detected duration are displayed. A waveform audio player renders for preview. A green "✓ Upload successful" confirmation appears.
  • If validation fails: a red error banner appears below the upload zone with a specific, actionable message (see Conditions).

Trigger:

  • The contractor selects the "Upload Audio File" option in Settings → Voicemail → Greeting AND drags a file into the drop zone or selects a file via the system file picker.

Condition:

  • File format is one of: `.mp3`, `.wav`, `.m4a`. Other formats show: "Unsupported file type. Please upload an .mp3, .wav, or .m4a file."
  • File size is ≤ 5 MB. Larger files show: "File too large ([actual size]). Maximum file size is 5 MB."
  • Audio duration is between 3 and 60 seconds. Under 3 seconds: "Greeting too short. Please record at least a 3-second greeting." Over 60 seconds: "Greeting too long ([actual duration]). Maximum duration is 60 seconds. Please trim your file and re-upload."
  • File is not corrupted / is a valid audio file (header check). Corrupted files show: "This file appears to be corrupted or is not a valid audio file. Please try a different file."
  • The contractor's account storage quota has not been exceeded (edge case for future consideration).

Result:

  • The audio file is uploaded to cloud storage (S3/GCS) via a signed upload URL (client-side direct upload for performance).
  • Server-side validation runs on the uploaded file (format, duration, integrity). If validation fails post-upload, the file is deleted and the error is shown to the user.
  • If validation passes, the file is processed (normalized to a consistent format/bitrate for telephony compatibility, e.g., 8kHz mono μ-law for PSTN, plus a high-quality copy for in-app playback).
  • The processed greeting is registered with the telephony provider.
  • A `greeting_uploaded` event is logged with: `contractor_id`, `file_name`, `file_size`, `duration`, `format`, `storage_url`, `timestamp`.
  • The greeting configuration view updates to show the "Currently Active" indicator on the uploaded greeting.
  • A success toast appears: "Greeting uploaded and activated! Your callers will hear this greeting on their next call."

---

Feature 6: Calendar Integration (Google Calendar)

---

VTCR 6.1 — Google Calendar Connection

Visual:

  • In Settings → Integrations → Calendar, a "Google Calendar" card displays:
  • Disconnected State: Google Calendar logo, description text ("Connect your Google Calendar to let AI check your availability and book appointments automatically."), and a prominent "Connect Google Calendar" button styled with Google's brand colors.
  • Connecting State: After tapping "Connect," a Google OAuth consent screen opens in a popup/browser redirect. The consent screen requests `calendar.readonly` and `calendar.events` scopes. The app name, icon, and requested permissions are clearly displayed per Google's OAuth requirements.
  • Connected State: A green "Connected ✓" badge, the connected Google account email displayed, the selected calendar name (from a dropdown if the account has multiple calendars), a "Last Synced: [timestamp]" indicator, and a "Disconnect" button (red, with confirmation dialog).
  • Below the connection card, a "Calendar Settings" section (only visible when connected) displays:
  • Appointment Duration: Segmented control with options: 15 / 30 / 45 / 60 minutes. Default: 30 min.
  • Buffer Time: Dropdown: 0 / 5 / 10 / 15 / 30 minutes between appointments. Default: 15 min.
  • Max Appointments Per Day: Number input (1–20) with stepper. Default: 8.
  • Booking Window: How far in advance callers can book. Dropdown: 3 / 7 / 14 / 30 days. Default: 14 days.
  • Business Hours for Availability: Inherited from the main business hours setting, with an override option for calendar-specific hours.

Trigger:

  • The contractor navigates to Settings → Integrations → Calendar and taps "Connect Google Calendar."
  • After authenticating with Google and granting consent, Google redirects back to the app with an authorization code.

Condition:

  • The contractor has a Google account with at least one Google Calendar.
  • The contractor grants both requested OAuth scopes (`calendar.readonly` for checking availability, `calendar.events` for creating appointments). If either scope is denied, the connection fails with a message: "We need both calendar permissions to check your availability and book appointments. Please try again and grant all permissions."
  • The OAuth authorization code is valid and not expired (codes expire in ~10 minutes).
  • The contractor does not already have a Google Calendar connected (if they do, they must disconnect first, or the system offers to replace the existing connection with a confirmation dialog).

Result:

  • The authorization code is exchanged for an access token and refresh token via Google's OAuth token endpoint.
  • Tokens are encrypted and stored securely in the database, associated with the contractor's account.
  • The system fetches the list of calendars from the Google Calendar API and presents a calendar selector if multiple calendars exist (defaults to the primary calendar).
  • An initial availability sync is performed: the system reads the next 14 days (or configured booking window) of events from the selected calendar to build an initial availability index.
  • The connection status updates to "Connected ✓" with the Google account email and selected calendar name.
  • A `calendar_connected` event is logged with: `contractor_id`, `google_account_email`, `calendar_id`, `calendar_name`, `scopes_granted`, `timestamp`.
  • The Voice AI and SMS AI modules are notified that calendar integration is active — they can now offer appointment booking to callers.
  • A success toast appears: "Google Calendar connected! Your AI assistant can now check your schedule and book appointments."
  • A background job is scheduled to refresh the access token proactively before expiration (tokens typically expire in 1 hour; the refresh token is used automatically).

---

VTCR 6.2 — Real-Time Availability Check

Visual:

  • Voice AI context (not directly visible to contractor, but audible to caller): When a caller asks about availability during a Voice AI conversation, the AI says: "Let me check [Business Name]'s schedule... [brief pause, ~1-2 seconds while API call completes]. I have a few openings this week: [reads available slots]."
  • Text AI context (visible in Inbox): The AI sends a message listing available slots in a structured format (see VTCR 4.4).
  • Admin visibility (Dashboard → Call Detail or Conversation): An inline system note appears in the conversation timeline: "📅 Availability checked — 6 slots found in next 7 days" with a timestamp. This note is only visible to the contractor/admin, not the caller.
  • Settings → Calendar → Availability Preview: A read-only calendar week view shows available slots in green and blocked time (existing events + buffer) in gray/red. This allows the contractor to verify that the AI is seeing the correct availability.

Trigger:

  • The Voice AI or SMS AI detects scheduling intent from a caller during an active conversation (intent keywords: "available," "schedule," "book," "appointment," "when can you come," "open slots," "this week," etc.).
  • OR the AI proactively offers scheduling (e.g., after answering an FAQ, the AI says "Would you like to book an appointment?" and the caller says "yes").

Condition:

  • Google Calendar is connected and the access token is valid (or can be refreshed). If the token is expired and refresh fails, see VTCR 6.5.
  • The contractor's calendar settings (appointment duration, buffer time, business hours, booking window) are fully configured.
  • The Google Calendar API is reachable and responding within 3 seconds. If the API times out, the AI falls back to: "I'm having trouble checking the schedule right now. Can I have [contractor name] call you back with available times?"

Result:

  • The system queries the Google Calendar API for events within the configured booking window (e.g., next 14 days).
  • Available slots are calculated by:

1. Starting with all possible slots within business hours, divided into blocks of [appointment duration].

2. Removing any slots that overlap with existing calendar events (fetched from Google).

3. Applying buffer time before and after each existing event.

4. Removing any slots on days where [max appointments per day] has already been reached.

5. Removing any slots in the past or within the next [minimum lead time, default 2 hours].

  • The resulting available slots are returned to the AI module as a structured array: `[{ date, start_time, end_time }]`.
  • The AI presents the first 3–5 available slots to the caller in a natural, conversational format (Voice AI reads them aloud; Text AI lists them in numbered format).
  • An `availability_checked` event is logged with: `call_id` or `conversation_id`, `slots_found`, `date_range_checked`, `duration_ms` (API response time), `timestamp`.
  • The availability data is cached for 2 minutes (to avoid redundant API calls if the caller asks follow-up questions about availability within the same conversation). Cache is invalidated if a new booking is made.

---

VTCR 6.3 — Appointment Booked, Confirmation Text & Reminder Texts

Visual:

  • Booking Confirmation (in Conversation): After the caller selects a slot and provides required info, the AI confirms:
  • Voice AI: "You're all set! I've booked you for [day, date] at [time] with [Business Name]. You'll receive a text confirmation shortly. Is there anything else I can help with?"
  • Text AI: *"✅ Appointment confirmed! [Service type] with [Business Name] on [Day, Date]

6.7 Missed Call Dashboard

6.8 CRM Sync

6.9 Onboarding Flow

# VTCR Specifications — Missed Call Command Center

---

Feature 7: Missed Call Dashboard

VTCR 7.1 — Dashboard Load (Primary View)

ElementSpecification
VisualFull-screen dashboard layout loads after onboarding is complete or upon app open for returning users. Top metrics bar spans the full width and displays four card-style KPIs in a horizontal row: Total Missed Calls (This Week) (integer with sparkline trend arrow vs. prior week), Recovery Rate (%) (percentage with green/yellow/red color coding: ≥75% green, 50–74% yellow, <50% red), Appointments Booked (integer, this week), and Avg Response Time (formatted as `Xm Xs`). Below the metrics bar is a filter toolbar with three controls: a date-range picker (default: "This Week"), a multi-select dropdown for Routing Method (`Voice`, `Text`, `Both`, `Voicemail`), and a multi-select dropdown for Outcome (`Appointment Booked`, `Question Answered`, `Callback Requested`, `Voicemail Left`, `No Response`, `Hung Up`). An "Export" button (icon: download arrow) sits right-aligned on the filter toolbar. Below the filters is the missed call table/list with sortable column headers: `Date/Time`, `Caller Number`, `Routing Method` (rendered as icons — phone for Voice, chat bubble for Text, both overlaid for Both, cassette icon for Voicemail), `Outcome` (color-coded pill badge), `CRM Status` (green checkmark = Synced, yellow spinner = Pending, red exclamation = Failed), and `Actions` (expandable chevron, "Push to CRM" icon button, "Call Back" icon button). Rows alternate with subtle zebra striping. Pagination controls appear at the bottom (25 rows default, options for 50/100). A loading skeleton (shimmer animation) is shown while data is being fetched.
TriggerUser completes onboarding and is routed to the dashboard for the first time OR user opens the app on any subsequent session OR user taps the "Dashboard" tab in the bottom/side navigation.
ConditionUser is authenticated and has completed onboarding (all required steps: Account, Company Details, VoIP Provider, Routing Rules, at least one response channel — AI Voice or SMS). The system queries the backend for missed call records scoped to the user's account and the default date range ("This Week," defined as Monday 00:00 to current moment in the user's local timezone).
ResultThe dashboard renders fully within 2 seconds on broadband / 4 seconds on 3G. Metrics bar populates with accurate, real-time-calculated KPIs. The table displays missed call rows sorted by `Date/Time` descending (most recent first). All filter controls are set to their defaults and are interactive. If any CRM sync status is "Failed," a subtle toast notification appears at the top: "X records failed to sync to CRM. Review and retry below." The "Export" button is enabled if at least one row exists. Analytics event `dashboard_loaded` fires with payload: `{user_id, total_rows, recovery_rate, load_time_ms}`.

---

VTCR 7.2 — Row Expand / Detail View

ElementSpecification
VisualWhen a row is expanded, an inline detail panel slides open beneath the row (accordion-style, 300ms ease-in-out animation). The panel content adapts to the routing method: If Voice: Displays `Call Duration` (formatted `Xm Xs`), an embedded audio player (waveform visualization, play/pause, scrub bar, speed toggle 1x/1.5x/2x, download button), full transcript (scrollable, timestamped, speaker-labeled: "AI" / "Caller"), and if outcome is `Appointment Booked`, an Appointment Card showing date, time, service type, and calendar link. If Text: Displays a conversation thread preview (last 5 messages shown in chat-bubble format with timestamps), and a "View Full Thread →" link that navigates to the Inbox/SMS module. If Voicemail: Displays an audio player (same design as voice), voicemail transcript (AI-generated, with a confidence indicator — e.g., "Transcript accuracy: High"), and voicemail duration. All types also show a CRM Sync Status section at the bottom of the panel: status badge (Synced/Pending/Failed), timestamp of last sync attempt, and a "Re-sync" button (visible only if status is Failed or if user wants to force a re-push). Only one row can be expanded at a time — expanding another auto-collapses the previous.
TriggerUser clicks/taps anywhere on a table row OR clicks the expand chevron icon in the `Actions` column.
ConditionThe row must have associated detail data available. If the call record exists but detail data (transcript, audio, thread) is still processing, a loading state is shown within the panel: spinner + "Processing transcript… This usually takes under a minute." Audio files must be available from cloud storage (S3/GCS signed URL, expiry: 7 days, auto-refreshed on request).
ResultThe detail panel expands and populates within 500ms (data pre-fetched on dashboard load for visible rows; lazy-loaded for off-screen rows). Audio player initializes but does not auto-play. Transcript renders with full formatting. If appointment was booked, appointment card data is pulled from the calendar integration module. The expanded row is visually highlighted with a left-side accent border (brand primary color). Analytics event `row_expanded` fires with payload: `{call_id, routing_method, outcome, time_to_expand_ms}`. Collapsing the row (clicking again or expanding another) fires `row_collapsed`.

---

VTCR 7.3 — Empty State (New User / No Data)

ElementSpecification
VisualWhen no missed call records exist, the metrics bar displays all zeros with neutral gray styling (no trend arrows, no color coding). The filter toolbar is visible but all controls are disabled (grayed out). The table area is replaced by a centered empty state illustration: a friendly line-art graphic of a phone ringing with a checkmark, accompanied by a headline: "No missed calls yet", subtext: "Make a test call to verify your setup is working correctly.", and a prominent CTA button: "Make a Test Call" (brand primary color, large tap target). Below the CTA, a smaller text link: "Review your setup in Settings →".
TriggerDashboard loads and the backend query returns zero missed call records for the user's account across all date ranges (not just the current filter — this is a true "never had any calls" state, not a "no results for this filter" state).
ConditionUser has completed onboarding (otherwise they would be in the onboarding flow, not the dashboard). The system has confirmed the query returned zero total records, not a query error. If the query fails, a different error state is shown instead (see error handling specs).
ResultThe empty state renders cleanly with no layout jank. Tapping "Make a Test Call" navigates the user to the Test Call screen (same as Onboarding Screen 10, but accessible standalone). Tapping "Review your setup in Settings →" navigates to the Settings screen. The "Export" button in the toolbar is hidden (not disabled — hidden, to reduce clutter). Analytics event `empty_state_shown` fires with payload: `{user_id, days_since_onboarding_complete}`. If the user has been in empty state for >48 hours post-onboarding, a push notification is triggered: "Your Missed Call Command Center is ready — but hasn't received any calls yet. Need help? Tap here." (links to support/troubleshooting).

---

VTCR 7.4 — Filter Application & Data Refresh

ElementSpecification
VisualWhen any filter is changed, the table and metrics bar update simultaneously. During the refresh, the table rows show a subtle shimmer overlay (not a full skeleton — preserving layout awareness) and the metrics cards show their numbers replaced with a brief pulse animation. After data loads: the table repopulates with filtered results, column sort state is preserved, and the metrics bar recalculates to reflect only the filtered data set. An active filter indicator appears as a small pill/badge next to each filter showing the active selection (e.g., `Method: Voice, Text` or `Date: Jun 1–15`). A "Clear All Filters" text link appears at the right end of the filter toolbar when any non-default filter is active. If filters return zero results, a contextual empty state appears (different from VTCR 7.3): illustration of a magnifying glass, text: "No calls match your filters", subtext: "Try adjusting your date range or removing a filter.", with the "Clear All Filters" button inlined.
TriggerUser selects a new value in any filter control: changes the date range via the date picker, checks/unchecks options in the Routing Method multi-select, or checks/unchecks options in the Outcome multi-select. Filters apply on change (no separate "Apply" button needed).
ConditionUser is on the dashboard with data loaded. The filter change produces a valid query (e.g., date range start ≤ end). If user selects an invalid date range (start > end), the date picker prevents submission and shows inline validation: "Start date must be before end date." Filter state is held in local/session state so it persists if the user navigates away and returns within the same session but resets to defaults on new sessions.
ResultFiltered data renders within 1 second. The URL/query params update to reflect filter state (enabling shareable/bookmarkable filtered views if web-based; for mobile, the state is stored in navigation state). Pagination resets to page 1 on filter change. The metrics bar shows recalculated KPIs scoped to the filtered dataset, with a small label beneath each metric: "Filtered" (to distinguish from unfiltered totals). Analytics event `filter_applied` fires with payload: `{filter_type, filter_values, result_count, response_time_ms}`.

---

VTCR 7.5 — Play Audio & View Transcript Actions

ElementSpecification
VisualAudio Player: Embedded within the expanded row detail panel. Renders as a horizontal bar with: play/pause toggle button (▶/⏸), waveform visualization (rendered from audio peaks data, highlights played portion in brand color), elapsed/total time display (`0:42 / 2:15`), playback speed button cycling through `1x → 1.5x → 2x → 0.75x`, and a download icon button (downloads MP3/WAV). While playing, the waveform animates a progress sweep. If audio is loading from CDN, a spinner replaces the play button with text: "Loading audio…". Transcript View: Rendered below the audio player (for Voice/Voicemail) as a scrollable container (max-height: 300px before scroll). Each line is formatted: `[MM:SS] Speaker Label: "Text"` — e.g., `[0:05] AI Assistant: "Thanks for calling. How can I help?"`. Clicking a timestamp in the transcript seeks the audio player to that timestamp. A "Copy Transcript" button (clipboard icon) appears in the top-right corner of the transcript container. For long transcripts, a "Search in transcript" search bar appears at the top of the container.
TriggerAudio: User clicks the play button within the expanded detail panel. Transcript: Renders automatically when the row is expanded (no separate trigger needed). Timestamp click: User clicks any timestamp label in the transcript. Copy: User clicks the "Copy Transcript" button. Download: User clicks the download icon on the audio player.
ConditionAudio file must be available and the signed URL must be valid (not expired). If the audio file is unavailable (e.g., deleted, storage error), the player shows a disabled state: "Audio unavailable" with a help-text tooltip explaining possible reasons. Transcript must have completed processing; if still in progress, show: "Transcript is being generated…" with an estimated completion indicator. Browser/device must support audio playback (HTML5 audio for web, native player for mobile).
ResultAudio plays immediately upon press (latency target: <500ms to first audio byte). Playback continues even if the user scrolls the dashboard (a mini-player bar appears at the bottom of the screen showing currently playing audio with play/pause and row identifier — tapping it scrolls back to the row). Only one audio can play at a time — starting a new one pauses the previous. Timestamp seek moves the audio playhead to within ±1 second accuracy and highlights the corresponding transcript line. Copy Transcript copies plain-text (without timestamps) to clipboard and shows a toast: "Transcript copied to clipboard." Download initiates a file download named `call_[caller_number]_[datetime].mp3`. Analytics events: `audio_played {call_id, duration_listened_seconds}`, `transcript_copied {call_id}`, `audio_downloaded {call_id}`.

---

Feature 8: CRM Sync

VTCR 8.1 — Auto-Sync on Call Completion

ElementSpecification
VisualNo direct visual is shown to the user at the moment of sync (it happens in the background). The result is reflected on the dashboard row's CRM Status column: a green checkmark badge with tooltip "Synced to [CRM Name] at [timestamp]" appears once the sync completes. While sync is in progress (typically 2–10 seconds after call completion), the status shows a yellow animated spinner badge with tooltip "Syncing to [CRM Name]…". In the Settings → CRM Integration panel, the Auto-Sync toggle is displayed as a standard on/off switch, defaulted to ON, with helper text: "Automatically push new call data to your connected CRM after each interaction. Disable to sync manually." When auto-sync is on, a small "Auto" label badge appears next to the CRM Status column header in the dashboard table.
TriggerA missed call interaction reaches a terminal state — defined as: (a) AI voice call ends (hangup detected or call duration timeout), (b) SMS conversation reaches inactivity timeout (configurable, default: 15 minutes of no reply), (c) voicemail recording is completed and transcription is finished, or (d) the system determines "No Response" (all routing attempts exhausted with no pickup/reply). The trigger fires as an asynchronous background job enqueued immediately upon terminal state detection.
ConditionAll of the following must be true: (1) Auto-Sync is toggled ON in Settings. (2) A CRM is connected and the connection is active (valid API key/OAuth token not expired). (3) The interaction produced a meaningful data payload — at minimum: phone number + outcome. If the call was a "Hung Up" with ≤3 seconds duration, the system still syncs but tags it as `low_quality_lead`. (4) The record has not already been synced (idempotency check via `call_id` + `crm_record_id` mapping). If auto-sync is OFF, the row's CRM Status shows a gray dash with tooltip: "Auto-sync disabled. Use Push to CRM to sync manually."
ResultThe system constructs a CRM payload using the field mapping configuration (see VTCR 8.5) and sends it to the appropriate CRM API: ServiceTitan → Create/update `Customer` + `Job/Booking`, Housecall Pro → Create/update `Customer` + `Estimate/Job`, Jobber → Create/update `Client` + `Request`, GoHighLevel → Create/update `Contact` + `Opportunity`, Webhook Fallback → POST JSON payload to user-configured endpoint. The system first searches the CRM for an existing contact matching the caller's phone number. If found: update the existing record and append new interaction notes. If not found: create a new lead/contact. Data pushed includes: `caller_name` (if collected during AI voice interaction or SMS), `phone_number`, `call_type` (voice/text/voicemail), `outcome`, `transcript_or_notes` (full transcript for voice/VM, conversation summary for SMS), `appointment_details` (date, time, service type — if booked), `call_timestamp`, and `recording_url` (signed, 30-day expiry). Upon success (HTTP 2xx from CRM API): CRM Status updates to green checkmark, `crm_record_id` is stored in the local database for future updates, and analytics event `crm_auto_sync_success` fires with `{call_id, crm_type, response_time_ms}`. Upon failure: see VTCR 8.4.

---

VTCR 8.2 — Manual "Push to CRM" Action

ElementSpecification
VisualEach row in the dashboard table has a "Push to CRM" icon button in the Actions column — rendered as a small cloud-upload icon. The button has three visual states: (1) Default/enabled (neutral icon, clickable) — shown when CRM Status is "Not synced" (gray dash) or "Failed" (red exclamation). (2) Loading — icon replaced with a spinner, button disabled, tooltip: "Pushing to [CRM Name]…". (3) Disabled/completed — icon is a green checkmark, button not clickable, tooltip: "Already synced to [CRM Name]. Synced at [timestamp]." Clicking the button when the record is already synced but the user wants to re-push (e.g., after editing data): the button remains enabled with a subtle "refresh" icon variant and tooltip: "Re-push updated data to [CRM Name]." A confirmation toast appears on successful manual push: "✓ Pushed to [CRM Name] successfully." with a clickable "View in CRM →" link (deep link to the CRM record, if supported by the CRM's API).
TriggerUser clicks the "Push to CRM" icon button on a specific dashboard row.
Condition(1) A CRM must be connected (if not, see VTCR 8.5 — CRM Not Connected State). (2) The row must have call data available (not in a processing/loading state). (3) If auto-sync is ON and the record is already successfully synced, the button behavior changes to "re-push" mode (update, not create duplicate). (4) The user must have an active internet connection — if offline, show a tooltip: "You're offline. Push to CRM requires an internet connection." and disable the button. Rate limiting: max 10 manual pushes per minute per user to prevent accidental spam clicks.
ResultThe system executes the same CRM sync logic as auto-sync (VTCR 8.1) but triggered synchronously from the user action. The button enters loading state immediately. On success: CRM Status column updates to green checkmark with current timestamp, success toast appears (auto-dismisses after 5 seconds), and analytics event `crm_manual_push_success` fires with `{call_id, crm_type, was_retry: boolean}`. On failure: CRM Status updates to red exclamation, error toast appears with actionable message (see VTCR 8.4), and the button returns to enabled/default state so the user can retry. If this is a re-push of an already-synced record: the existing CRM record is updated (not duplicated) using the stored `crm_record_id`.

---

VTCR 8.3 — Sync Success Indicator & Aggregate Metrics

ElementSpecification
VisualPer-row indicator: The `CRM Status` column in the dashboard table displays one of four states as a color-coded pill badge: "Synced" (green, checkmark icon), "Pending" (yellow, spinner icon — used during active sync and for queued items), "Failed" (red, exclamation icon — with a small retry arrow sub-icon), "Not Synced" (gray, dash icon — when auto-sync is off and no manual push has been attempted). Hovering/tapping the badge reveals a tooltip with details: CRM name, record ID (linked), timestamp of last attempt, and error message (if failed). Aggregate metrics: In the Settings → CRM Integration panel, a Sync Health section displays: `Total Records Synced` (all-time integer), `Sync Success Rate` (percentage, last 30 days, color-coded same as Recovery Rate), `Pending Syncs` (count, with a "Process Now" button if >0), `Failed Syncs` (count, with a "Retry All" button if >0). A mini sync status indicator also appears in the dashboard's top metrics bar as a small icon next to the CRM-related metrics — green dot = all healthy, yellow dot = pending items exist, red dot = failures exist.
TriggerPer-row: Status updates in real-time via WebSocket/polling (every 10 seconds) whenever a sync job's state changes. Aggregate metrics: Calculated on load of the Settings → CRM Integration panel and refreshable via a "Refresh" icon button. Mini indicator on dashboard: Updates on dashboard load and on any row-level status change.
ConditionA CRM must be connected for any sync status to be meaningful. If no CRM is connected, the entire CRM Status column shows a uniform gray state: "No CRM" on every row (see VTCR 8.5). Aggregate metrics require at least one sync attempt to have been made; otherwise, the Sync Health section shows: "No sync activity yet. Connect a CRM and enable auto-sync to get started."
ResultUsers have at-a-glance visibility into sync health at both the individual record and system-wide level. The per-row status updates without requiring a page refresh (real-time). The aggregate metrics provide confidence that the CRM integration is functioning. The "Retry All" button (on failed syncs) enqueues all failed records for re-sync as a batch job and shows a toast: "Retrying X failed syncs… This may take a few minutes." The "Process Now" button forces immediate processing of any queued/pending items. Analytics event `crm_sync_health_viewed` fires when the user views the Sync Health section.

---

VTCR 8.4 — Sync Failure with Retry

ElementSpecification
VisualWhen a sync fails, the affected dashboard row's CRM Status changes to a red exclamation pill badge labeled "Failed." Tapping/hovering the badge shows a tooltip with the error reason categorized into user-friendly language: (a) "Authentication expired — please reconnect your CRM in Settings." (401/403 errors), (b) "CRM service is temporarily unavailable. We'll retry automatically." (5xx errors, timeouts), (c) "Data validation error — a required field is missing. Check field mapping." (400 errors), (d) "Rate limit exceeded — retrying in X minutes." (429 errors), (e) "Unknown error — contact support with code [error_code]." (catch-all). A "Retry" button (circular arrow icon) appears inline next to the failed badge. If 3+ records have failed, a banner notification appears at the top of the dashboard: "⚠ [X] CRM syncs have failed. [View Details] [Retry All]" — the banner is dismissible but reappears on next load if failures persist. In the expanded row detail panel, the CRM Sync Status section shows full error details: error code, timestamp, number of retry attempts made, and next scheduled auto-retry time.
TriggerThe CRM API returns a non-2xx response OR the sync request times out (>30 seconds) OR the system cannot reach the CRM API endpoint (network error).
ConditionThe system implements an automatic retry strategy with exponential backoff: Retry 1 at +1 minute, Retry 2 at +5 minutes, Retry 3 at +30 minutes, Retry 4 at +2 hours, Retry 5 (final) at +12 hours. After 5 failed auto-retries, the record is marked as "Failed — Manual Retry Required" and no further automatic retries are attempted. For authentication errors (401/403): auto-retries are not attempted (they will all fail); instead, the user is prompted to reconnect the CRM. For rate limit errors (429): the system respects the `Retry-After` header from the CRM API if provided. The retry counter and history are stored per-record in the database.
ResultOn auto-retry success: CRM Status silently updates from "Failed" to "Synced," the dashboard banner updates its failure count, and analytics event `crm_sync_retry_success` fires with `{call_id, retry_attempt_number, total_time_to_success}`. On manual retry (user clicks Retry button): The button enters a loading state, the system makes one immediate sync attempt (bypassing the backoff schedule), and the result (success or failure) is shown via toast notification and status badge update within 10 seconds. On final failure (all retries exhausted): The record's status shows "Failed — Requires Attention" in a darker red, the expanded detail panel shows a "Contact Support" link pre-populated with the error details, and a weekly digest email is sent to the user summarizing all permanently failed syncs. Analytics event `crm_sync_permanent_failure` fires, and an internal alert is sent to the engineering/support team if the failure rate across all users exceeds 5% in any 24-hour period.

---

VTCR 8.5 — CRM Not Connected State & Field Mapping Configuration

ElementSpecification
VisualCRM Not Connected State: If no CRM integration is configured, the following visual changes apply across the app: (1) The dashboard's `CRM Status` column header shows a subtle warning icon (⚠); every row displays a gray "No CRM" badge in that column. (2) The "Push to CRM" action button on each row is replaced with a "Connect CRM" link button (underlined text, no icon). (3) The dashboard's top metrics bar omits any CRM-related metrics (or shows them as `—`). (4) A dismissible info banner appears at the top of the dashboard on the first 5 sessions: "📎 Connect your CRM to automatically sync leads and appointments. [Connect Now] [Dismiss]". In Settings → CRM Integration (when not connected): A selection screen displays supported CRM logos in a grid: ServiceTitan, Housecall Pro, Jobber, GoHighLevel, and a "Custom Webhook" option. Each has a "Connect" button. Below the grid: "Don't see your CRM? Use our webhook integration or [request an integration]." Field Mapping Configuration (shown after CRM is connected): A two-column mapping table. Left column: Missed Call Command Center fields (caller_name, phone_number, call_type, outcome, transcript, appointment_date, appointment_time, service_type, notes). Right column: CRM fields (auto-populated with best-guess defaults based on the CRM type, editable via dropdown showing all available fields from the CRM schema). A "Restore Defaults" link resets all mappings. A "Test Mapping" button sends a sample record to the CRM and shows the result. Required fields are marked with a red asterisk; if a required CRM field is unmapped, a validation error appears: "[Field Name] is required by [CRM Name]. Please map it before saving."
TriggerNot Connected State: Renders on dashboard load and settings load whenever `crm_connected = false` in the user's configuration. Connect Now / Connect button: User clicks to initiate CRM OAuth flow or API key entry. Field Mapping: Displayed automatically after successful CRM connection, and accessible anytime via Settings → CRM Integration → Field Mapping.
ConditionNot Connected: No CRM credentials are stored or all stored credentials have been explicitly disconnected/revoked by the user. The "Connect CRM" prompts must not be shown during onboarding (handled separately in Feature 9) or to users who have explicitly dismissed the banner 5 times (respect `crm_banner_dismissed_count`). Field Mapping: The CRM must be successfully connected and authenticated. The system must have successfully fetched the CRM's field schema (custom fields included) via API. If schema fetch fails, show an error: "Couldn't load fields from [CRM Name]. Check your connection and try again." with a retry button. Default mappings are defined per CRM type in a configuration file maintained by the engineering team.
ResultCRM Connection Flow: Clicking "Connect" for OAuth-based CRMs (ServiceTitan, Housecall Pro, Jobber, GoHighLevel) opens the CRM's OAuth authorization page in a browser/webview. Upon successful authorization, the user is returned to the app, a success toast shows "✓ Connected to [CRM Name]!", and the field mapping screen is presented. For API-key-based connections or webhook: an inline form appears for credential entry with a "Verify Connection" button that makes a test API call. Field Mapping Save: Upon saving, the system validates all required fields are mapped, persists the mapping configuration, and triggers a test sync of a sample record (with user confirmation). If test sync succeeds: "✓ Field mapping saved and verified." If it fails: the mapping is still saved but a warning is shown: "Mapping saved, but test sync failed: [reason]. You may need to adjust your mappings." Webhook Fallback: User provides a URL endpoint, selects HTTP method (POST default), can add custom headers (e.g., API keys), and previews the JSON payload structure. A "Send Test Webhook" button fires a sample payload and displays the response status code and body. Analytics events: `crm_connected {crm_type}`, `crm_field_mapping_saved {crm_type, custom_mappings_count}`, `crm_disconnected {crm_type}`.

---

Feature 9: Onboarding Flow (Multi-Step Setup Wizard)

VTCR 9.1 — Onboarding Screen Transitions & Progress Bar

ElementSpecification
VisualThe onboarding flow is a full-screen wizard with a fixed progress bar at the top of every screen. The progress bar is a horizontal segmented track with 10 segments (one per screen), filled with the brand primary color as the user advances. Each segment has a small label beneath it (visible on desktop; on mobile, only the current step's label is shown): `Welcome → Account → Company → VoIP → Routing → Voice AI → SMS → Calendar → CRM → Test Call`. Completed steps show a checkmark inside the segment circle. The current step is highlighted (larger circle, pulsing or glowing). Future steps are grayed out but labeled. Skipped optional steps show an open circle with a skip icon (↷) to indicate they can be revisited. Below the progress bar, a text indicator reads: "Step X of 10 — [Step Name]". Each screen transition uses a horizontal slide animation (left-to-right for forward, right-to-left for back), duration: 300ms ease-in-out. A "Back" button (left arrow + "Back") appears in the top-left on screens 2–10. A persistent "Save & Exit" option appears in the top-right on screens 2–10, allowing users to quit and resume later.
TriggerUser clicks the primary CTA button on each screen (e.g., "Next," "Continue," "Save & Continue," "Skip for Now") to advance OR clicks "Back" to go to the previous screen OR clicks "Save & Exit" to leave the wizard.
ConditionForward navigation: The current screen's required fields must pass validation before allowing progression. If validation fails, inline error messages appear on the offending fields and the screen does not advance. A shake animation plays on the CTA button to indicate the action was blocked. Back navigation: Always allowed; no data is lost — form state is preserved in local/session storage. Save & Exit: All data entered so far is persisted to the backend (draft state). The user can resume from the last completed step upon next login. The progress bar accurately reflects which steps have saved data, which are incomplete, and which have been skipped. Skip eligibility: Only screens 8 (Calendar) and 9 (CRM) show a "Skip for Now" secondary button. Screens 1–7 and 10 are required and cannot be skipped.
ResultOn each forward transition: (1) Form data from the current screen is saved to the backend via API call (optimistic save — animation plays immediately, error toast shown if save fails in background). (2) The progress bar advances — the completed segment fills with color and shows a checkmark. (3) The next screen slides in from the right. (4) Analytics event `onboarding_step_completed` fires with `{step_number, step_name, time_spent_on_step_seconds, fields_completed}`. On "Save & Exit": the user is taken to a "Welcome Back" landing screen on next login showing their progress: "You're X% done with setup. Pick up where you left off." with a "Continue Setup" button that takes them to the next incomplete step. The onboarding state is stored as `{current_step, completed_steps[], skipped_steps[], draft_data{}}` in the user's profile.

---

VTCR 9.2 — VoIP Provider Setup (Screen 4)

ElementSpecification
VisualScreen 4 presents a two-section layout. Section 1 — Select Your VoIP Provider: A grid/list of supported VoIP providers with logos and names (e.g., RingCentral, Vonage, Grasshopper, Google Voice, Ooma, 8x8, custom/other). Each provider is a selectable card with a radio-button behavior (one selection at a time). Selecting a provider highlights the card with a border and checkmark. Below the grid, a "Other / I'm not sure" option opens a text field for the provider name and a note: "We'll provide generic call forwarding instructions." Section 2 — Set Up Call Forwarding (appears after provider selection, with a smooth expand animation): Displays provider-specific step-by-step instructions with numbered steps and screenshots/diagrams. Example for RingCentral: "1. Log in to your RingCentral admin portal. 2. Go to Phone System → Auto-Receptionist. 3. Under 'After Hours,' select 'Forward to external number.' 4. Enter this forwarding number: [GENERATED NUMBER]. 5. Save changes." The forwarding number is displayed in a large, bold, copy-friendly format inside a bordered box with a "Copy" button. This number is generated by the system (via Twilio/VoIP infrastructure) and is unique to this user's account. Below the instructions: a "Verify Setup" button that initiates an automated test call. When clicked, a modal appears: "We're calling your business number now. Let it ring — don't answer. We'll check if the call forwards to us correctly." The modal shows a progress indicator: `Calling… → Ringing… → Checking forward… → ✓ Success! / ✗ Failed`.
TriggerUser arrives at Screen 4 via the "Next" button from Screen 3 (Company Details).
ConditionThe user must have completed Screens 1–3 (account created, company details saved). The system must be able to provision a forwarding number — this requires the backend telephony infrastructure (Twilio) to be operational. If number provisioning fails, show an error: "We couldn't generate your forwarding number. This is usually temporary — please try again in a few minutes." with a "Retry" button. The forwarding instructions are stored in a content management system keyed by provider name, allowing non-engineering updates. If instructions for a selected provider are unavailable, show generic instructions with a note: "These are general instructions. Your provider's interface may look slightly different. [Contact support for help.]"
ResultUpon provider selection: instructions render within 500ms (pre-loaded for common providers). The forwarding number is generated and persisted to the user's account — it does not change if the user navigates back and forth. On "Verify Setup" click: The system initiates an outbound call to the user's business phone number (collected in Screen 3 or requested here if not provided). The system waits up to 45 seconds for the call to be forwarded to the generated number. If verification succeeds: A green success state replaces the modal: "✓ Call forwarding is working perfectly!" with confetti/celebration micro-animation. The "Next" button becomes enabled/prominent. If verification fails: A red failure state: "✗ The call didn't reach us. Common fixes:" followed by troubleshooting tips (e.g., "Make sure you entered the forwarding number correctly," "Check that call forwarding is enabled, not just voicemail," "Some providers take 5–10 minutes to activate changes — wait and try again"). A "Retry Verification" button and a "Skip Verification & Continue" link (with warning: "Proceeding without verification may result in missed calls not being captured.") are provided. Analytics events: `voip_provider_selected {provider_name}`, `forwarding_number_generated {number}`, `forwarding_verification_attempted`, `forwarding_verification_success` / `forwarding_verification_failed {failure_reason}`.

---

VTCR 9.3 — Skip Optional Step (Screens 8 & 9)

ElementSpecification
VisualOn Screen 8 (Calendar) and Screen 9 (CRM), the screen layout includes: a description of the integration's value proposition (e.g., Screen 8: "Connect Google Calendar so your AI assistant can check your real-time availability and book appointments without double-booking."), the integration setup form/OAuth button, and a clearly visible "Skip for Now" secondary button positioned below the primary CTA ("Connect Google Calendar" / "Connect CRM"). The "Skip for Now" button is styled as an outlined/ghost button (not competing visually with the primary CTA) with helper text beneath it: "You can set this up later in Settings → Integrations." Clicking "Skip for Now" triggers a brief confirmation tooltip/popover (not a full modal — low friction): "Are you sure? [Feature benefit one-liner]. Skip anyway?" with two options: "Yes, Skip" and "Go Back." After skipping, the progress bar updates that step's segment to show an open circle with a skip icon (↷) in a neutral gray color, distinguishing it from completed (filled + checkmark) and incomplete (empty) states.
TriggerUser clicks the "Skip for Now" button on Screen 8 or Screen 9.
ConditionThe step must be designated as optional in the onboarding configuration (only Screens 8 and 9). Required steps (1–7, 10) do not render a "Skip" option. The skip confirmation popover must appear and the user must confirm "Yes, Skip" — single-click skip without confirmation is not allowed (prevents accidental skips). No data validation is required for skipping (the forms can be empty). If the user has partially filled the form before clicking Skip, their partial data is preserved in draft state so it's pre-filled if they return later.

| Result | The wizard advances to the next screen with the standard slide animation. The skipped step is recorded in the onboarding state as `skipped_steps: [8]` or `skipped_steps: [8, 9]`. The progress bar reflects the skip visually. On the final completion of onboarding (after Screen 10), the celebration screen (VTCR 9.5) includes a reminder section if any steps were skipped: *"

---

# Recipe 002: Missed Call Command Center — Infrastructure PRD Sections

---

Recipe-Specific Setup (Onboarding Screen Details)

Overview

The onboarding wizard consists of 10 screens that progressively configure the Missed Call Command Center. Screens 1–3 follow the master template patterns established in the platform's shared onboarding framework. Screens 4–10 are recipe-specific and detailed below. Each screen validates before advancing, and users can navigate backward without data loss (all inputs are persisted to draft state on each step transition).

Wizard State Management:

```

onboarding_progress: {

current_step: integer (1-10),

completed_steps: integer[],

is_complete: boolean,

draft_data: jsonb, // Holds all in-progress field values

started_at: timestamptz,

completed_at: timestamptz

}

```

---

Screens 1–3: Standard Master Template Patterns

These screens are inherited from the platform-wide onboarding template and are documented in the Master Template PRD. For completeness:

ScreenTitleFieldsValidation
1Create Your AccountEmail, password, full name, phone numberEmail uniqueness check, password strength (min 8 chars, 1 uppercase, 1 number), phone format (E.164)
2Company DetailsBusiness name, industry (pre-selected: "Home Services"), service area (zip codes or radius), timezoneBusiness name required, at least one service area, timezone required
3Team SetupInvite team members (optional), assign roles (Owner, Manager, Dispatcher)Valid email format for invites, max 50 initial invites

Data Model: These screens collect standard account setup data (business name, industry, service area, timezone, team members). The specific data model for account and user management is handled at the platform level and is outside the scope of this recipe.

---

Screen 4: VoIP Provider Selection & Forwarding Setup

Purpose: Connect the user's existing business phone system so missed calls can be forwarded to the Command Center.

UI Layout:

1. Provider Selection — Radio card group with logos:

  • Google Voice
  • Grasshopper
  • RingCentral
  • Vonage Business
  • OpenPhone
  • Other / Generic SIP
  • "I don't have a business phone yet" → triggers a generated Twilio number flow

2. Forwarding Instructions — Dynamic instruction panel that changes based on provider selection. Each provider shows:

  • Step-by-step instructions with annotated screenshots
  • The forwarding destination number (auto-generated or displayed)
  • "Copy Number" button
  • Deep link to provider's settings page (where available)

3. Forwarding Number Generation:

  • If user selects "I don't have a business phone yet," provision a new Twilio number
  • Area code preference selector (based on service area from Screen 2)
  • Number is provisioned via Twilio API and stored

4. Verification:

  • "I've set up forwarding" checkbox
  • "Send Test Call" button — initiates an automated test call to verify forwarding is working
  • Verification status indicator: Pending → Ringing → Verified ✓ / Failed ✗

Data Model:

```

voip_config: {

id: uuid (PK),

provider: text, // "google_voice" | "grasshopper" | "ringcentral" | "vonage" | "openphone" | "generic_sip" | "twilio_provisioned"

forwarding_number: text, // The number calls are forwarded TO (our Twilio number)

original_business_number: text, // The user's existing business number (optional)

twilio_number_sid: text, // Twilio Phone Number SID (if provisioned)

is_verified: boolean DEFAULT false,

verified_at: timestamptz,

forwarding_instructions_version: text, // Track which instruction set was shown

created_at: timestamptz,

updated_at: timestamptz

}

```

Validation Rules:

  • Provider selection is required
  • Forwarding number is auto-assigned; user cannot edit
  • Verification is recommended but not blocking (user can proceed with a warning)
  • If provisioning a new Twilio number, area code must be valid and number must be successfully purchased

---

Screen 5: Routing Rules (Weekly Schedule Grid)

Purpose: Define what happens when a call comes in at different times — route to AI Voice, send to voicemail, or auto-text.

UI Layout:

1. Weekly Schedule Grid:

  • 7 rows (Monday–Sunday), each row has:
  • Toggle: Active / Inactive for the day
  • Time range picker: Start time – End time (15-min increments)
  • Routing method dropdown: `ai_voice` | `voicemail` | `auto_text_only`
  • "Add Time Block" button per day (allows split shifts, e.g., 8AM–12PM AI Voice, 1PM–5PM Voicemail)
  • Visual timeline bar showing coverage

2. Default After-Hours Rule:

  • Dropdown: What happens outside scheduled hours?
  • Options: `voicemail` | `auto_text_only` | `ai_voice` (24/7)

3. Holiday/Override Rules (collapsed section, optional):

  • Date picker + routing method
  • "Copy from template" (e.g., "All holidays → voicemail")

4. Defaults Pre-Populated:

  • Mon–Fri: 8:00 AM – 6:00 PM → `ai_voice`
  • Sat: 9:00 AM – 2:00 PM → `ai_voice`
  • Sun: Inactive → `voicemail`
  • After-hours default: `auto_text_only`

Data Model:

```

routing_rules: {

id: uuid (PK),

rule_type: text, // "weekly" | "override" | "after_hours_default"

day_of_week: integer, // 0=Sunday, 1=Monday, ..., 6=Saturday (NULL for overrides/defaults)

override_date: date, // For holiday/one-off overrides (NULL for weekly rules)

start_time: time, // e.g., '08:00:00'

end_time: time, // e.g., '18:00:00'

routing_method: text, // "ai_voice" | "voicemail" | "auto_text_only"

is_active: boolean DEFAULT true,

priority: integer DEFAULT 0, // Higher = takes precedence (overrides > weekly)

created_at: timestamptz,

updated_at: timestamptz

}

```

Validation Rules:

  • Time blocks within the same day cannot overlap
  • `start_time` must be before `end_time`
  • At least one routing rule must exist (the after-hours default satisfies this)
  • Override dates must be today or future

---

Screen 6: AI Voice Setup (Vapi Configuration)

Purpose: Configure the AI voice agent that answers missed calls — its voice, personality, greeting, and information-gathering behavior.

UI Layout:

1. Vapi API Key:

  • Secure input field (masked after entry)
  • "Get your API key" link → opens Vapi dashboard in new tab
  • Inline validation: tests key against Vapi API and shows ✓ or ✗
  • Help tooltip explaining what Vapi is and pricing implications

2. Voice Selector:

  • Dropdown or audio card grid with voice previews
  • Filter by: Gender, Accent, Tone (professional, friendly, casual)
  • "Preview" button plays a sample greeting in selected voice
  • Default: `rachel` (Vapi's default professional female voice)

3. Greeting Script:

  • Textarea with smart defaults and variable support
  • Default: `"Hi, thanks for calling {{business_name}}. I'm sorry we missed your call. I'm an AI assistant and I'd love to help you. How can I assist you today?"`
  • Variable chips: `{{business_name}}`, `{{caller_name}}` (if available from CRM), `{{current_time_greeting}}` (Good morning/afternoon/evening)
  • Character count indicator (recommended: 150–300 chars)

4. Information to Collect — Checklist with toggles:

FieldDefaultRequired?
Caller's name✅ OnRequired
Service needed✅ OnRequired
Preferred callback time✅ OnOptional
Address/location⬜ OffOptional
Urgency level⬜ OffOptional
How they heard about us⬜ OffOptional
Custom field 1-3⬜ OffOptional
  • Custom fields: label input + type selector (text, yes/no, multiple choice)

5. Advanced Settings (collapsed):

  • System prompt overrides (textarea for power users)
  • Max call duration: slider, 1–10 minutes, default 5
  • Silence timeout: 5–30 seconds, default 10
  • End-of-call behavior: `offer_appointment` | `promise_callback` | `both`
  • Language: dropdown (English, Spanish — future: more)

Data Model:

```

vapi_config: {

id: uuid (PK),

api_key_encrypted: text, // Encrypted at rest

voice_id: text DEFAULT 'rachel', // Vapi voice identifier

voice_provider: text DEFAULT 'elevenlabs', // "elevenlabs" | "playht" | "deepgram"

greeting_text: text,

info_to_collect: jsonb,

/* Example:

[

{ "field": "caller_name", "label": "Name", "type": "text", "required": true, "enabled": true },

{ "field": "service_needed", "label": "Service Needed", "type": "text", "required": true, "enabled": true },

{ "field": "preferred_callback", "label": "Preferred Callback Time", "type": "text", "required": false, "enabled": true },

{ "field": "address", "label": "Address", "type": "text", "required": false, "enabled": false },

{ "field": "urgency", "label": "Urgency", "type": "multiple_choice", "options": ["Low","Medium","High","Emergency"], "required": false, "enabled": false },

{ "field": "custom_1", "label": "Referral Source", "type": "text", "required": false, "enabled": false }

]

*/

system_prompt_overrides: text, // Custom system prompt additions

max_call_duration_seconds: integer DEFAULT 300,

silence_timeout_seconds: integer DEFAULT 10,

end_of_call_behavior: text DEFAULT 'both', // "offer_appointment" | "promise_callback" | "both"

language: text DEFAULT 'en',

is_active: boolean DEFAULT true,

created_at: timestamptz,

updated_at: timestamptz

}

```

Validation Rules:

  • API key must pass Vapi validation endpoint before saving
  • Greeting text is required and must be ≤ 1000 characters
  • At least `caller_name` must be enabled in info_to_collect
  • Max call duration: 60–600 seconds
  • If system_prompt_overrides is provided, it must be ≤ 2000 characters

---

Screen 7: SMS / Auto-Text Setup (Twilio Configuration)

Purpose: Configure the automatic text message sent to callers when a call is missed, and the Twilio credentials powering SMS.

UI Layout:

1. Twilio Credentials:

  • Account SID input (validated format: starts with "AC", 34 chars)
  • Auth Token input (masked)
  • "Get your credentials" link → Twilio console
  • Inline validation: tests credentials against Twilio API
  • Note: If user provisioned a Twilio number on Screen 4, these may already be captured

2. SMS Template Configuration:

  • Immediate Auto-Text Template:
  • Textarea with variable support
  • Default: `"Hi {{caller_name}}, thanks for calling {{business_name}}! We missed your call but we'll get back to you shortly. Reply STOP to opt out."`
  • Variables: `{{caller_name}}`, `{{business_name}}`, `{{callback_eta}}`
  • Character count with SMS segment indicator (160 chars = 1 segment)
  • Follow-Up Text Template (sent if no callback within X minutes):
  • Enable/disable toggle
  • Delay: dropdown (15 min, 30 min, 1 hour, 2 hours) — default: 30 min
  • Template textarea
  • Default: `"Hi {{caller_name}}, just following up from {{business_name}}. Would you like to schedule an appointment? Reply YES and we'll find a time that works."`
  • Appointment Confirmation Template:
  • Triggered when AI books appointment
  • Default: `"Your appointment with {{business_name}} is confirmed for {{appointment_date}} at {{appointment_time}}. Reply C to confirm or R to reschedule."`

3. Opt-Out Handling:

  • Auto-enabled: STOP/UNSUBSCRIBE keywords automatically handled
  • Display compliance notice about TCPA requirements
  • Checkbox: "I confirm my use of SMS complies with TCPA and carrier guidelines"

4. Sending Number:

  • Auto-populated from Screen 4's forwarding number (same Twilio number)
  • Or: select from available Twilio numbers

Data Model:

```

sms_config: {

id: uuid (PK),

twilio_account_sid_encrypted: text,

twilio_auth_token_encrypted: text,

sending_number: text, // E.164 format

auto_text_template: text,

auto_text_enabled: boolean DEFAULT true,

followup_template: text,

followup_enabled: boolean DEFAULT true,

followup_delay_minutes: integer DEFAULT 30,

appointment_confirmation_template: text,

appointment_confirmation_enabled: boolean DEFAULT true,

tcpa_consent_confirmed: boolean DEFAULT false,

tcpa_consent_confirmed_at: timestamptz,

created_at: timestamptz,

updated_at: timestamptz

}

sms_log: {

id: uuid (PK),

missed_call_id: uuid (FK → missed_calls, nullable),

direction: text, // "outbound" | "inbound"

from_number: text,

to_number: text,

message_body: text,

template_type: text, // "auto_text" | "followup" | "appointment_confirmation" | "reply"

twilio_message_sid: text,

status: text, // "queued" | "sent" | "delivered" | "failed" | "undelivered"

error_code: text,

error_message: text,

sent_at: timestamptz,

delivered_at: timestamptz,

created_at: timestamptz

}

```

Validation Rules:

  • Twilio credentials must pass API validation
  • TCPA consent checkbox must be checked
  • Auto-text template is required if auto_text_enabled is true
  • Templates must contain `{{business_name}}` (compliance)
  • Templates must not exceed 480 characters (3 SMS segments max)
  • Sending number must be a verified Twilio number on the account

---

Screen 8: Calendar Integration (Google Calendar OAuth — Optional)

Purpose: Enable the AI voice agent to check real-time availability and book appointments directly into the contractor's calendar.

UI Layout:

1. Connection:

  • "Connect Google Calendar" OAuth button (Google-branded per guidelines)
  • "Skip for now" link (clearly visible; this screen is optional)
  • Post-connection: shows connected Google account email + ✓
  • "Disconnect" option

2. Calendar Selection:

  • After OAuth: dropdown of available calendars from the connected account
  • User selects which calendar to use for availability + booking
  • Option to create a new "Appointments" calendar

3. Availability Settings:

  • Appointment Duration: dropdown (15 min, 30 min, 45 min, 1 hour, 1.5 hours, 2 hours) — default: 1 hour
  • Buffer Between Appointments: dropdown (0, 15, 30, 45, 60 min) — default: 15 min
  • Max Appointments Per Day: number input (1–20) — default: 8
  • Booking Window: how far out can appointments be booked? (1 day – 30 days) — default: 14 days
  • Available Hours: inherits from Screen 5 routing rules, but can be overridden here

4. Reminder Settings:

  • Toggle: Send SMS reminder before appointment
  • Timing: dropdown (1 hour, 2 hours, 24 hours, 48 hours) — default: 24 hours
  • Template: uses appointment confirmation template from Screen 7

Data Model:

```

calendar_config: {

id: uuid (PK),

provider: text DEFAULT 'google', // Future: "outlook" | "apple"

google_tokens_encrypted: jsonb, // { access_token, refresh_token, expiry }

google_account_email: text,

selected_calendar_id: text, // Google Calendar ID

appointment_duration_minutes: integer DEFAULT 60,

buffer_minutes: integer DEFAULT 15,

max_per_day: integer DEFAULT 8,

booking_window_days: integer DEFAULT 14,

available_hours_override: jsonb, // Overrides routing_rules for booking; null = use routing_rules

/* Example:

{ "monday": {"start": "09:00", "end": "17:00"}, ... }

*/

reminder_enabled: boolean DEFAULT true,

reminder_minutes_before: integer DEFAULT 1440, // 24 hours

is_connected: boolean DEFAULT false,

connected_at: timestamptz,

created_at: timestamptz,

updated_at: timestamptz

}

```

Validation Rules:

  • OAuth flow must complete successfully if user clicks "Connect"
  • At least one calendar must be selected after connection
  • appointment_duration_minutes must be > 0
  • buffer_minutes must be ≥ 0
  • max_per_day must be 1–20
  • booking_window_days must be 1–30
  • Screen can be skipped entirely; `is_connected` remains false

---

Screen 9: CRM Connection (Optional)

Purpose: Sync missed call data, caller info, and appointments to the contractor's existing CRM.

UI Layout:

1. CRM Selection — Radio card group:

  • ServiceTitan
  • Housecall Pro
  • Jobber
  • GoHighLevel
  • HubSpot
  • Salesforce (coming soon — disabled)
  • Generic Webhook (for custom CRMs)
  • "Skip for now"

2. Connection Flow (varies by CRM):

  • OAuth-based (HubSpot, GoHighLevel): OAuth button → authorize → callback
  • API Key-based (ServiceTitan, Housecall Pro, Jobber): API key input + validation
  • Webhook-based (Generic): webhook URL input + secret key generation

3. Field Mapping:

  • Auto-mapped defaults shown in a two-column table:
Command Center FieldCRM Field
Caller NameContact / Customer Name
Caller PhonePhone Number
Service NeededJob Type / Notes
Appointment DateScheduled Date
Call Recording URLAttachment / Note
  • User can adjust mappings via dropdowns
  • "Refresh CRM Fields" button (fetches schema from CRM API)

4. Sync Settings:

  • Sync direction: `push_only` | `bidirectional` (default: push_only)
  • Auto-create contacts: toggle (default: on)
  • Auto-create jobs/opportunities: toggle (default: off — requires confirmation)
  • Sync frequency: `real_time` | `every_5_min` | `every_15_min` (default: real_time)

Data Model:

```

crm_config: {

id: uuid (PK),

provider: text, // "servicetitan" | "housecall_pro" | "jobber" | "gohighlevel" | "hubspot" | "generic_webhook"

connection_type: text, // "oauth" | "api_key" | "webhook"

credentials_encrypted: jsonb, // Provider-specific; encrypted at rest

/* Examples:

OAuth: { access_token, refresh_token, expiry, account_id }

API Key: { api_key, account_id }

Webhook: { webhook_url, secret_key }

*/

field_mapping: jsonb,

/* Example:

[

{ "source": "caller_name", "target": "customer.name", "transform": null },

{ "source": "caller_phone", "target": "customer.phone", "transform": "e164" },

{ "source": "service_needed", "target": "job.description", "transform": null },

...

]

*/

sync_direction: text DEFAULT 'push_only',

auto_create_contacts: boolean DEFAULT true,

auto_create_jobs: boolean DEFAULT false,

sync_frequency: text DEFAULT 'real_time',

is_connected: boolean DEFAULT false,

connected_at: timestamptz,

last_sync_at: timestamptz,

last_sync_status: text, // "success" | "partial" | "failed"

last_sync_error: text,

created_at: timestamptz,

updated_at: timestamptz

}

```

Validation Rules:

  • Credentials must pass provider-specific validation
  • Field mapping must include at minimum: caller phone → CRM phone field
  • Webhook URL must be valid HTTPS URL
  • Screen can be skipped entirely; `is_connected` remains false

---

Screen 10: Test Call (Live Verification)

Purpose: End-to-end verification that the entire system works before going live.

UI Layout:

1. Pre-Flight Checklist — Visual status list:

ComponentStatusAction
Call Forwarding✓ Verified / ⚠ Not VerifiedRe-verify
AI Voice Agent✓ Configured / ✗ Missing API KeyGo to Screen 6
SMS Auto-Text✓ Ready / ✗ Missing CredentialsGo to Screen 7
Calendar✓ Connected / ○ SkippedGo to Screen 8
CRM✓ Connected / ○ SkippedGo to Screen 9

2. Test Call Trigger:

  • "Make a Test Call" button (primary CTA, large)
  • User enters their personal phone number (pre-filled from Screen 1)
  • System initiates an outbound call via Twilio TO the forwarding number, simulating a missed call
  • OR: Instructions for user to call their business number directly

3. Live Status Monitor — Real-time updates during test:

```

⏳ Initiating test call...

✓ Call received by system

✓ Routing rule evaluated → AI Voice

✓ Vapi agent connected

⏳ AI conversation in progress...

✓ Call completed (duration: 1m 23s)

✓ Auto-text sent to [phone]

✓ Voicemail recorded (if applicable)

✓ Appointment created (if calendar connected)

✓ CRM record created (if CRM connected)

```

  • Each step shows real-time via WebSocket subscription
  • Failed steps show error detail + "Retry" or "Fix" link

4. Completion:

  • All checks pass → "Go Live!" button activates the system
  • Some checks fail → "Go Live Anyway" (with warning) or "Fix Issues"
  • Confetti animation on successful completion 🎉

Data Model:

```

test_calls: {

id: uuid (PK),

initiated_at: timestamptz,

test_phone_number: text,

steps_completed: jsonb,

/* Example:

{

"call_received": { "status": "success", "at": "...", "detail": null },

"routing_evaluated": { "status": "success", "at": "...", "detail": "ai_voice" },

"vapi_connected": { "status": "success", "at": "...", "detail": null },

"call_completed": { "status": "success", "at": "...", "detail": { "duration": 83 } },

"sms_sent": { "status": "success", "at": "...", "detail": { "sid": "SM..." } },

"calendar_booked": { "status": "skipped", "at": null, "detail": "not_configured" },

"crm_synced": { "status": "skipped", "at": null, "detail": "not_configured" }

}

*/

overall_status: text, // "success" | "partial" | "failed"

completed_at: timestamptz

}

```

---

Database Migration Script

```sql

-- ============================================================================

-- Recipe 002: Missed Call Command Center

-- Database Migration Script

-- Version: 1.0.0

-- ============================================================================

-- Enable required extensions

CREATE EXTENSION IF NOT EXISTS "pgcrypto";

CREATE EXTENSION IF NOT EXISTS "pgsodium";

-- ============================================================================

-- ENUM TYPES

-- ============================================================================

CREATE TYPE routing_method_enum AS ENUM (

'ai_voice',

'voicemail',

'auto_text_only'

);

CREATE TYPE call_outcome_enum AS ENUM (

'answered_by_ai',

'voicemail_left',

'voicemail_no_message',

'auto_text_sent',

'appointment_booked',

'callback_requested',

'no_action',

'failed'

);

CREATE TYPE appointment_status_enum AS ENUM (

'scheduled',

'confirmed',

'completed',

'cancelled',

'no_show',

'rescheduled'

);

CREATE TYPE appointment_source_enum AS ENUM (

'ai_voice',

'sms_reply',

'manual',

'web_booking'

);

CREATE TYPE crm_sync_status_enum AS ENUM (

'pending',

'synced',

'failed',

'not_applicable'

);

CREATE TYPE sms_direction_enum AS ENUM (

'outbound',

'inbound'

);

CREATE TYPE sms_status_enum AS ENUM (

'queued',

'sent',

'delivered',

'failed',

'undelivered'

);

CREATE TYPE sms_template_type_enum AS ENUM (

'auto_text',

'followup',

'appointment_confirmation',

'appointment_reminder',

'reply'

);

CREATE TYPE rule_type_enum AS ENUM (

'weekly',

'override',

'after_hours_default'

);

-- ============================================================================

-- TABLE: routing_rules

-- Defines time-based call routing rules (weekly schedule, overrides)

-- ============================================================================

CREATE TABLE public.routing_rules (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

rule_type rule_type_enum NOT NULL DEFAULT 'weekly',

day_of_week SMALLINT, -- 0=Sunday ... 6=Saturday

override_date DATE, -- For one-off overrides

start_time TIME, -- NULL for after_hours_default

end_time TIME, -- NULL for after_hours_default

routing_method routing_method_enum NOT NULL,

is_active BOOLEAN NOT NULL DEFAULT true,

priority INTEGER NOT NULL DEFAULT 0, -- Higher wins

created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

-- Weekly rules need day_of_week; overrides need override_date

CONSTRAINT routing_rules_weekly_day CHECK (

rule_type != 'weekly' OR (day_of_week IS NOT NULL AND day_of_week BETWEEN 0 AND 6)

),

CONSTRAINT routing_rules_override_date CHECK (

rule_type != 'override' OR override_date IS NOT NULL

),

CONSTRAINT routing_rules_time_order CHECK (

start_time IS NULL OR end_time IS NULL OR start_time < end_time

)

);

COMMENT ON TABLE public.routing_rules IS 'Time-based call routing rules defining when AI voice, voicemail, or auto-text activates';

-- ============================================================================

-- TABLE: voicemail_greetings

-- Custom voicemail greeting recordings

-- ============================================================================

CREATE TABLE public.voicemail_greetings (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

audio_url TEXT, -- Storage URL

greeting_text TEXT, -- Text-to-speech fallback or transcript

is_default BOOLEAN NOT NULL DEFAULT false,

is_active BOOLEAN NOT NULL DEFAULT true,

created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

COMMENT ON TABLE public.voicemail_greetings IS 'Custom voicemail greeting recordings and text-to-speech configurations';

-- Ensure only one default greeting

CREATE UNIQUE INDEX voicemail_greetings_default_unique

ON public.voicemail_greetings (id)

WHERE is_default = true;

-- ============================================================================

-- TABLE: missed_calls

-- Core table recording every missed call and its outcome

-- ============================================================================

CREATE TABLE public.missed_calls (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

caller_phone TEXT NOT NULL, -- E.164 format

caller_name TEXT, -- From AI extraction or CRM lookup

forwarded_from TEXT, -- The business number that forwarded

routing_method routing_method_enum NOT NULL,

outcome call_outcome_enum NOT NULL DEFAULT 'no_action',

call_duration_seconds INTEGER, -- Total call duration

recording_url TEXT, -- Vapi/Twilio URL or file storage URL

recording_duration_seconds INTEGER,

transcript TEXT, -- Full call transcript (AI calls)

transcript_summary TEXT, -- AI-generated summary

vapi_call_id TEXT, -- Vapi's call identifier

collected_info JSONB, -- Structured data from AI conversation

/* Example:

{

"caller_name": "John Smith",

"service_needed": "AC repair",

"preferred_callback": "Tomorrow morning",

"urgency": "High",

"address": "123 Main St"

}

*/

auto_text_sent BOOLEAN NOT NULL DEFAULT false,

auto_text_sent_at TIMESTAMPTZ,

followup_text_sent BOOLEAN NOT NULL DEFAULT false,

followup_text_sent_at TIMESTAMPTZ,

appointment_id UUID, -- FK added after appointments table

crm_sync_status crm_sync_status_enum NOT NULL DEFAULT 'pending',

crm_record_id TEXT, -- External CRM record identifier

crm_synced_at TIMESTAMPTZ,

is_read BOOLEAN NOT NULL DEFAULT false,

read_at TIMESTAMPTZ,

callback_completed BOOLEAN NOT NULL DEFAULT false,

callback_completed_at TIMESTAMPTZ,

notes TEXT, -- Internal team notes

tags TEXT[], -- User-defined tags

created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

CONSTRAINT missed_calls_caller_e164 CHECK (caller_phone ~ '^\+[1-9]\d{1,14}$'),

CONSTRAINT missed_calls_duration_positive CHECK (

call_duration_seconds IS NULL OR call_duration_seconds >= 0

)

);

COMMENT ON TABLE public.missed_calls IS 'Core record for every missed call processed by the system, including routing, AI interaction, and follow-up status';

-- ============================================================================

-- TABLE: appointments

-- Appointments booked via AI, SMS, or manually from missed calls

-- ============================================================================

CREATE TABLE public.appointments (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

missed_call_id UUID REFERENCES public.missed_calls(id) ON DELETE SET NULL,

calendar_event_id TEXT, -- Google Calendar event ID

customer_name TEXT NOT NULL,

customer_phone TEXT NOT NULL,

customer_email TEXT,

service_type TEXT, -- What service is needed

scheduled_at TIMESTAMPTZ NOT NULL,

duration_minutes INTEGER NOT NULL DEFAULT 60,

status appointment_status_enum NOT NULL DEFAULT 'scheduled',

source appointment_source_enum NOT NULL,

location TEXT, -- Service address

notes TEXT,

reminder_sent BOOLEAN NOT NULL DEFAULT false,

reminder_sent_at TIMESTAMPTZ,

confirmed_at TIMESTAMPTZ,

cancelled_at TIMESTAMPTZ,

cancellation_reason TEXT,

crm_sync_status crm_sync_status_enum NOT NULL DEFAULT 'pending',

crm_record_id TEXT,

created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

CONSTRAINT appointments_customer_e164 CHECK (customer_phone ~ '^\+[1-9]\d{1,14}$'),

CONSTRAINT appointments_duration_positive CHECK (duration_minutes > 0),

CONSTRAINT appointments_schedule_future CHECK (scheduled_at > '2024-01-01'::timestamptz)

);

COMMENT ON TABLE public.appointments IS 'Appointments booked through the missed call system via AI, SMS, or manual entry';

-- Add FK from missed_calls to appointments

ALTER TABLE public.missed_calls

ADD CONSTRAINT missed_calls_appointment_fk

FOREIGN KEY (appointment_id) REFERENCES public.appointments(id) ON DELETE SET NULL;

-- ============================================================================

-- TABLE: sms_log

-- Log of all SMS messages sent and received

-- ============================================================================

CREATE TABLE public.sms_log (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

missed_call_id UUID REFERENCES public.missed_calls(id) ON DELETE SET NULL,

appointment_id UUID REFERENCES public.appointments(id) ON DELETE SET NULL,

direction sms_direction_enum NOT NULL,

from_number TEXT NOT NULL,

to_number TEXT NOT NULL,

message_body TEXT NOT NULL,

template_type sms_template_type_enum,

twilio_message_sid TEXT,

status sms_status_enum NOT NULL DEFAULT 'queued',

error_code TEXT,

error_message TEXT,

sent_at TIMESTAMPTZ,

delivered_at TIMESTAMPTZ,

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

COMMENT ON TABLE public.sms_log IS 'Complete log of all SMS messages sent and received through the system';

-- ============================================================================

-- TABLE: test_calls

-- Records from Screen 10 onboarding test calls

-- ============================================================================

CREATE TABLE public.test_calls (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

test_phone_number TEXT NOT NULL,

steps_completed JSONB NOT NULL DEFAULT '{}'::jsonb,

overall_status TEXT NOT NULL DEFAULT 'pending',

initiated_at TIMESTAMPTZ NOT NULL DEFAULT now(),

completed_at TIMESTAMPTZ

);

COMMENT ON TABLE public.test_calls IS 'Onboarding test call records for verifying end-to-end system functionality';

-- ============================================================================

-- TABLE: call_recordings

-- Metadata for stored call recordings

-- ============================================================================

CREATE TABLE public.call_recordings (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

missed_call_id UUID NOT NULL REFERENCES public.missed_calls(id) ON DELETE CASCADE,

storage_path TEXT NOT NULL, -- File storage path

storage_bucket TEXT NOT NULL DEFAULT 'call-recordings',

file_size_bytes BIGINT,

duration_seconds INTEGER,

mime_type TEXT NOT NULL DEFAULT 'audio/wav',

source TEXT NOT NULL DEFAULT 'vapi', -- "vapi" | "twilio" | "voicemail"

retention_expires_at TIMESTAMPTZ, -- For automated cleanup

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

COMMENT ON TABLE public.call_recordings IS 'Metadata for call recordings stored in file storage with retention tracking';

-- ============================================================================

-- TABLE: sms_opt_outs

-- TCPA compliance: track opted-out phone numbers

-- ============================================================================

CREATE TABLE public.sms_opt_outs (

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

phone_number TEXT NOT NULL UNIQUE,

opted_out_at TIMESTAMPTZ NOT NULL DEFAULT now(),

opt_out_keyword TEXT, -- "STOP", "UNSUBSCRIBE", etc.

opted_back_in_at TIMESTAMPTZ,

CONSTRAINT sms_opt_outs_phone_e164 CHECK (phone_number ~ '^\+[1-9]\d{1,14}$')

);

COMMENT ON TABLE public.sms_opt_outs IS 'TCPA compliance tracking for SMS opt-outs';

-- ============================================================================

-- INDEXES

-- ============================================================================

-- missed_calls: Primary query patterns

CREATE INDEX idx_missed_calls_created

ON public.missed_calls (created_at DESC);

CREATE INDEX idx_missed_calls_unread

ON public.missed_calls (is_read, created_at DESC)

WHERE is_read = false;

CREATE INDEX idx_missed_calls_outcome

ON public.missed_calls (outcome);

CREATE INDEX idx_missed_calls_caller_phone

ON public.missed_calls (caller_phone);

CREATE INDEX idx_missed_calls_vapi_call_id

ON public.missed_calls (vapi_call_id)

WHERE vapi_call_id IS NOT NULL;

CREATE INDEX idx_missed_calls_crm_sync

ON public.missed_calls (crm_sync_status)

WHERE crm_sync_status = 'pending';

CREATE INDEX idx_missed_calls_callback_pending

ON public.missed_calls (callback_completed, created_at DESC)

WHERE callback_completed = false;

-- routing_rules: Lookup by day/type

CREATE INDEX idx_routing_rules_weekly

ON public.routing_rules (day_of_week, start_time, end_time)

WHERE rule_type = 'weekly' AND is_active = true;

CREATE INDEX idx_routing_rules_override

ON public.routing_rules (override_date)

WHERE rule_type = 'override' AND is_active = true;

-- appointments: Primary query patterns

CREATE INDEX idx_appointments_scheduled

ON public.appointments (scheduled_at)

WHERE status IN ('scheduled', 'confirmed');

CREATE INDEX idx_appointments_status

ON public.appointments (status, scheduled_at);

CREATE INDEX idx_appointments_customer_phone

ON public.appointments (customer_phone);

CREATE INDEX idx_appointments_calendar_event

ON public.appointments (calendar_event_id)

WHERE calendar_event_id IS NOT NULL;

CREATE INDEX idx_appointments_reminder_due

ON public.appointments (scheduled_at)

WHERE reminder_sent = false AND status IN ('scheduled', 'confirmed');

-- sms_log: Lookup patterns

CREATE INDEX idx_sms_log_created

ON public.sms_log (created_at DESC);

CREATE INDEX idx_sms_log_missed_call

ON public.sms_log (missed_call_id)

WHERE missed_call_id IS NOT NULL;

CREATE INDEX idx_sms_log_twilio_sid

ON public.sms_log (twilio_message_sid)

WHERE twilio_message_sid IS NOT NULL;

-- call_recordings: Retention cleanup

CREATE INDEX idx_call_recordings_retention

ON public.call_recordings (retention_expires_at)

WHERE retention_expires_at IS NOT NULL;

CREATE INDEX idx_call_recordings_missed_call

ON public.call_recordings (missed_call_id);

-- sms_opt_outs: Quick lookup before sending

CREATE INDEX idx_sms_opt_outs_lookup

ON public.sms_opt_outs (phone_number)

WHERE opted_back_in_at IS NULL;

-- ============================================================================

-- UPDATED_AT TRIGGER FUNCTION

-- ============================================================================

CREATE OR REPLACE FUNCTION public.handle_updated_at()

RETURNS TRIGGER AS $$

BEGIN

NEW.updated_at = now();

RETURN NEW;

END;

$$ LANGUAGE plpgsql;

-- Apply updated_at triggers

CREATE TRIGGER set_updated_at BEFORE UPDATE ON public.routing_rules

FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();

CREATE TRIGGER set_updated_at BEFORE UPDATE ON public.voicemail_greetings

FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();

CREATE TRIGGER set_updated_at BEFORE UPDATE ON public.missed_calls

FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();

CREATE TRIGGER set_updated_at BEFORE UPDATE ON public.appointments

FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();

-- NOTE: Access control should be enforced at the application layer

RolesCSRDispatcherOwner
PrinciplesSpeed Wins the JobCapture What's Already Happening