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>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
630
scripts/generate_all_permutations.py
Normal file
630
scripts/generate_all_permutations.py
Normal file
|
|
@ -0,0 +1,630 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue