Status: Planning Last Updated: 2025-11-21 Owner: Operations Team
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.
When customers fail to pay for dumpster rental services and the preliminary notice window has closed, Quarry Rentals needs an efficient process to:
California Small Claims Limits:
# 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
# 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
# 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
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
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
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
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
# 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
# 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
# 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
# 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
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>
Estimated Time: 2 days
Estimated Time: 3-4 days
Estimated Time: TBD (requires court approval/credentials)
Estimated Time: 2-3 days
To be implemented: Database of California small claims courts by county/city for auto-routing based on defendant address.
Every collection action logged:
Break-even: Demand letters cost-effective for any amount > $20. Small claims worth pursuing for amounts > $300.
Related Documentation:
docs/ for preliminary notice implementation (completed)