40376-vm/core/views.py
Flatlogic Bot 2447ac66a4 gg
2026-07-03 20:03:39 +00:00

286 lines
9.5 KiB
Python

import ipaddress
import json
import logging
from django.contrib import messages
from django.db.models import Count
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from .forms import PilotCallForm
from .models import PilotCall, WebhookEvent
logger = logging.getLogger(__name__)
MAX_WEBHOOK_BODY_BYTES = 512 * 1024
def home(request):
"""Render the branded landing dashboard for the real-time intake pilot."""
recent_calls = PilotCall.objects.all()[:3]
total_calls = PilotCall.objects.count()
urgent_count = sum(1 for call in PilotCall.objects.all() if call.urgent_flags)
avg_completion = 0
if total_calls:
avg_completion = round(sum(call.completion_score for call in PilotCall.objects.all()) / total_calls)
context = {
'project_name': 'ReliefSignal',
'meta_description': 'Real-time tax relief call intake pilot for live transcription, structured extraction, completion scoring, and CRM readiness.',
'recent_calls': recent_calls,
'total_calls': total_calls,
'urgent_count': urgent_count,
'avg_completion': avg_completion,
}
return render(request, 'core/index.html', context)
def call_list(request):
calls = PilotCall.objects.all()
summary = calls.aggregate(total=Count('id'))
context = {
'project_name': 'ReliefSignal',
'meta_description': 'Review tax relief pilot calls, completion scores, and urgency flags.',
'calls': calls,
'total_calls': summary['total'] or 0,
}
return render(request, 'core/call_list.html', context)
def call_create(request):
if request.method == 'POST':
form = PilotCallForm(request.POST)
if form.is_valid():
call = form.save()
messages.success(request, 'Pilot call saved. Review the completion score and missing intake fields below.')
return redirect('call_detail', pk=call.pk)
else:
form = PilotCallForm()
context = {
'project_name': 'ReliefSignal',
'meta_description': 'Create a tax relief pilot call intake from a live transcript segment.',
'form': form,
}
return render(request, 'core/call_form.html', context)
def call_detail(request, pk):
call = get_object_or_404(PilotCall, pk=pk)
intake_fields = [
('IRS Debt', call.irs_debt),
('State Debt', call.state_debt),
('Tax Years', call.tax_years),
('Filing Status', call.filing_status),
('Employment', call.employment_type),
('Occupation', call.occupation),
('Monthly Gross Income', call.monthly_gross_income),
('Spouse Income', call.spouse_income),
('Mortgage', call.mortgage),
('Rent', call.rent),
('Utilities', call.utilities),
('Car Payment', call.car_payment),
('Insurance', call.insurance),
('Student Loans', call.student_loans),
('Credit Cards', call.credit_cards),
('Business Expenses', call.business_expenses),
('Best Callback Time', call.best_callback_time),
]
context = {
'project_name': 'ReliefSignal',
'meta_description': f'Pilot call detail for {call.client_name}: transcript, extracted tax relief fields, urgency flags, and completion score.',
'call': call,
'intake_fields': intake_fields,
}
return render(request, 'core/call_detail.html', context)
def audio_capture(request):
context = {
'project_name': 'ReliefSignal',
'meta_description': 'Browser-based computer audio capture prototype for microphone input, shared tab/system output audio, local level monitoring, and local recording tests.',
}
return render(request, 'core/audio_capture.html', context)
def webhook_list(request):
events = WebhookEvent.objects.all()[:25]
context = {
'project_name': 'ReliefSignal',
'meta_description': 'Provider-neutral webhook inbox for testing Ytel-style call events before a dedicated integration is finalized.',
'events': events,
'total_events': WebhookEvent.objects.count(),
'endpoint_url': _public_absolute_uri(request, reverse('webhook_generic')),
}
return render(request, 'core/webhook_list.html', context)
@csrf_exempt
@require_http_methods(['GET', 'POST'])
def webhook_generic(request):
if request.method == 'GET':
return JsonResponse({
'ok': True,
'name': 'ReliefSignal generic webhook receiver',
'usage': 'POST JSON or form payloads to this endpoint. Add ?provider=ytel later when testing Ytel deliveries.',
'accepted_methods': ['POST'],
})
body = request.body or b''
if len(body) > MAX_WEBHOOK_BODY_BYTES:
logger.warning('Rejected oversized webhook body: %s bytes', len(body))
return JsonResponse({'ok': False, 'error': 'Payload is too large.'}, status=413)
content_type = request.META.get('CONTENT_TYPE', '')[:180]
raw_body = body.decode('utf-8', errors='replace')
payload, parsed = _parse_webhook_payload(request, raw_body, content_type)
provider = _normalise_label(
request.GET.get('provider')
or request.headers.get('X-Webhook-Provider')
or _payload_lookup(payload, 'provider', 'source'),
fallback='generic',
max_length=60,
)
event_type = _normalise_label(
request.headers.get('X-Event-Type')
or request.headers.get('X-Webhook-Event')
or request.headers.get('X-Hook-Event')
or request.headers.get('X-Ytel-Event-Type')
or _payload_lookup(payload, 'event_type', 'eventType', 'event', 'type', 'action', 'status'),
fallback='',
max_length=120,
)
delivery_id = _normalise_label(
request.headers.get('X-Delivery-Id')
or request.headers.get('X-Webhook-Id')
or request.headers.get('X-Request-Id')
or request.headers.get('X-Event-Id')
or request.headers.get('X-Ytel-Delivery-Id')
or request.headers.get('X-Ytel-Event-Id')
or request.headers.get('Cf-Ray'),
fallback='',
max_length=160,
)
external_id = _normalise_label(
_payload_lookup(payload, 'external_id', 'externalId', 'event_id', 'eventId', 'id', 'call_id', 'callId', 'uuid', 'sid'),
fallback='',
max_length=160,
)
event = WebhookEvent.objects.create(
provider=provider,
event_type=event_type,
external_id=external_id,
delivery_id=delivery_id,
source_ip=_source_ip(request),
content_type=content_type,
headers=_safe_request_headers(request),
query_params=_query_params(request),
payload=payload,
raw_body=raw_body,
)
return JsonResponse({
'ok': True,
'accepted': True,
'parsed': parsed,
'id': event.pk,
'provider': event.provider,
'event_type': event.display_event_type,
'received_at': event.received_at.isoformat(),
}, status=202)
def _public_absolute_uri(request, path):
url = request.build_absolute_uri(path)
host = request.get_host().split(':')[0]
if host not in ('127.0.0.1', 'localhost') and url.startswith('http://'):
return 'https://' + url[len('http://'):]
return url
def _parse_webhook_payload(request, raw_body, content_type):
if raw_body and ('application/json' in content_type or raw_body.lstrip().startswith(('{', '['))):
try:
return json.loads(raw_body), True
except json.JSONDecodeError:
return {'_unparsed_preview': raw_body[:2000]}, False
if request.POST:
return _multi_value_dict(request.POST), True
if raw_body:
return {'_raw_preview': raw_body[:2000]}, False
return {}, True
def _safe_request_headers(request):
sensitive_markers = ('authorization', 'cookie', 'token', 'secret', 'key', 'signature')
safe_headers = {}
for name, value in request.headers.items():
if any(marker in name.lower() for marker in sensitive_markers):
safe_headers[name] = '[redacted]'
else:
safe_headers[name] = str(value)[:500]
return safe_headers
def _query_params(request):
return _multi_value_dict(request.GET)
def _multi_value_dict(query_dict):
return {
key: values if len(values := query_dict.getlist(key)) > 1 else query_dict.get(key)
for key in query_dict
}
def _payload_lookup(payload, *keys):
if not isinstance(payload, dict):
return None
for key in keys:
value = payload.get(key)
if value not in (None, ''):
return value
for nested_key in ('data', 'event', 'call', 'payload'):
nested = payload.get(nested_key)
if isinstance(nested, dict):
value = _payload_lookup(nested, *keys)
if value not in (None, ''):
return value
return None
def _normalise_label(value, fallback='', max_length=120):
if value in (None, ''):
return fallback
if isinstance(value, (dict, list)):
value = json.dumps(value, sort_keys=True)
return str(value).strip()[:max_length] or fallback
def _source_ip(request):
raw_ip = (
request.headers.get('Cf-Connecting-Ip')
or request.headers.get('X-Real-Ip')
or request.headers.get('X-Forwarded-For')
or request.META.get('REMOTE_ADDR')
or ''
)
candidate = raw_ip.split(',')[0].strip()
try:
ipaddress.ip_address(candidate)
except ValueError:
return None
return candidate