lien-waiver-tracker
skillTrack and manage construction lien waivers. Monitor conditional and unconditional waivers, ensure compliance before payments, and prevent lien exposure.
apm::install
apm install @datadrivenconstruction/lien-waiver-trackerapm::skill.md
---
name: "lien-waiver-tracker"
description: "Track and manage construction lien waivers. Monitor conditional and unconditional waivers, ensure compliance before payments, and prevent lien exposure."
homepage: "https://datadrivenconstruction.io"
metadata: {"openclaw": {"emoji": "📝", "os": ["darwin", "linux", "win32"], "homepage": "https://datadrivenconstruction.io", "requires": {"bins": ["python3"]}}}
---
# Lien Waiver Tracker
## Overview
Track and manage lien waivers throughout the construction payment process. Ensure proper waivers are received before releasing payments, monitor waiver status by subcontractor, and minimize lien exposure.
## Lien Waiver Types
```
┌─────────────────────────────────────────────────────────────────┐
│ LIEN WAIVER TYPES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ CONDITIONAL UNCONDITIONAL │
│ ─────────── ───────────── │
│ 📋 Progress - Conditional ✅ Progress - Unconditional │
│ Effective when paid Immediately effective │
│ Use with payment Use after check clears │
│ │
│ 📋 Final - Conditional ✅ Final - Unconditional │
│ For final payment For final payment │
│ Upon receipt of funds After funds received │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Technical Implementation
```python
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from datetime import datetime, timedelta
from enum import Enum
class WaiverType(Enum):
CONDITIONAL_PROGRESS = "conditional_progress"
UNCONDITIONAL_PROGRESS = "unconditional_progress"
CONDITIONAL_FINAL = "conditional_final"
UNCONDITIONAL_FINAL = "unconditional_final"
class WaiverStatus(Enum):
REQUESTED = "requested"
RECEIVED = "received"
VERIFIED = "verified"
REJECTED = "rejected"
MISSING = "missing"
class PaymentStatus(Enum):
PENDING = "pending"
APPROVED = "approved"
HELD = "held"
RELEASED = "released"
@dataclass
class Subcontractor:
id: str
name: str
trade: str
contract_amount: float
contact_name: str
contact_email: str
tier: int = 1 # 1 = direct, 2 = sub-sub
@dataclass
class LienWaiver:
id: str
subcontractor_id: str
waiver_type: WaiverType
payment_application: int # Pay app number
through_date: datetime
amount: float
status: WaiverStatus = WaiverStatus.REQUESTED
requested_date: datetime = field(default_factory=datetime.now)
received_date: Optional[datetime] = None
verified_by: str = ""
file_path: str = ""
notes: str = ""
@dataclass
class PaymentApplication:
number: int
period_end: datetime
subcontractor_id: str
amount_requested: float
amount_approved: float
retainage: float
status: PaymentStatus = PaymentStatus.PENDING
waivers_complete: bool = False
payment_date: Optional[datetime] = None
@dataclass
class LienExposure:
subcontractor_id: str
subcontractor_name: str
total_paid: float
unconditional_waivers: float
conditional_pending: float
exposure: float
class LienWaiverTracker:
"""Track and manage construction lien waivers."""
def __init__(self, project_id: str, project_name: str):
self.project_id = project_id
self.project_name = project_name
self.subcontractors: Dict[str, Subcontractor] = {}
self.waivers: Dict[str, LienWaiver] = {}
self.pay_apps: Dict[str, PaymentApplication] = {}
def add_subcontractor(self, id: str, name: str, trade: str,
contract_amount: float, contact_name: str,
contact_email: str, tier: int = 1) -> Subcontractor:
"""Add subcontractor to tracking."""
sub = Subcontractor(
id=id,
name=name,
trade=trade,
contract_amount=contract_amount,
contact_name=contact_name,
contact_email=contact_email,
tier=tier
)
self.subcontractors[id] = sub
return sub
def create_payment_application(self, number: int, period_end: datetime,
subcontractor_id: str, amount_requested: float,
retainage_rate: float = 0.10) -> PaymentApplication:
"""Create payment application record."""
if subcontractor_id not in self.subcontractors:
raise ValueError(f"Subcontractor {subcontractor_id} not found")
retainage = amount_requested * retainage_rate
amount_approved = amount_requested - retainage
pay_app = PaymentApplication(
number=number,
period_end=period_end,
subcontractor_id=subcontractor_id,
amount_requested=amount_requested,
amount_approved=amount_approved,
retainage=retainage
)
key = f"{subcontractor_id}-{number}"
self.pay_apps[key] = pay_app
# Create waiver request
self.request_waiver(subcontractor_id, number, period_end, amount_approved)
return pay_app
def request_waiver(self, subcontractor_id: str, pay_app_number: int,
through_date: datetime, amount: float,
waiver_type: WaiverType = WaiverType.CONDITIONAL_PROGRESS) -> LienWaiver:
"""Request lien waiver from subcontractor."""
waiver_id = f"LW-{subcontractor_id}-{pay_app_number}"
waiver = LienWaiver(
id=waiver_id,
subcontractor_id=subcontractor_id,
waiver_type=waiver_type,
payment_application=pay_app_number,
through_date=through_date,
amount=amount
)
self.waivers[waiver_id] = waiver
return waiver
def receive_waiver(self, waiver_id: str, file_path: str,
verified_by: str = "") -> LienWaiver:
"""Record receipt of lien waiver."""
if waiver_id not in self.waivers:
raise ValueError(f"Waiver {waiver_id} not found")
waiver = self.waivers[waiver_id]
waiver.status = WaiverStatus.RECEIVED
waiver.received_date = datetime.now()
waiver.file_path = file_path
waiver.verified_by = verified_by
# Check if all waivers for pay app complete
self._check_pay_app_waivers(waiver.subcontractor_id, waiver.payment_application)
return waiver
def verify_waiver(self, waiver_id: str, verified_by: str) -> LienWaiver:
"""Verify waiver details are correct."""
if waiver_id not in self.waivers:
raise ValueError(f"Waiver {waiver_id} not found")
waiver = self.waivers[waiver_id]
waiver.status = WaiverStatus.VERIFIED
waiver.verified_by = verified_by
return waiver
def reject_waiver(self, waiver_id: str, reason: str) -> LienWaiver:
"""Reject waiver (incorrect amount, wrong form, etc.)."""
if waiver_id not in self.waivers:
raise ValueError(f"Waiver {waiver_id} not found")
waiver = self.waivers[waiver_id]
waiver.status = WaiverStatus.REJECTED
waiver.notes = f"Rejected: {reason}"
return waiver
def convert_to_unconditional(self, waiver_id: str, payment_date: datetime) -> LienWaiver:
"""Convert conditional waiver to unconditional after payment clears."""
if waiver_id not in self.waivers:
raise ValueError(f"Waiver {waiver_id} not found")
waiver = self.waivers[waiver_id]
# Create new unconditional waiver
new_type = (WaiverType.UNCONDITIONAL_PROGRESS
if waiver.waiver_type == WaiverType.CONDITIONAL_PROGRESS
else WaiverType.UNCONDITIONAL_FINAL)
return self.request_waiver(
waiver.subcontractor_id,
waiver.payment_application,
waiver.through_date,
waiver.amount,
new_type
)
def _check_pay_app_waivers(self, subcontractor_id: str, pay_app_number: int):
"""Check if all waivers for pay app are received."""
key = f"{subcontractor_id}-{pay_app_number}"
if key not in self.pay_apps:
return
pay_app = self.pay_apps[key]
# Check all related waivers
related_waivers = [
w for w in self.waivers.values()
if w.subcontractor_id == subcontractor_id
and w.payment_application == pay_app_number
]
pay_app.waivers_complete = all(
w.status in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED]
for w in related_waivers
)
def calculate_exposure(self, subcontractor_id: str) -> LienExposure:
"""Calculate lien exposure for subcontractor."""
if subcontractor_id not in self.subcontractors:
raise ValueError(f"Subcontractor {subcontractor_id} not found")
sub = self.subcontractors[subcontractor_id]
# Sum payments
total_paid = sum(
pa.amount_approved for pa in self.pay_apps.values()
if pa.subcontractor_id == subcontractor_id
and pa.status == PaymentStatus.RELEASED
)
# Sum unconditional waivers
unconditional = sum(
w.amount for w in self.waivers.values()
if w.subcontractor_id == subcontractor_id
and w.waiver_type in [WaiverType.UNCONDITIONAL_PROGRESS, WaiverType.UNCONDITIONAL_FINAL]
and w.status in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED]
)
# Sum conditional pending
conditional = sum(
w.amount for w in self.waivers.values()
if w.subcontractor_id == subcontractor_id
and w.waiver_type in [WaiverType.CONDITIONAL_PROGRESS, WaiverType.CONDITIONAL_FINAL]
and w.status in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED]
)
# Exposure = Paid - Unconditional waivers
exposure = total_paid - unconditional
return LienExposure(
subcontractor_id=subcontractor_id,
subcontractor_name=sub.name,
total_paid=total_paid,
unconditional_waivers=unconditional,
conditional_pending=conditional,
exposure=exposure
)
def get_missing_waivers(self) -> List[Dict]:
"""Get list of missing or pending waivers."""
missing = []
for waiver in self.waivers.values():
if waiver.status in [WaiverStatus.REQUESTED, WaiverStatus.MISSING]:
sub = self.subcontractors.get(waiver.subcontractor_id)
missing.append({
"waiver_id": waiver.id,
"subcontractor": sub.name if sub else waiver.subcontractor_id,
"pay_app": waiver.payment_application,
"amount": waiver.amount,
"type": waiver.waiver_type.value,
"requested_date": waiver.requested_date,
"days_outstanding": (datetime.now() - waiver.requested_date).days
})
return sorted(missing, key=lambda x: -x["days_outstanding"])
def get_waiver_status_by_sub(self, subcontractor_id: str) -> Dict:
"""Get waiver status summary for subcontractor."""
if subcontractor_id not in self.subcontractors:
raise ValueError(f"Subcontractor {subcontractor_id} not found")
sub = self.subcontractors[subcontractor_id]
waivers = [w for w in self.waivers.values() if w.subcontractor_id == subcontractor_id]
by_status = {}
for w in waivers:
s = w.status.value
by_status[s] = by_status.get(s, 0) + 1
return {
"subcontractor": sub.name,
"total_waivers": len(waivers),
"by_status": by_status,
"total_amount": sum(w.amount for w in waivers),
"verified_amount": sum(w.amount for w in waivers if w.status == WaiverStatus.VERIFIED)
}
def can_release_payment(self, subcontractor_id: str, pay_app_number: int) -> Dict:
"""Check if payment can be released."""
key = f"{subcontractor_id}-{pay_app_number}"
if key not in self.pay_apps:
return {"can_release": False, "reason": "Payment application not found"}
pay_app = self.pay_apps[key]
# Check for conditional waiver
waivers = [
w for w in self.waivers.values()
if w.subcontractor_id == subcontractor_id
and w.payment_application == pay_app_number
and w.waiver_type == WaiverType.CONDITIONAL_PROGRESS
]
if not waivers:
return {"can_release": False, "reason": "No conditional waiver received"}
for w in waivers:
if w.status not in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED]:
return {"can_release": False, "reason": f"Waiver {w.id} not verified"}
return {"can_release": True, "reason": "All waivers verified", "amount": pay_app.amount_approved}
def generate_report(self) -> str:
"""Generate lien waiver status report."""
lines = [
"# Lien Waiver Status Report",
"",
f"**Project:** {self.project_name}",
f"**Date:** {datetime.now().strftime('%Y-%m-%d')}",
"",
"## Summary",
"",
f"- Total Subcontractors: {len(self.subcontractors)}",
f"- Total Waivers Tracked: {len(self.waivers)}",
f"- Missing Waivers: {len(self.get_missing_waivers())}",
"",
"## Exposure by Subcontractor",
"",
"| Subcontractor | Paid | Unconditional | Exposure |",
"|---------------|------|---------------|----------|"
]
total_exposure = 0
for sub_id in self.subcontractors:
exposure = self.calculate_exposure(sub_id)
total_exposure += exposure.exposure
lines.append(
f"| {exposure.subcontractor_name} | ${exposure.total_paid:,.0f} | "
f"${exposure.unconditional_waivers:,.0f} | ${exposure.exposure:,.0f} |"
)
lines.extend([
f"| **TOTAL** | | | **${total_exposure:,.0f}** |",
"",
"## Missing Waivers",
""
])
missing = self.get_missing_waivers()
if missing:
lines.append("| Subcontractor | Pay App | Amount | Days Outstanding |")
lines.append("|---------------|---------|--------|------------------|")
for m in missing[:10]:
lines.append(
f"| {m['subcontractor']} | #{m['pay_app']} | "
f"${m['amount']:,.0f} | {m['days_outstanding']} |"
)
else:
lines.append("*No missing waivers*")
return "\n".join(lines)
```
## Quick Start
```python
# Initialize tracker
tracker = LienWaiverTracker("PRJ-001", "Office Tower")
# Add subcontractors
tracker.add_subcontractor(
"SUB-001", "ABC Mechanical", "HVAC",
contract_amount=500000,
contact_name="John Smith",
contact_email="john@abcmech.com"
)
tracker.add_subcontractor(
"SUB-002", "XYZ Electric", "Electrical",
contract_amount=350000,
contact_name="Jane Doe",
contact_email="jane@xyzelectric.com"
)
# Create payment applications
pa1 = tracker.create_payment_application(
number=1,
period_end=datetime.now(),
subcontractor_id="SUB-001",
amount_requested=50000
)
# Receive waiver
waiver_id = f"LW-SUB-001-1"
tracker.receive_waiver(waiver_id, "/waivers/sub001_pa1.pdf", "PM")
# Check if can release payment
result = tracker.can_release_payment("SUB-001", 1)
print(f"Can release: {result['can_release']} - {result['reason']}")
# Calculate exposure
exposure = tracker.calculate_exposure("SUB-001")
print(f"Lien exposure: ${exposure.exposure:,.2f}")
# Generate report
print(tracker.generate_report())
```
## Requirements
```bash
pip install (no external dependencies)
```