from datetime import datetime, timedelta
from typing import Dict, Any
import io
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate,
Paragraph,
Spacer,
Table,
TableStyle,
Image,
)
from reportlab.lib.units import inch
from reportlab.lib import colors
from reportlab.platypus.flowables import HRFlowable
def generate_claim_report_pdf(
damage_analysis: Dict[str, Any],
incident_data: Dict[str, Any],
image_path: str = None,
) -> bytes:
"""
Generate a professional insurance claim report as PDF from analyzed data.
Args:
damage_analysis: Results from image damage analysis
incident_data: Processed incident data from transcript
image_path: Optional path to the damage photo to include in appendix
Returns:
PDF bytes for the formatted claim report
"""
# Create a BytesIO buffer to hold the PDF
buffer = io.BytesIO()
# Generate claim reference number
timestamp = datetime.now()
claim_ref = f"CLM-{timestamp.strftime('%Y%m%d')}-{timestamp.strftime('%H%M%S')}"
# Extract key information safely
damage_description = damage_analysis.get("description", "Vehicle damage detected")
damage_severity = damage_analysis.get("severity", "moderate")
damage_location = damage_analysis.get("location", "unknown")
# Get incident details safely with date conversion
date_location = incident_data.get("date_location", {})
# Convert relative dates to actual dates
actual_date = _convert_relative_date(date_location.get("date", "Not specified"))
date_location_converted = {**date_location, "date": actual_date}
parties_involved = incident_data.get("parties_involved", {})
fault_assessment = incident_data.get("fault_assessment", {})
incident_description = incident_data.get("incident_description", {})
injuries_medical = incident_data.get("injuries_medical", {})
# Generate assessments
priority = _get_priority_level(
damage_severity, injuries_medical.get("anyone_injured", "no")
)
cost_estimate = _estimate_cost_range(damage_severity)
recommendation = _get_recommendation(
damage_severity, injuries_medical.get("anyone_injured", "no")
)
# Create PDF document with professional margins
doc = SimpleDocTemplate(
buffer,
pagesize=letter,
rightMargin=0.75 * inch,
leftMargin=0.75 * inch,
topMargin=0.75 * inch,
bottomMargin=0.75 * inch,
)
# Get styles
styles = getSampleStyleSheet()
# Professional custom styles - all black text
title_style = ParagraphStyle(
"ProfessionalTitle",
parent=styles["Heading1"],
fontSize=20,
textColor=colors.black,
spaceAfter=16,
spaceBefore=0,
alignment=TA_CENTER,
fontName="Helvetica-Bold",
)
header_style = ParagraphStyle(
"ProfessionalHeader",
parent=styles["Heading2"],
fontSize=14,
textColor=colors.black,
spaceAfter=8,
spaceBefore=16,
fontName="Helvetica-Bold",
)
subheader_style = ParagraphStyle(
"ProfessionalSubHeader",
parent=styles["Heading3"],
fontSize=12,
textColor=colors.black,
spaceAfter=6,
spaceBefore=8,
fontName="Helvetica-Bold",
)
body_style = ParagraphStyle(
"ProfessionalBody",
parent=styles["Normal"],
fontSize=10,
spaceAfter=4,
fontName="Helvetica",
textColor=colors.black,
alignment=TA_JUSTIFY,
)
# Professional table style
def create_professional_table_style():
return TableStyle(
[
("BACKGROUND", (0, 0), (0, -1), colors.lightgrey),
("TEXTCOLOR", (0, 0), (-1, -1), colors.black),
("ALIGN", (0, 0), (-1, -1), "LEFT"),
("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
("FONTNAME", (1, 0), (1, -1), "Helvetica"),
("FONTSIZE", (0, 0), (-1, -1), 10),
("GRID", (0, 0), (-1, -1), 1, colors.black),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
# Helper function to create table cells with proper text wrapping
def create_table_cell(text: str, is_header: bool = False) -> Paragraph:
style = ParagraphStyle(
"TableCell",
parent=styles["Normal"],
fontSize=10,
textColor=colors.black,
fontName="Helvetica-Bold" if is_header else "Helvetica",
leftIndent=0,
rightIndent=0,
spaceAfter=0,
spaceBefore=0,
)
return Paragraph(str(text), style)
# Build the document content
story = []
# Professional Header
story.append(Paragraph("AUTOMOBILE INSURANCE CLAIM REPORT", title_style))
story.append(Spacer(1, 12))
# Claim Information Section
claim_info_data = [
[
create_table_cell("Claim Reference Number:", True),
create_table_cell(claim_ref),
],
[
create_table_cell("Report Generated:", True),
create_table_cell(timestamp.strftime("%B %d, %Y at %I:%M %p")),
],
[
create_table_cell("Claim Status:", True),
create_table_cell("Under Review - Pending Adjuster Assignment"),
],
[
create_table_cell("Processing Priority:", True),
create_table_cell(
priority.replace("🔴", "").replace("🟡", "").replace("🟢", "").strip()
),
],
]
claim_info_table = Table(claim_info_data, colWidths=[2.2 * inch, 4.5 * inch])
claim_info_table.setStyle(create_professional_table_style())
story.append(claim_info_table)
story.append(Spacer(1, 16))
# Executive Summary
story.append(Paragraph("EXECUTIVE SUMMARY", header_style))
story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
story.append(Spacer(1, 8))
summary_text = f"""
This claim involves a motor vehicle accident resulting in {damage_severity.lower()} damage to the
insured vehicle's {damage_location.replace('-', ' ').lower()} area. Based on initial assessment
of submitted photographic evidence and incident description, this appears to be a legitimate claim
{"requiring expedited processing due to reported injuries" if injuries_medical.get('anyone_injured', 'no').lower() == 'yes' else "suitable for standard processing procedures"}.
Primary Recommendation: {recommendation}
Preliminary Cost Assessment: {cost_estimate}
"""
story.append(Paragraph(summary_text, body_style))
story.append(Spacer(1, 16))
# Incident Details
story.append(Paragraph("INCIDENT DETAILS", header_style))
story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
story.append(Spacer(1, 8))
# Date, Time & Location
story.append(Paragraph("Date, Time and Location of Loss", subheader_style))
incident_data_table = [
[
create_table_cell("Date of Loss:", True),
create_table_cell(
date_location_converted.get("date", "Not specified in report")
),
],
[
create_table_cell("Time of Loss:", True),
create_table_cell(date_location.get("time", "Not specified in report")),
],
[
create_table_cell("Location of Loss:", True),
create_table_cell(date_location.get("location", "Not specified in report")),
],
]
incident_table = Table(incident_data_table, colWidths=[2 * inch, 4.7 * inch])
incident_table.setStyle(create_professional_table_style())
story.append(incident_table)
story.append(Spacer(1, 12))
# Description of Incident
story.append(Paragraph("Description of Incident", subheader_style))
incident_desc = incident_description.get(
"what_happened", "No detailed description provided in initial report"
)
story.append(Paragraph(incident_desc, body_style))
story.append(Spacer(1, 16))
# Parties Involved
story.append(Paragraph("PARTIES INVOLVED", header_style))
story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
story.append(Spacer(1, 8))
parties_data = [
[
create_table_cell("Other Party Driver Name:", True),
create_table_cell(
parties_involved.get("other_driver_name", "Information not provided")
),
],
[
create_table_cell("Other Party Vehicle:", True),
create_table_cell(
parties_involved.get("other_driver_vehicle", "Information not provided")
),
],
[
create_table_cell("Witness Information:", True),
create_table_cell(
parties_involved.get("witnesses", "No witnesses reported at this time")
),
],
]
parties_table = Table(parties_data, colWidths=[2 * inch, 4.7 * inch])
parties_table.setStyle(create_professional_table_style())
story.append(parties_table)
story.append(Spacer(1, 16))
# Vehicle Damage Assessment
story.append(Paragraph("VEHICLE DAMAGE ASSESSMENT", header_style))
story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
story.append(Spacer(1, 8))
# Clean up damage description - wrap long text properly
damage_desc_clean = _format_damage_description(damage_description)
damage_data = [
[
create_table_cell("Damage Severity Classification:", True),
create_table_cell(damage_severity.title()),
],
[
create_table_cell("Primary Damage Location:", True),
create_table_cell(damage_location.replace("-", " ").title()),
],
[
create_table_cell("Damage Description:", True),
create_table_cell(damage_desc_clean),
],
[
create_table_cell("Preliminary Repair Estimate:", True),
create_table_cell(cost_estimate),
],
]
damage_table = Table(damage_data, colWidths=[2 * inch, 4.7 * inch])
damage_table.setStyle(create_professional_table_style())
story.append(damage_table)
story.append(Spacer(1, 12))
# Evidence Documentation
evidence_text = """
Photographic Evidence: Digital photographs of vehicle damage received and analyzed using automated assessment tools.
Incident Documentation: Verbal account transcribed and processed for key incident details.
"""
story.append(Paragraph(evidence_text, body_style))
story.append(Spacer(1, 16))
# Injury and Medical Information
story.append(Paragraph("INJURY AND MEDICAL INFORMATION", header_style))
story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
story.append(Spacer(1, 8))
injury_data = [
[
create_table_cell("Personal Injuries Reported:", True),
create_table_cell(
injuries_medical.get("anyone_injured", "Not specified").title()
),
],
[
create_table_cell("Injury Details:", True),
create_table_cell(
injuries_medical.get(
"injury_details", "No specific injury details provided"
)
),
],
[
create_table_cell("Medical Treatment Sought:", True),
create_table_cell(
injuries_medical.get("medical_attention", "Information not available")
),
],
[
create_table_cell("Injury Severity Assessment:", True),
create_table_cell(
injuries_medical.get("injury_severity", "None reported").title()
),
],
]
injury_table = Table(injury_data, colWidths=[2 * inch, 4.7 * inch])
injury_table.setStyle(create_professional_table_style())
story.append(injury_table)
story.append(Spacer(1, 16))
# Liability Assessment
story.append(Paragraph("PRELIMINARY LIABILITY ASSESSMENT", header_style))
story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
story.append(Spacer(1, 8))
fault_data = [
[
create_table_cell("Initial Fault Determination:", True),
create_table_cell(
_format_fault_determination(
fault_assessment.get("who_at_fault", "Under investigation")
)
),
],
[
create_table_cell("Basis for Determination:", True),
create_table_cell(
fault_assessment.get(
"reason", "Pending detailed investigation and evidence review"
)
),
],
]
fault_table = Table(fault_data, colWidths=[2 * inch, 4.7 * inch])
fault_table.setStyle(create_professional_table_style())
story.append(fault_table)
story.append(Spacer(1, 16))
# Cost Analysis
story.append(Paragraph("PRELIMINARY COST ANALYSIS", header_style))
story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
story.append(Spacer(1, 8))
medical_costs = _format_medical_costs(injuries_medical)
total_estimate = _calculate_total_estimate(cost_estimate, injuries_medical)
cost_data = [
[
create_table_cell("Vehicle Repair Estimate:", True),
create_table_cell(cost_estimate),
],
[
create_table_cell("Medical Expense Estimate:", True),
create_table_cell(medical_costs),
],
[
create_table_cell("Total Preliminary Estimate:", True),
create_table_cell(total_estimate),
],
]
cost_table = Table(cost_data, colWidths=[2 * inch, 4.7 * inch])
cost_style = create_professional_table_style()
# Highlight total row
cost_style.add("BACKGROUND", (0, 2), (1, 2), colors.lightgrey)
cost_style.add("FONTNAME", (0, 2), (1, 2), "Helvetica-Bold")
cost_table.setStyle(cost_style)
story.append(cost_table)
story.append(Spacer(1, 16))
# Action Items and Next Steps
story.append(Paragraph("RECOMMENDED ACTION ITEMS", header_style))
story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
story.append(Spacer(1, 8))
next_steps = _generate_next_steps_professional(
damage_severity, injuries_medical, fault_assessment
)
story.append(Paragraph(next_steps, body_style))
story.append(Spacer(1, 16))
# Processing Notes
story.append(Paragraph("PROCESSING NOTES", header_style))
story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
story.append(Spacer(1, 8))
processing_notes = f"""
This preliminary assessment was generated using automated analysis tools to expedite initial claim processing.
Photographic evidence and incident descriptions were processed using artificial intelligence to provide rapid
initial assessment. {"Given the reported injuries, this claim has been flagged for expedited human review." if injuries_medical.get('anyone_injured', 'no').lower() == 'yes' else "Standard processing timeline applies per company guidelines."}
Final claim determination requires licensed adjuster review and approval.
"""
story.append(Paragraph(processing_notes, body_style))
story.append(Spacer(1, 20))
# Footer Information
footer_data = [
[
create_table_cell("Report Generated By:", True),
create_table_cell("AI Claims Processing System"),
],
[
create_table_cell("Processing Timestamp:", True),
create_table_cell(timestamp.strftime("%I:%M %p EST")),
],
[
create_table_cell("Human Review Status:", True),
create_table_cell("Required - Pending Assignment"),
],
[
create_table_cell("System Confidence Level:", True),
create_table_cell("High - Standard Processing Recommended"),
],
]
footer_table = Table(footer_data, colWidths=[2 * inch, 4.7 * inch])
footer_table.setStyle(create_professional_table_style())
story.append(footer_table)
story.append(Spacer(1, 12))
# Evidence/Appendix Section
story.append(Paragraph("APPENDIX - EVIDENCE DOCUMENTATION", header_style))
story.append(HRFlowable(width="100%", thickness=1, color=colors.black))
story.append(Spacer(1, 8))
# Raw transcript section
story.append(Paragraph("Raw Incident Description Transcript", subheader_style))
# Get the original raw description
raw_description = incident_description.get(
"what_happened", "No transcript available"
)
# Create a formatted transcript
transcript_text = f"""
Original Incident Account (Unedited):
"{raw_description}"
Note: This is the unedited transcript of the policyholder's account of the incident as provided during initial report.
"""
story.append(Paragraph(transcript_text, body_style))
story.append(Spacer(1, 12))
# Damage photo section
story.append(Paragraph("Photographic Evidence", subheader_style))
if image_path:
try:
# Add the damage photo
img = Image(image_path, width=4 * inch, height=3 * inch)
story.append(img)
story.append(Spacer(1, 8))
photo_caption = Paragraph(
"Figure 1: Vehicle damage photograph submitted with initial claim report. "
"Image analyzed using automated damage assessment tools.",
ParagraphStyle(
"PhotoCaption",
parent=styles["Normal"],
fontSize=9,
textColor=colors.black,
alignment=TA_CENTER,
spaceAfter=8,
),
)
story.append(photo_caption)
except Exception as e:
# If image can't be loaded, show placeholder text
print(f"Error loading damage photo: {e}")
story.append(
Paragraph(
"Damage photograph submitted with claim (unable to display in this report format).",
body_style,
)
)
else:
story.append(
Paragraph(
"Damage photograph submitted with claim and analyzed using automated assessment tools. "
"Original digital file maintained in claim documentation system.",
body_style,
)
)
story.append(Spacer(1, 12))
# Raw damage analysis
story.append(Paragraph("Technical Damage Analysis Output", subheader_style))
# Get the raw damage description
raw_damage_analysis = damage_analysis.get(
"description", "No technical analysis available"
)
technical_analysis_text = f"""
Automated Damage Assessment Output (Technical):
{_format_technical_description(raw_damage_analysis)}
Note: This is the raw output from the automated damage assessment system.
The summary version appears in the main report above.
"""
story.append(Paragraph(technical_analysis_text, body_style))
story.append(Spacer(1, 16))
# Legal Disclaimer
disclaimer = Paragraph(
"This automated preliminary assessment is provided for initial processing purposes only. "
"All claim determinations are subject to policy terms, conditions, and coverage verification. "
"Final settlement authority rests with assigned licensed adjuster pending completion of full investigation.",
ParagraphStyle(
"Disclaimer",
parent=styles["Normal"],
fontSize=8,
textColor=colors.black,
alignment=TA_CENTER,
leftIndent=0.5 * inch,
rightIndent=0.5 * inch,
),
)
story.append(disclaimer)
# Build PDF
doc.build(story)
# Get the PDF bytes
pdf_bytes = buffer.getvalue()
buffer.close()
return pdf_bytes
def _format_damage_description(description: str) -> str:
"""Clean and format damage description for professional presentation"""
if not description or len(description) < 50:
return description
# Remove redundant technical formatting
cleaned = description.replace("###", "").replace("**", "").replace("- **", "• ")
# Extract key summary if description is very long
if len(cleaned) > 300:
lines = cleaned.split("\n")
summary_lines = [
line.strip()
for line in lines
if line.strip()
and not line.strip().startswith("##")
and len(line.strip()) > 10
][:3]
return " ".join(summary_lines[:2]) + "..."
return cleaned[:250] + "..." if len(cleaned) > 250 else cleaned
def _format_fault_determination(fault: str) -> str:
"""Format fault determination for professional presentation"""
fault_map = {
"other_driver": "Other Party - Preliminary",
"policyholder": "Policyholder - Preliminary",
"unclear": "Undetermined - Investigation Required",
"both": "Comparative Negligence - Investigation Required",
}
return fault_map.get(fault.lower(), "Under Investigation")
def _get_priority_level(damage_severity: str, injuries_reported: str) -> str:
"""Determine claim priority using professional terminology"""
if injuries_reported.lower() == "yes":
return "HIGH PRIORITY - Personal Injury Claim"
elif damage_severity.lower() == "major":
return "ELEVATED PRIORITY - Significant Property Damage"
elif damage_severity.lower() == "moderate":
return "STANDARD PRIORITY - Moderate Property Damage"
else:
return "ROUTINE PRIORITY - Minor Property Damage"
def _estimate_cost_range(damage_severity: str) -> str:
"""Estimate repair costs based on damage severity"""
severity_costs = {
"minor": "$750 - $2,500",
"moderate": "$2,500 - $7,500",
"major": "$7,500 - $18,000",
"severe": "$18,000 - $35,000",
}
return severity_costs.get(damage_severity.lower(), "$2,000 - $5,000")
def _get_recommendation(damage_severity: str, injuries_reported: str) -> str:
"""Generate professional recommendation"""
if injuries_reported.lower() == "yes":
return "IMMEDIATE ACTION REQUIRED: Assign specialist adjuster for personal injury claim within 24 hours"
elif damage_severity.lower() in ["major", "severe"]:
return "PRIORITY PROCESSING: Schedule comprehensive inspection within 48 hours"
else:
return "STANDARD PROCESSING: Assign adjuster within normal service level agreement timeframe"
def _format_medical_costs(injuries_medical: Dict[str, Any]) -> str:
"""Format medical cost estimate professionally"""
if injuries_medical.get("anyone_injured", "no").lower() == "yes":
severity = injuries_medical.get("injury_severity", "minor").lower()
if severity == "severe":
return "$15,000 - $75,000 (Preliminary)"
elif severity == "moderate":
return "$3,000 - $15,000 (Preliminary)"
else:
return "$500 - $3,000 (Preliminary)"
return "No medical expenses anticipated"
def _calculate_total_estimate(
repair_cost: str, injuries_medical: Dict[str, Any]
) -> str:
"""Calculate total claim estimate professionally"""
if injuries_medical.get("anyone_injured", "no").lower() == "yes":
return f"{repair_cost} plus medical expenses (subject to investigation)"
return repair_cost
def _generate_next_steps_professional(
damage_severity: str,
injuries_medical: Dict[str, Any],
fault_assessment: Dict[str, Any],
) -> str:
"""Generate professional action items"""
steps = []
# Always required steps
steps.append(
"1. Adjuster Assignment: Assign licensed adjuster for detailed investigation and coverage verification."
)
steps.append(
"2. Vehicle Inspection: Schedule comprehensive damage assessment with approved appraiser."
)
steps.append(
"3. Third Party Contact: Attempt contact with other party's insurance carrier for coordination."
)
# Conditional steps based on circumstances
step_num = 4
if injuries_medical.get("anyone_injured", "no").lower() == "yes":
steps.append(
f"{step_num}. Medical Documentation: Request medical records and treatment documentation from healthcare providers."
)
step_num += 1
steps.append(
f"{step_num}. Injury Specialist: Engage personal injury specialist for claim evaluation."
)
step_num += 1
if fault_assessment.get("who_at_fault", "unclear").lower() == "unclear":
steps.append(
f"{step_num}. Police Report: Obtain official police report if available for liability determination."
)
step_num += 1
if damage_severity.lower() in ["major", "severe"]:
steps.append(
f"{step_num}. Multiple Estimates: Secure at least two independent repair estimates for cost validation."
)
step_num += 1
# Final step
steps.append(
f"{step_num}. Customer Communication: Contact policyholder within 24 hours to confirm receipt and outline next steps."
)
return "
".join(steps)
def _convert_relative_date(date_str: str) -> str:
"""Convert relative dates like 'today', 'yesterday' to actual dates"""
if not date_str or date_str.lower() in ["not specified", "unknown", ""]:
return "Not specified in report"
# Get current date
today = datetime.now()
# Dictionary of relative date conversions
relative_dates = {
"today": today.strftime("%B %d, %Y"),
"yesterday": (today - timedelta(days=1)).strftime("%B %d, %Y"),
"yesterday night": (today - timedelta(days=1)).strftime("%B %d, %Y (evening)"),
"yesterday evening": (today - timedelta(days=1)).strftime(
"%B %d, %Y (evening)"
),
"last night": (today - timedelta(days=1)).strftime("%B %d, %Y (night)"),
"this morning": today.strftime("%B %d, %Y (morning)"),
"this afternoon": today.strftime("%B %d, %Y (afternoon)"),
"this evening": today.strftime("%B %d, %Y (evening)"),
"2 days ago": (today - timedelta(days=2)).strftime("%B %d, %Y"),
"3 days ago": (today - timedelta(days=3)).strftime("%B %d, %Y"),
"a few days ago": (today - timedelta(days=2)).strftime(
"%B %d, %Y (approximate)"
),
"earlier today": today.strftime("%B %d, %Y (earlier)"),
"day before yesterday": (today - timedelta(days=2)).strftime("%B %d, %Y"),
}
# Check for exact matches first
date_lower = date_str.lower().strip()
if date_lower in relative_dates:
return relative_dates[date_lower]
# Check for partial matches
for relative_term, actual_date in relative_dates.items():
if relative_term in date_lower:
return actual_date
# If no relative date found, return original
return date_str
def _format_technical_description(description: str) -> str:
"""Format technical damage description for appendix"""
if not description:
return "No technical analysis data available"
# Clean up technical formatting but preserve more detail than main report
cleaned = description.replace("###", "").replace("**", "")
# If it's very long, keep more content than the main report version
if len(cleaned) > 800:
# Split into sections and keep first few sections
sections = cleaned.split("\n\n")
important_sections = [
s.strip() for s in sections if s.strip() and len(s.strip()) > 20
]
return (
"\n\n".join(important_sections[:4])
+ "\n\n[Additional technical details available in system logs]"
)
return cleaned