1. System Architecture

End-to-end component view — what runs where.

flowchart TB
      subgraph Browser["Browser"]
          FE[React Frontend\n@zango-core/crud]
      end

      subgraph Docker["Docker Compose"]
          APP[Django App\nZango Framework\n:8000]
          CELERY[Celery Workers\n2+ concurrent]
          BEAT[Celery Beat\nScheduler]
          REDIS[(Redis\nBroker + Cache)]
          PG[(PostgreSQL\nzango_project DB)]
      end

      subgraph Claude["Claude API"]
          CV[ClaimValidator\nAgent]
          DA[DenialAnalyzer\nAgent]
          AD[AppealDrafter\nAgent]
      end

      FE -->|HTTP / REST| APP
      APP --> PG
      APP --> REDIS
      CELERY --> REDIS
      CELERY --> PG
      BEAT --> REDIS
      CELERY -->|"agent.run()"| CV
      CELERY -->|"agent.run() concurrent"| DA
      CELERY -->|"agent.run() concurrent"| AD
      CV -->|update_claim_ai_result| PG
      DA -->|update_claim_ai_result| PG
      AD -->|update_claim_ai_result| PG

      classDef frontend fill:#e0f2fe,stroke:#0284c7,color:#0c4a6e;
      classDef service fill:#fef3c7,stroke:#d97706,color:#78350f;
      classDef datastore fill:#dcfce7,stroke:#16a34a,color:#14532d;
      classDef agent fill:#ede9fe,stroke:#7c3aed,color:#3b0764;

      class FE frontend;
      class APP,CELERY,BEAT service;
      class REDIS,PG datastore;
      class CV,DA,AD agent

2. Claim Lifecycle (State Machine)

[CORRECTED 2026-07-19] Reconciled against claims/workflows.py — previous version had direct Submitted→Approved/Denied paths that don't exist (UnderReview is mandatory before either), stale Claim "PartiallyPaid" transitions that belong to Invoice not Claim, a direct Denied→Closed path the code doesn't have, and was missing reopen/approve_appeal entirely. Also: deny is BillingManager-only, not BillingStaff — BillingStaff can only submit and appeal.

stateDiagram-v2
    [*] --> Draft
    Draft --> Submitted : submit\n[any role]\n→ fires ClaimValidator
    Submitted --> UnderReview : begin_review\n[BillingManager]
    UnderReview --> Approved : approve\n[BillingManager]
    UnderReview --> Denied : deny + form\n[BillingManager]\n→ fires DenialAnalyzer\n+ AppealDrafter CONCURRENTLY
    Denied --> Appealed : appeal\n[any role]
    Denied --> UnderReview : reopen\n[BillingManager]
    Appealed --> Approved : approve_appeal\n[BillingManager]
    Appealed --> Closed : close_from_appealed\n[BillingManager]
    Approved --> Closed : close\n[BillingManager]

    note right of Denied
        Two Celery tasks
        dispatched simultaneously:
        run_denial_analyzer()
        run_appeal_drafter()
    end note

3. Concurrent Agent Dispatch Flow

What happens the moment a claim is denied.

%%{init: {"theme": "dark"}}%%
  sequenceDiagram
      participant Staff as BillingManager
      participant UI as React UI
      participant APP as Django App
      participant WF as ClaimWorkflow
      participant REDIS as Redis Broker
      participant W1 as Celery Worker 1
      participant W2 as Celery Worker 2
      participant CLAUDE as Claude API
      participant DB as PostgreSQL

      Note over Staff: Deny is BillingManager-only (corrected 2026-07-19); participant renamed from BillingStaff
      Staff->>UI: Click Deny and fill denial form
      UI->>APP: POST /claims/workflow/transition/
      APP->>WF: execute denial transition
      WF->>DB: update Claim status to Denied
      WF->>DB: save denial_reason_code and description
      WF->>REDIS: delay(run_denial_analyzer, claim_id)
      WF->>REDIS: delay(run_appeal_drafter, claim_id)
      APP-->>UI: 200 OK transition complete

      par Concurrent execution
          REDIS->>W1: dequeue run_denial_analyzer
          W1->>CLAUDE: DenialAnalyzer agent.run()
          CLAUDE-->>W1: JSON analysis result
          W1->>DB: Claim.ai_denial_analysis = result
      and
          REDIS->>W2: dequeue run_appeal_drafter
          W2->>CLAUDE: AppealDrafter agent.run()
          CLAUDE-->>W2: Appeal letter text
          W2->>DB: Claim.ai_appeal_draft = letter
      end

      Note over UI: AI Insights tab polls every 5s
      UI->>APP: GET /claims/{id}/ poll
      APP->>DB: fetch Claim
      DB-->>APP: ai_denial_analysis and ai_appeal_draft populated
      APP-->>UI: both fields present
      UI->>Staff: Show DenialAnalysis and AppealDraft

4. Invoice Lifecycle (State Machine)

[CORRECTED 2026-07-19] Reconciled against invoices/workflows.pysend is available to any role, not BillingStaff-only; a direct Sent→Paid path and an Overdue→PartiallyPaid path both exist and were missing.

stateDiagram-v2
    [*] --> Draft
    Draft --> Sent : send\n[any role]
    Sent --> PartiallyPaid : record_partial\n[any role]
    Sent --> Paid : mark_paid_from_sent\n[any role]
    Sent --> Overdue : mark_overdue\n[BillingManager]
    Sent --> Voided : void\n[BillingManager]
    Overdue --> PartiallyPaid : record_partial_from_overdue\n[any role]
    Overdue --> Paid : mark_paid_from_overdue\n[any role]
    PartiallyPaid --> Paid : mark_paid\n[any role]

5. Backend Module Dependency Graph

graph LR
      subgraph "Zango App: patientbilling"
          PAT[patients module\nPatient model]
          CLM[claims module\nInsurancePayer\nClaim\nClaimLineItem\nClaimWorkflow]
          INV[invoices module\nInvoice\nInvoiceLineItem\nPayment\nInvoiceWorkflow]
          AGT[agents module\ntools.py\ntasks.py]
          APP[app module\nserves React build]
          PAY[payers module\nInsurancePayer CRUD views/forms/policies\n**added 2026-07-19: was missing from this diagram**]
      end

      CLM -->|ZForeignKey| PAT
      INV -->|ZForeignKey| PAT
      AGT -->|imports models| CLM
      AGT -->|imports models| PAT
      CLM -->|"done() triggers"| AGT
      APP -.->|static files| FE[React Build]
      CLM -.->|InsurancePayer FK, but CRUD views live in payers module| PAY

6. Data Model (ER Diagram)