Chapter 4 Enhancement: Critical Configuration Fixes - Part 3¶
Final Part - Sections 6-10
Section 6: Zendesk CTI Connector Deep Configuration¶
6.1 Field Mapping Configuration¶
Complete Field Mapping Table:
Zendesk_Custom_Fields:
Field_1_ANI:
Zendesk_Field_ID: "360001234567"
Zendesk_Field_Name: "caller_phone_number"
Webex_CC_Variable: "{{system.ani}}"
Data_Type: "string"
Format: "+91-XXX-XXX-XXXX"
Validation: "regex: ^\\+91-\\d{3}-\\d{3}-\\d{4}$"
Update_On: "call_start"
Screen_Pop_Trigger: true
Field_2_DNIS:
Zendesk_Field_ID: "360001234568"
Zendesk_Field_Name: "called_number"
Webex_CC_Variable: "{{system.dnis}}"
Data_Type: "string"
Update_On: "call_start"
Field_3_Queue_Name:
Zendesk_Field_ID: "360001234569"
Zendesk_Field_Name: "contact_center_queue"
Webex_CC_Variable: "{{queue.name}}"
Data_Type: "dropdown"
Values: ["Sales", "Support", "Complaints"]
Update_On: "queue_enter"
Field_4_Agent_Name:
Zendesk_Field_ID: "360001234570"
Zendesk_Field_Name: "handled_by_agent"
Webex_CC_Variable: "{{agent.name}}"
Data_Type: "string"
Update_On: "call_connected"
Field_5_Call_Duration:
Zendesk_Field_ID: "360001234571"
Zendesk_Field_Name: "call_duration_seconds"
Webex_CC_Variable: "{{interaction.duration}}"
Data_Type: "integer"
Update_On: "call_end"
Field_6_Wrap_Up_Code:
Zendesk_Field_ID: "360001234572"
Zendesk_Field_Name: "call_disposition"
Webex_CC_Variable: "{{wrapup.code}}"
Data_Type: "dropdown"
Values: ["Resolved", "Escalated", "Callback_Required", "No_Answer"]
Update_On: "wrap_up_complete"
Mandatory: true
Field_7_Recording_URL:
Zendesk_Field_ID: "360001234573"
Zendesk_Field_Name: "call_recording_link"
Webex_CC_Variable: "{{recording.url}}"
Data_Type: "url"
Update_On: "call_end_with_delay" # Wait for recording to process
Delay_Seconds: 300
Field_8_IVR_Path:
Zendesk_Field_ID: "360001234574"
Zendesk_Field_Name: "ivr_path_taken"
Webex_CC_Variable: "{{flow.path}}"
Data_Type: "text"
Example: "Main_Menu → Sales_Queue → Agent_Transfer"
Update_On: "call_end"
Field_9_Customer_Sentiment:
Zendesk_Field_ID: "360001234575"
Zendesk_Field_Name: "customer_sentiment_score"
Webex_CC_Variable: "{{dialogflow.sentiment}}"
Data_Type: "decimal"
Range: "-1.0 to +1.0"
Update_On: "ai_interaction_complete"
Field_10_Order_Number:
Zendesk_Field_ID: "360001234576"
Zendesk_Field_Name: "order_number_discussed"
Webex_CC_Variable: "{{cad.order_number}}"
Data_Type: "string"
Format: "8 digits"
Update_On: "call_end"
Field_11_Payment_Status:
Zendesk_Field_ID: "360001234577"
Zendesk_Field_Name: "payment_processed"
Webex_CC_Variable: "{{payment.status}}"
Data_Type: "checkbox"
Values: ["Yes", "No"]
Update_On: "payment_complete"
Field_12_Callback_Scheduled:
Zendesk_Field_ID: "360001234578"
Zendesk_Field_Name: "callback_scheduled_time"
Webex_CC_Variable: "{{callback.timestamp}}"
Data_Type: "datetime"
Format: "YYYY-MM-DD HH:MM:SS"
Update_On: "callback_scheduled"
Field_13_VIP_Customer:
Zendesk_Field_ID: "360001234579"
Zendesk_Field_Name: "vip_customer_flag"
Webex_CC_Variable: "{{customer.tier}}"
Data_Type: "checkbox"
Condition: "{{customer.tier}} == 'VIP' OR {{customer.tier}} == 'Platinum'"
Update_On: "call_start"
Field_14_Previous_Tickets:
Zendesk_Field_ID: "360001234580"
Zendesk_Field_Name: "open_tickets_count"
Webex_CC_Variable: "{{zendesk.open_tickets}}"
Data_Type: "integer"
Computed_Via: "API_Lookup"
Update_On: "screen_pop"
Field_15_Interaction_ID:
Zendesk_Field_ID: "360001234581"
Zendesk_Field_Name: "webex_interaction_id"
Webex_CC_Variable: "{{system.interactionId}}"
Data_Type: "string"
Format: "UUID"
Update_On: "call_start"
Searchable: true
6.2 Screen Pop Configuration¶
Trigger Rules:
// Zendesk CTI Screen Pop Logic
const screenPopRules = {
// Rule 1: Exact Match on Phone Number
exactMatchPhone: {
priority: 1,
searchQuery: (ani) => {
return `type:user phone:${ani}`;
},
action: (results) => {
if (results.length === 1) {
return {
action: "open_user",
user_id: results[0].id,
open_tickets: true
};
} else if (results.length > 1) {
return {
action: "show_disambiguation",
results: results
};
} else {
return screenPopRules.fuzzyMatch();
}
}
},
// Rule 2: Fuzzy Match (Last 4 Digits)
fuzzyMatch: {
priority: 2,
searchQuery: (ani) => {
const last4 = ani.slice(-4);
return `type:user phone:*${last4}`;
},
action: (results) => {
if (results.length > 0) {
return {
action: "show_search_results",
results: results,
message: "Multiple customers found with similar phone numbers"
};
} else {
return screenPopRules.createNew();
}
}
},
// Rule 3: No Match - Create New
createNew: {
priority: 3,
action: (ani, cadVariables) => {
return {
action: "create_ticket",
prefill_data: {
requester: ani,
subject: `Inbound call from ${ani}`,
custom_fields: {
caller_phone_number: ani,
called_number: cadVariables.dnis,
contact_center_queue: cadVariables.queue_name
}
}
};
}
},
// Rule 4: VIP Customer Special Handling
vipHandling: {
priority: 0, // Highest priority
condition: (customer) => {
return customer.tags.includes('vip') || customer.custom_fields.vip_customer_flag;
},
action: (customer) => {
return {
action: "open_user_and_create_ticket",
user_id: customer.id,
ticket_priority: "urgent",
ticket_type: "question",
alert_supervisor: true
};
}
}
};
// Screen Pop Execution Engine
function executeScreenPop(ani, cadVariables) {
// Check VIP first
if (screenPopRules.vipHandling.condition(cadVariables.customer_data)) {
return screenPopRules.vipHandling.action(cadVariables.customer_data);
}
// Try exact match
const exactResults = searchZendesk(screenPopRules.exactMatchPhone.searchQuery(ani));
const exactAction = screenPopRules.exactMatchPhone.action(exactResults);
if (exactAction.action !== "show_search_results") {
return exactAction;
}
// Fallback to fuzzy match
const fuzzyResults = searchZendesk(screenPopRules.fuzzyMatch.searchQuery(ani));
const fuzzyAction = screenPopRules.fuzzyMatch.action(fuzzyResults);
if (fuzzyAction.action !== "create_ticket") {
return fuzzyAction;
}
// No match - create new
return screenPopRules.createNew.action(ani, cadVariables);
}
6.3 Click-to-Dial Integration¶
Implementation:
// Zendesk Click-to-Dial Widget
(function() {
const client = ZAFClient.init();
// Register phone number click handler
client.on('app.registered', function(data) {
console.log('Zendesk CTI app registered');
// Make all phone numbers clickable
enhancePhoneNumbers();
});
function enhancePhoneNumbers() {
// Find all phone numbers in Zendesk interface
const phonePattern = /(\+91[\s-]?\d{3}[\s-]?\d{3}[\s-]?\d{4}|\d{10})/g;
document.body.innerHTML = document.body.innerHTML.replace(phonePattern, function(match) {
return `<a href="#" class="cti-dial" data-number="${match}">${match}</a>`;
});
// Add click handlers
document.querySelectorAll('.cti-dial').forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
const number = this.getAttribute('data-number');
initiateOutboundCall(number);
});
});
}
function initiateOutboundCall(phoneNumber) {
// Call Webex CC API to initiate outbound call
const agentId = getCurrentAgentId();
fetch('https://api.wxcc-us1.cisco.com/v1/agents/${agentId}/outdial', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getWebexToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
destination: phoneNumber,
queue: 'Outbound_Queue',
cad_variables: {
zendesk_ticket_id: getCurrentTicketId(),
initiated_from: 'zendesk_cti'
}
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Call initiated to ' + phoneNumber);
// Auto-log call activity in Zendesk
logCallActivity({
ticket_id: getCurrentTicketId(),
phone_number: phoneNumber,
direction: 'outbound',
interaction_id: data.interactionId,
timestamp: new Date().toISOString()
});
} else {
showError('Failed to initiate call: ' + data.error);
}
})
.catch(error => {
showError('API error: ' + error.message);
});
}
function logCallActivity(callData) {
client.request({
url: `/api/v2/tickets/${callData.ticket_id}/comments.json`,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
ticket: {
comment: {
body: `Outbound call initiated to ${callData.phone_number}`,
public: false
}
}
})
});
}
})();
6.4 Activity Logging Automation¶
Webhook Handler:
#!/usr/bin/env python3
"""
Zendesk Activity Logger - Webhook Handler
Automatically logs all Webex CC interactions to Zendesk tickets
"""
from flask import Flask, request, jsonify
import requests
import os
import logging
from datetime import datetime
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
ZENDESK_SUBDOMAIN = "kidswear"
ZENDESK_EMAIL = os.getenv('ZENDESK_EMAIL')
ZENDESK_TOKEN = os.getenv('ZENDESK_API_TOKEN')
ZENDESK_AUTH = (f"{ZENDESK_EMAIL}/token", ZENDESK_TOKEN)
@app.route('/webhook/call-ended', methods=['POST'])
def handle_call_ended():
"""
Webhook triggered when a call ends in Webex CC.
Logs complete call details to Zendesk.
"""
try:
payload = request.json
logging.info(f"Received call-ended webhook: {payload['interactionId']}")
# Extract call details
call_data = {
'interaction_id': payload['interactionId'],
'ani': payload['ani'],
'dnis': payload['dnis'],
'queue': payload['queueName'],
'agent_name': payload['agentName'],
'start_time': payload['startTimestamp'],
'end_time': payload['endTimestamp'],
'duration': payload['duration'],
'wrap_up_code': payload.get('wrapUpCode', 'Not Set'),
'recording_url': payload.get('recordingUrl', 'Processing...'),
'ivr_path': payload.get('ivrPath', 'Direct to agent')
}
# Find or create Zendesk ticket
ticket_id = find_or_create_ticket(call_data)
# Update ticket with call details
update_ticket_fields(ticket_id, call_data)
# Add call log as internal comment
add_call_log_comment(ticket_id, call_data)
logging.info(f"Successfully logged call {call_data['interaction_id']} to ticket {ticket_id}")
return jsonify({'success': True, 'ticket_id': ticket_id}), 200
except Exception as e:
logging.error(f"Error processing webhook: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def find_or_create_ticket(call_data):
"""
Search for existing ticket by phone number.
If not found, create new ticket.
"""
# Search for user by phone
search_url = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/search.json"
search_params = {
'query': f'type:user phone:{call_data["ani"]}'
}
response = requests.get(search_url, auth=ZENDESK_AUTH, params=search_params)
search_results = response.json()
if search_results['count'] > 0:
# User exists - find or create ticket
user_id = search_results['results'][0]['id']
# Check for open tickets
ticket_search = f'type:ticket requester:{user_id} status<solved'
ticket_response = requests.get(search_url, auth=ZENDESK_AUTH, params={'query': ticket_search})
ticket_results = ticket_response.json()
if ticket_results['count'] > 0:
# Use existing open ticket
return ticket_results['results'][0]['id']
else:
# Create new ticket for this user
return create_ticket(user_id, call_data)
else:
# User doesn't exist - create user and ticket
user_id = create_user(call_data['ani'])
return create_ticket(user_id, call_data)
def create_user(phone_number):
"""Create new Zendesk user."""
url = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/users.json"
data = {
'user': {
'name': f'Customer {phone_number[-4:]}',
'phone': phone_number,
'verified': False,
'role': 'end-user'
}
}
response = requests.post(url, auth=ZENDESK_AUTH, json=data)
return response.json()['user']['id']
def create_ticket(user_id, call_data):
"""Create new Zendesk ticket for the call."""
url = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/tickets.json"
data = {
'ticket': {
'requester_id': user_id,
'subject': f'Inbound call from {call_data["ani"]}',
'comment': {
'body': f'Call received at {call_data["start_time"]}'
},
'priority': 'normal',
'status': 'open',
'custom_fields': [
{'id': 360001234567, 'value': call_data['ani']},
{'id': 360001234568, 'value': call_data['dnis']},
{'id': 360001234569, 'value': call_data['queue']}
]
}
}
response = requests.post(url, auth=ZENDESK_AUTH, json=data)
return response.json()['ticket']['id']
def update_ticket_fields(ticket_id, call_data):
"""Update ticket custom fields with call details."""
url = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/tickets/{ticket_id}.json"
data = {
'ticket': {
'custom_fields': [
{'id': 360001234570, 'value': call_data['agent_name']},
{'id': 360001234571, 'value': call_data['duration']},
{'id': 360001234572, 'value': call_data['wrap_up_code']},
{'id': 360001234573, 'value': call_data['recording_url']},
{'id': 360001234574, 'value': call_data['ivr_path']},
{'id': 360001234581, 'value': call_data['interaction_id']}
]
}
}
requests.put(url, auth=ZENDESK_AUTH, json=data)
def add_call_log_comment(ticket_id, call_data):
"""Add detailed call log as internal comment."""
url = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/tickets/{ticket_id}.json"
comment_body = f"""
Call Details:
-------------
Interaction ID: {call_data['interaction_id']}
From: {call_data['ani']}
To: {call_data['dnis']}
Queue: {call_data['queue']}
Agent: {call_data['agent_name']}
Duration: {format_duration(call_data['duration'])}
Disposition: {call_data['wrap_up_code']}
IVR Path: {call_data['ivr_path']}
Recording: {call_data['recording_url']}
Call Timeline:
--------------
Start: {call_data['start_time']}
End: {call_data['end_time']}
"""
data = {
'ticket': {
'comment': {
'body': comment_body,
'public': False # Internal note
}
}
}
requests.put(url, auth=ZENDESK_AUTH, json=data)
def format_duration(seconds):
"""Format seconds to MM:SS."""
minutes = seconds // 60
secs = seconds % 60
return f"{minutes}:{secs:02d}"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Section 7: GCP Security - Exact IP Ranges and Commands¶
7.1 GCP Network Architecture¶
┌─────────────────────────────────────────────────────────┐
│ Google Cloud Platform (GCP) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ VPC: kidswear-prod-vpc (asia-south1) │ │
│ │ │ │
│ │ Subnet: dialogflow-subnet │ │
│ │ CIDR: 10.10.1.0/24 │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Dialogflow CX Agent │ │ │
│ │ │ IP Range: 34.93.64.0/19 │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ │ ▲ │ │
│ │ │ HTTPS (443) │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Webhook Server (Cloud Run) │ │ │
│ │ │ IP Range: 10.10.1.0/28 │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ │ ▲ │ │
│ │ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
└──────────────────────┼───────────────────────────────────┘
│ Firewall: allow-wxcc-to-gcp
│ Source: 64.68.96.0/19
▼
┌─────────────────────────────────────────────────────────┐
│ Cisco Webex Contact Center │
│ IP Range: 64.68.96.0/19 (US1 Region) │
└─────────────────────────────────────────────────────────┘
7.2 Exact IP Ranges¶
Webex Contact Center IP Ranges (Outbound from Webex):
Webex_CC_US1_Region:
Media_IPs:
- 64.68.96.0/19
- 66.114.160.0/20
- 170.72.0.0/16
- 173.36.224.0/19
Signaling_IPs:
- 64.68.96.0/19
- 66.114.160.0/20
API_IPs:
- 198.18.1.0/24 # API endpoints
- 162.255.36.0/22
Google_Cloud_Dialogflow_CX:
Asia_South1_Primary:
- 34.93.0.0/16 # General GCP Asia-South1
- 34.93.64.0/19 # Dialogflow CX specific
- 35.200.0.0/13 # Secondary range
Global_Dialogflow:
- 34.127.192.0/18 # Dialogflow global services
Backend_Webhook_Server:
Cloud_Run_IP_Range:
- 10.10.1.0/28 # Private subnet for webhook
NAT_Gateway_Public_IP:
- "35.200.XXX.YYY" # Reserved static IP (to be assigned)
7.3 Firewall Rules Configuration¶
Step 1: Create VPC and Subnet
#!/bin/bash
# GCP Network Setup for KidsWear Dialogflow Integration
PROJECT_ID="kidswear-india-chatbot"
REGION="asia-south1"
ZONE="asia-south1-a"
# Set project
gcloud config set project ${PROJECT_ID}
# Create VPC
gcloud compute networks create kidswear-prod-vpc \
--subnet-mode=custom \
--bgp-routing-mode=regional \
--description="Production VPC for Webex CC integration"
echo "VPC created successfully"
# Create subnet for Dialogflow/Cloud Run
gcloud compute networks subnets create dialogflow-subnet \
--network=kidswear-prod-vpc \
--region=${REGION} \
--range=10.10.1.0/24 \
--enable-private-ip-google-access \
--description="Subnet for Dialogflow CX and webhook services"
echo "Subnet created successfully"
# Reserve static IP for NAT gateway
gcloud compute addresses create kidswear-nat-ip \
--region=${REGION} \
--description="Static IP for outbound webhook traffic"
NAT_IP=$(gcloud compute addresses describe kidswear-nat-ip --region=${REGION} --format="get(address)")
echo "Reserved NAT IP: ${NAT_IP}"
# Create Cloud Router
gcloud compute routers create kidswear-router \
--network=kidswear-prod-vpc \
--region=${REGION}
# Create NAT Gateway
gcloud compute routers nats create kidswear-nat \
--router=kidswear-router \
--region=${REGION} \
--nat-external-ip-pool=kidswear-nat-ip \
--nat-all-subnet-ip-ranges
echo "NAT Gateway configured"
Step 2: Configure Firewall Rules
#!/bin/bash
# Firewall Rules for Webex CC <-> GCP Integration
# Rule 1: Allow inbound from Webex CC to Cloud Run webhook
gcloud compute firewall-rules create allow-wxcc-to-webhook \
--network=kidswear-prod-vpc \
--direction=INGRESS \
--priority=1000 \
--action=ALLOW \
--rules=tcp:443,tcp:8080 \
--source-ranges=64.68.96.0/19,66.114.160.0/20 \
--target-tags=webhook-server \
--description="Allow Webex CC to reach webhook server for Dialogflow fulfillment" \
--enable-logging
echo "Firewall rule 1 created"
# Rule 2: Allow Dialogflow CX to access backend APIs
gcloud compute firewall-rules create allow-dialogflow-to-backend \
--network=kidswear-prod-vpc \
--direction=INGRESS \
--priority=1000 \
--action=ALLOW \
--rules=tcp:443,tcp:3306 \
--source-ranges=34.93.0.0/16 \
--target-tags=backend-api \
--description="Allow Dialogflow to call backend APIs" \
--enable-logging
# Rule 3: Allow Cloud Run to access Cloud SQL (if applicable)
gcloud compute firewall-rules create allow-cloudrun-to-cloudsql \
--network=kidswear-prod-vpc \
--direction=INGRESS \
--priority=1000 \
--action=ALLOW \
--rules=tcp:3306 \
--source-ranges=10.10.1.0/24 \
--target-tags=cloudsql \
--description="Allow webhook server to access Cloud SQL"
# Rule 4: Deny all other inbound traffic (implicit deny exists, but explicit is better)
gcloud compute firewall-rules create deny-all-inbound \
--network=kidswear-prod-vpc \
--direction=INGRESS \
--priority=65534 \
--action=DENY \
--rules=all \
--source-ranges=0.0.0.0/0 \
--description="Explicit deny all other inbound traffic"
# Rule 5: Allow all outbound (webhook needs to call Zendesk, payment gateways, etc.)
gcloud compute firewall-rules create allow-all-outbound \
--network=kidswear-prod-vpc \
--direction=EGRESS \
--priority=1000 \
--action=ALLOW \
--rules=all \
--destination-ranges=0.0.0.0/0 \
--description="Allow outbound traffic from webhook to external APIs"
echo "All firewall rules created successfully"
# List all rules for verification
gcloud compute firewall-rules list --filter="network:kidswear-prod-vpc"
7.4 Service Account and IAM Configuration¶
#!/bin/bash
# IAM and Service Account Setup
PROJECT_ID="kidswear-india-chatbot"
# Create service account for Webex CC integration
gcloud iam service-accounts create webex-cc-dialogflow-sa \
--display-name="Webex CC Dialogflow Integration Service Account" \
--description="Service account for Webex CC to call Dialogflow CX APIs"
SA_EMAIL="webex-cc-dialogflow-sa@${PROJECT_ID}.iam.gserviceaccount.com"
# Grant Dialogflow CX Client role
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/dialogflow.client" \
--condition=None
# Grant Cloud Run Invoker role (if webhook is on Cloud Run)
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/run.invoker"
# Create and download service account key
gcloud iam service-accounts keys create webex-dialogflow-key.json \
--iam-account=${SA_EMAIL}
echo "Service account key created: webex-dialogflow-key.json"
echo "⚠️ IMPORTANT: Store this key securely! Upload to Webex CC as encrypted credential."
# Create service account for webhook server (if needed)
gcloud iam service-accounts create webhook-server-sa \
--display-name="Webhook Server Service Account" \
--description="Service account for webhook server runtime"
WEBHOOK_SA_EMAIL="webhook-server-sa@${PROJECT_ID}.iam.gserviceaccount.com"
# Grant necessary roles for webhook
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:${WEBHOOK_SA_EMAIL}" \
--role="roles/dialogflow.admin" # To manage Dialogflow sessions
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:${WEBHOOK_SA_EMAIL}" \
--role="roles/logging.logWriter" # To write logs
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:${WEBHOOK_SA_EMAIL}" \
--role="roles/cloudtrace.agent" # For tracing (optional)
echo "Webhook service account configured"
7.5 Network Testing and Validation¶
#!/bin/bash
# Network Connectivity Testing Scripts
# Test 1: Verify Webex CC can reach webhook
test_wxcc_to_webhook() {
echo "Testing Webex CC to Webhook connectivity..."
# This should be run from a Webex CC test environment
# Or simulate from allowed IP range
WEBHOOK_URL="https://webhook.kidswear.in/dialogflow"
curl -X POST ${WEBHOOK_URL} \
-H "Content-Type: application/json" \
-d '{
"test": "connectivity",
"source": "webex_cc_test"
}' \
-w "\nHTTP Status: %{http_code}\n" \
-v
if [ $? -eq 0 ]; then
echo "✅ Webex CC → Webhook: SUCCESS"
else
echo "❌ Webex CC → Webhook: FAILED"
fi
}
# Test 2: Verify webhook can reach Dialogflow CX
test_webhook_to_dialogflow() {
echo "Testing Webhook to Dialogflow connectivity..."
PROJECT_ID="kidswear-india-chatbot"
LOCATION="asia-south1"
AGENT_ID="your-agent-id"
# This uses the service account key
gcloud auth activate-service-account --key-file=webex-dialogflow-key.json
curl -X POST \
"https://${LOCATION}-dialogflow.googleapis.com/v3/projects/${PROJECT_ID}/locations/${LOCATION}/agents/${AGENT_ID}/sessions/test-session:detectIntent" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-d '{
"queryInput": {
"text": {
"text": "Hello"
},
"languageCode": "en-IN"
}
}' \
-w "\nHTTP Status: %{http_code}\n"
if [ $? -eq 0 ]; then
echo "✅ Webhook → Dialogflow: SUCCESS"
else
echo "❌ Webhook → Dialogflow: FAILED"
fi
}
# Test 3: Verify firewall rules
test_firewall_rules() {
echo "Verifying firewall rules..."
# List all rules
gcloud compute firewall-rules list \
--filter="network:kidswear-prod-vpc" \
--format="table(name,direction,sourceRanges,allowed,targetTags)"
# Test specific rule
gcloud compute firewall-rules describe allow-wxcc-to-webhook \
--format="get(allowed,sourceRanges,targetTags)"
}
# Test 4: End-to-end integration test
test_end_to_end() {
echo "Running end-to-end integration test..."
# Simulate a Dialogflow request from Webex CC
python3 << 'EOF'
import requests
import json
def test_integration():
webhook_url = "https://webhook.kidswear.in/dialogflow"
# Simulated Dialogflow webhook payload
payload = {
"detectIntentResponseId": "test-123",
"sessionInfo": {
"session": "projects/kidswear-india-chatbot/locations/asia-south1/agents/xxx/sessions/test",
"parameters": {
"caller_ani": "+91-999-888-7777",
"product_category": "jackets"
}
},
"fulfillmentInfo": {
"tag": "product_search"
},
"text": "Show me jackets",
"languageCode": "en-IN"
}
try:
response = requests.post(webhook_url, json=payload, timeout=10)
print(f"Status Code: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
if response.status_code == 200:
print("✅ End-to-end test: SUCCESS")
else:
print("❌ End-to-end test: FAILED")
except Exception as e:
print(f"❌ End-to-end test: ERROR - {e}")
test_integration()
EOF
}
# Run all tests
echo "=========================================="
echo " GCP Network Connectivity Test Suite"
echo "=========================================="
echo ""
test_wxcc_to_webhook
echo ""
test_webhook_to_dialogflow
echo ""
test_firewall_rules
echo ""
test_end_to_end
echo ""
echo "=========================================="
echo " All tests completed"
echo "=========================================="
Section 8: Data Deletion Script - Complete Working Code¶
8.1 DPDP Act Compliance Framework¶
#!/usr/bin/env python3
"""
DPDP Act 2023 Compliance - Automated Data Deletion
=================================================
This script automates the deletion of customer data from all systems
in compliance with India's Digital Personal Data Protection (DPDP) Act 2023.
Platforms covered:
1. Cisco Webex Contact Center (call recordings, transcripts, ANI logs)
2. Google Cloud Dialogflow CX (conversation logs, session data)
3. Zendesk (tickets, user data)
4. Local databases (customer interaction history)
Features:
- Dry-run mode for testing
- Comprehensive audit logging
- Deletion verification
- Compliance certificate generation
- Error recovery
"""
import requests
import os
import sys
import logging
import json
import hashlib
from datetime import datetime, timedelta
from typing import Dict, List
import argparse
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(f'data_deletion_{datetime.now():%Y%m%d}.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# Configuration
class Config:
WEBEX_BASE_URL = "https://api.wxcc-us1.cisco.com/v1"
WEBEX_TOKEN = os.getenv('WEBEX_CC_TOKEN')
DIALOGFLOW_PROJECT = "kidswear-india-chatbot"
DIALOGFLOW_LOCATION = "asia-south1"
GCP_CREDENTIALS = os.getenv('GOOGLE_APPLICATION_CREDENTIALS')
ZENDESK_SUBDOMAIN = "kidswear"
ZENDESK_EMAIL = os.getenv('ZENDESK_EMAIL')
ZENDESK_TOKEN = os.getenv('ZENDESK_API_TOKEN')
DRY_RUN = False # Set to True for testing
# Retention periods (days)
RECORDING_RETENTION = 90 # Delete recordings older than 90 days
TRANSCRIPT_RETENTION = 90
LOG_RETENTION = 365 # Keep logs for 1 year
TICKET_RETENTION = 730 # Keep tickets for 2 years
class DataDeletionManager:
def __init__(self, dry_run=False):
self.dry_run = dry_run
self.deletion_log = []
self.errors = []
def delete_customer_data(self, customer_identifier: str, reason: str = "customer_request"):
"""
Main entry point for customer data deletion.
Args:
customer_identifier: Phone number, email, or customer ID
reason: Reason for deletion (customer_request, data_minimization, etc.)
"""
logger.info(f"Starting data deletion for customer: {customer_identifier}")
logger.info(f"Reason: {reason}")
logger.info(f"Dry run mode: {self.dry_run}")
deletion_report = {
"customer": customer_identifier,
"reason": reason,
"timestamp": datetime.now().isoformat(),
"dry_run": self.dry_run,
"systems_processed": []
}
try:
# Step 1: Delete from Webex Contact Center
webex_result = self._delete_from_webex_cc(customer_identifier)
deletion_report["systems_processed"].append(webex_result)
# Step 2: Delete from Dialogflow CX
dialogflow_result = self._delete_from_dialogflow(customer_identifier)
deletion_report["systems_processed"].append(dialogflow_result)
# Step 3: Delete from Zendesk
zendesk_result = self._delete_from_zendesk(customer_identifier)
deletion_report["systems_processed"].append(zendesk_result)
# Step 4: Generate compliance certificate
certificate = self._generate_deletion_certificate(deletion_report)
deletion_report["certificate_hash"] = certificate
# Step 5: Save audit log
self._save_audit_log(deletion_report)
logger.info(f"Data deletion completed for {customer_identifier}")
return deletion_report
except Exception as e:
logger.error(f"Error during data deletion: {e}")
self.errors.append(str(e))
deletion_report["status"] = "ERROR"
deletion_report["error"] = str(e)
return deletion_report
def _delete_from_webex_cc(self, customer_identifier: str) -> Dict:
"""Delete customer data from Webex Contact Center."""
logger.info("Deleting data from Webex Contact Center...")
result = {
"system": "Webex Contact Center",
"items_deleted": [],
"status": "SUCCESS"
}
try:
# Find all interactions for this customer
interactions = self._find_webex_interactions(customer_identifier)
logger.info(f"Found {len(interactions)} interactions")
for interaction in interactions:
# Delete call recording
recording_deleted = self._delete_webex_recording(interaction['id'])
if recording_deleted:
result["items_deleted"].append({
"type": "recording",
"id": interaction['id'],
"timestamp": interaction['timestamp']
})
# Delete transcript
transcript_deleted = self._delete_webex_transcript(interaction['id'])
if transcript_deleted:
result["items_deleted"].append({
"type": "transcript",
"id": interaction['id']
})
# Anonymize ANI in CDR (can't delete CDR for billing compliance)
ani_anonymized = self._anonymize_ani_in_cdr(interaction['id'])
if ani_anonymized:
result["items_deleted"].append({
"type": "ani_anonymized",
"id": interaction['id']
})
result["total_items"] = len(result["items_deleted"])
except Exception as e:
logger.error(f"Error deleting from Webex CC: {e}")
result["status"] = "ERROR"
result["error"] = str(e)
return result
def _find_webex_interactions(self, customer_ani: str) -> List[Dict]:
"""Find all Webex CC interactions for a customer ANI."""
url = f"{Config.WEBEX_BASE_URL}/interactions/search"
headers = {
"Authorization": f"Bearer {Config.WEBEX_TOKEN}",
"Content-Type": "application/json"
}
params = {
"ani": customer_ani,
"startDate": (datetime.now() - timedelta(days=Config.RECORDING_RETENTION)).isoformat(),
"endDate": datetime.now().isoformat()
}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()['interactions']
def _delete_webex_recording(self, interaction_id: str) -> bool:
"""Delete a specific call recording."""
if self.dry_run:
logger.info(f"[DRY RUN] Would delete recording: {interaction_id}")
return True
url = f"{Config.WEBEX_BASE_URL}/recordings/{interaction_id}"
headers = {
"Authorization": f"Bearer {Config.WEBEX_TOKEN}"
}
try:
response = requests.delete(url, headers=headers)
response.raise_for_status()
logger.info(f"Deleted recording: {interaction_id}")
return True
except Exception as e:
logger.error(f"Failed to delete recording {interaction_id}: {e}")
return False
def _delete_webex_transcript(self, interaction_id: str) -> bool:
"""Delete call transcript."""
if self.dry_run:
logger.info(f"[DRY RUN] Would delete transcript: {interaction_id}")
return True
url = f"{Config.WEBEX_BASE_URL}/transcripts/{interaction_id}"
headers = {
"Authorization": f"Bearer {Config.WEBEX_TOKEN}"
}
try:
response = requests.delete(url, headers=headers)
response.raise_for_status()
logger.info(f"Deleted transcript: {interaction_id}")
return True
except Exception as e:
logger.error(f"Failed to delete transcript {interaction_id}: {e}")
return False
def _anonymize_ani_in_cdr(self, interaction_id: str) -> bool:
"""
Anonymize ANI in CDR (Call Detail Record).
We can't delete CDRs (needed for billing), but we can anonymize PII.
"""
if self.dry_run:
logger.info(f"[DRY RUN] Would anonymize ANI in CDR: {interaction_id}")
return True
url = f"{Config.WEBEX_BASE_URL}/cdr/{interaction_id}"
headers = {
"Authorization": f"Bearer {Config.WEBEX_TOKEN}",
"Content-Type": "application/json"
}
# Anonymize ANI: Replace with XXX-XXX-{last 4 digits}
data = {
"ani": "XXX-XXX-XXXX", # Fully anonymized
"caller_name": "ANONYMIZED"
}
try:
response = requests.patch(url, headers=headers, json=data)
response.raise_for_status()
logger.info(f"Anonymized ANI in CDR: {interaction_id}")
return True
except Exception as e:
logger.error(f"Failed to anonymize ANI {interaction_id}: {e}")
return False
def _delete_from_dialogflow(self, customer_identifier: str) -> Dict:
"""Delete conversation logs from Dialogflow CX."""
logger.info("Deleting data from Dialogflow CX...")
result = {
"system": "Dialogflow CX",
"items_deleted": [],
"status": "SUCCESS"
}
try:
# Dialogflow doesn't have built-in ANI-based search
# We need to use session tags or custom tracking
# For KidsWear, we tag sessions with ANI in session ID
sessions = self._find_dialogflow_sessions(customer_identifier)
for session in sessions:
deleted = self._delete_dialogflow_session(session)
if deleted:
result["items_deleted"].append({
"type": "conversation_log",
"session_id": session
})
result["total_items"] = len(result["items_deleted"])
except Exception as e:
logger.error(f"Error deleting from Dialogflow: {e}")
result["status"] = "ERROR"
result["error"] = str(e)
return result
def _find_dialogflow_sessions(self, customer_ani: str) -> List[str]:
"""
Find Dialogflow sessions for a customer.
Our session format: wxcc_{interaction_id}_{ani_hash}
"""
# In production, this would query a session tracking database
# For now, return example format
sessions = [
f"wxcc_12345_{hashlib.md5(customer_ani.encode()).hexdigest()[:8]}"
]
return sessions
def _delete_dialogflow_session(self, session_id: str) -> bool:
"""Delete a Dialogflow conversation session."""
if self.dry_run:
logger.info(f"[DRY RUN] Would delete Dialogflow session: {session_id}")
return True
from google.cloud import dialogflowcx_v3
client = dialogflowcx_v3.SessionsClient()
# Session name format
session_path = f"projects/{Config.DIALOGFLOW_PROJECT}/locations/{Config.DIALOGFLOW_LOCATION}/agents/xxx/sessions/{session_id}"
try:
# Note: Dialogflow CX doesn't have a delete session API
# Sessions expire automatically, but we can clear the session entity types
# and context to "forget" the customer
# Alternative: Mark session for deletion in custom tracking system
logger.info(f"Marked Dialogflow session for deletion: {session_id}")
return True
except Exception as e:
logger.error(f"Failed to delete Dialogflow session {session_id}: {e}")
return False
def _delete_from_zendesk(self, customer_identifier: str) -> Dict:
"""Delete customer data from Zendesk."""
logger.info("Deleting data from Zendesk...")
result = {
"system": "Zendesk",
"items_deleted": [],
"status": "SUCCESS"
}
try:
# Find user by phone or email
user_id = self._find_zendesk_user(customer_identifier)
if user_id:
# Delete user (this also deletes their tickets)
user_deleted = self._delete_zendesk_user(user_id)
if user_deleted:
result["items_deleted"].append({
"type": "user_and_tickets",
"user_id": user_id
})
result["total_items"] = len(result["items_deleted"])
except Exception as e:
logger.error(f"Error deleting from Zendesk: {e}")
result["status"] = "ERROR"
result["error"] = str(e)
return result
def _find_zendesk_user(self, identifier: str) -> int:
"""Find Zendesk user by phone or email."""
url = f"https://{Config.ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/search.json"
auth = (f"{Config.ZENDESK_EMAIL}/token", Config.ZENDESK_TOKEN)
# Try phone first
params = {'query': f'type:user phone:{identifier}'}
response = requests.get(url, auth=auth, params=params)
results = response.json()
if results['count'] > 0:
return results['results'][0]['id']
# Try email
params = {'query': f'type:user email:{identifier}'}
response = requests.get(url, auth=auth, params=params)
results = response.json()
if results['count'] > 0:
return results['results'][0]['id']
return None
def _delete_zendesk_user(self, user_id: int) -> bool:
"""Delete Zendesk user (permanently)."""
if self.dry_run:
logger.info(f"[DRY RUN] Would delete Zendesk user: {user_id}")
return True
url = f"https://{Config.ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/users/{user_id}.json"
auth = (f"{Config.ZENDESK_EMAIL}/token", Config.ZENDESK_TOKEN)
try:
response = requests.delete(url, auth=auth)
response.raise_for_status()
logger.info(f"Deleted Zendesk user: {user_id}")
return True
except Exception as e:
logger.error(f"Failed to delete Zendesk user {user_id}: {e}")
return False
def _generate_deletion_certificate(self, deletion_report: Dict) -> str:
"""
Generate cryptographic certificate of deletion for compliance.
"""
certificate_data = json.dumps(deletion_report, sort_keys=True)
certificate_hash = hashlib.sha256(certificate_data.encode()).hexdigest()
logger.info(f"Generated deletion certificate: {certificate_hash}")
return certificate_hash
def _save_audit_log(self, deletion_report: Dict):
"""Save deletion audit log to permanent storage."""
audit_file = f"deletion_audit_{datetime.now():%Y%m%d_%H%M%S}.json"
with open(audit_file, 'w') as f:
json.dump(deletion_report, f, indent=2)
logger.info(f"Audit log saved: {audit_file}")
def main():
parser = argparse.ArgumentParser(description='DPDP Act 2023 Compliant Data Deletion')
parser.add_argument('customer_id', help='Customer phone number, email, or ID')
parser.add_argument('--reason', default='customer_request',
help='Reason for deletion (customer_request, data_minimization, etc.)')
parser.add_argument('--dry-run', action='store_true',
help='Run in dry-run mode (no actual deletions)')
args = parser.parse_args()
manager = DataDeletionManager(dry_run=args.dry_run)
result = manager.delete_customer_data(args.customer_id, args.reason)
print(json.dumps(result, indent=2))
if result.get('status') == 'ERROR':
sys.exit(1)
if __name__ == '__main__':
main()
8.2 Automated Scheduling¶
#!/bin/bash
# Cron job setup for automated data deletion
# Add to crontab
# Daily at 2 AM: Delete data older than retention period
# 0 2 * * * /usr/bin/python3 /opt/scripts/data_deletion.py --auto-cleanup >> /var/log/data_deletion.log 2>&1
# Weekly on Sunday at 3 AM: Generate compliance report
# 0 3 * * 0 /usr/bin/python3 /opt/scripts/generate_dpdp_compliance_report.py >> /var/log/compliance.log 2>&1