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>
640 lines
26 KiB
Python
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()
|