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