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>
630 lines
23 KiB
Python
630 lines
23 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Generate all document permutations (DOCX + PDF) for a sample company
|
||
and email them to ops@performancewest.net for review.
|
||
|
||
Usage:
|
||
python -m scripts.generate_all_permutations
|
||
|
||
Environment variables:
|
||
SMTP_HOST – outbound mail server (default: co.carrierone.com)
|
||
SMTP_PORT – mail port (default: 587)
|
||
SMTP_USER – SMTP login
|
||
SMTP_PASS – SMTP password
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
import smtplib
|
||
import subprocess
|
||
import sys
|
||
from email.mime.application import MIMEApplication
|
||
from email.mime.multipart import MIMEMultipart
|
||
from email.mime.text import MIMEText
|
||
from pathlib import Path
|
||
|
||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
|
||
LOG = logging.getLogger("generate_all_permutations")
|
||
|
||
# ── SMTP config ──────────────────────────────────────────────────────────────
|
||
SMTP_HOST = os.environ.get("SMTP_HOST", "co.carrierone.com")
|
||
SMTP_PORT = int(os.environ.get("SMTP_PORT", "587"))
|
||
SMTP_USER = os.environ.get("SMTP_USER", "")
|
||
SMTP_PASS = os.environ.get("SMTP_PASS", "")
|
||
FROM_EMAIL = os.environ.get("FROM_EMAIL", "noreply@performancewest.net")
|
||
TO_EMAIL = "ops@performancewest.net"
|
||
|
||
# ── Sample company data used across all documents ───────────────────────────
|
||
COMPANY = {
|
||
"entity_name": "Falcon Broadband LLC",
|
||
"dba_name": "Falcon Communications",
|
||
"frn": "0027160886",
|
||
"rmd_number": "RMD-00123456",
|
||
"filer_id_499": "831234",
|
||
"address_street": "1234 Telecom Drive, Suite 400",
|
||
"address_city": "Denver",
|
||
"address_state": "CO",
|
||
"address_zip": "80202",
|
||
"contact_name": "Sarah Mitchell",
|
||
"contact_title": "VP of Regulatory Affairs",
|
||
"contact_email": "regulatory@falconbroadband.com",
|
||
"contact_phone": "+1 (303) 555-0142",
|
||
"ceo_name": "James R. Falcon",
|
||
"ceo_title": "Chief Executive Officer",
|
||
}
|
||
|
||
OUTPUT_DIR = Path("/tmp/permutation_output")
|
||
|
||
|
||
def _convert_to_pdf(docx_path: Path) -> Path | None:
|
||
"""Convert DOCX to PDF via LibreOffice headless (local fallback)."""
|
||
pdf_path = docx_path.with_suffix(".pdf")
|
||
try:
|
||
result = subprocess.run(
|
||
["libreoffice", "--headless", "--convert-to", "pdf",
|
||
"--outdir", str(docx_path.parent), str(docx_path)],
|
||
capture_output=True, text=True, timeout=60,
|
||
)
|
||
if result.returncode == 0 and pdf_path.exists():
|
||
LOG.info(" PDF: %s", pdf_path.name)
|
||
return pdf_path
|
||
else:
|
||
LOG.warning(" LibreOffice conversion failed for %s: %s", docx_path.name, result.stderr[:200])
|
||
except FileNotFoundError:
|
||
LOG.warning(" LibreOffice not installed — skipping PDF for %s", docx_path.name)
|
||
except Exception as exc:
|
||
LOG.warning(" PDF conversion error for %s: %s", docx_path.name, exc)
|
||
return None
|
||
|
||
|
||
def generate_rmd_letters() -> list[Path]:
|
||
"""Generate RMD certification letters for all classification × STIR/SHAKEN permutations."""
|
||
from scripts.document_gen.templates.rmd_letter_generator import generate_rmd_letter
|
||
|
||
# Map to actual FCC provider classifications per 47 CFR § 64.6305
|
||
# Each with a realistic switch/ucaas combo to test auto-fill
|
||
classifications = [
|
||
{
|
||
"label": "voice_svc_oasis",
|
||
"provider_classification": "voice_service_provider",
|
||
"infra_type": "facilities",
|
||
"is_gateway_provider": False,
|
||
"is_wholesale": False,
|
||
"switch_platform": "Oasis",
|
||
},
|
||
{
|
||
"label": "voice_svc_bcmone",
|
||
"provider_classification": "voice_service_provider",
|
||
"infra_type": "reseller",
|
||
"is_gateway_provider": False,
|
||
"is_wholesale": False,
|
||
"ucaas_host": "BCM One",
|
||
},
|
||
{
|
||
"label": "voice_svc_metaswitch",
|
||
"provider_classification": "voice_service_provider",
|
||
"infra_type": "facilities",
|
||
"is_gateway_provider": False,
|
||
"is_wholesale": False,
|
||
"switch_platform": "Metaswitch",
|
||
},
|
||
{
|
||
"label": "gateway",
|
||
"provider_classification": "gateway_provider",
|
||
"infra_type": "facilities",
|
||
"is_gateway_provider": True,
|
||
"is_wholesale": True,
|
||
"switch_platform": "Ribbon (OASIS SBC)",
|
||
},
|
||
{
|
||
"label": "intermediate",
|
||
"provider_classification": "intermediate_provider",
|
||
"infra_type": "facilities",
|
||
"is_gateway_provider": False,
|
||
"is_wholesale": True,
|
||
"switch_platform": "FreeSWITCH",
|
||
},
|
||
]
|
||
|
||
stir_shaken_statuses = [
|
||
"complete_implementation",
|
||
"partial_implementation",
|
||
"robocall_mitigation_only",
|
||
"exempt_small_carrier",
|
||
"not_applicable",
|
||
]
|
||
|
||
carrier_categories = ["interconnected_voip", "clec", "ixc", "cmrs"]
|
||
|
||
files: list[Path] = []
|
||
|
||
for cls in classifications:
|
||
for ss_status in stir_shaken_statuses:
|
||
cat = carrier_categories[classifications.index(cls) % len(carrier_categories)]
|
||
label = cls["label"]
|
||
filename = f"rmd_letter_{label}_{ss_status}.docx"
|
||
out = str(OUTPUT_DIR / filename)
|
||
|
||
LOG.info("RMD Letter: classification=%s, stir_shaken=%s", label, ss_status)
|
||
result = generate_rmd_letter(
|
||
**COMPANY,
|
||
ocn="8765",
|
||
former_names=["Falcon Voice Services LLC"],
|
||
is_foreign_provider=(cls["label"] == "gateway"),
|
||
provider_classification=cls["provider_classification"],
|
||
carrier_category=cat,
|
||
infra_type=cls["infra_type"],
|
||
is_wholesale=cls.get("is_wholesale", False),
|
||
is_gateway_provider=cls.get("is_gateway_provider", False),
|
||
switch_platform=cls.get("switch_platform", ""),
|
||
ucaas_host=cls.get("ucaas_host", ""),
|
||
stir_shaken_status=ss_status,
|
||
stir_shaken_cert_authority="TransNexus",
|
||
stir_shaken_extension_type="non_ip_network" if ss_status == "partial_implementation" else "",
|
||
stir_shaken_extension_basis="TDM portions of the network cannot support SIP-based STIR/SHAKEN signing." if ss_status == "partial_implementation" else "",
|
||
carrier_metadata={
|
||
"gateway_countries": ["Canada", "Mexico", "United Kingdom"],
|
||
"wholesale_customer_types": ["CLEC", "VoIP", "CMRS"],
|
||
"downstream_count_approx": 47,
|
||
},
|
||
output_path=out,
|
||
)
|
||
if result:
|
||
files.append(Path(result))
|
||
|
||
return files
|
||
|
||
|
||
def generate_exhibit_a_docs() -> list[Path]:
|
||
"""Generate Exhibit A (robocall mitigation program) for each carrier role."""
|
||
from scripts.document_gen.templates.rmd_exhibit_a_generator import generate_exhibit_a
|
||
|
||
roles = ["facilities", "reseller", "gateway", "ucaas", "wholesale_domestic", "international_only"]
|
||
files: list[Path] = []
|
||
|
||
for role in roles:
|
||
filename = f"exhibit_a_{role}.docx"
|
||
out = str(OUTPUT_DIR / filename)
|
||
|
||
# Vary the intake answers per role to exercise auto-fill
|
||
role_extras = {
|
||
"facilities": {"switch_platform": "Oasis"},
|
||
"reseller": {"ucaas_host": "BCM One"},
|
||
"gateway": {"switch_platform": "Ribbon SBC"},
|
||
"ucaas": {"ucaas_host": "RingCentral"},
|
||
"wholesale_domestic": {"switch_platform": "Cataleya"},
|
||
"international_only": {"switch_platform": "46Labs"},
|
||
}
|
||
|
||
LOG.info("Exhibit A: role=%s (auto-fill from intake)", role)
|
||
result = generate_exhibit_a(
|
||
entity_name=COMPANY["entity_name"],
|
||
frn=COMPANY["frn"],
|
||
carrier_role=role,
|
||
carrier_metadata={
|
||
"gateway_countries": ["Canada", "Mexico", "United Kingdom"],
|
||
"wholesale_customer_types": ["CLEC", "VoIP", "CMRS"],
|
||
},
|
||
**role_extras.get(role, {}),
|
||
output_path=out,
|
||
)
|
||
if result:
|
||
files.append(Path(result))
|
||
|
||
return files
|
||
|
||
|
||
def generate_cpni_letters() -> list[Path]:
|
||
"""Generate CPNI cert letters: retail vs wholesale, complaints vs no complaints."""
|
||
from scripts.document_gen.templates.cpni_cert_letter_generator import generate_cpni_cert_letter
|
||
|
||
permutations = [
|
||
{
|
||
"label": "retail_clean",
|
||
"is_wholesale": False,
|
||
"complaints_count": 0,
|
||
},
|
||
{
|
||
"label": "retail_complaints_breaches",
|
||
"is_wholesale": False,
|
||
"complaints_count": 3,
|
||
"complaints_description": "Three complaints were received regarding third-party marketing access to CPNI. Each was investigated within 5 business days and resolved by reaffirming customer opt-out preferences.",
|
||
"breaches": [
|
||
{
|
||
"description": "Employee inadvertently disclosed call detail records for 12 accounts to an unauthorized third party during a vendor integration.",
|
||
"date": "2025-06-22",
|
||
"customers_affected": 12,
|
||
"response_actions": "Affected customers notified within 30 days per 47 CFR § 64.2011. Employee received corrective training. Vendor access controls tightened.",
|
||
},
|
||
],
|
||
"disciplinary_actions_taken": True,
|
||
"disciplinary_actions_description": "One employee received a written warning and mandatory retraining for the June 2025 inadvertent CPNI disclosure.",
|
||
"uses_cpni_for_marketing": True,
|
||
"cpni_approval_method": "opt_in",
|
||
},
|
||
{
|
||
"label": "wholesale_clean",
|
||
"is_wholesale": True,
|
||
"complaints_count": 0,
|
||
},
|
||
{
|
||
"label": "wholesale_breach",
|
||
"is_wholesale": True,
|
||
"complaints_count": 1,
|
||
"complaints_description": "One complaint regarding unauthorized disclosure of wholesale traffic data to a third party.",
|
||
"breaches": [
|
||
{
|
||
"description": "Wholesale traffic routing data for 3 downstream carriers was inadvertently exposed via a misconfigured API endpoint.",
|
||
"date": "2025-09-10",
|
||
"customers_affected": 3,
|
||
"response_actions": "API endpoint secured within 2 hours. Affected carriers notified same day. FCC notified within 30 days per § 64.2011.",
|
||
},
|
||
],
|
||
},
|
||
]
|
||
|
||
files: list[Path] = []
|
||
|
||
for perm in permutations:
|
||
label = perm.pop("label")
|
||
filename = f"cpni_cert_{label}.docx"
|
||
out = str(OUTPUT_DIR / filename)
|
||
|
||
LOG.info("CPNI Letter: %s", label)
|
||
result = generate_cpni_cert_letter(
|
||
entity_name=COMPANY["entity_name"],
|
||
frn=COMPANY["frn"],
|
||
filer_id_499=COMPANY["filer_id_499"],
|
||
address_street=COMPANY["address_street"],
|
||
address_city=COMPANY["address_city"],
|
||
address_state=COMPANY["address_state"],
|
||
address_zip=COMPANY["address_zip"],
|
||
officer_name=COMPANY["ceo_name"],
|
||
officer_title=COMPANY["ceo_title"],
|
||
contact_email=COMPANY["contact_email"],
|
||
contact_phone=COMPANY["contact_phone"],
|
||
reporting_year=2025,
|
||
output_path=out,
|
||
**perm,
|
||
)
|
||
if result:
|
||
files.append(Path(result))
|
||
|
||
return files
|
||
|
||
|
||
def generate_499a_checklists() -> list[Path]:
|
||
"""Generate 499-A checklists for various filer/infra/status combinations."""
|
||
from scripts.document_gen.templates.fcc_499a_checklist_generator import generate_499a_checklist
|
||
|
||
permutations = [
|
||
{
|
||
"label": "voip_facilities_full",
|
||
"filer_type": "interconnected_voip",
|
||
"infra_type": "facilities",
|
||
"service_categories": ["interconnected_voip", "long_distance"],
|
||
"is_deminimis": False,
|
||
"is_lire": False,
|
||
},
|
||
{
|
||
"label": "voip_reseller_deminimis",
|
||
"filer_type": "interconnected_voip",
|
||
"infra_type": "reseller",
|
||
"service_categories": ["interconnected_voip"],
|
||
"is_deminimis": True,
|
||
"is_lire": False,
|
||
},
|
||
{
|
||
"label": "clec_facilities_lire",
|
||
"filer_type": "clec",
|
||
"infra_type": "facilities",
|
||
"service_categories": ["local_exchange", "long_distance", "toll_free"],
|
||
"is_deminimis": False,
|
||
"is_lire": True,
|
||
},
|
||
{
|
||
"label": "ixc_both_full",
|
||
"filer_type": "ixc",
|
||
"infra_type": "both",
|
||
"service_categories": ["long_distance", "dedicated_line", "toll_free"],
|
||
"is_deminimis": False,
|
||
"is_lire": False,
|
||
"total_revenue_cents": 125000000,
|
||
"interstate_pct": 72.5,
|
||
"international_pct": 8.3,
|
||
"last_filing_year": 2024,
|
||
"contribution_factor_pct": 35.8,
|
||
"uncollectible_revenue_cents": 2500000,
|
||
"resold_service_costs_cents": 15000000,
|
||
},
|
||
{
|
||
"label": "cmrs_facilities_deminimis",
|
||
"filer_type": "cmrs",
|
||
"infra_type": "facilities",
|
||
"service_categories": ["wireless", "interconnected_voip"],
|
||
"is_deminimis": True,
|
||
"is_lire": False,
|
||
},
|
||
{
|
||
"label": "voip_safe_harbor",
|
||
"filer_type": "interconnected_voip",
|
||
"infra_type": "facilities",
|
||
"service_categories": ["interconnected_voip"],
|
||
"is_deminimis": False,
|
||
"is_lire": False,
|
||
"uses_voip_safe_harbor": True,
|
||
"voip_safe_harbor_pct": 64.9,
|
||
"total_revenue_cents": 80000000,
|
||
"contribution_factor_pct": 35.8,
|
||
},
|
||
{
|
||
"label": "reseller_stale_filing",
|
||
"filer_type": "interconnected_voip",
|
||
"infra_type": "reseller",
|
||
"service_categories": ["interconnected_voip", "resale"],
|
||
"is_deminimis": False,
|
||
"is_lire": False,
|
||
"last_filing_year": 2022,
|
||
},
|
||
{
|
||
"label": "red_light_debt",
|
||
"filer_type": "interconnected_voip",
|
||
"infra_type": "facilities",
|
||
"service_categories": ["interconnected_voip"],
|
||
"is_deminimis": False,
|
||
"is_lire": False,
|
||
"has_outstanding_fcc_debt": True,
|
||
"total_revenue_cents": 50000000,
|
||
"contribution_factor_pct": 35.8,
|
||
},
|
||
]
|
||
|
||
files: list[Path] = []
|
||
|
||
for perm in permutations:
|
||
label = perm.pop("label")
|
||
filename = f"499a_checklist_{label}.docx"
|
||
out = str(OUTPUT_DIR / filename)
|
||
|
||
LOG.info("499-A Checklist: %s", label)
|
||
result = generate_499a_checklist(
|
||
entity_name=COMPANY["entity_name"],
|
||
frn=COMPANY["frn"],
|
||
filer_id_499=COMPANY["filer_id_499"],
|
||
address_street=COMPANY["address_street"],
|
||
address_city=COMPANY["address_city"],
|
||
address_state=COMPANY["address_state"],
|
||
address_zip=COMPANY["address_zip"],
|
||
output_path=out,
|
||
**perm,
|
||
)
|
||
if result:
|
||
files.append(Path(result))
|
||
|
||
return files
|
||
|
||
|
||
def generate_crtc_letters() -> list[Path]:
|
||
"""Generate CRTC notification letters: with and without BITS."""
|
||
from scripts.document_gen.templates.crtc_letter_generator import generate_crtc_letter
|
||
|
||
permutations = [
|
||
{"label": "bc_with_bits", "include_bits": True},
|
||
{"label": "bc_without_bits", "include_bits": False},
|
||
]
|
||
|
||
files: list[Path] = []
|
||
|
||
for perm in permutations:
|
||
label = perm.pop("label")
|
||
filename = f"crtc_letter_{label}.docx"
|
||
out = str(OUTPUT_DIR / filename)
|
||
|
||
LOG.info("CRTC Letter: %s", label)
|
||
result = generate_crtc_letter(
|
||
entity_name="1234567 B.C. Ltd.",
|
||
incorporation_number="BC1234567",
|
||
registered_office="329 Howe St, Suite 200, Vancouver, BC V6C 3N2",
|
||
services_description=(
|
||
"Resale of local and long distance voice services, data services, "
|
||
"and wireless services to business and residential customers across Canada."
|
||
),
|
||
geographic_coverage="Canada-wide",
|
||
regulatory_contact_name="Regulatory Director",
|
||
regulatory_contact_email="regulatory@1234567bc.ca",
|
||
regulatory_contact_phone="+1 (604) 555-0199",
|
||
director_name="James R. Falcon",
|
||
ca_domain="1234567bc.ca",
|
||
output_path=out,
|
||
**perm,
|
||
)
|
||
if result:
|
||
files.append(Path(result))
|
||
|
||
return files
|
||
|
||
|
||
def generate_operating_agreements() -> list[Path]:
|
||
"""Generate operating agreements: member-managed vs manager-managed."""
|
||
from scripts.formation.base import EntityType, FormationOrder, Member
|
||
from scripts.formation.operating_agreement import generate_operating_agreement
|
||
|
||
members = [
|
||
Member(
|
||
name="James R. Falcon",
|
||
address="1234 Telecom Drive, Suite 400",
|
||
city="Denver",
|
||
state="CO",
|
||
zip_code="80202",
|
||
title="Managing Member",
|
||
ownership_pct=60.0,
|
||
is_organizer=True,
|
||
),
|
||
Member(
|
||
name="Sarah Mitchell",
|
||
address="5678 Signal Blvd",
|
||
city="Aurora",
|
||
state="CO",
|
||
zip_code="80012",
|
||
title="Member",
|
||
ownership_pct=40.0,
|
||
),
|
||
]
|
||
|
||
permutations = [
|
||
{"label": "member_managed", "management_type": "member_managed"},
|
||
{"label": "manager_managed", "management_type": "manager_managed"},
|
||
]
|
||
|
||
files: list[Path] = []
|
||
|
||
for perm in permutations:
|
||
label = perm["label"]
|
||
LOG.info("Operating Agreement: %s", label)
|
||
|
||
order = FormationOrder(
|
||
order_id=f"PERM-OA-{label.upper()}",
|
||
state_code="CO",
|
||
entity_type=EntityType.LLC,
|
||
entity_name="Falcon Broadband LLC",
|
||
management_type=perm["management_type"],
|
||
purpose="Telecommunications services, including the provision of voice, data, and internet services",
|
||
members=members,
|
||
registered_agent_name="Northwest Registered Agent",
|
||
registered_agent_address="7700 E Arapahoe Rd, Suite 220, Centennial, CO 80112",
|
||
principal_address="1234 Telecom Drive, Suite 400",
|
||
principal_city="Denver",
|
||
principal_state="CO",
|
||
principal_zip="80202",
|
||
fiscal_year_end="12/31",
|
||
filed_at="2026-03-15",
|
||
)
|
||
|
||
docx_path, pdf_path = generate_operating_agreement(order)
|
||
if docx_path:
|
||
import shutil
|
||
src = Path(docx_path)
|
||
dst = OUTPUT_DIR / f"operating_agreement_{label}.docx"
|
||
shutil.move(str(src), str(dst))
|
||
files.append(dst)
|
||
if pdf_path and pdf_path != "" and Path(pdf_path).is_file():
|
||
pdf_dst = OUTPUT_DIR / f"operating_agreement_{label}.pdf"
|
||
shutil.move(str(pdf_path), str(pdf_dst))
|
||
files.append(pdf_dst)
|
||
|
||
return files
|
||
|
||
|
||
def send_email(all_files: list[Path]) -> None:
|
||
"""Send all generated files as attachments to ops@performancewest.net."""
|
||
if not SMTP_USER or not SMTP_PASS:
|
||
LOG.error("SMTP_USER / SMTP_PASS not set — cannot send email")
|
||
LOG.info("Generated %d files in %s — attach and send manually", len(all_files), OUTPUT_DIR)
|
||
return
|
||
|
||
# Split into batches to avoid oversized emails (max ~20 attachments per email)
|
||
BATCH_SIZE = 20
|
||
batches = [all_files[i:i + BATCH_SIZE] for i in range(0, len(all_files), BATCH_SIZE)]
|
||
|
||
for batch_num, batch in enumerate(batches, 1):
|
||
msg = MIMEMultipart()
|
||
msg["From"] = FROM_EMAIL
|
||
msg["To"] = TO_EMAIL
|
||
msg["Subject"] = f"Document Permutations — All Templates (Batch {batch_num}/{len(batches)})"
|
||
|
||
# Build summary
|
||
summary_lines = [f"<li>{f.name} ({f.stat().st_size / 1024:.0f} KB)</li>" for f in batch]
|
||
body_html = f"""<html><body>
|
||
<h2>Document Permutation Output — Batch {batch_num}/{len(batches)}</h2>
|
||
<p>{len(batch)} files attached. {len(all_files)} total across all batches.</p>
|
||
<h3>Files in this batch:</h3>
|
||
<ul>{''.join(summary_lines)}</ul>
|
||
<p>Generated from <code>scripts/generate_all_permutations.py</code></p>
|
||
</body></html>"""
|
||
|
||
msg.attach(MIMEText(body_html, "html"))
|
||
|
||
for filepath in batch:
|
||
with open(filepath, "rb") as f:
|
||
part = MIMEApplication(f.read(), Name=filepath.name)
|
||
part["Content-Disposition"] = f'attachment; filename="{filepath.name}"'
|
||
msg.attach(part)
|
||
|
||
try:
|
||
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as server:
|
||
server.ehlo()
|
||
server.starttls()
|
||
server.ehlo()
|
||
server.login(SMTP_USER, SMTP_PASS)
|
||
server.sendmail(FROM_EMAIL, [TO_EMAIL], msg.as_string())
|
||
LOG.info("Email batch %d/%d sent to %s (%d files)", batch_num, len(batches), TO_EMAIL, len(batch))
|
||
except Exception:
|
||
LOG.exception("Failed to send email batch %d to %s", batch_num, TO_EMAIL)
|
||
|
||
|
||
def main() -> None:
|
||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
||
all_files: list[Path] = []
|
||
|
||
# 1. RMD Certification Letters (6 roles × 5 STIR/SHAKEN statuses = 30 DOCX)
|
||
LOG.info("=" * 60)
|
||
LOG.info("GENERATING RMD CERTIFICATION LETTERS")
|
||
LOG.info("=" * 60)
|
||
all_files.extend(generate_rmd_letters())
|
||
|
||
# 2. Exhibit A — Robocall Mitigation Program (6 roles = 6 DOCX)
|
||
LOG.info("=" * 60)
|
||
LOG.info("GENERATING EXHIBIT A DOCUMENTS")
|
||
LOG.info("=" * 60)
|
||
all_files.extend(generate_exhibit_a_docs())
|
||
|
||
# 3. CPNI Certification Letters (4 permutations)
|
||
LOG.info("=" * 60)
|
||
LOG.info("GENERATING CPNI CERTIFICATION LETTERS")
|
||
LOG.info("=" * 60)
|
||
all_files.extend(generate_cpni_letters())
|
||
|
||
# 4. FCC 499-A Filing Checklists (6 permutations)
|
||
LOG.info("=" * 60)
|
||
LOG.info("GENERATING 499-A CHECKLISTS")
|
||
LOG.info("=" * 60)
|
||
all_files.extend(generate_499a_checklists())
|
||
|
||
# 5. CRTC Notification Letters (2 permutations)
|
||
LOG.info("=" * 60)
|
||
LOG.info("GENERATING CRTC LETTERS")
|
||
LOG.info("=" * 60)
|
||
all_files.extend(generate_crtc_letters())
|
||
|
||
# 6. LLC Operating Agreements (2 permutations)
|
||
LOG.info("=" * 60)
|
||
LOG.info("GENERATING OPERATING AGREEMENTS")
|
||
LOG.info("=" * 60)
|
||
all_files.extend(generate_operating_agreements())
|
||
|
||
# Convert all DOCX to PDF
|
||
LOG.info("=" * 60)
|
||
LOG.info("CONVERTING DOCX → PDF")
|
||
LOG.info("=" * 60)
|
||
docx_files = [f for f in all_files if f.suffix == ".docx"]
|
||
for docx in docx_files:
|
||
pdf = _convert_to_pdf(docx)
|
||
if pdf:
|
||
all_files.append(pdf)
|
||
|
||
LOG.info("=" * 60)
|
||
LOG.info("TOTAL FILES GENERATED: %d", len(all_files))
|
||
LOG.info(" DOCX: %d", sum(1 for f in all_files if f.suffix == ".docx"))
|
||
LOG.info(" PDF: %d", sum(1 for f in all_files if f.suffix == ".pdf"))
|
||
LOG.info("=" * 60)
|
||
|
||
# Email everything
|
||
send_email(all_files)
|
||
|
||
LOG.info("Done. Output directory: %s", OUTPUT_DIR)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|