AI Voice Agents for Debt Collection

Build compliant, effective AI voice agents for debt collection with payment reminders, arrangement negotiation, and dispute handling while adhering to FDCPA and RBI guidelines.

AI Voice Agents for Debt Collection

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", "payment.arranged", "dispute.raised"]
    }
  }
}

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