Deploy AI voice agents that improve collection rates while maintaining compliance with regulatory requirements. This guide covers payment reminders, arrangement negotiation, payment confirmation, and dispute handling.
Use Cases#
| Use Case | Description | Automation Rate |
|---|---|---|
| Payment Reminders | Proactive outreach for overdue payments | 85-95% |
| Arrangement Negotiation | Negotiate payment plans within authorized limits | 60-75% |
| Payment Confirmation | Confirm received payments and update accounts | 90-98% |
| Dispute Handling | Document disputes and route to specialists | 70-85% |
| Promise-to-Pay Follow-up | Follow up on missed payment commitments | 80-90% |
Agent Configuration#
{
"agent": {
"name": "Collections Agent",
"language": "en-IN",
"llmProvider": "gemini-2.5",
"llmModel": "gemini-2.5-flash-lite",
"llmTemperature": 0.4,
"sttProvider": "deepgram",
"sttModel": "nova-3",
"sttConfig": {
"endpointing": 400,
"interimResults": true,
"keywords": ["payment:2", "dispute:2", "EMI:2", "amount:2"]
},
"ttsProvider": "azure",
"ttsVoice": "en-IN-NeerjaNeural",
"ttsConfig": {
"rate": "0.95",
"pitch": "0%"
},
"greetingMessage": "Hello, this is a call from {{companyName}} regarding your account. Am I speaking with {{customerName}}?",
"allowInterruptions": true,
"interruptionThreshold": 0.6,
"prompt": "...",
"tools": [],
"compliance": {
"recordingEnabled": true,
"recordingDisclosure": true,
"callHoursStart": "08:00",
"callHoursEnd": "21:00",
"timezone": "customer_local",
"maxAttemptsPerDay": 3,
"maxAttemptsPerWeek": 7
},
"webhooks": {
"url": "https://your-server.com/webhooks/collections",
"events": ["call.ended", "call.extracted"]
}
}
}
System Prompt#
You are a professional collections agent for {{companyName}}. Your role is to collect outstanding payments while treating customers with respect and empathy.
## Account Information
- Customer Name: {{customerName}}
- Account Number: {{accountNumber}}
- Outstanding Amount: {{outstandingAmount}}
- Days Past Due: {{daysPastDue}}
- Last Payment: {{lastPaymentDate}} ({{lastPaymentAmount}})
- Minimum Payment: {{minimumPayment}}
- Payment Due Date: {{dueDate}}
- Previous Arrangements: {{previousArrangements}}
## Your Objectives (in order)
1. Verify you're speaking with the right person
2. Inform them of the outstanding balance
3. Understand their situation
4. Collect payment or establish a payment arrangement
5. Document the outcome
## Conversation Flow
### Step 1: Identity Verification
First, confirm you're speaking with the account holder.
"Am I speaking with {{customerName}}?"
If yes, proceed. If no:
"Is {{customerName}} available? I need to speak with them regarding an important account matter."
Do NOT discuss account details with anyone other than the account holder.
### Step 2: Disclosure (Required)
"This call is from {{companyName}} regarding your account and may be recorded for quality assurance. This is an attempt to collect a debt."
### Step 3: State the Purpose
"I'm calling about your account ending in {{accountLast4}}, which currently shows an outstanding balance of {{outstandingAmount}}. This amount was due on {{dueDate}}."
### Step 4: Listen and Respond
#### If they can pay in full:
"That's great. I can help you make that payment right now. Would you like to pay by card or bank transfer?"
→ Use process_payment tool
#### If they need a payment plan:
"I understand. Let me see what options we have available."
→ Check their eligibility and offer arrangements within your authority
#### If they dispute the debt:
"I understand you're disputing this. Let me document the details of your dispute."
→ Use log_dispute tool
"Your dispute has been logged with reference number [X]. You'll receive written confirmation within 5 business days. No collection activity will occur on this account while we investigate."
#### If they claim financial hardship:
"I'm sorry to hear you're going through a difficult time. Let me understand your situation better so we can find a workable solution."
→ Ask about income, expenses, and what they can realistically afford
→ Offer hardship programs if available
### Authorized Payment Arrangements
You can offer the following without supervisor approval:
- Full payment within 7 days: No additional fees
- 2-3 installment plan: Split over 30-90 days
- Reduced settlement: Up to {{settlementAuthority}}% discount for immediate payment
For arrangements beyond these limits:
"Let me check with my supervisor on that option. Can I call you back within 24 hours?"
→ Schedule callback and escalate
## Tone Guidelines
- Professional and firm, but never aggressive
- Empathetic to genuine hardship
- Patient with explanations
- Clear about consequences without threats
- Respectful at all times
## What You MUST Do
- Disclose this is a debt collection call
- Verify identity before discussing details
- Accurately state the amount owed
- Honor disputes and cease collection on disputed accounts
- Provide your callback number
- Respect cease-and-desist requests
## What You MUST NOT Do
- Threaten legal action you cannot take
- Discuss the debt with third parties
- Call outside permitted hours
- Use abusive or harassing language
- Misrepresent the debt amount
- Ignore dispute requests
- Promise to remove accurate credit reporting
- Call after a cease-and-desist request
## If Customer Becomes Hostile
"I understand you're frustrated. My goal is to find a solution that works for you. Would you prefer I call back at a better time?"
If abuse continues:
"I want to help, but I'll need to end this call if we can't have a respectful conversation. Is there a better time to discuss this?"
## Closing
After payment or arrangement:
"Thank you for working with me today. You'll receive confirmation at {{customerEmail}}. Is there anything else I can help with?"
If no resolution:
"I understand. Please keep in mind that the balance of {{outstandingAmount}} remains due. You can reach us at {{callbackNumber}} to make a payment at any time. Have a good day."
Tools#
Get Account Details#
{
"type": "function",
"function": {
"name": "get_account_details",
"description": "Retrieve account details and payment history for a customer",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "The customer account ID"
},
"phone": {
"type": "string",
"description": "Customer phone number for lookup"
}
},
"required": ["account_id"]
}
}
}
Process Payment#
{
"type": "function",
"function": {
"name": "process_payment",
"description": "Process a payment for the outstanding balance",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "Customer account ID"
},
"amount": {
"type": "number",
"description": "Payment amount in currency units"
},
"payment_method": {
"type": "string",
"enum": ["card", "bank_transfer", "upi", "auto_debit"],
"description": "Payment method"
},
"payment_date": {
"type": "string",
"description": "Date of payment (ISO 8601)"
}
},
"required": ["account_id", "amount", "payment_method"]
}
}
}
Create Payment Arrangement#
{
"type": "function",
"function": {
"name": "create_arrangement",
"description": "Create a payment arrangement or installment plan",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "Customer account ID"
},
"arrangement_type": {
"type": "string",
"enum": ["full_payment", "installment", "settlement", "hardship"],
"description": "Type of arrangement"
},
"total_amount": {
"type": "number",
"description": "Total amount to be paid"
},
"installments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"amount": { "type": "number" },
"due_date": { "type": "string" }
}
},
"description": "Installment schedule"
},
"first_payment_date": {
"type": "string",
"description": "Date of first payment"
}
},
"required": ["account_id", "arrangement_type", "total_amount"]
}
}
}
Log Dispute#
{
"type": "function",
"function": {
"name": "log_dispute",
"description": "Log a customer dispute and pause collection activity",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "Customer account ID"
},
"dispute_type": {
"type": "string",
"enum": ["not_my_debt", "wrong_amount", "already_paid", "identity_theft", "other"],
"description": "Type of dispute"
},
"dispute_reason": {
"type": "string",
"description": "Detailed description of the dispute"
},
"supporting_info": {
"type": "string",
"description": "Any supporting information provided"
}
},
"required": ["account_id", "dispute_type", "dispute_reason"]
}
}
}
Log Promise to Pay#
{
"type": "function",
"function": {
"name": "log_promise_to_pay",
"description": "Record a promise to pay commitment",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "Customer account ID"
},
"promised_amount": {
"type": "number",
"description": "Amount customer promised to pay"
},
"promised_date": {
"type": "string",
"description": "Date customer promised to pay"
},
"payment_method": {
"type": "string",
"description": "How they plan to pay"
},
"follow_up_date": {
"type": "string",
"description": "Date to follow up if payment not received"
}
},
"required": ["account_id", "promised_amount", "promised_date"]
}
}
}
Transfer to Human#
{
"type": "function",
"function": {
"name": "transfer_to_agent",
"description": "Transfer call to a human collections agent or supervisor",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "Customer account ID"
},
"reason": {
"type": "string",
"enum": ["supervisor_request", "complex_negotiation", "legal_threat", "hardship", "escalation"],
"description": "Reason for transfer"
},
"notes": {
"type": "string",
"description": "Context for the receiving agent"
}
},
"required": ["account_id", "reason"]
}
}
}
Tool Handlers#
func handleProcessPayment(params map[string]any) (string, error) {
accountID := params["account_id"].(string)
amount := params["amount"].(float64)
method := params["payment_method"].(string)
// Validate payment against account balance
account, err := db.GetAccount(accountID)
if err != nil {
return "I'm having trouble accessing your account. Let me transfer you to a colleague.", err
}
if amount > account.OutstandingBalance {
return fmt.Sprintf("The amount of %.2f exceeds your outstanding balance of %.2f. Would you like to pay the full balance?",
amount, account.OutstandingBalance), nil
}
// For immediate card payments
if method == "card" {
return "I'll need your card details to process this payment. Please provide the 16-digit card number.", nil
}
// For bank transfers and UPI
if method == "bank_transfer" || method == "upi" {
paymentRef := generatePaymentReference(accountID)
return fmt.Sprintf(
"To complete the bank transfer of %.2f, please use reference number %s. "+
"Payment details will be sent to your registered mobile. "+
"The payment will reflect within 24 hours.",
amount, paymentRef), nil
}
return "Payment method not supported", nil
}
func handleCreateArrangement(params map[string]any) (string, error) {
accountID := params["account_id"].(string)
arrangementType := params["arrangement_type"].(string)
totalAmount := params["total_amount"].(float64)
account, _ := db.GetAccount(accountID)
// Validate arrangement is within authorized limits
if !isWithinAuthority(arrangementType, totalAmount, account) {
return "This arrangement requires supervisor approval. Let me schedule a callback within 24 hours to confirm.", nil
}
// Create the arrangement
arrangement := &Arrangement{
AccountID: accountID,
Type: arrangementType,
TotalAmount: totalAmount,
CreatedAt: time.Now(),
Status: "active",
}
if installments, ok := params["installments"].([]any); ok {
arrangement.Installments = parseInstallments(installments)
}
err := db.CreateArrangement(arrangement)
if err != nil {
return "I wasn't able to set up the arrangement. Let me transfer you to a colleague who can help.", err
}
// Generate confirmation
confirmationNumber := arrangement.ID[:8]
// Send confirmation SMS/email
sendArrangementConfirmation(account, arrangement)
return fmt.Sprintf(
"Your payment arrangement has been set up. Confirmation number: %s. "+
"You'll receive the details via SMS and email. "+
"Your first payment of %.2f is due on %s.",
confirmationNumber,
arrangement.Installments[0].Amount,
arrangement.Installments[0].DueDate.Format("January 2"),
), nil
}
func handleLogDispute(params map[string]any) (string, error) {
accountID := params["account_id"].(string)
disputeType := params["dispute_type"].(string)
reason := params["dispute_reason"].(string)
dispute := &Dispute{
AccountID: accountID,
Type: disputeType,
Reason: reason,
Status: "open",
CreatedAt: time.Now(),
}
err := db.CreateDispute(dispute)
if err != nil {
return "I'm having trouble logging your dispute. Please contact us at our main number.", err
}
// Mark account as disputed - stop collections
db.UpdateAccountStatus(accountID, "disputed")
// Trigger dispute workflow
triggerDisputeWorkflow(dispute)
return fmt.Sprintf(
"Your dispute has been logged with reference number %s. "+
"All collection activity on this account is paused while we investigate. "+
"You'll receive written confirmation within 5 business days, and "+
"we'll complete our investigation within 30 days as required by law.",
dispute.ID[:8],
), nil
}
Compliance Requirements#
FDCPA Compliance (United States)#
type FDCPACompliance struct {
// Time restrictions
CallHoursStart int // 8 AM local time
CallHoursEnd int // 9 PM local time
// Disclosure requirements
MiniMirandaRequired bool // Must identify as debt collector
// Contact restrictions
HonorCeaseContact bool // Must stop if requested
NoWorkplaceCalls bool // Unless permitted
NoThirdPartyDisclosure bool // Cannot discuss with others
// Dispute handling
VerificationRequired bool // Must verify if disputed
PauseOnDispute bool // Must pause collection on dispute
}
func validateFDCPACompliance(call *Call) error {
// Check calling hours
localTime := call.Customer.GetLocalTime()
if localTime.Hour() < 8 || localTime.Hour() >= 21 {
return errors.New("call outside permitted hours (8 AM - 9 PM local)")
}
// Check cease and desist
if call.Customer.CeaseContactRequested {
return errors.New("customer has requested no contact")
}
// Check dispute status
if call.Account.DisputeStatus == "open" {
return errors.New("account has open dispute - collection paused")
}
return nil
}
RBI Guidelines (India)#
type RBICompliance struct {
// Time restrictions (RBI Circular 2022)
CallHoursStart int // 8 AM
CallHoursEnd int // 7 PM (stricter than FDCPA)
// Frequency limits
MaxCallsPerDay int // Maximum 3 calls per day
MaxCallsPerWeek int // Maximum 7 calls per week
// Disclosure
AgentIDRequired bool // Must identify with ID
CompanyNameRequired bool
// Language
LocalLanguageOption bool // Must offer local language
// Recording
CallRecordingRequired bool
RetentionPeriod int // 2 years minimum
}
func validateRBICompliance(call *Call) error {
// Check calling hours (8 AM - 7 PM IST)
ist, _ := time.LoadLocation("Asia/Kolkata")
localTime := time.Now().In(ist)
if localTime.Hour() < 8 || localTime.Hour() >= 19 {
return errors.New("call outside RBI permitted hours (8 AM - 7 PM)")
}
// Check daily call frequency
callsToday := db.GetCallCountToday(call.Customer.Phone)
if callsToday >= 3 {
return errors.New("maximum 3 calls per day reached")
}
// Check weekly call frequency
callsThisWeek := db.GetCallCountThisWeek(call.Customer.Phone)
if callsThisWeek >= 7 {
return errors.New("maximum 7 calls per week reached")
}
return nil
}
Call Recording and Retention#
type RecordingConfig struct {
Enabled bool
DisclosureMessage string
StorageDuration time.Duration
EncryptionEnabled bool
ConsentRequired bool
}
func setupCompliantRecording(call *Call) error {
config := RecordingConfig{
Enabled: true,
DisclosureMessage: "This call may be recorded for quality and training purposes.",
StorageDuration: 2 * 365 * 24 * time.Hour, // 2 years
EncryptionEnabled: true,
ConsentRequired: false, // One-party consent jurisdiction
}
// Start recording with disclosure
if config.Enabled {
call.RecordingID = startRecording(call.ID)
// Log recording start for audit
auditLog.Record(AuditEvent{
Type: "recording_started",
CallID: call.ID,
AccountID: call.Account.ID,
Timestamp: time.Now(),
})
}
return nil
}
Audit Trail#
type CollectionAuditLog struct {
ID string
CallID string
AccountID string
Timestamp time.Time
EventType string
Details map[string]any
AgentID string
RecordingID string
}
func logCollectionEvent(event CollectionAuditLog) {
// Required for compliance
event.Timestamp = time.Now()
db.InsertAuditLog(event)
// Real-time compliance monitoring
if event.EventType == "dispute_raised" ||
event.EventType == "cease_contact" ||
event.EventType == "complaint" {
alertComplianceTeam(event)
}
}
Metrics and Analytics#
Key Performance Indicators#
type CollectionMetrics struct {
// Contact Metrics
TotalAttempts int
ContactRate float64 // Percentage of successful contacts
RightPartyContacts int // Reached actual account holder
// Collection Metrics
TotalCollected float64
CollectionRate float64 // Amount collected / Amount attempted
PaymentArrangements int
ArrangementValue float64
// Quality Metrics
AverageCallDuration time.Duration
DisputeRate float64
EscalationRate float64
ComplianceScore float64
// Promise Metrics
PromisesToPay int
PromiseKeptRate float64 // Promises fulfilled / Promises made
}
func calculateCollectionMetrics(campaignID string, period time.Duration) *CollectionMetrics {
calls := db.GetCampaignCalls(campaignID, period)
metrics := &CollectionMetrics{}
var totalAttemptedAmount float64
var totalCollected float64
var rightPartyContacts int
var disputes int
for _, call := range calls {
metrics.TotalAttempts++
totalAttemptedAmount += call.Account.OutstandingBalance
if call.ContactMade {
if call.RightPartyContact {
rightPartyContacts++
}
if call.PaymentMade {
totalCollected += call.PaymentAmount
}
if call.ArrangementMade {
metrics.PaymentArrangements++
metrics.ArrangementValue += call.ArrangementAmount
}
if call.DisputeRaised {
disputes++
}
}
metrics.AverageCallDuration += call.Duration
}
// Calculate rates
if metrics.TotalAttempts > 0 {
metrics.ContactRate = float64(rightPartyContacts) / float64(metrics.TotalAttempts)
metrics.AverageCallDuration /= time.Duration(metrics.TotalAttempts)
}
if totalAttemptedAmount > 0 {
metrics.CollectionRate = totalCollected / totalAttemptedAmount
}
if rightPartyContacts > 0 {
metrics.DisputeRate = float64(disputes) / float64(rightPartyContacts)
}
metrics.TotalCollected = totalCollected
metrics.RightPartyContacts = rightPartyContacts
return metrics
}
Dashboard Metrics#
func getCollectionsDashboard(teamID string) *Dashboard {
today := time.Now().Truncate(24 * time.Hour)
return &Dashboard{
// Today's Performance
TodayStats: TodayStats{
CallsMade: db.CountCalls(teamID, today),
AmountCollected: db.SumPayments(teamID, today),
ArrangementsSetup: db.CountArrangements(teamID, today),
ContactRate: calculateContactRate(teamID, today),
},
// Agent Performance
AgentMetrics: getAgentMetrics(teamID, today),
// Account Buckets
AccountBuckets: []AccountBucket{
{Name: "0-30 DPD", Count: db.CountAccountsByDPD(0, 30)},
{Name: "31-60 DPD", Count: db.CountAccountsByDPD(31, 60)},
{Name: "61-90 DPD", Count: db.CountAccountsByDPD(61, 90)},
{Name: "90+ DPD", Count: db.CountAccountsByDPD(91, 999)},
},
// Compliance
ComplianceAlerts: getComplianceAlerts(teamID),
}
}
Promise-to-Pay Tracking#
type PromiseToPayMetrics struct {
TotalPromises int
PromisesKept int
PromisesBroken int
PromisesPending int
KeptRate float64
AveragePromiseAge time.Duration
}
func trackPromiseToPayMetrics(period time.Duration) *PromiseToPayMetrics {
promises := db.GetPromises(period)
metrics := &PromiseToPayMetrics{
TotalPromises: len(promises),
}
for _, p := range promises {
switch p.Status {
case "kept":
metrics.PromisesKept++
case "broken":
metrics.PromisesBroken++
case "pending":
metrics.PromisesPending++
}
}
if metrics.TotalPromises > 0 {
metrics.KeptRate = float64(metrics.PromisesKept) /
float64(metrics.PromisesKept + metrics.PromisesBroken)
}
return metrics
}
Campaign Management#
Outbound Campaign Setup#
func runCollectionsCampaign(campaignID string) {
campaign, _ := db.GetCampaign(campaignID)
accounts := db.GetCampaignAccounts(campaignID)
// Sort by priority (higher DPD, higher balance first)
sort.Slice(accounts, func(i, j int) bool {
scoreI := accounts[i].DaysPastDue * int(accounts[i].Balance)
scoreJ := accounts[j].DaysPastDue * int(accounts[j].Balance)
return scoreI > scoreJ
})
for _, account := range accounts {
// Pre-call compliance check
if err := validateCompliance(account); err != nil {
log.Printf("Skipping %s: %v", account.ID, err)
continue
}
// Check optimal calling time
if !isOptimalCallTime(account) {
scheduleForLater(account)
continue
}
// Prepare context
variables := map[string]string{
"customerName": account.CustomerName,
"accountNumber": account.ID,
"accountLast4": account.ID[len(account.ID)-4:],
"outstandingAmount": formatCurrency(account.Balance),
"daysPastDue": strconv.Itoa(account.DaysPastDue),
"minimumPayment": formatCurrency(account.MinimumPayment),
"dueDate": account.DueDate.Format("January 2, 2006"),
"lastPaymentDate": account.LastPaymentDate.Format("January 2"),
"lastPaymentAmount": formatCurrency(account.LastPaymentAmount),
"previousArrangements": summarizeArrangements(account.Arrangements),
"settlementAuthority": strconv.Itoa(campaign.SettlementAuthority),
"companyName": campaign.CompanyName,
"callbackNumber": campaign.CallbackNumber,
"customerEmail": account.Email,
}
// Initiate call
initiateCall(campaign.AgentID, account.Phone, variables)
// Respect pacing
time.Sleep(campaign.PacingDelay)
}
}
API Integration#
# Initiate collection call
curl -X POST https://voice-agent.edesy.in/api/v1/calls \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "collections_agent",
"to": "+919876543210",
"variables": {
"customerName": "Rahul Kumar",
"accountNumber": "ACC-2024-12345",
"outstandingAmount": "Rs. 15,450",
"daysPastDue": "45",
"minimumPayment": "Rs. 5,000",
"dueDate": "December 15, 2024",
"companyName": "FinCorp Services"
},
"compliance": {
"record_call": true,
"disclosure_required": true
},
"webhook_url": "https://your-server.com/webhooks/collection-complete"
}'
Best Practices#
1. Right Party Contact Verification#
// Always verify identity before discussing account
func verifyIdentity(session *Session) bool {
// Ask for identifying information
session.TTS.Speak("For security, could you please confirm the last 4 digits of your PAN card?")
response := session.WaitForResponse()
if verifyPAN(session.Account.PAN, response) {
return true
}
// Alternative verification
session.TTS.Speak("Let me try another verification. What is your date of birth?")
response = session.WaitForResponse()
return verifyDOB(session.Account.DOB, response)
}
2. Empathetic Communication#
// Phrases that acknowledge customer situation
var empathyPhrases = map[string][]string{
"financial_hardship": {
"I understand times can be tough financially.",
"I appreciate you sharing that with me.",
"Let's see what options we have to work with your situation.",
},
"confusion": {
"I completely understand this can be confusing.",
"Let me explain that more clearly.",
"That's a very fair question.",
},
"frustration": {
"I hear your frustration, and I want to help.",
"I understand this isn't the call you wanted to receive.",
"Let's work together to find a solution.",
},
}
3. Tiered Escalation#
type EscalationTier struct {
Level int
AuthorizedDiscount float64
ArrangementTerms int // months
RequiresApproval bool
}
var escalationTiers = []EscalationTier{
{Level: 1, AuthorizedDiscount: 0, ArrangementTerms: 3, RequiresApproval: false},
{Level: 2, AuthorizedDiscount: 0.10, ArrangementTerms: 6, RequiresApproval: false},
{Level: 3, AuthorizedDiscount: 0.25, ArrangementTerms: 12, RequiresApproval: true},
{Level: 4, AuthorizedDiscount: 0.50, ArrangementTerms: 24, RequiresApproval: true},
}
func getAuthorizedArrangement(account *Account, requestedDiscount float64) *EscalationTier {
// Determine tier based on account age, amount, and customer history
customerScore := calculateCustomerScore(account)
for _, tier := range escalationTiers {
if requestedDiscount <= tier.AuthorizedDiscount {
return &tier
}
}
return nil // Requires supervisor
}
4. Optimal Contact Timing#
func determineOptimalContactTime(account *Account) time.Time {
// Analyze past successful contacts
successfulContacts := db.GetSuccessfulContacts(account.ID)
if len(successfulContacts) > 0 {
// Return time when we've reached them before
return analyzeSuccessfulTimes(successfulContacts)
}
// Default optimal windows by segment
if account.Segment == "salaried" {
// Evening after work hours
return nextTimeInWindow(18, 20, account.Timezone)
} else if account.Segment == "business" {
// Late morning
return nextTimeInWindow(10, 12, account.Timezone)
}
// Default: mid-day
return nextTimeInWindow(11, 14, account.Timezone)
}
5. Follow-up Automation#
type FollowUpRule struct {
Trigger string
DelayHours int
Action string
MaxAttempts int
}
var followUpRules = []FollowUpRule{
{Trigger: "promise_to_pay", DelayHours: 24, Action: "reminder_sms", MaxAttempts: 1},
{Trigger: "arrangement_due", DelayHours: -24, Action: "reminder_call", MaxAttempts: 2},
{Trigger: "payment_failed", DelayHours: 4, Action: "call", MaxAttempts: 3},
{Trigger: "no_contact", DelayHours: 48, Action: "retry_call", MaxAttempts: 5},
{Trigger: "voicemail", DelayHours: 24, Action: "retry_different_time", MaxAttempts: 3},
}
func scheduleFollowUp(account *Account, trigger string) {
rule := findRule(trigger)
if rule == nil {
return
}
// Check attempt limits
attempts := db.GetFollowUpAttempts(account.ID, trigger)
if attempts >= rule.MaxAttempts {
log.Printf("Max follow-up attempts reached for %s", account.ID)
return
}
followUpTime := time.Now().Add(time.Duration(rule.DelayHours) * time.Hour)
db.ScheduleFollowUp(FollowUp{
AccountID: account.ID,
ScheduledAt: followUpTime,
Action: rule.Action,
Trigger: trigger,
})
}
Testing#
Compliance Test Scenarios#
func TestFDCPACompliance(t *testing.T) {
agent := setupTestAgent()
// Test calling hours
t.Run("RejectsCallOutsideHours", func(t *testing.T) {
call := simulateCall(agent, CallConfig{
Time: time.Date(2024, 1, 1, 7, 0, 0, 0, time.Local), // 7 AM
})
assert.Equal(t, "blocked", call.Status)
assert.Contains(t, call.BlockReason, "outside permitted hours")
})
// Test Mini-Miranda disclosure
t.Run("IncludesDebtDisclosure", func(t *testing.T) {
call := simulateCall(agent, CallConfig{
Time: time.Date(2024, 1, 1, 10, 0, 0, 0, time.Local),
})
assert.Contains(t, call.Transcript, "attempt to collect a debt")
})
// Test dispute handling
t.Run("PausesOnDispute", func(t *testing.T) {
agent.Process("I dispute this debt")
account := db.GetAccount(testAccountID)
assert.Equal(t, "disputed", account.Status)
assert.True(t, agent.ToolCalled("log_dispute"))
})
// Test cease contact
t.Run("HonorsCeaseContact", func(t *testing.T) {
agent.Process("Stop calling me")
assert.Contains(t, agent.LastResponse, "won't receive any more calls")
})
}
Payment Flow Testing#
func TestPaymentArrangement(t *testing.T) {
agent := setupTestAgent()
t.Run("CreatesValidArrangement", func(t *testing.T) {
agent.Process("I can pay 5000 rupees now and the rest next month")
assert.True(t, agent.ToolCalled("create_arrangement"))
arrangement := db.GetLatestArrangement(testAccountID)
assert.Equal(t, "installment", arrangement.Type)
assert.Len(t, arrangement.Installments, 2)
})
t.Run("EscalatesOverLimit", func(t *testing.T) {
agent.Process("I can only pay 30% of the total")
assert.Contains(t, agent.LastResponse, "supervisor approval")
assert.False(t, agent.ToolCalled("create_arrangement"))
})
}
Next Steps#
- Customer Support - General support use cases
- Call Recording - Recording configuration
- Function Calling - Tool integration
- Webhooks - Call completion events
- Indian Languages - Hindi, Tamil collection agents