← Back to Dashboard
Team-Connect Architecture

Credit & Notification System Map

How SMS, email, minutes, and credits flow through every file

The Big Picture: A call comes in via Twilio → server.js bridges it to Deepgram → AI talks to customer → AI calls functions like save_electrician_job → the flow file (electrician.js) saves to Firebase, sends SMS & email, deducts SMS/email credits → call ends → server.js finaliseCall() saves duration & transcript → separately, Twilio's phone-number-level Status Callback fires twilioStatusCallback which deducts call minutes.

CALL LIFECYCLE

📞 Twilio Call
server.js
Deepgram Agent
AI Function Call
function-handlers.js
routes to
electrician.js
(or udriveme.js, florist.js etc)
Flow saves to Firebase
Send SMS + Email
Deduct credits
Call ends (stream stop event)
server.js finaliseCall()
saves duration/transcript to call doc (no deduction)
Separately, Twilio fires phone-number-level Status Callback
Twilio number-level callback
twilioStatusCallback()
Deduct minutes
Bridge / Router
Business Flow
Function Routing
Notifications
Credit Deduction
Twilio / External

Key Rule: Check → Send → Deduct

1. CHECK
Call checkAndConsumeSMSCredit() or checkAndConsumeEmailAllowance()
Reads users/{uid}.features.availableSMSCredits — returns {allowed: true/false}
Does NOT deduct yet.
2. SEND
If allowed → send via twilio.messages.create() or sgMail.send()
If send FAILS → credit stays intact (nothing lost)
3. DEDUCT
Only after successful send → call deductSMSCredit() or deductEmailCredit()
Decrements features.availableSMSCredits by segment count
4. TRACK STATS
Increment phoneNumbers/{id}.stats.totalSMSSent / totalEmailsSent (all-time counters)
Increment phoneNumbers/{id}/monthlyStats/{YYYY-MM}.smsSent / emailsSent (monthly breakdown)
These are display counters on the phone number doc, separate from credits

SMS: Who Gets What

📱 SMS to Business Owner

Send to:

users/{uid}/phoneNumbers/{phoneNumId}.transferNumber

Send from:

users/{uid}/phoneNumbers/{phoneNumId}.aiSMSNumber

Sent for: jobs, quotes, emergencies, callbacks, enquiries, messages

📱 SMS to Customer (caller)

Send to:

jobDoc.customerPhone // captured from call

Send from:

users/{uid}/phoneNumbers/{phoneNumId}.aiSMSNumber

Sent for: job confirmations, emergency acknowledgements, quote confirmations

SMS Credit Lifecycle

calculateSMSSegments(body)
counts chars → returns segment count (1, 2, 3...)
checkAndConsumeSMSCredit(userId, reason, segments)
reads credits, returns {allowed}
if allowed=true
twilio.messages.create({to, from, body})
actual SMS send
if send succeeds
deductSMSCredit(userId, reason, segments)
decrements credits
increment stats.totalSMSSent on phoneNumber doc
increment monthlyStats/{YYYY-MM}.smsSent
monthly breakdown for dashboard
Segment Counting: A standard SMS is 160 chars. Longer messages split into 153-char segments. Unicode (emojis) drops to 70/67 chars. Each segment = 1 SMS credit. The calculateSMSSegments() function handles this.

Firebase Paths — SMS

// Credits (deducted per SMS segment, main credit pool)
users/{uid}.features.availableSMSCredits increment(-segments)
users/{uid}.features.lastSMSUsageAt serverTimestamp()
users/{uid}.stats.totalSMSCreditsUsed increment(+segments)
users/{uid}.stats.lastSMSUsageReason "electrician_job_customer_sms"

// Display stats (tracked per phone number — all-time)
users/{uid}/phoneNumbers/{phoneNumId}.stats.totalSMSSent increment(+segments)
users/{uid}/phoneNumbers/{phoneNumId}.stats.lastSMSSentAt serverTimestamp()

// Daily stats (subcollection — per day breakdown)
users/{uid}/phoneNumbers/{phoneNumId}/dailyStats/{YYYY-MM-DD}.smsSent increment(+segments)

// Monthly stats (subcollection — per month breakdown)
users/{uid}/phoneNumbers/{phoneNumId}/monthlyStats/{MM-YYYY}.smsSent increment(+segments)

// SMS History (NEW — individual message log)
users/{uid}/phoneNumbers/{phoneNumId}/smsHistory/{autoId} → see History Status tab

Email: Business Owner Only

📧 Email to Business

Send to:

users/{uid}/phoneNumbers/{phoneNumId}.businessEmail

Send from:

notifications@team-connect.co.uk // via SendGrid

Professional HTML email with job details, customer info, pricing, timestamps. Different colours per type (red=emergency, blue=quote, amber=job).

Email Credit Lifecycle

checkAndConsumeEmailAllowance(userId, reason)
reads allowance, returns {allowed}
if allowed=true
sgMail.send({to, from, subject, html})
SendGrid sends HTML email
if send succeeds
deductEmailCredit(userId, reason)
decrements -1
increment stats.totalEmailsSent on phoneNumber doc

Firebase Paths — Email

// Credits (deducted 1 per email, main email pool)
users/{uid}.features.availableEmailAllowance increment(-1)
users/{uid}.features.lastEmailUsageAt serverTimestamp()
users/{uid}.stats.totalEmailsUsed increment(+1)
users/{uid}.stats.lastEmailUsageReason "electrician_job_business_email"

// Display stats (tracked per phone number — all-time)
users/{uid}/phoneNumbers/{phoneNumId}.stats.totalEmailsSent increment(+1)
users/{uid}/phoneNumbers/{phoneNumId}.stats.lastEmailSentAt serverTimestamp()

// Daily stats (subcollection — per day breakdown)
users/{uid}/phoneNumbers/{phoneNumId}/dailyStats/{YYYY-MM-DD}.emailsSent increment(+1)

// Monthly stats (subcollection — per month breakdown)
users/{uid}/phoneNumbers/{phoneNumId}/monthlyStats/{MM-YYYY}.emailsSent increment(+1)

// Email History (NEW — individual email log)
users/{uid}/phoneNumbers/{phoneNumId}/emailHistory/{autoId} → see History Status tab

Call Minutes: Two Types

🤖 AI Minutes

Time the AI agent is talking to the caller (before any transfer).

users/{uid}.availableAIMinutes increment(-aiMinutes)

Calculated from call start → transfer bridge time (or call end if no transfer).

📞 Normal Minutes

Total call time including any transfer to a human.

users/{uid}.availableMinutes increment(-totalMinutes)

Uses Twilio's actual CallDuration for accuracy.

Important: Minutes are NOT deducted inside server.js finaliseCall(). That only saves the duration split (AI vs transfer) to the call doc. The actual deduction is triggered by the Twilio phone-number-level Status Callback URL (set in the Twilio console under each number's Voice Configuration → Call Status Changes), which POSTs to twilioStatusCallback on every call end — regardless of TwiML or call type (voice_agent, direct_divert, standard AI). This uses Twilio's real CallDuration to avoid double-charging.
Note: This is separate from statusCallbackEvent attributes on <Number> elements in TwiML, which are an additional layer used only in <Dial> transfer scenarios. The number-level callback fires for ALL calls. The TwiML-level callbacks only fire for direct_divert and standard AI transfer calls.

Minutes Flow

Twilio call ends
Number-level Status Callback fires
Configured in Twilio console per phone number (not in TwiML)
twilio-handlers.js → twilioStatusCallback()
updateCallWithDuration()
reads call doc for AI vs transfer split
deductMinutesFromUserAccount()
decrements both minute pools
checkLowBalanceAndAlert()
warns if running low

Where the Duration Split is Saved

// Saved by server.js finaliseCall()
users/{uid}/phoneNumbers/{phoneNumId}/calls/{callSid}.duration // total seconds
users/{uid}/phoneNumbers/{phoneNumId}/calls/{callSid}.aiDuration // AI seconds only
users/{uid}/phoneNumbers/{phoneNumId}/calls/{callSid}.transferDuration // transfer seconds
users/{uid}/phoneNumbers/{phoneNumId}/calls/{callSid}.endedBy // "ai" or "caller"
users/{uid}/phoneNumbers/{phoneNumId}/calls/{callSid}.flowCompleted // true if AI finished naturally
What is History Tracking? Each SMS/email sent should save a full record to smsHistory / emailHistory subcollections. This lets you see exactly what was sent, when, to whom, with Twilio SID for delivery tracking.

📊 Implementation Status by File

Flow File SMS Sends smsHistory Email Sends emailHistory monthlyStats
vet-flow.js 2 1
nimble-tek.js 2 1
sushi-grill.js 5 2
take-out.js 5 2
team-connect.js 2 0
electrician.js ? 🔍 ? 🔍 🔍
plumber.js ? 🔍 ? 🔍 🔍
✅ Implemented
❌ Missing
🔍 Not checked yet
— Not applicable

📜 smsHistory Document Structure

// Path: users/{uid}/phoneNumbers/{phoneNumId}/smsHistory/{autoId}

direction: "outbound"
type: "customer_confirmation" | "business_notification"
to: "+44..." // recipient phone
from: "+44..." // aiSMSNumber
body: "Full SMS text content..."
segments: 2 // SMS segment count
twilioSid: "SM123abc..." // for delivery tracking
status: "sent"
relatedId: "JOB-abc123" // job/appointment/enquiry ID
relatedType: "job" | "appointment" | "enquiry"
customerName: "John Smith"
reason: "vet_owner_sms" // for credit tracking
dayKey: "2025-02-26" // YYYY-MM-DD for daily queries
monthKey: "02-2025" // MM-YYYY for monthly queries
sentAt: serverTimestamp()
createdAt: serverTimestamp()

📧 emailHistory Document Structure

// Path: users/{uid}/phoneNumbers/{phoneNumId}/emailHistory/{autoId}

direction: "outbound"
type: "business_notification"
to: "business@email.com"
from: "notifications@team-connect.co.uk"
fromName: "Practice Name Appointments"
subject: "New Appointment — Pet (Owner) — APT-123"
htmlBody: "<full HTML email content>"
status: "sent"
relatedId: "APT-abc123"
relatedType: "appointment"
customerName: "John Smith"
customerPhone: "+44..."
reason: "vet_practice_email"
dayKey: "2025-02-26"
monthKey: "02-2025"
sentAt: serverTimestamp()
createdAt: serverTimestamp()

📅 Stats Collections Summary

📊 dailyStats/{YYYY-MM-DD}

smsSent: increment(+segments)
emailsSent: increment(+1)
updatedAt: serverTimestamp()

📆 monthlyStats/{MM-YYYY}

smsSent: increment(+segments)
emailsSent: increment(+1)
updatedAt: serverTimestamp()
To-Do: Add smsHistory + emailHistory + monthlyStats to all remaining flow files: nimble-tek.js, sushi-grill.js, take-out.js, team-connect.js, electrician.js, plumber.js, and any others.

server.js

The Bridge — connects Twilio ↔ Deepgram ↔ AI
// All-time display stats (per phone number)
stats.totalSMSSent // total SMS segments sent from this number
stats.totalEmailsSent // total emails sent for this number
stats.lastSMSSentAt
stats.lastEmailSentAt

// AI & call handling config
aiEnabled // boolean — is AI answering active on this number?
basicMode // boolean — simplified AI mode (no custom flow)
allowTransfer // boolean — can AI transfer calls?
desktopEnabled // boolean — can this number ring in desktop app?
transferNumber // string — business owner's mobile/landline e.g. "+447446695686"
aiSMSNumber // string — Twilio number used as SMS "from" e.g. "+447378401590"

// Business identity
businessName // string — e.g. "Building Group"
businessType // string — flow slug e.g. "general-builder", "electrician"
businessEmail // string — e.g. "info@team-connect.co.uk"
businessAddress // map — structured address object
  .street // string — e.g. "7 chelford road"
  .city // string — e.g. "handforth"
  .postcode // string — e.g. "SK9 3SQ"
  .country // string — e.g. "United Kingdom"
🔧

function-handlers.js

The Router — maps AI function names to handlers
  • save_electrician_jobflow.handleSaveOrder()
  • save_enquiryflow.handleSave()
  • save_udriveme_enquiryflow.handleSaveOrder()
  • transfer_call → handled locally → returns pending_transfer
  • end_call → handled locally → returns end_call: true
  • check_returning_customer → auto-routed via dynamic handler name
  • → Falls back to genericSaveEnquiry() if no flow handler exists
  • Does NOT send SMS/email (that's the flow file's job)
Dynamic routing: For unknown function names, it converts check_returning_customerhandleCheckReturningCustomer and checks if the flow exports that method.

electrician.js

The Flow — business logic, prompts, saves, notifications
  • getFullPrompt() — builds the entire AI system prompt
  • getFunctions() — defines what tools the AI can call
  • handleSaveOrder() — saves jobs/quotes/emergencies, marks flowCompleted, triggers hangup
  • handleSave() — saves enquiries/callbacks/messages
  • sendJobNotifications() — SMS to business + SMS to customer + email to business
  • sendEnquiryNotifications() — SMS + email for callbacks/enquiries
  • checkAndConsumeSMSCredit() — checks credits (no deduction)
  • deductSMSCredit() — deducts after successful send
  • checkAndConsumeEmailAllowance() — checks email allowance
  • deductEmailCredit() — deducts after successful send
  • handleCheckReturningCustomer() — recognises repeat callers
  • saveCustomerForRecognition() — stores caller for next time
📞

twilio-handlers.js

Cloud Functions — webhooks, minute deduction, status callbacks
  • twilioVoiceWebhook — initial call setup, routes to Deepgram bridge
  • twilioStatusCallback — fires when call ends via Twilio phone-number-level Status Callback URL (fires for ALL call types: voice_agent, direct_divert, standard AI)
  • updateCallWithDuration() — saves final duration to call doc
  • deductMinutesFromUserAccount()THIS deducts AI + normal minutes
  • checkLowBalanceAndAlert() — warns user if minutes running low
  • switchAllPhonesToDivert() — auto-diverts if minutes hit 0
  • smsStatusCallback — tracks SMS delivery status from Twilio
  • → Imports ALL flow files for legacy TwiML-based call handling
  • processCompletedCall()⚠️ DEAD CODE — defined but never called
  • updateCallWithDurationEnhanced()⚠️ DEAD CODE — defined but never called
Single source of truth for minutes. Never deduct minutes anywhere else. twilioStatusCallback is triggered by the Twilio phone-number-level Status Callback URL (set in Twilio console, not TwiML), so it fires for all call types including voice_agent. Uses Twilio's actual CallDuration field which is always accurate.

All Firebase Paths at a Glance

💳

User Credits (top-level)

users/{uid}
// SMS credits
features.availableSMSCredits // decremented per segment sent

// Email credits
features.availableEmailAllowance // decremented per email sent

// AI minutes
availableAIMinutes // decremented per call (AI portion)

// Normal minutes
availableMinutes // decremented per call (total duration)

// Usage stats
stats.totalSMSCreditsUsed // running total of SMS segments used
stats.totalEmailsUsed // running total of emails used
📊

Phone Number Stats

users/{uid}/phoneNumbers/{phoneNumId}
// All-time display stats (per phone number)
stats.totalSMSSent // total SMS segments sent from this number
stats.totalEmailsSent // total emails sent for this number
stats.lastSMSSentAt
stats.lastEmailSentAt

// AI & call handling config
aiEnabled // boolean — is AI answering active on this number?
basicMode // boolean — simplified AI mode (no custom flow)
allowTransfer // boolean — can AI transfer calls?
desktopEnabled // boolean — can this number ring in desktop app?
transferNumber // string — business owner's mobile/landline e.g. "+447446695686"
aiSMSNumber // string — Twilio number used as SMS "from" e.g. "+447378401590"

// Business identity
businessName // string — e.g. "Building Group"
businessType // string — flow slug e.g. "general-builder", "electrician"
businessEmail // string — e.g. "info@building-group.co.uk"
businessAddress // map — structured address object
  .street // string — e.g. "7 chelford road"
  .city // string — e.g. "handforth"
  .postcode // string — e.g. "SK9 3SQ"
  .country // string — e.g. "United Kingdom"
📅

Monthly Stats (subcollection)

users/{uid}/phoneNumbers/{phoneNumId}/monthlyStats/{YYYY-MM}
// Monthly breakdown — one doc per month (e.g. "2026-02")
smsSent // SMS segments sent this month
emailsSent // emails sent this month
updatedAt // serverTimestamp() — last write
Why subcollection? One doc per month keeps reads fast — admin dashboard can query a single doc for "This Month" stats instead of aggregating all-time data. Docs auto-create on first write via .set({ merge: true }).
📂

Subcollections

users/{uid}/phoneNumbers/{phoneNumId}/...
// Electrician saves to these:
/jobs/{jobId} // confirmed work
/quotes/{jobId} // quote requests
/emergency/{jobId} // emergency callouts
/enquiries/{enquiryId} // general enquiries
/call_backs/{enquiryId} // callback requests
/messages/{enquiryId} // messages left
/customers/{cleanPhone} // returning customer recognition
/calls/{callSid} // call logs with duration/transcript
Three levels of tracking:
Credits live on users/{uid}.features.* — these are the "balance" that gets decremented
All-time stats live on users/{uid}/phoneNumbers/{id}.stats.* — display counters per number
Monthly stats live on users/{uid}/phoneNumbers/{id}/monthlyStats/{YYYY-MM} — per-month breakdown for dashboards

Feb 2026 Migration — Segments, Emojis & monthlyStats

Three changes applied across every flow file:
1. SMS stats now track by segment count (not always 1) — fixes billing discrepancy
2. All emojis removed from SMS/email bodies — prevents Unicode inflation (70 vs 160 char segments)
3. New monthlyStats subcollection writes added alongside all-time phone number stats
🐛

Bug Fix: SMS Segment Counting

stats.totalSMSSent was always increment(1) — now increment(segments)
// BEFORE — always counted 1 credit regardless of message length
stats.totalSMSSent: increment(1) // 320-char SMS = 3 segments but only counted 1

// AFTER — counts actual Twilio segments
stats.totalSMSSent: increment(segments) // 320-char SMS = 3 segments, correctly counted
Revenue impact: Multi-segment SMS (over 160 chars GSM / 70 chars Unicode) were only deducting 1 credit from phone number stats. The user-level credit deduction was already correct via deductSMSCredit(), but the phone number stats were under-reporting.
✂️

Emoji Removal from SMS/Email Bodies

Emojis force Unicode encoding — 70 chars/segment instead of 160

GSM (no emojis)

  • Single segment: up to 160 chars
  • Multi-segment: 153 chars each
  • A 300-char SMS = 2 segments

Unicode (with emojis)

  • Single segment: up to 70 chars
  • Multi-segment: 67 chars each
  • A 300-char SMS = 5 segments (!)
Cost saving: Removing a single emoji from a 300-char SMS can reduce it from 5 segments to 2 segments — saving 3 credits per message.
📅

New: monthlyStats Subcollection

Per-month SMS and email counters for dashboard display
// New subcollection — one doc per month
users/{uid}/phoneNumbers/{phoneNumId}/monthlyStats/2026-02
  smsSent: increment(segments) // SMS segments this month
  emailsSent: increment(1) // emails this month
  updatedAt: serverTimestamp()
// Code pattern (every flow file, every stat site)
const monthKey = new Date().toISOString().slice(0, 7);
const phoneRef = db.collection('users').doc(userId)
  .collection('phoneNumbers').doc(phoneNumberId);

// All-time stat (unchanged)
phoneRef.set({ stats: { totalSMSSent: increment(segments) } }, { merge: true });

// Monthly stat (NEW)
phoneRef.collection('monthlyStats').doc(monthKey)
  .set({ smsSent: increment(segments), updatedAt: serverTimestamp() }, { merge: true });

File-by-File Migration Status

Flow File monthKey monthlyStats increment fix emoji fix Status
Migration complete: All 24 flow files now have segment-based SMS stat tracking, monthlyStats subcollection writes, and no emojis in SMS/email bodies. Fire-and-forget pattern preserved (no await on stat writes).

Deployment Notes

  • → Monitor console for [ERR] Monthly SMS stat: / [ERR] Monthly email stat: errors
  • → Admin dashboard needs update to read monthlyStats/{YYYY-MM} for "This Month" display
  • → Existing all-time totals (stats.totalSMSSent) remain accurate and unchanged
  • → monthlyStats docs auto-create via .set({ merge: true }) — no migration needed for existing data
  • → Historical months won't have monthlyStats — only new activity going forward