152 lines
6.1 KiB
Python
152 lines
6.1 KiB
Python
from decimal import Decimal
|
|
|
|
from django.db import models
|
|
from django.urls import reverse
|
|
|
|
|
|
class PilotCall(models.Model):
|
|
SOURCE_YTEL = 'ytel'
|
|
SOURCE_RINGCENTRAL = 'ringcentral'
|
|
SOURCE_MANUAL = 'manual'
|
|
|
|
CALL_SOURCE_CHOICES = [
|
|
(SOURCE_YTEL, 'Ytel'),
|
|
(SOURCE_RINGCENTRAL, 'RingCentral fallback'),
|
|
(SOURCE_MANUAL, 'Manual/recording test'),
|
|
]
|
|
|
|
STATUS_UNCONFIRMED = 'unconfirmed'
|
|
STATUS_AVAILABLE = 'available'
|
|
STATUS_BLOCKED = 'blocked'
|
|
STATUS_PARTNER = 'partner'
|
|
|
|
INTEGRATION_STATUS_CHOICES = [
|
|
(STATUS_UNCONFIRMED, 'Not confirmed'),
|
|
(STATUS_AVAILABLE, 'Available'),
|
|
(STATUS_BLOCKED, 'Blocked / not exposed'),
|
|
(STATUS_PARTNER, 'Needs enterprise/partner access'),
|
|
]
|
|
|
|
client_name = models.CharField(max_length=140)
|
|
phone = models.CharField(max_length=40)
|
|
email = models.EmailField(blank=True)
|
|
call_source = models.CharField(max_length=24, choices=CALL_SOURCE_CHOICES, default=SOURCE_YTEL)
|
|
ytel_audio_status = models.CharField(max_length=24, choices=INTEGRATION_STATUS_CHOICES, default=STATUS_UNCONFIRMED)
|
|
logics_api_status = models.CharField(max_length=24, choices=INTEGRATION_STATUS_CHOICES, default=STATUS_UNCONFIRMED)
|
|
|
|
transcript = models.TextField(help_text='Paste a live transcript segment or recorded-call transcript for the pilot.')
|
|
irs_debt = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
state_debt = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
tax_years = models.CharField(max_length=120, blank=True)
|
|
filing_status = models.CharField(max_length=80, blank=True)
|
|
employment_type = models.CharField(max_length=120, blank=True)
|
|
occupation = models.CharField(max_length=120, blank=True)
|
|
monthly_gross_income = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
spouse_income = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
mortgage = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
rent = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
utilities = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
car_payment = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
insurance = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
student_loans = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
credit_cards = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
assets = models.TextField(blank=True)
|
|
business_expenses = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
irs_payment_plan = models.BooleanField(default=False)
|
|
levy = models.BooleanField(default=False)
|
|
garnishment = models.BooleanField(default=False)
|
|
best_callback_time = models.CharField(max_length=120, blank=True)
|
|
agent_notes = models.TextField(blank=True)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at']
|
|
verbose_name = 'pilot call'
|
|
verbose_name_plural = 'pilot calls'
|
|
|
|
def __str__(self):
|
|
return f'{self.client_name} · {self.get_call_source_display()}'
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('call_detail', args=[self.pk])
|
|
|
|
@property
|
|
def required_fields(self):
|
|
return [
|
|
('IRS debt', self.irs_debt),
|
|
('Tax years', self.tax_years),
|
|
('Employment', self.employment_type),
|
|
('Monthly income', self.monthly_gross_income),
|
|
('Best callback', self.best_callback_time),
|
|
]
|
|
|
|
@property
|
|
def completion_score(self):
|
|
if not self.required_fields:
|
|
return 0
|
|
completed = sum(1 for _label, value in self.required_fields if self._has_value(value))
|
|
return round((completed / len(self.required_fields)) * 100)
|
|
|
|
@property
|
|
def missing_required_labels(self):
|
|
return [label for label, value in self.required_fields if not self._has_value(value)]
|
|
|
|
@property
|
|
def urgent_flags(self):
|
|
flags = []
|
|
if self.levy:
|
|
flags.append('Levy mentioned')
|
|
if self.garnishment:
|
|
flags.append('Wage garnishment mentioned')
|
|
if self.irs_debt and self.irs_debt > Decimal('50000'):
|
|
flags.append('Debt over $50,000')
|
|
if self.monthly_gross_income is not None and self.monthly_gross_income <= 0:
|
|
flags.append('No monthly income captured')
|
|
return flags
|
|
|
|
@staticmethod
|
|
def _has_value(value):
|
|
if value is None:
|
|
return False
|
|
if isinstance(value, str):
|
|
return bool(value.strip())
|
|
return True
|
|
|
|
|
|
|
|
class WebhookEvent(models.Model):
|
|
PROVIDER_GENERIC = 'generic'
|
|
|
|
provider = models.CharField(max_length=60, default=PROVIDER_GENERIC)
|
|
event_type = models.CharField(max_length=120, blank=True)
|
|
external_id = models.CharField(max_length=160, blank=True)
|
|
delivery_id = models.CharField(max_length=160, blank=True)
|
|
source_ip = models.GenericIPAddressField(null=True, blank=True)
|
|
content_type = models.CharField(max_length=180, blank=True)
|
|
headers = models.JSONField(default=dict, blank=True)
|
|
query_params = models.JSONField(default=dict, blank=True)
|
|
payload = models.JSONField(default=dict, blank=True)
|
|
raw_body = models.TextField(blank=True)
|
|
processed = models.BooleanField(default=False)
|
|
notes = models.TextField(blank=True)
|
|
received_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ['-received_at']
|
|
indexes = [
|
|
models.Index(fields=['provider', 'event_type']),
|
|
models.Index(fields=['received_at']),
|
|
]
|
|
verbose_name = 'webhook event'
|
|
verbose_name_plural = 'webhook events'
|
|
|
|
def __str__(self):
|
|
label = self.event_type or self.external_id or f'event #{self.pk}'
|
|
return f'{self.provider}: {label}'
|
|
|
|
@property
|
|
def display_event_type(self):
|
|
return self.event_type or 'unclassified'
|