new-site/scripts/formation/operating_agreement.py
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

640 lines
26 KiB
Python

"""
operating_agreement.py — Generate LLC Operating Agreement documents.
Uses a template-based approach with python-docx to produce a professional
operating agreement in both .docx and .pdf formats.
DISCLAIMER: This operating agreement template is for informational purposes
only and does not constitute legal advice. Consult a licensed attorney for
legal guidance specific to your situation.
Output:
/tmp/formations/{order_id}/operating-agreement.docx
/tmp/formations/{order_id}/operating-agreement.pdf
Usage:
# Programmatic
from formation.operating_agreement import generate_operating_agreement
docx_path, pdf_path = generate_operating_agreement(order)
# CLI
python -m formation.operating_agreement <order_id>
"""
from __future__ import annotations
import json
import logging
import os
import sys
from datetime import datetime
from pathlib import Path
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from .base import EntityType, FormationOrder, Member
from .states import STATES
LOG = logging.getLogger("formation.oa")
DISCLAIMER = (
"DISCLAIMER: This operating agreement template is for informational purposes "
"only and does not constitute legal advice. Every business situation is unique. "
"You should consult with a licensed attorney in your jurisdiction before relying "
"on this document for legal purposes."
)
# ---------------------------------------------------------------------------
# Document generation
# ---------------------------------------------------------------------------
def generate_operating_agreement(order: FormationOrder) -> tuple[str, str]:
"""
Generate an LLC Operating Agreement in .docx and .pdf formats.
Args:
order: FormationOrder with entity, member, and management details.
Returns:
Tuple of (docx_path, pdf_path).
"""
output_dir = Path(f"/tmp/formations/{order.order_id}")
output_dir.mkdir(parents=True, exist_ok=True)
docx_path = output_dir / "operating-agreement.docx"
pdf_path = output_dir / "operating-agreement.pdf"
state_name = STATES.get(order.state_code.upper(), {}).get("name", order.state_code)
formation_date = order.filed_at or order.effective_date or datetime.now().strftime("%B %d, %Y")
# Parse formation_date if it's ISO format
if "T" in formation_date or (len(formation_date) == 10 and "-" in formation_date):
try:
dt = datetime.fromisoformat(formation_date.replace("Z", "+00:00"))
formation_date = dt.strftime("%B %d, %Y")
except ValueError:
pass
management_display = (
"Member-Managed" if order.management_type == "member_managed" else "Manager-Managed"
)
doc = Document()
# -- Document styles --
style = doc.styles["Normal"]
font = style.font
font.name = "Times New Roman"
font.size = Pt(11)
style.paragraph_format.space_after = Pt(6)
style.paragraph_format.line_spacing = 1.15
# -- Disclaimer --
disclaimer_para = doc.add_paragraph()
disclaimer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = disclaimer_para.add_run(DISCLAIMER)
run.font.size = Pt(9)
run.font.italic = True
run.font.color.rgb = RGBColor(128, 128, 128)
doc.add_paragraph() # spacer
# -- Title --
title = doc.add_heading("OPERATING AGREEMENT", level=0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
subtitle = doc.add_heading(f"OF\n{order.entity_name.upper()}", level=1)
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
type_para = doc.add_paragraph()
type_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = type_para.add_run(f"A {state_name} Limited Liability Company")
run.font.size = Pt(12)
date_para = doc.add_paragraph()
date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = date_para.add_run(f"Effective Date: {formation_date}")
run.font.size = Pt(11)
run.font.italic = True
doc.add_paragraph() # spacer
# ======================================================================
# ARTICLE I — FORMATION
# ======================================================================
_add_article(doc, "I", "FORMATION")
_add_section(doc, "1.1", "Formation", (
f"The Members hereby form a Limited Liability Company (the \"Company\") "
f"under the laws of the State of {state_name}, pursuant to the "
f"{state_name} Limited Liability Company Act (the \"Act\")."
))
_add_section(doc, "1.2", "Name", (
f"The name of the Company shall be {order.entity_name} "
f"(the \"Company\")."
))
_add_section(doc, "1.3", "Principal Office", (
f"The principal office of the Company shall be located at "
f"{order.principal_address or '[ADDRESS]'}, "
f"{order.principal_city or '[CITY]'}, "
f"{order.principal_state or '[STATE]'} "
f"{order.principal_zip or '[ZIP]'}. "
f"The Company may change its principal office upon written notice to all Members."
))
_add_section(doc, "1.4", "Registered Agent", (
f"The registered agent for service of process shall be "
f"{order.registered_agent_name or '[REGISTERED AGENT]'}, "
f"located at {order.registered_agent_address or '[REGISTERED AGENT ADDRESS]'}."
))
_add_section(doc, "1.5", "Purpose", (
f"The purpose of the Company is to engage in {order.purpose}. "
f"The Company may engage in any other lawful activity permitted under "
f"the Act and the laws of the State of {state_name}."
))
_add_section(doc, "1.6", "Duration", (
"The Company shall have perpetual existence unless dissolved in accordance "
"with this Agreement or as required by law."
))
_add_section(doc, "1.7", "Fiscal Year", (
f"The fiscal year of the Company shall end on {order.fiscal_year_end or 'December 31'} "
f"of each year."
))
# ======================================================================
# ARTICLE II — MEMBERS
# ======================================================================
_add_article(doc, "II", "MEMBERS")
_add_section(doc, "2.1", "Members", (
"The names, addresses, and ownership interests of the Members are as follows:"
))
# Members table
if order.members:
table = doc.add_table(rows=1, cols=4)
table.style = "Table Grid"
table.alignment = WD_TABLE_ALIGNMENT.CENTER
# Header row
hdr = table.rows[0].cells
for i, text in enumerate(["Member Name", "Address", "Ownership %", "Title"]):
hdr[i].text = text
for paragraph in hdr[i].paragraphs:
for run in paragraph.runs:
run.font.bold = True
run.font.size = Pt(10)
# Member rows
for member in order.members:
row = table.add_row().cells
row[0].text = member.name
addr = f"{member.address}, {member.city}, {member.state} {member.zip_code}"
row[1].text = addr.strip(", ")
row[2].text = f"{member.ownership_pct:.1f}%"
row[3].text = member.title
for cell in row:
for paragraph in cell.paragraphs:
for run in paragraph.runs:
run.font.size = Pt(10)
# Set column widths
for row in table.rows:
row.cells[0].width = Inches(1.8)
row.cells[1].width = Inches(2.5)
row.cells[2].width = Inches(1.0)
row.cells[3].width = Inches(1.0)
doc.add_paragraph() # spacer after table
_add_section(doc, "2.2", "Admission of New Members", (
"New Members may be admitted to the Company only with the unanimous written "
"consent of all existing Members. Any new Member shall execute a counterpart "
"of this Agreement and shall be bound by all terms herein."
))
# ======================================================================
# ARTICLE III — MANAGEMENT
# ======================================================================
_add_article(doc, "III", "MANAGEMENT")
if order.management_type == "member_managed":
_add_section(doc, "3.1", "Member-Managed", (
"The Company shall be managed by its Members. Each Member shall have "
"the right to participate in the management of the Company and shall "
"have the authority to bind the Company in the ordinary course of business."
))
_add_section(doc, "3.2", "Voting Rights", (
"Each Member shall have voting rights in proportion to their ownership "
"interest. Unless otherwise specified in this Agreement, decisions "
"shall be made by a majority vote of the membership interests."
))
_add_section(doc, "3.3", "Major Decisions", (
"The following actions shall require the unanimous consent of all Members: "
"(a) sale of all or substantially all Company assets; "
"(b) merger or consolidation of the Company; "
"(c) any amendment to this Operating Agreement; "
"(d) admission of a new Member; "
"(e) any act that would make it impossible to carry on the ordinary business "
"of the Company."
))
else:
_add_section(doc, "3.1", "Manager-Managed", (
"The Company shall be managed by one or more Managers appointed by the "
"Members. The Manager(s) shall have full authority to manage the business "
"and affairs of the Company, including the authority to bind the Company "
"in the ordinary course of business."
))
_add_section(doc, "3.2", "Appointment of Managers", (
"Managers shall be appointed by a majority vote of the membership interests. "
"A Manager may be removed at any time, with or without cause, by a majority "
"vote of the membership interests."
))
_add_section(doc, "3.3", "Manager Authority", (
"The Manager(s) shall manage the day-to-day operations of the Company. "
"Members who are not Managers shall not participate in the management "
"or control of the Company's business and shall have no authority to "
"bind the Company."
))
_add_section(doc, "3.4", "Major Decisions", (
"The following actions shall require the unanimous consent of all Members, "
"regardless of management structure: "
"(a) sale of all or substantially all Company assets; "
"(b) merger or consolidation of the Company; "
"(c) any amendment to this Operating Agreement; "
"(d) admission of a new Member."
))
# ======================================================================
# ARTICLE IV — CAPITAL CONTRIBUTIONS
# ======================================================================
_add_article(doc, "IV", "CAPITAL CONTRIBUTIONS")
_add_section(doc, "4.1", "Initial Contributions", (
"Each Member shall make an initial capital contribution to the Company "
"in cash or property as agreed upon by the Members. The value of each "
"Member's initial contribution shall be recorded in the Company's books."
))
_add_section(doc, "4.2", "Additional Contributions", (
"No Member shall be required to make additional capital contributions "
"to the Company without the unanimous consent of all Members. Any "
"additional contributions shall be made in proportion to the Members' "
"ownership interests unless otherwise agreed."
))
_add_section(doc, "4.3", "Capital Accounts", (
"The Company shall maintain a separate capital account for each Member. "
"Each Member's capital account shall be credited with the Member's "
"contributions and share of profits, and debited with the Member's "
"distributions and share of losses."
))
_add_section(doc, "4.4", "No Interest on Capital", (
"No Member shall receive interest on their capital contribution or "
"capital account balance."
))
# ======================================================================
# ARTICLE V — DISTRIBUTIONS
# ======================================================================
_add_article(doc, "V", "DISTRIBUTIONS")
_add_section(doc, "5.1", "Distributions", (
"Distributions of the Company's net cash flow shall be made to the Members "
"pro rata in accordance with their respective ownership percentages, at such "
"times and in such amounts as determined by the Members (or Manager(s), if "
"manager-managed)."
))
_add_section(doc, "5.2", "Tax Distributions", (
"The Company shall, at a minimum, distribute to each Member an amount "
"sufficient to cover each Member's estimated tax liability arising from "
"the Company's income allocated to such Member, calculated at the highest "
"applicable marginal tax rate."
))
_add_section(doc, "5.3", "Limitation on Distributions", (
"No distribution shall be made if, after giving effect to the distribution, "
"the Company would not be able to pay its debts as they become due in the "
"ordinary course of business."
))
# ======================================================================
# ARTICLE VI — MEETINGS AND VOTING
# ======================================================================
_add_article(doc, "VI", "MEETINGS AND VOTING")
_add_section(doc, "6.1", "Meetings", (
"The Members shall hold an annual meeting at such time and place as "
"determined by the Members. Special meetings may be called by any Member "
"upon not less than ten (10) days' written notice to all other Members."
))
_add_section(doc, "6.2", "Quorum", (
"A quorum for any meeting of Members shall consist of Members holding "
"more than fifty percent (50%) of the total ownership interests."
))
_add_section(doc, "6.3", "Action Without Meeting", (
"Any action that may be taken at a meeting of the Members may be taken "
"without a meeting if the action is consented to in writing by Members "
"holding sufficient ownership interests to authorize such action at a meeting."
))
_add_section(doc, "6.4", "Voting", (
"Each Member shall be entitled to vote in proportion to their ownership "
"interest. Except as otherwise provided in this Agreement, decisions "
"shall be made by a majority vote of the total ownership interests."
))
# ======================================================================
# ARTICLE VII — TRANSFER OF MEMBERSHIP INTERESTS
# ======================================================================
_add_article(doc, "VII", "TRANSFER OF MEMBERSHIP INTERESTS")
_add_section(doc, "7.1", "Restrictions on Transfer", (
"No Member may sell, assign, pledge, or otherwise transfer all or any "
"portion of their membership interest without the prior written consent "
"of all other Members."
))
_add_section(doc, "7.2", "Right of First Refusal", (
"Before any Member may transfer their interest to a non-Member, the "
"transferring Member shall first offer the interest to the remaining "
"Members, pro rata, at the same price and on the same terms as the "
"proposed transfer. The remaining Members shall have thirty (30) days "
"to accept or decline the offer."
))
_add_section(doc, "7.3", "Permitted Transfers", (
"Notwithstanding the foregoing, a Member may transfer their interest "
"to a revocable trust established by such Member for estate planning "
"purposes, provided that the transferring Member remains the trustee "
"or retains control of the trust."
))
# ======================================================================
# ARTICLE VIII — DISSOLUTION
# ======================================================================
_add_article(doc, "VIII", "DISSOLUTION")
_add_section(doc, "8.1", "Events of Dissolution", (
"The Company shall be dissolved upon the occurrence of any of the following: "
"(a) the unanimous written agreement of all Members; "
"(b) entry of a decree of judicial dissolution; "
"(c) any event that makes it unlawful for the Company to continue its business; "
f"(d) as otherwise required by the laws of the State of {state_name}."
))
_add_section(doc, "8.2", "Winding Up", (
"Upon dissolution, the Company's affairs shall be wound up. The assets "
"shall be liquidated and the proceeds applied in the following order: "
"(a) payment of debts and liabilities to creditors; "
"(b) payment of debts and liabilities to Members; "
"(c) distribution to Members in accordance with their positive capital "
"account balances."
))
# ======================================================================
# ARTICLE IX — MISCELLANEOUS
# ======================================================================
_add_article(doc, "IX", "MISCELLANEOUS")
_add_section(doc, "9.1", "Governing Law", (
f"This Agreement shall be governed by and construed in accordance with "
f"the laws of the State of {state_name}, without regard to its conflict "
f"of laws principles."
))
_add_section(doc, "9.2", "Amendments", (
"This Agreement may be amended only by a written instrument signed by "
"all Members."
))
_add_section(doc, "9.3", "Severability", (
"If any provision of this Agreement is held to be invalid, illegal, or "
"unenforceable, the remaining provisions shall continue in full force "
"and effect."
))
_add_section(doc, "9.4", "Entire Agreement", (
"This Agreement constitutes the entire agreement among the Members with "
"respect to the subject matter hereof and supersedes all prior agreements, "
"understandings, negotiations, and discussions."
))
_add_section(doc, "9.5", "Binding Effect", (
"This Agreement shall be binding upon and inure to the benefit of the "
"Members and their respective heirs, executors, administrators, "
"successors, and permitted assigns."
))
_add_section(doc, "9.6", "Indemnification", (
"The Company shall indemnify and hold harmless each Member and Manager "
"from and against any and all claims, liabilities, damages, and expenses "
"(including reasonable attorneys' fees) arising out of or relating to "
"the Company's business, except to the extent caused by such person's "
"gross negligence or willful misconduct."
))
# ======================================================================
# ARTICLE X — SIGNATURE BLOCK
# ======================================================================
_add_article(doc, "X", "EXECUTION")
doc.add_paragraph(
"IN WITNESS WHEREOF, the undersigned Members have executed this "
f"Operating Agreement as of {formation_date}."
)
doc.add_paragraph() # spacer
# Signature lines for each member
for member in order.members:
sig_block = doc.add_paragraph()
sig_block.add_run("\n")
sig_block.add_run("_" * 50)
sig_block.add_run("\n")
name_run = sig_block.add_run(member.name)
name_run.font.bold = True
sig_block.add_run(f"\n{member.title}")
sig_block.add_run(f"\nOwnership: {member.ownership_pct:.1f}%")
sig_block.add_run("\nDate: _________________")
doc.add_paragraph() # spacer between signature blocks
# If no members listed, add a generic signature block
if not order.members:
sig_block = doc.add_paragraph()
sig_block.add_run("\n")
sig_block.add_run("_" * 50)
sig_block.add_run("\nMember Name: _________________________")
sig_block.add_run("\nTitle: _______________________________")
sig_block.add_run("\nDate: ________________________________")
# -- Save .docx --
doc.save(str(docx_path))
LOG.info("Operating agreement .docx saved: %s", docx_path)
# -- Convert to PDF --
try:
from docx2pdf import convert
convert(str(docx_path), str(pdf_path))
LOG.info("Operating agreement .pdf saved: %s", pdf_path)
except ImportError:
LOG.warning(
"docx2pdf not available — PDF conversion skipped. "
"Install with: pip install docx2pdf"
)
# Attempt LibreOffice fallback
try:
import subprocess
result = subprocess.run(
[
"libreoffice",
"--headless",
"--convert-to", "pdf",
"--outdir", str(output_dir),
str(docx_path),
],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode == 0 and pdf_path.exists():
LOG.info("Operating agreement .pdf saved (LibreOffice): %s", pdf_path)
else:
LOG.warning("LibreOffice conversion failed: %s", result.stderr)
pdf_path = Path("") # No PDF available
except FileNotFoundError:
LOG.warning(
"Neither docx2pdf nor LibreOffice available for PDF conversion."
)
pdf_path = Path("")
except Exception as exc:
LOG.error("PDF conversion failed: %s", exc)
pdf_path = Path("")
return str(docx_path), str(pdf_path)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _add_article(doc: Document, number: str, title: str):
"""Add an article heading."""
heading = doc.add_heading(f"ARTICLE {number}{title}", level=2)
heading.alignment = WD_ALIGN_PARAGRAPH.LEFT
def _add_section(doc: Document, number: str, title: str, text: str):
"""Add a numbered section with bold title and body text."""
para = doc.add_paragraph()
run_num = para.add_run(f"Section {number}. {title}. ")
run_num.font.bold = True
para.add_run(text)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
"""Generate an operating agreement from a formation order in the database."""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s %(message)s",
)
if len(sys.argv) < 2:
print("Usage: python -m formation.operating_agreement <order_id>")
print()
print("Generates an LLC operating agreement (.docx and .pdf)")
print("from the formation order data in the database.")
sys.exit(1)
order_id = sys.argv[1]
database_url = os.environ.get("DATABASE_URL", "")
if not database_url:
print("Error: DATABASE_URL not set.", file=sys.stderr)
sys.exit(1)
import psycopg2
import psycopg2.extras
conn = psycopg2.connect(database_url)
try:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute("SELECT * FROM formation_orders WHERE order_id = %s", (order_id,))
row = cur.fetchone()
finally:
conn.close()
if not row:
print(f"Error: Order {order_id} not found.", file=sys.stderr)
sys.exit(1)
# Build FormationOrder
members_raw = row.get("members")
if isinstance(members_raw, str):
members_raw = json.loads(members_raw)
elif members_raw is None:
members_raw = []
members = [
Member(
name=m.get("name", ""),
address=m.get("address", ""),
city=m.get("city", ""),
state=m.get("state", ""),
zip_code=m.get("zip_code", ""),
title=m.get("title", "Member"),
ownership_pct=float(m.get("ownership_pct", 0)),
is_organizer=bool(m.get("is_organizer", False)),
)
for m in members_raw
]
try:
entity_type = EntityType(row.get("entity_type", "llc"))
except ValueError:
entity_type = EntityType.LLC
order = FormationOrder(
order_id=str(row["order_id"]),
state_code=row.get("state_code", ""),
entity_type=entity_type,
entity_name=row.get("entity_name", ""),
management_type=row.get("management_type", "member_managed"),
purpose=row.get("purpose", "Any lawful business activity"),
members=members,
registered_agent_name=row.get("registered_agent_name", "Northwest Registered Agent"),
registered_agent_address=row.get("registered_agent_address", ""),
principal_address=row.get("principal_address", ""),
principal_city=row.get("principal_city", ""),
principal_state=row.get("principal_state", ""),
principal_zip=row.get("principal_zip", ""),
fiscal_year_end=row.get("fiscal_year_end", "12/31"),
effective_date=row.get("effective_date", "") or "",
filed_at=row.get("filed_at", "") or "",
)
docx_path, pdf_path = generate_operating_agreement(order)
print(f"Generated operating agreement:")
print(f" DOCX: {docx_path}")
print(f" PDF: {pdf_path or '(not available — install docx2pdf or libreoffice)'}")
if __name__ == "__main__":
main()