Collections Workflow - Small Claims Process

Status: Planning Last Updated: 2025-11-21 Owner: Operations Team

Overview

This document outlines the automated collections workflow for unpaid invoices when the California preliminary notice deadline (20 days from first service) has been missed. The system provides a structured path from demand letters through small claims court filing.

Business Context

When customers fail to pay for dumpster rental services and the preliminary notice window has closed, Quarry Rentals needs an efficient process to:

  1. Send formal demand letters via certified mail
  2. Prepare small claims court filings (SC-100 forms)
  3. Coordinate with process servers for legal service
  4. Track all collection actions at the project level

California Small Claims Limits:

System Architecture

Data Models

DemandLetter Model

# app/models/demand_letter.rb
class DemandLetter < ApplicationRecord
  belongs_to :project
  belongs_to :company  # The debtor

  # Lob.com integration fields
  # lob_id (string) - Lob letter ID
  # lob_url (string) - URL to view letter on Lob
  # tracking_number (string) - USPS tracking
  # tracking_events (jsonb) - Delivery tracking history

  # Letter details
  # total_amount_owed (decimal)
  # payment_deadline (date) - Typically 10-30 days from send date
  # services_description (text)
  # letter_data (jsonb) - All merge variables for template

  # Status tracking
  # status (string) - draft, sent, delivered, failed
  # sent_at (datetime)
  # delivered_at (datetime)
  # error_message (text)

  # Response tracking
  # payment_received (boolean)
  # payment_received_at (datetime)
  # response_notes (text)
end

SmallClaimsFiling Model

# app/models/small_claims_filing.rb
class SmallClaimsFiling < ApplicationRecord
  belongs_to :project
  belongs_to :company  # The defendant
  has_one :demand_letter, through: :project
  has_many :filing_documents, dependent: :destroy

  # Court information
  # court_county (string) - Based on defendant's address
  # court_name (string) - Specific small claims court
  # court_address (text)

  # Claim details
  # claim_amount (decimal)
  # filing_fee (decimal) - Varies by county and amount
  # case_number (string) - Assigned after filing

  # AI-generated narratives
  # why_owed_narrative (text) - "Why does defendant owe you money?"
  # calculation_narrative (text) - "How did you calculate the money owed?"

  # Form data
  # form_data (jsonb) - All SC-100 form fields
  # sc100_pdf_url (string) - Generated PDF stored in R2

  # Status tracking
  # status (string) - draft, ready_for_filing, filed, served, hearing_scheduled, judgment, closed
  # filed_at (datetime)
  # hearing_date (datetime)
  # judgment_amount (decimal)
  # judgment_date (datetime)

  # Process server integration
  # process_server_id (string) - External API reference
  # served_at (datetime)
  # service_method (string) - personal, substituted, posting
  # proof_of_service_url (string)
end

FilingDocument Model

# app/models/filing_document.rb
class FilingDocument < ApplicationRecord
  belongs_to :small_claims_filing

  # document_type (string) - sc100, proof_of_service, exhibits, etc.
  # file_url (string) - Cloudflare R2 URL
  # uploaded_at (datetime)
  # notes (text)
end

Services

1. DemandLetterService

Purpose: Generate and send formal demand letters via Lob certified mail

# app/services/demand_letter_service.rb
class DemandLetterService
  def initialize(project)
    @project = project
    @company = project.company
  end

  def create_and_send(payment_deadline_days: 30)
    demand_letter = build_demand_letter(payment_deadline_days)

    if demand_letter.save && send_via_lob(demand_letter)
      demand_letter
    else
      raise "Failed to send demand letter: #{demand_letter.errors.full_messages.join(', ')}"
    end
  end

  private

  def build_demand_letter(days)
    DemandLetter.new(
      project: @project,
      company: @company,
      total_amount_owed: @project.total_amount_owed,
      payment_deadline: days.days.from_now.to_date,
      services_description: build_services_description,
      letter_data: build_letter_data(days),
      status: 'draft'
    )
  end

  def build_letter_data(days)
    {
      letter_date: Date.current.strftime("%B %d, %Y"),
      debtor_name: @company.name,
      debtor_address: @company.full_address,
      amount_owed: @project.total_amount_owed.to_s,
      payment_deadline: days.days.from_now.strftime("%B %d, %Y"),
      services_summary: build_services_description,
      invoice_numbers: @project.invoices.pluck(:id).join(", "),
      # Itemized breakdown
      rental_charges: calculate_rental_charges,
      delivery_fees: calculate_delivery_fees,
      overweight_fees: calculate_overweight_fees,
      # Quarry Rentals info
      company_name: "Quarry Rentals LLC",
      company_address: "20500 Belshaw Avenue Suite T1-1474\nCarson, CA 90746",
      company_phone: "(310) 513-2500",
      company_email: "billing@quarryrents.com"
    }
  end

  def send_via_lob(demand_letter)
    # Generate PDF using Prawn
    pdf_content = DemandLetterPdfGenerator.new(demand_letter.letter_data).generate.render

    # Upload to R2
    pdf_url = upload_to_r2(pdf_content, demand_letter)

    # Send via Lob certified mail
    result = LobService.send_certified_letter(
      to_address: parse_address(@company),
      from_address: quarry_address,
      file_url: pdf_url,
      metadata: {
        project_id: @project.id,
        company_id: @company.id,
        letter_type: 'demand_letter'
      }
    )

    if result[:success]
      demand_letter.update!(
        status: 'sent',
        lob_id: result[:lob_id],
        lob_url: result[:url],
        tracking_number: result[:tracking_number],
        sent_at: Time.current
      )
      true
    else
      demand_letter.update!(
        status: 'failed',
        error_message: result[:error]
      )
      false
    end
  end
end

2. SmallClaimsNarrativeService

Purpose: Use AI to generate legal narratives for SC-100 form fields

# app/services/small_claims_narrative_service.rb
class SmallClaimsNarrativeService
  # Uses dual-AI model (Gemini + Grok) for validation
  # Similar to SEO consensus approach

  def initialize(project)
    @project = project
    @company = project.company
    @orders = project.orders
    @total_owed = project.total_amount_owed
  end

  def generate_narratives
    {
      why_owed: generate_why_owed,
      how_calculated: generate_how_calculated,
      confidence: :high  # Both AIs agree
    }
  end

  private

  def generate_why_owed
    prompt = <<~PROMPT
      Generate a concise legal narrative for California small claims court form SC-100.
      This explains WHY the defendant owes money. Must be clear, factual, and formal.

      CONSTRAINTS:
      - Maximum 300 characters (court form limitation)
      - No legal jargon, plain language
      - Include: service type, location, dates, failure to pay
      - Third person ("Defendant contracted...")

      FACTS:
      - Plaintiff: Quarry Rentals LLC (dumpster rental company)
      - Defendant: #{@company.name}
      - Service: Commercial dumpster rental
      - Location: #{@project.full_delivery_address}
      - Service period: #{format_date_range}
      - Number of orders: #{@orders.count}
      - Total owed: $#{@total_owed}
      - Invoices sent: #{invoice_dates}
      - Demand letter sent: #{demand_letter_date}

      OUTPUT FORMAT:
      Plain text, 2-3 sentences, exactly as it should appear on the court form.
    PROMPT

    # Use consensus service for validation
    ConsensusAiService.new.generate(prompt)
  end

  def generate_how_calculated
    prompt = <<~PROMPT
      Generate a calculation explanation for California small claims form SC-100.
      This explains HOW the money owed was calculated. Must be itemized and precise.

      CONSTRAINTS:
      - Maximum 500 characters (court form limitation)
      - Show itemized breakdown with dollar amounts
      - Reference invoice numbers
      - Include dates for each charge

      FACTS:
      #{build_itemized_charges}

      OUTPUT FORMAT:
      Plain text itemized list as it should appear on court form.
    PROMPT

    ConsensusAiService.new.generate(prompt)
  end

  def build_itemized_charges
    @orders.map do |order|
      <<~ORDER
        Order ##{order.order_number} (#{order.delivery_date}):
        - Product: #{order.product.name}
        - Rental period: #{order.rental_days} days @ $#{order.daily_rate}/day = $#{order.rental_charge}
        - Delivery: $#{order.delivery_fee}
        - Pickup: $#{order.pickup_fee}
        - Overweight: #{order.overweight_tons} tons @ $#{order.overweight_rate}/ton = $#{order.overweight_charge}
        - Subtotal: $#{order.total}
      ORDER
    end.join("\n")
  end
end

3. Sc100FormFillerService

Purpose: Fill out California SC-100 form with project data + AI narratives

# app/services/sc100_form_filler_service.rb
class Sc100FormFillerService
  require 'pdf-forms'  # Gem for filling PDF forms

  def initialize(small_claims_filing)
    @filing = small_claims_filing
    @project = small_claims_filing.project
    @company = small_claims_filing.company
  end

  def generate_pdf
    # Generate AI narratives
    narratives = SmallClaimsNarrativeService.new(@project).generate_narratives

    # Fill form data
    form_data = build_form_data(narratives)
    @filing.update!(form_data: form_data)

    # Fill PDF
    pdf_path = fill_pdf_form(form_data)

    # Upload to R2
    pdf_url = upload_to_r2(pdf_path)

    @filing.update!(
      sc100_pdf_url: pdf_url,
      why_owed_narrative: narratives[:why_owed],
      calculation_narrative: narratives[:how_calculated],
      status: 'ready_for_filing'
    )

    pdf_url
  end

  private

  def build_form_data(narratives)
    {
      # Court info (auto-detect based on defendant address)
      court_name: determine_court,
      court_county: @company.county,

      # Plaintiff (Quarry Rentals)
      plaintiff_name: "Quarry Rentals LLC",
      plaintiff_street: "20500 Belshaw Avenue",
      plaintiff_unit: "Suite T1-1474",
      plaintiff_city: "Carson",
      plaintiff_state: "CA",
      plaintiff_zip: "90746",
      plaintiff_phone: "310-513-2500",
      plaintiff_email: "billing@quarryrents.com",

      # Defendant
      defendant_name: @company.name,
      defendant_street: @company.address,
      defendant_city: @company.city,
      defendant_state: @company.state,
      defendant_zip: @company.zip_code,

      # Claim
      amount_claimed: @filing.claim_amount,

      # AI-generated narratives
      why_owed: narratives[:why_owed],
      how_calculated: narratives[:how_calculated],

      # Plaintiff signature
      signature_date: Date.current.strftime("%m/%d/%Y"),
      signature_name: "Robert Quarry",
      signature_title: "Owner"
    }
  end

  def fill_pdf_form(data)
    pdftk = PdfForms.new('/usr/bin/pdftk')
    template_path = Rails.root.join('lib/templates/sc100.pdf')
    output_path = Rails.root.join("tmp/sc100_#{@filing.id}_#{Time.current.to_i}.pdf")

    pdftk.fill_form(template_path, output_path, data, flatten: true)

    output_path.to_s
  end

  def determine_court
    # California court lookup based on county
    # Each county has specific small claims courts
    # This would use a lookup table or API
    CaliforniaCourtLookupService.find_small_claims_court(@company.county, @company.city)
  end
end

4. ProcessServerService (Future)

Purpose: Integrate with process server API for legal document service

# app/services/process_server_service.rb
class ProcessServerService
  # Integration with ServeNow, ProServe, or similar API

  def initialize(small_claims_filing)
    @filing = small_claims_filing
  end

  def request_service
    # Upload documents to process server
    # Provide defendant address
    # Track service status
    # Receive proof of service
  end
end

PDF Generation

DemandLetterPdfGenerator

# app/services/demand_letter_pdf_generator.rb
class DemandLetterPdfGenerator
  def initialize(letter_data)
    @data = letter_data
    @pdf = Prawn::Document.new(page_size: 'LETTER', margin: 72)
  end

  def generate
    add_header
    add_date_and_recipient
    add_reference_line
    add_salutation
    add_body
    add_payment_instructions
    add_consequences
    add_signature

    @pdf
  end

  private

  def add_header
    @pdf.text "QUARRY RENTALS LLC", size: 16, style: :bold, align: :center
    @pdf.text @data[:company_address], size: 10, align: :center
    @pdf.text "Phone: #{@data[:company_phone]} | Email: #{@data[:company_email]}",
              size: 10, align: :center
    @pdf.move_down 30
  end

  def add_body
    @pdf.text "FINAL DEMAND FOR PAYMENT", size: 14, style: :bold
    @pdf.move_down 15

    body_text = <<~BODY
      This letter constitutes formal demand for payment of #{@data[:amount_owed]}
      for dumpster rental services provided to #{@data[:debtor_name]} as detailed below:

      #{@data[:services_summary]}

      Despite our previous invoices (Invoice Numbers: #{@data[:invoice_numbers]})
      and repeated attempts to collect payment, your account remains outstanding.

      ITEMIZED CHARGES:
      - Rental charges: $#{@data[:rental_charges]}
      - Delivery fees: $#{@data[:delivery_fees]}
      - Overweight fees: $#{@data[:overweight_fees]}

      TOTAL AMOUNT DUE: $#{@data[:amount_owed]}
    BODY

    @pdf.text body_text, size: 11, leading: 3
  end

  def add_payment_instructions
    @pdf.move_down 20
    @pdf.text "PAYMENT REQUIRED BY: #{@data[:payment_deadline]}",
              size: 12, style: :bold
    # ... payment methods
  end

  def add_consequences
    @pdf.move_down 20
    warning = <<~WARNING
      NOTICE: If payment is not received by #{@data[:payment_deadline]},
      we will pursue all available legal remedies including:

      - Filing a claim in California Small Claims Court
      - Reporting to credit bureaus
      - Engaging collection agency
      - Pursuing mechanics lien on property (if applicable)

      These actions will result in additional costs including court fees,
      interest, and attorney fees, which will be added to your balance.
    WARNING

    @pdf.text warning, size: 10, style: :italic
  end
end

Controllers

Admin::DemandLettersController

# app/controllers/admin/demand_letters_controller.rb
class Admin::DemandLettersController < ApplicationController
  before_action :set_project, only: [:new, :create]

  def new
    @demand_letter = @project.demand_letters.build
    @demand_letter.total_amount_owed = @project.total_amount_owed
    @demand_letter.payment_deadline = 30.days.from_now
  end

  def create
    service = DemandLetterService.new(@project)
    @demand_letter = service.create_and_send(
      payment_deadline_days: params[:payment_deadline_days].to_i
    )

    redirect_to admin_project_path(@project),
                notice: "Demand letter sent via certified mail to #{@project.company.name}"
  rescue => e
    redirect_to admin_project_path(@project),
                alert: "Failed to send demand letter: #{e.message}"
  end

  def show
    @demand_letter = DemandLetter.find(params[:id])
    @project = @demand_letter.project
  end

  private

  def set_project
    @project = Project.find(params[:project_id])
  end
end

Admin::SmallClaimsFilingsController

# app/controllers/admin/small_claims_filings_controller.rb
class Admin::SmallClaimsFilingsController < ApplicationController
  before_action :set_project, only: [:new, :create]

  def new
    @filing = @project.build_small_claims_filing
    @filing.claim_amount = @project.total_amount_owed
    @filing.court_county = @project.company.county

    # Preview AI narratives
    narratives = SmallClaimsNarrativeService.new(@project).generate_narratives
    @filing.why_owed_narrative = narratives[:why_owed]
    @filing.calculation_narrative = narratives[:how_calculated]
  end

  def create
    @filing = @project.create_small_claims_filing!(filing_params)

    # Generate SC-100 PDF
    pdf_url = Sc100FormFillerService.new(@filing).generate_pdf

    redirect_to admin_small_claims_filing_path(@filing),
                notice: "SC-100 form generated and ready for download"
  end

  def show
    @filing = SmallClaimsFiling.find(params[:id])
  end

  def download_sc100
    @filing = SmallClaimsFiling.find(params[:id])
    redirect_to @filing.sc100_pdf_url, allow_other_host: true
  end

  private

  def filing_params
    params.require(:small_claims_filing).permit(
      :claim_amount,
      :court_county,
      :court_name,
      :why_owed_narrative,
      :calculation_narrative
    )
  end
end

Routes

# config/routes.rb
namespace :admin do
  resources :projects do
    resources :demand_letters, only: [:new, :create]
    resources :small_claims_filings, only: [:new, :create]
  end

  resources :demand_letters, only: [:show] do
    member do
      post :refresh_tracking
      post :mark_payment_received
    end
  end

  resources :small_claims_filings, only: [:show, :update] do
    member do
      get :download_sc100
      post :regenerate_narratives
      post :file_with_court  # Future: API integration
      post :request_service  # Future: Process server API
    end
  end
end

UI/UX - Project Page Integration

Collections Actions Section

Add to app/views/admin/projects/show.html.erb below Lien Notice History:

<!-- Collections Actions -->
<div class="bg-white shadow-sm ring-1 ring-slate-900/5 rounded-lg p-6 mt-6">
  <h3 class="text-sm font-semibold text-slate-900 mb-4">Collections Actions</h3>

  <% if @project.total_amount_owed > 0 %>
    <div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
      <div class="flex items-center justify-between">
        <div>
          <p class="text-sm font-semibold text-red-900">Outstanding Balance</p>
          <p class="text-2xl font-bold text-red-600">
            <%= number_to_currency(@project.total_amount_owed) %>
          </p>
        </div>
        <div class="text-right text-xs text-red-700">
          <p><%= @project.unpaid_invoices.count %> unpaid invoices</p>
          <p>Overdue: <%= @project.days_overdue %> days</p>
        </div>
      </div>
    </div>

    <!-- Step 1: Demand Letter -->
    <div class="space-y-3">
      <% if @project.demand_letter.blank? %>
        <%= link_to new_admin_project_demand_letter_path(@project),
            class: "block w-full px-4 py-3 text-sm font-medium rounded-md text-white bg-orange-600 hover:bg-orange-700 transition-colors" do %>
          📬 Send Demand Letter (Step 1)
          <span class="block text-xs text-orange-200 mt-1">
            30-day final notice via certified mail
          </span>
        <% end %>
      <% else %>
        <div class="border border-green-200 bg-green-50 rounded-lg p-3">
          <p class="text-xs font-semibold text-green-900">✓ Demand Letter Sent</p>
          <p class="text-xs text-green-700">
            Sent <%= @project.demand_letter.sent_at.strftime('%b %d, %Y') %>
            • Deadline: <%= @project.demand_letter.payment_deadline.strftime('%b %d, %Y') %>
          </p>
          <%= link_to "View Letter →", admin_demand_letter_path(@project.demand_letter),
              class: "text-xs text-green-700 hover:text-green-900 font-medium" %>
        </div>
      <% end %>

      <!-- Step 2: Small Claims Filing -->
      <% if @project.demand_letter&.delivered? && @project.past_payment_deadline? %>
        <% if @project.small_claims_filing.blank? %>
          <%= link_to new_admin_project_small_claims_filing_path(@project),
              class: "block w-full px-4 py-3 text-sm font-medium rounded-md text-white bg-red-700 hover:bg-red-800 transition-colors" do %>
            ⚖️ File Small Claims (Step 2)
            <span class="block text-xs text-red-200 mt-1">
              Generate SC-100 form with AI assistance
            </span>
          <% end %>
        <% else %>
          <div class="border border-blue-200 bg-blue-50 rounded-lg p-3">
            <p class="text-xs font-semibold text-blue-900">
              Small Claims Filing: <%= @project.small_claims_filing.status.humanize %>
            </p>
            <%= link_to "View Filing →", admin_small_claims_filing_path(@project.small_claims_filing),
                class: "text-xs text-blue-700 hover:text-blue-900 font-medium" %>
          </div>
        <% end %>
      <% end %>
    </div>
  <% else %>
    <p class="text-sm text-slate-500 italic">No outstanding balance</p>
  <% end %>
</div>

Implementation Phases

Phase 1: Demand Letter (Week 1)

Estimated Time: 2 days

Phase 2: SC-100 Form Generation (Week 2)

Estimated Time: 3-4 days

Phase 3: Court Filing Automation (Future)

Estimated Time: TBD (requires court approval/credentials)

Phase 4: Process Server Integration (Future)

Estimated Time: 2-3 days

California Court Reference

Small Claims Court Resources

Court Locations by County

To be implemented: Database of California small claims courts by county/city for auto-routing based on defendant address.

Testing Strategy

Demand Letter Testing

  1. Test with project that has multiple unpaid orders
  2. Verify PDF generation matches legal requirements
  3. Test Lob certified mail delivery
  4. Verify tracking webhook updates

SC-100 Form Testing

  1. Test AI narrative generation with various project types
  2. Verify all form fields populate correctly
  3. Test PDF form filling (ensure fields are editable if needed)
  4. Validate character limits on narrative fields
  5. Test with different California counties

Integration Testing

  1. Full workflow: Unpaid invoice → Demand letter → SC-100 filing
  2. Test project page UI for all states
  3. Verify access controls (only authorized users)

Security & Compliance

Legal Considerations

Data Privacy

Audit Trail

Every collection action logged:

Success Metrics

Key Performance Indicators

Cost Analysis

Break-even: Demand letters cost-effective for any amount > $20. Small claims worth pursuing for amounts > $300.

Future Enhancements

  1. Payment Plans: Allow defendants to request payment plans directly
  2. Settlement Negotiations: Track settlement offers and counter-offers
  3. Credit Reporting: Integrate with credit bureaus for non-payment reporting
  4. Analytics Dashboard: Track collection success rates, aging, trends
  5. Bulk Actions: Send multiple demand letters at once
  6. Templates: Multiple demand letter templates for different scenarios
  7. E-Signature: Allow defendants to sign payment agreements electronically

Notes


Related Documentation: