# Multi-Provider VoIP Architecture

## Overview

This SaaS application supports **dual VoIP providers** for cost comparison and redundancy:

1. **Twilio + OpenAI Realtime API** (Existing)
2. **Telnyx Voice AI Native** (New)

Both providers support:
- French medical assistant voicebot
- Patient data extraction
- Appointment scheduling
- SMS confirmations with delivery tracking
- Multi-tenant routing via phone number lookup

---

## Architecture Comparison

### Twilio + OpenAI Stack

```
Inbound Call → Twilio Webhook → WebSocket to OpenAI Realtime API
                                 ↓
                           Function Calling (extract patient data)
                                 ↓
                           Save to tenant DB → Send SMS via Twilio
```

**Components:**
- `TwilioVoiceController` - Webhook handler
- `OpenAIRealtimeWebSocketHandler` - WebSocket connection to OpenAI
- `TwilioSmsService` - SMS via Twilio API
- `TwilioSmsCallbackController` - SMS status tracking

**Cost:** Twilio voice + OpenAI Realtime API + Twilio SMS

---

### Telnyx Voice AI Stack

```
Inbound Call → Telnyx Webhook → Telnyx Voice AI (native)
                                 ↓
                           Gather using AI / Function Calling
                                 ↓
                           Save to tenant DB → Send SMS via Telnyx
```

**Components:**
- `TelnyxVoiceController` - Webhook handler
- `TelnyxVoiceAIService` - Telnyx AI API wrapper
- `TelnyxEventController` - AI conversation events
- `TelnyxSmsService` - SMS via Telnyx API
- `TelnyxSmsCallbackController` - SMS status tracking

**Cost:** Telnyx voice + Telnyx AI (bundled) + Telnyx SMS

---

## Unified Phone Number Management

### Provider Enum

```java
public enum Provider {
    TWILIO,
    TELNYX
}
```

### PhoneNumber Entity (Refactored from TwilioPhoneNumber)

```java
@Entity
@Table(name = "phone_numbers", schema = "saas_db")
public class PhoneNumber {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String phoneNumber;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Provider provider;  // TWILIO or TELNYX
    
    @Column(nullable = false)
    private String tenantId;
    
    @Column(nullable = false)
    private Boolean isActive = true;
    
    private String friendlyName;
    private String description;
}
```

**Repository Methods:**
```java
Optional<PhoneNumber> findByPhoneNumber(String phoneNumber);
Optional<PhoneNumber> findByPhoneNumberAndProvider(String phoneNumber, Provider provider);
List<PhoneNumber> findByTenantIdAndProvider(String tenantId, Provider provider);
```

---

## Tenant Detection Flow (Same for Both Providers)

### Webhook → Tenant Identification → Data Persistence

```java
// Step 1: Extract phone number from webhook (To field)
String toNumber = request.getParameter("To"); // Twilio
String toNumber = payload.get("to");          // Telnyx

// Step 2: Lookup in admin.phone_numbers table
TenantContext.setTenantId("admin");
Optional<PhoneNumber> phoneOpt = phoneNumberRepository.findByPhoneNumber(toNumber);

// Step 3: Get tenant schema
String tenantId = phoneOpt.get().getTenantId();
Optional<Tenant> tenant = tenantRepository.findByTenantId(tenantId);
String schemaName = tenant.get().getSchemaName();

// Step 4: Switch to tenant schema and save data
TenantContext.setTenantId(schemaName);
inboundCallService.saveCallData(callData);
```

**No URL parameters needed** - Tenant identified automatically via phone number

---

## SMS Delivery Tracking (Both Providers)

### Callback Flow

```
Twilio/Telnyx → SMS Status Callback
                ↓
        Extract "From" field (our phone number)
                ↓
        Lookup in admin.phone_numbers → Get tenantId
                ↓
        Get schemaName from admin.tenants
                ↓
        Switch to tenant schema → Update SMS status
```

**Single callback URL** works for all tenants:
- Twilio: `/api/voip/sms/status-callback`
- Telnyx: `/api/voip/telnyx/sms/status-callback`

---

## API Endpoints

### Twilio Endpoints
- `POST /api/voip/incoming-call` - Incoming call webhook
- `POST /api/voip/sms/status-callback` - SMS delivery status
- `WS /media-stream` - OpenAI WebSocket

### Telnyx Endpoints
- `POST /api/voip/telnyx/incoming-call` - Incoming call webhook
- `POST /api/voip/telnyx/call-initiated` - Call initiated event
- `POST /api/voip/telnyx/call-answered` - Call answered event
- `POST /api/voip/telnyx/call-hangup` - Call hangup event
- `POST /api/voip/telnyx/ai/conversation-event` - AI events & function calls
- `POST /api/voip/telnyx/sms/status-callback` - SMS delivery status

All endpoints are **public** (configured in SecurityConfig):
```java
.requestMatchers("/api/voip/**").permitAll()
```

---

## Environment Variables

### Twilio
```bash
TWILIO_ACCOUNT_SID=your-account-sid
TWILIO_AUTH_TOKEN=your-auth-token
TWILIO_PHONE_NUMBER=+1234567890
```

### Telnyx
```bash
TELNYX_API_KEY=your-telnyx-api-key-v2
TELNYX_VOICE_AI_ASSISTANT_ID=your-ai-assistant-id
TELNYX_MESSAGING_PROFILE_ID=your-messaging-profile-id
```

### OpenAI
```bash
OPENAI_API_KEY=your-openai-api-key-here
```

---

## Data Model (Shared)

Both providers save to the same tenant-specific entities:

### InboundCallData
- `callSid` (Call Session ID)
- `fromNumber`, `toNumber`
- `callStatus`, `direction`
- `callToken` (Twilio: AccountSid, Telnyx: call_control_id)
- Location data, timestamps, etc.

### InboundCallRequest
- Patient info: `patientName`, `patientPhone`, `patientBirthdate`, `illness`
- Appointment: `doctorName`, `appointmentDateTime`, `visitReason`, `confirmed`
- SMS tracking: `smsSent`, `smsSid`, `smsStatus`
- `conversationTranscript` (JSON array)

---

## Provider Selection Strategy

### Use Twilio + OpenAI When:
- Need advanced OpenAI GPT-4o Realtime capabilities
- Require custom function calling logic
- Prefer WebSocket-based real-time streaming

### Use Telnyx When:
- Lower cost is priority (bundled Voice AI)
- Need native "Gather using AI" feature
- Prefer simpler webhook-based integration

### Cost Comparison Example

**Twilio + OpenAI:**
- Twilio Voice: $0.0085/min
- OpenAI Realtime API: $0.06/min (input) + $0.24/min (output)
- Twilio SMS: $0.0079/message
- **Total: ~$0.30/min + SMS**

**Telnyx:**
- Telnyx Voice + AI: $0.012/min (bundled)
- Telnyx SMS: $0.0035/message
- **Total: ~$0.012/min + SMS**

**Savings: ~95% on voice costs** 💰

---

## Migration Path (Not Required)

This is a **parallel implementation**, not a migration. Both providers run simultaneously.

To add a phone number:
```java
PhoneNumber twilioNumber = PhoneNumber.builder()
    .phoneNumber("+15551234567")
    .provider(Provider.TWILIO)
    .tenantId("tenant_acme")
    .isActive(true)
    .build();

PhoneNumber telnyxNumber = PhoneNumber.builder()
    .phoneNumber("+15559876543")
    .provider(Provider.TELNYX)
    .tenantId("tenant_acme")
    .isActive(true)
    .build();
```

Same tenant can have numbers from both providers ✅

---

## Testing

### Twilio
```bash
ngrok http 8000
# Configure Twilio webhook: https://your-ngrok.ngrok.io/api/voip/incoming-call
curl -X POST http://localhost:8000/api/voip/incoming-call -d "To=+15551234567"
```

### Telnyx
```bash
ngrok http 8000
# Configure Telnyx webhook: https://your-ngrok.ngrok.io/api/voip/telnyx/incoming-call
curl -X POST http://localhost:8000/api/voip/telnyx/incoming-call \
  -H "Content-Type: application/json" \
  -d '{"data": {"payload": {"to": "+15559876543"}}}'
```

---

## Future Enhancements

1. **Auto-failover**: If Twilio fails, automatically route to Telnyx
2. **Load balancing**: Distribute calls across providers
3. **Cost analytics**: Track spend per provider, per tenant
4. **Provider preferences**: Let tenants choose their preferred provider
5. **A/B testing**: Compare quality and accuracy between providers

---

## Summary

✅ **Dual VoIP stack** for cost optimization  
✅ **Unified phone number management** with Provider enum  
✅ **Same multi-tenant routing** for both providers  
✅ **Parallel implementation** - no breaking changes  
✅ **95% cost savings** with Telnyx Voice AI  

Choose the best provider for each use case! 🚀
