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
5
scripts/document_gen/__init__.py
Normal file
5
scripts/document_gen/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Document generation library — DOCX templates, PDF conversion, MinIO storage, LLM writing
|
||||
from .docx_builder import DocxBuilder
|
||||
from .pdf_converter import convert_to_pdf
|
||||
from .minio_client import MinioStorage
|
||||
from .llm_writer import LLMWriter
|
||||
222
scripts/document_gen/docx_builder.py
Normal file
222
scripts/document_gen/docx_builder.py
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
"""
|
||||
DOCX template builder using python-docx + Jinja2.
|
||||
|
||||
Templates use Jinja2 placeholders: {{ variable_name }}
|
||||
Supports:
|
||||
- Simple variable substitution
|
||||
- Conditional sections ({% if ... %})
|
||||
- Loops for member tables ({% for member in members %})
|
||||
- Section insertion (replace a placeholder paragraph with multi-paragraph LLM output)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from docx import Document
|
||||
from docx.shared import Inches, Pt, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from jinja2 import Template
|
||||
|
||||
LOG = logging.getLogger("document_gen.docx")
|
||||
|
||||
TEMPLATES_DIR = Path(os.getenv("TEMPLATES_DIR", "/app/scripts/templates"))
|
||||
|
||||
|
||||
class DocxBuilder:
|
||||
"""Build DOCX documents from templates with variable substitution."""
|
||||
|
||||
def __init__(self, template_name: str):
|
||||
"""Load a DOCX template by name (e.g., 'operating-agreement')."""
|
||||
self.template_path = TEMPLATES_DIR / f"{template_name}.docx"
|
||||
if not self.template_path.exists():
|
||||
raise FileNotFoundError(f"Template not found: {self.template_path}")
|
||||
self.doc = Document(str(self.template_path))
|
||||
self.variables: dict[str, Any] = {}
|
||||
|
||||
def set_variables(self, variables: dict[str, Any]) -> "DocxBuilder":
|
||||
"""Set template variables for substitution."""
|
||||
self.variables = variables
|
||||
return self
|
||||
|
||||
def fill(self) -> "DocxBuilder":
|
||||
"""Fill all Jinja2 placeholders in the document."""
|
||||
# Process paragraphs
|
||||
for para in self.doc.paragraphs:
|
||||
self._fill_paragraph(para)
|
||||
|
||||
# Process table cells
|
||||
for table in self.doc.tables:
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
for para in cell.paragraphs:
|
||||
self._fill_paragraph(para)
|
||||
|
||||
# Process headers and footers
|
||||
for section in self.doc.sections:
|
||||
for header_para in section.header.paragraphs:
|
||||
self._fill_paragraph(header_para)
|
||||
for footer_para in section.footer.paragraphs:
|
||||
self._fill_paragraph(footer_para)
|
||||
|
||||
return self
|
||||
|
||||
def _fill_paragraph(self, para):
|
||||
"""Replace Jinja2 placeholders in a paragraph, preserving formatting."""
|
||||
full_text = para.text
|
||||
if "{{" not in full_text and "{%" not in full_text:
|
||||
return
|
||||
|
||||
# Render the full paragraph text through Jinja2
|
||||
try:
|
||||
template = Template(full_text)
|
||||
rendered = template.render(**self.variables)
|
||||
except Exception as e:
|
||||
LOG.warning("Template render error in paragraph: %s — %s", full_text[:80], e)
|
||||
return
|
||||
|
||||
if rendered == full_text:
|
||||
return
|
||||
|
||||
# Clear all runs and set the rendered text in the first run
|
||||
if para.runs:
|
||||
# Preserve the formatting of the first run
|
||||
first_run = para.runs[0]
|
||||
first_run.text = rendered
|
||||
for run in para.runs[1:]:
|
||||
run.text = ""
|
||||
else:
|
||||
para.text = rendered
|
||||
|
||||
def insert_section(self, placeholder: str, content: str) -> "DocxBuilder":
|
||||
"""Replace a placeholder paragraph with multi-paragraph content.
|
||||
|
||||
Used for LLM-generated sections — the placeholder (e.g., '{{findings_section}}')
|
||||
is replaced with multiple paragraphs of formatted text.
|
||||
"""
|
||||
for i, para in enumerate(self.doc.paragraphs):
|
||||
if placeholder in para.text:
|
||||
# Split content into paragraphs
|
||||
lines = content.strip().split("\n\n")
|
||||
|
||||
# Replace the placeholder paragraph with the first line
|
||||
para.text = lines[0] if lines else ""
|
||||
|
||||
# Insert remaining lines as new paragraphs after the current one
|
||||
for j, line in enumerate(lines[1:], 1):
|
||||
new_para = copy.deepcopy(para)
|
||||
new_para.text = line
|
||||
para._element.addnext(new_para._element)
|
||||
|
||||
return self
|
||||
LOG.warning("Placeholder not found: %s", placeholder)
|
||||
return self
|
||||
|
||||
def add_cover_page(
|
||||
self,
|
||||
title: str,
|
||||
subtitle: str = "",
|
||||
client_name: str = "",
|
||||
order_number: str = "",
|
||||
date: str = "",
|
||||
) -> "DocxBuilder":
|
||||
"""Add a branded cover page at the beginning of the document."""
|
||||
# Insert paragraphs at the top
|
||||
first_para = self.doc.paragraphs[0] if self.doc.paragraphs else self.doc.add_paragraph()
|
||||
|
||||
# We'll prepend by inserting before the first paragraph
|
||||
cover_elements = []
|
||||
|
||||
# Spacer
|
||||
spacer = self.doc.add_paragraph()
|
||||
spacer.space_after = Pt(72)
|
||||
|
||||
# Title
|
||||
title_para = self.doc.add_paragraph()
|
||||
title_run = title_para.add_run(title)
|
||||
title_run.font.size = Pt(28)
|
||||
title_run.font.color.rgb = RGBColor(0x2D, 0x4E, 0x78) # pw-700
|
||||
title_run.font.bold = True
|
||||
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
# Subtitle
|
||||
if subtitle:
|
||||
sub_para = self.doc.add_paragraph()
|
||||
sub_run = sub_para.add_run(subtitle)
|
||||
sub_run.font.size = Pt(14)
|
||||
sub_run.font.color.rgb = RGBColor(0x6B, 0x72, 0x80)
|
||||
sub_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
# Client info
|
||||
if client_name:
|
||||
client_para = self.doc.add_paragraph()
|
||||
client_para.space_before = Pt(36)
|
||||
client_run = client_para.add_run(f"Prepared for: {client_name}")
|
||||
client_run.font.size = Pt(12)
|
||||
client_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
# Order number + date
|
||||
meta_para = self.doc.add_paragraph()
|
||||
meta_parts = []
|
||||
if order_number:
|
||||
meta_parts.append(f"Order: {order_number}")
|
||||
meta_parts.append(f"Date: {date or datetime.now().strftime('%B %d, %Y')}")
|
||||
meta_run = meta_para.add_run(" | ".join(meta_parts))
|
||||
meta_run.font.size = Pt(10)
|
||||
meta_run.font.color.rgb = RGBColor(0x9C, 0xA3, 0xAF)
|
||||
meta_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
# Performance West branding
|
||||
brand_para = self.doc.add_paragraph()
|
||||
brand_para.space_before = Pt(48)
|
||||
brand_run = brand_para.add_run("Performance West Inc.")
|
||||
brand_run.font.size = Pt(10)
|
||||
brand_run.font.color.rgb = RGBColor(0x2D, 0x4E, 0x78)
|
||||
brand_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
addr_para = self.doc.add_paragraph()
|
||||
addr_run = addr_para.add_run("525 Randall Ave Ste 100-1195, Cheyenne, WY 82001 | 1-888-411-0383")
|
||||
addr_run.font.size = Pt(8)
|
||||
addr_run.font.color.rgb = RGBColor(0x9C, 0xA3, 0xAF)
|
||||
addr_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
# Page break after cover
|
||||
self.doc.add_page_break()
|
||||
|
||||
# Move cover elements to the beginning
|
||||
body = self.doc.element.body
|
||||
# The paragraphs we just added are at the end — move them to the front
|
||||
added = list(body)[-8:] # Last 8 elements we added (spacer, title, sub, client, meta, brand, addr, pagebreak)
|
||||
for elem in reversed(added):
|
||||
body.insert(0, elem)
|
||||
|
||||
return self
|
||||
|
||||
def add_disclaimer(self, text: str = "") -> "DocxBuilder":
|
||||
"""Add a disclaimer paragraph at the end of the document."""
|
||||
default = (
|
||||
"DISCLAIMER: This document is prepared by Performance West Inc. for compliance consulting purposes only. "
|
||||
"It does not constitute legal advice, legal representation, or create an attorney-client relationship. "
|
||||
"For legal matters, consult a licensed attorney in your jurisdiction."
|
||||
)
|
||||
para = self.doc.add_paragraph()
|
||||
para.space_before = Pt(24)
|
||||
run = para.add_run(text or default)
|
||||
run.font.size = Pt(8)
|
||||
run.font.italic = True
|
||||
run.font.color.rgb = RGBColor(0x9C, 0xA3, 0xAF)
|
||||
return self
|
||||
|
||||
def save(self, output_path: str | Path) -> Path:
|
||||
"""Save the filled document to a file."""
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.doc.save(str(output_path))
|
||||
LOG.info("DOCX saved: %s", output_path)
|
||||
return output_path
|
||||
137
scripts/document_gen/llm_writer.py
Normal file
137
scripts/document_gen/llm_writer.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
LLM content writer for compliance report sections.
|
||||
|
||||
Uses Ollama (local LLM) to generate analysis and prose for compliance reports.
|
||||
Each service type provides its own system prompt and section templates.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
LOG = logging.getLogger("document_gen.llm")
|
||||
|
||||
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
|
||||
DEFAULT_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:7b")
|
||||
|
||||
|
||||
class LLMWriter:
|
||||
"""Generate compliance report content using a local LLM."""
|
||||
|
||||
def __init__(self, model: str = DEFAULT_MODEL):
|
||||
self.model = model
|
||||
self.base_url = OLLAMA_HOST
|
||||
self.client = httpx.Client(timeout=300.0) # 5 min timeout for long generations
|
||||
|
||||
def generate_section(
|
||||
self,
|
||||
system_prompt: str,
|
||||
user_prompt: str,
|
||||
temperature: float = 0.3,
|
||||
max_tokens: int = 4096,
|
||||
) -> str:
|
||||
"""Generate a single section of a compliance report.
|
||||
|
||||
Args:
|
||||
system_prompt: System instructions (compliance rules, format requirements)
|
||||
user_prompt: The specific section to generate (includes customer data)
|
||||
temperature: Lower = more factual, higher = more creative
|
||||
max_tokens: Maximum output length
|
||||
|
||||
Returns:
|
||||
Generated text content for the section
|
||||
"""
|
||||
LOG.info("Generating section (model=%s, temp=%.1f)...", self.model, temperature)
|
||||
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.base_url}/api/chat",
|
||||
json={
|
||||
"model": self.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
"options": {
|
||||
"temperature": temperature,
|
||||
"num_predict": max_tokens,
|
||||
},
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
content = data.get("message", {}).get("content", "")
|
||||
LOG.info("Generated %d characters", len(content))
|
||||
return content.strip()
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
LOG.error("Ollama request failed: %s", e)
|
||||
raise RuntimeError(f"LLM generation failed: {e}") from e
|
||||
|
||||
def generate_report(
|
||||
self,
|
||||
service_type: str,
|
||||
customer_data: dict[str, Any],
|
||||
sections: list[dict[str, str]],
|
||||
system_prompt: str,
|
||||
) -> dict[str, str]:
|
||||
"""Generate all sections of a compliance report.
|
||||
|
||||
Args:
|
||||
service_type: Service identifier (e.g., 'flsa_audit')
|
||||
customer_data: Customer and order information
|
||||
sections: List of {"name": "section_name", "prompt": "section-specific instructions"}
|
||||
system_prompt: Base system prompt for this service type
|
||||
|
||||
Returns:
|
||||
Dict mapping section names to generated content
|
||||
"""
|
||||
results: dict[str, str] = {}
|
||||
customer_json = json.dumps(customer_data, indent=2)
|
||||
|
||||
for section in sections:
|
||||
section_name = section["name"]
|
||||
section_prompt = section["prompt"]
|
||||
|
||||
user_prompt = (
|
||||
f"SERVICE: {service_type}\n"
|
||||
f"SECTION: {section_name}\n\n"
|
||||
f"CUSTOMER DATA:\n{customer_json}\n\n"
|
||||
f"INSTRUCTIONS:\n{section_prompt}"
|
||||
)
|
||||
|
||||
try:
|
||||
content = self.generate_section(
|
||||
system_prompt=system_prompt,
|
||||
user_prompt=user_prompt,
|
||||
temperature=0.3,
|
||||
)
|
||||
results[section_name] = content
|
||||
LOG.info("Section '%s' generated (%d chars)", section_name, len(content))
|
||||
except Exception as e:
|
||||
LOG.error("Section '%s' failed: %s", section_name, e)
|
||||
results[section_name] = f"[GENERATION FAILED: {e}]"
|
||||
|
||||
return results
|
||||
|
||||
def health_check(self) -> bool:
|
||||
"""Check if Ollama is reachable and the model is available."""
|
||||
try:
|
||||
resp = self.client.get(f"{self.base_url}/api/tags")
|
||||
if resp.status_code != 200:
|
||||
return False
|
||||
models = resp.json().get("models", [])
|
||||
model_names = [m.get("name", "") for m in models]
|
||||
available = any(self.model in name for name in model_names)
|
||||
if not available:
|
||||
LOG.warning("Model %s not found. Available: %s", self.model, model_names)
|
||||
return available
|
||||
except Exception as e:
|
||||
LOG.error("Ollama health check failed: %s", e)
|
||||
return False
|
||||
128
scripts/document_gen/minio_client.py
Normal file
128
scripts/document_gen/minio_client.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
"""
|
||||
MinIO (S3-compatible) client for document storage.
|
||||
|
||||
Uploads generated documents to MinIO and returns accessible URLs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from minio import Minio
|
||||
from minio.error import S3Error
|
||||
|
||||
LOG = logging.getLogger("document_gen.minio")
|
||||
|
||||
BUCKET = os.getenv("MINIO_BUCKET", "performancewest")
|
||||
|
||||
|
||||
class MinioStorage:
|
||||
"""S3-compatible document storage via MinIO."""
|
||||
|
||||
def __init__(self):
|
||||
self.client = Minio(
|
||||
endpoint=f"{os.getenv('MINIO_ENDPOINT', 'localhost')}:{os.getenv('MINIO_PORT', '9000')}",
|
||||
access_key=os.getenv("MINIO_ACCESS_KEY", ""),
|
||||
secret_key=os.getenv("MINIO_SECRET_KEY", ""),
|
||||
secure=os.getenv("MINIO_SECURE", "false").lower() == "true",
|
||||
)
|
||||
self._ensure_bucket()
|
||||
|
||||
def _ensure_bucket(self):
|
||||
"""Create the bucket if it doesn't exist."""
|
||||
try:
|
||||
if not self.client.bucket_exists(BUCKET):
|
||||
self.client.make_bucket(BUCKET)
|
||||
LOG.info("Created MinIO bucket: %s", BUCKET)
|
||||
except S3Error as e:
|
||||
LOG.error("MinIO bucket check failed: %s", e)
|
||||
|
||||
def upload(
|
||||
self,
|
||||
local_path: str | Path,
|
||||
remote_path: str,
|
||||
content_type: str = "application/octet-stream",
|
||||
) -> str:
|
||||
"""Upload a file to MinIO.
|
||||
|
||||
Args:
|
||||
local_path: Local file path
|
||||
remote_path: Object key in the bucket (e.g., "formations/PW-2026-XXXX/articles.pdf")
|
||||
content_type: MIME type
|
||||
|
||||
Returns:
|
||||
The object URL (internal MinIO URL)
|
||||
"""
|
||||
local_path = Path(local_path)
|
||||
if not local_path.exists():
|
||||
raise FileNotFoundError(f"File not found: {local_path}")
|
||||
|
||||
# Auto-detect content type
|
||||
if content_type == "application/octet-stream":
|
||||
suffix = local_path.suffix.lower()
|
||||
content_types = {
|
||||
".pdf": "application/pdf",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".doc": "application/msword",
|
||||
".html": "text/html",
|
||||
".txt": "text/plain",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
}
|
||||
content_type = content_types.get(suffix, content_type)
|
||||
|
||||
try:
|
||||
self.client.fput_object(
|
||||
BUCKET,
|
||||
remote_path,
|
||||
str(local_path),
|
||||
content_type=content_type,
|
||||
)
|
||||
LOG.info("Uploaded: %s → %s/%s", local_path.name, BUCKET, remote_path)
|
||||
return f"{BUCKET}/{remote_path}"
|
||||
except S3Error as e:
|
||||
LOG.error("MinIO upload failed: %s", e)
|
||||
raise
|
||||
|
||||
def download(self, remote_path: str, local_path: str | Path) -> Path:
|
||||
"""Download a file from MinIO."""
|
||||
local_path = Path(local_path)
|
||||
local_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
self.client.fget_object(BUCKET, remote_path, str(local_path))
|
||||
LOG.info("Downloaded: %s/%s → %s", BUCKET, remote_path, local_path)
|
||||
return local_path
|
||||
except S3Error as e:
|
||||
LOG.error("MinIO download failed: %s", e)
|
||||
raise
|
||||
|
||||
def get_url(self, remote_path: str, expires_hours: int = 24) -> str:
|
||||
"""Get a presigned URL for a file (for client download)."""
|
||||
from datetime import timedelta
|
||||
try:
|
||||
url = self.client.presigned_get_object(
|
||||
BUCKET, remote_path, expires=timedelta(hours=expires_hours),
|
||||
)
|
||||
return url
|
||||
except S3Error as e:
|
||||
LOG.error("MinIO presign failed: %s", e)
|
||||
raise
|
||||
|
||||
def list_objects(self, prefix: str) -> list[str]:
|
||||
"""List all objects under a prefix."""
|
||||
try:
|
||||
objects = self.client.list_objects(BUCKET, prefix=prefix, recursive=True)
|
||||
return [obj.object_name for obj in objects]
|
||||
except S3Error as e:
|
||||
LOG.error("MinIO list failed: %s", e)
|
||||
return []
|
||||
|
||||
def delete(self, remote_path: str):
|
||||
"""Delete an object from MinIO."""
|
||||
try:
|
||||
self.client.remove_object(BUCKET, remote_path)
|
||||
LOG.info("Deleted: %s/%s", BUCKET, remote_path)
|
||||
except S3Error as e:
|
||||
LOG.error("MinIO delete failed: %s", e)
|
||||
285
scripts/document_gen/pdf_converter.py
Normal file
285
scripts/document_gen/pdf_converter.py
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
"""
|
||||
DOCX → PDF conversion.
|
||||
|
||||
Primary: Windows Word VM via MinIO (pixel-perfect, no open ports required).
|
||||
Fallback: LibreOffice headless (70-80% fidelity, always available in container).
|
||||
|
||||
MinIO transport protocol
|
||||
─────────────────────────
|
||||
PUT docx → {bucket}/to-convert/{job_id}.docx (this module)
|
||||
WAIT poll → {bucket}/converted/{job_id}.pdf (this module)
|
||||
GET pdf ← {bucket}/converted/{job_id}.pdf (this module)
|
||||
DEL docx ← {bucket}/to-convert/{job_id}.docx (docserver_worker.py)
|
||||
DEL pdf ← {bucket}/converted/{job_id}.pdf (this module, after download)
|
||||
|
||||
The Windows VM runs docserver_worker.py which:
|
||||
1. Polls to-convert/ every 12 seconds
|
||||
2. Downloads the DOCX, converts via Word COM, uploads the PDF to converted/
|
||||
3. Deletes the source DOCX from to-convert/
|
||||
|
||||
No HTTP server, no open ports, no SSH tunnel. Only MinIO is needed.
|
||||
|
||||
Environment variables (same MinIO creds as the workers):
|
||||
MINIO_ENDPOINT — MinIO host (default: minio)
|
||||
MINIO_PORT — MinIO port (default: 9000)
|
||||
MINIO_ACCESS_KEY — access key
|
||||
MINIO_SECRET_KEY — secret key
|
||||
MINIO_BUCKET — bucket name (default: performancewest)
|
||||
USE_DOCSERVER — enable Word VM path (default: true)
|
||||
DOCSERVER_TIMEOUT — max seconds to wait for Word to produce the PDF (default: 120)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
LOG = logging.getLogger("document_gen.pdf")
|
||||
|
||||
# MinIO settings — inherited from the workers container env
|
||||
_MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "minio")
|
||||
_MINIO_PORT = int(os.getenv("MINIO_PORT", "9000"))
|
||||
_MINIO_ACCESS = os.getenv("MINIO_ACCESS_KEY", "")
|
||||
_MINIO_SECRET = os.getenv("MINIO_SECRET_KEY", "")
|
||||
_MINIO_BUCKET = os.getenv("MINIO_BUCKET", "performancewest")
|
||||
_MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true"
|
||||
|
||||
USE_DOCSERVER = os.getenv("USE_DOCSERVER", "true").lower() == "true"
|
||||
DOCSERVER_TIMEOUT = int(os.getenv("DOCSERVER_TIMEOUT", "120")) # seconds
|
||||
_POLL_INTERVAL = 12 # seconds between polls for the converted PDF
|
||||
|
||||
# MinIO key prefixes
|
||||
_PREFIX_IN = "to-convert" # docx files waiting to be processed
|
||||
_PREFIX_OUT = "converted" # pdf files ready for pickup
|
||||
|
||||
|
||||
def _minio_client():
|
||||
"""Return a configured MinIO client."""
|
||||
from minio import Minio # type: ignore
|
||||
return Minio(
|
||||
f"{_MINIO_ENDPOINT}:{_MINIO_PORT}",
|
||||
access_key=_MINIO_ACCESS,
|
||||
secret_key=_MINIO_SECRET,
|
||||
secure=_MINIO_SECURE,
|
||||
)
|
||||
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
def convert_to_pdf(docx_path: str | Path, output_dir: str | Path | None = None) -> Path:
|
||||
"""Convert a DOCX to PDF.
|
||||
|
||||
Tries the Word VM via MinIO first (pixel-perfect).
|
||||
Falls back to LibreOffice headless if the VM is unavailable or slow.
|
||||
|
||||
Args:
|
||||
docx_path: Path to the .docx file on disk
|
||||
output_dir: Where to write the PDF (defaults to same dir as docx)
|
||||
|
||||
Returns:
|
||||
Path to the generated PDF file
|
||||
"""
|
||||
docx_path = Path(docx_path)
|
||||
if not docx_path.exists():
|
||||
raise FileNotFoundError(f"DOCX not found: {docx_path}")
|
||||
|
||||
out_dir = Path(output_dir) if output_dir else docx_path.parent
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
pdf_path = out_dir / docx_path.with_suffix(".pdf").name
|
||||
|
||||
if USE_DOCSERVER and _MINIO_ACCESS:
|
||||
try:
|
||||
return _convert_via_minio(docx_path, pdf_path)
|
||||
except Exception as exc:
|
||||
LOG.warning(
|
||||
"Word VM via MinIO unavailable (%s) — falling back to LibreOffice", exc
|
||||
)
|
||||
|
||||
return _convert_via_libreoffice(docx_path, pdf_path, out_dir)
|
||||
|
||||
|
||||
def convert_batch(docx_paths: list[str | Path], output_dir: str | Path) -> list[Path]:
|
||||
"""Convert multiple DOCX files to PDFs.
|
||||
|
||||
Submits all jobs to the Word VM concurrently (each gets its own MinIO key),
|
||||
then collects results as they arrive. Falls back per-file to LibreOffice.
|
||||
"""
|
||||
docx_paths = [Path(p) for p in docx_paths]
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if USE_DOCSERVER and _MINIO_ACCESS and docx_paths:
|
||||
try:
|
||||
return _batch_via_minio(docx_paths, output_dir)
|
||||
except Exception as exc:
|
||||
LOG.warning("Batch via Word VM failed (%s) — converting one by one via LibreOffice", exc)
|
||||
|
||||
results = []
|
||||
for docx_path in docx_paths:
|
||||
try:
|
||||
results.append(convert_to_pdf(docx_path, output_dir))
|
||||
except Exception as exc:
|
||||
LOG.error("Failed to convert %s: %s", docx_path.name, exc)
|
||||
return results
|
||||
|
||||
|
||||
def health_check() -> dict:
|
||||
"""Return status of both conversion backends."""
|
||||
status: dict = {"libreoffice": False, "docserver_minio": False}
|
||||
|
||||
# LibreOffice
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["libreoffice", "--version"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
status["libreoffice"] = r.returncode == 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Word VM — check if the MinIO bucket is accessible and if the worker
|
||||
# has recently touched a heartbeat object
|
||||
if USE_DOCSERVER and _MINIO_ACCESS:
|
||||
try:
|
||||
mc = _minio_client()
|
||||
mc.bucket_exists(_MINIO_BUCKET) # just checks connectivity
|
||||
status["docserver_minio"] = True
|
||||
status["minio_bucket"] = _MINIO_BUCKET
|
||||
except Exception as exc:
|
||||
status["minio_error"] = str(exc)
|
||||
|
||||
return status
|
||||
|
||||
|
||||
# ── MinIO transport ───────────────────────────────────────────────────────────
|
||||
|
||||
def _convert_via_minio(docx_path: Path, pdf_path: Path) -> Path:
|
||||
"""Upload DOCX to MinIO, wait for the Word VM to convert it, download PDF.
|
||||
|
||||
Atomic upload: the DOCX is first uploaded to a .tmp key, then renamed
|
||||
(copy + delete) to the final key. This prevents the Windows worker from
|
||||
downloading a partially-uploaded file.
|
||||
"""
|
||||
from minio.commonconfig import CopySource # type: ignore
|
||||
|
||||
job_id = str(uuid.uuid4()).replace("-", "")
|
||||
tmp_key = f"{_PREFIX_IN}/.tmp_{job_id}.docx"
|
||||
in_key = f"{_PREFIX_IN}/{job_id}.docx"
|
||||
out_key = f"{_PREFIX_OUT}/{job_id}.pdf"
|
||||
|
||||
mc = _minio_client()
|
||||
|
||||
# Ensure bucket exists
|
||||
if not mc.bucket_exists(_MINIO_BUCKET):
|
||||
mc.make_bucket(_MINIO_BUCKET)
|
||||
|
||||
# Upload DOCX to temp key first (invisible to worker — it ignores .tmp_ prefix)
|
||||
LOG.info("[%s] Uploading %s → minio://%s/%s (staging)", job_id[:8], docx_path.name, _MINIO_BUCKET, tmp_key)
|
||||
mc.fput_object(
|
||||
_MINIO_BUCKET, tmp_key, str(docx_path),
|
||||
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
metadata={"x-amz-meta-source": docx_path.name},
|
||||
)
|
||||
|
||||
# Atomic rename: copy tmp → final, then delete tmp
|
||||
# MinIO copy_object is a server-side operation — the object appears
|
||||
# at the destination key atomically (no partial state visible)
|
||||
mc.copy_object(
|
||||
_MINIO_BUCKET, in_key,
|
||||
CopySource(_MINIO_BUCKET, tmp_key),
|
||||
)
|
||||
mc.remove_object(_MINIO_BUCKET, tmp_key)
|
||||
LOG.info("[%s] Staged → minio://%s/%s (live)", job_id[:8], _MINIO_BUCKET, in_key)
|
||||
|
||||
# Poll for the converted PDF
|
||||
deadline = time.monotonic() + DOCSERVER_TIMEOUT
|
||||
LOG.info("[%s] Waiting for Word VM to convert (timeout=%ds)...", job_id[:8], DOCSERVER_TIMEOUT)
|
||||
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
mc.stat_object(_MINIO_BUCKET, out_key)
|
||||
# Object exists — download it
|
||||
LOG.info("[%s] PDF ready — downloading", job_id[:8])
|
||||
mc.fget_object(_MINIO_BUCKET, out_key, str(pdf_path))
|
||||
# Clean up the converted output from MinIO
|
||||
try:
|
||||
mc.remove_object(_MINIO_BUCKET, out_key)
|
||||
except Exception:
|
||||
pass
|
||||
LOG.info("[%s] PDF written: %s (%d bytes)", job_id[:8], pdf_path.name, pdf_path.stat().st_size)
|
||||
return pdf_path
|
||||
except Exception:
|
||||
# Object not there yet — keep waiting
|
||||
time.sleep(_POLL_INTERVAL)
|
||||
|
||||
# Timed out — clean up the orphaned DOCX and raise
|
||||
try:
|
||||
mc.remove_object(_MINIO_BUCKET, in_key)
|
||||
except Exception:
|
||||
pass
|
||||
raise TimeoutError(
|
||||
f"Word VM did not convert {docx_path.name} within {DOCSERVER_TIMEOUT}s. "
|
||||
f"Is docserver_worker.py running and connected to MinIO?"
|
||||
)
|
||||
|
||||
|
||||
def _batch_via_minio(docx_paths: list[Path], output_dir: Path) -> list[Path]:
|
||||
"""Submit all DOCX files in parallel, collect results."""
|
||||
import threading
|
||||
|
||||
results: list[Path | None] = [None] * len(docx_paths)
|
||||
errors: list[str | None] = [None] * len(docx_paths)
|
||||
|
||||
def _convert_one(idx: int, docx_path: Path) -> None:
|
||||
pdf_path = output_dir / docx_path.with_suffix(".pdf").name
|
||||
try:
|
||||
results[idx] = _convert_via_minio(docx_path, pdf_path)
|
||||
except Exception as exc:
|
||||
LOG.error("Batch item %d (%s) failed: %s", idx, docx_path.name, exc)
|
||||
errors[idx] = str(exc)
|
||||
# Fallback per-file
|
||||
try:
|
||||
results[idx] = _convert_via_libreoffice(docx_path, pdf_path, output_dir)
|
||||
except Exception as lo_exc:
|
||||
LOG.error("LibreOffice fallback also failed for %s: %s", docx_path.name, lo_exc)
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=_convert_one, args=(i, p), daemon=True)
|
||||
for i, p in enumerate(docx_paths)
|
||||
]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join(timeout=DOCSERVER_TIMEOUT + 10)
|
||||
|
||||
return [r for r in results if r is not None]
|
||||
|
||||
|
||||
# ── LibreOffice fallback ──────────────────────────────────────────────────────
|
||||
|
||||
def _convert_via_libreoffice(docx_path: Path, pdf_path: Path, out_dir: Path) -> Path:
|
||||
"""Convert DOCX to PDF using LibreOffice headless (fallback)."""
|
||||
LOG.info("Converting %s via LibreOffice headless...", docx_path.name)
|
||||
|
||||
cmd = [
|
||||
"libreoffice", "--headless",
|
||||
"--convert-to", "pdf",
|
||||
"--outdir", str(out_dir),
|
||||
str(docx_path),
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
|
||||
if result.returncode != 0:
|
||||
LOG.error("LibreOffice conversion failed: %s", result.stderr)
|
||||
raise RuntimeError(f"LibreOffice failed: {result.stderr[:300]}")
|
||||
|
||||
if not pdf_path.exists():
|
||||
raise RuntimeError(f"PDF not found at expected path after LibreOffice: {pdf_path}")
|
||||
|
||||
LOG.info("PDF created via LibreOffice: %s (%d bytes)", pdf_path.name, pdf_path.stat().st_size)
|
||||
return pdf_path
|
||||
0
scripts/document_gen/templates/__init__.py
Normal file
0
scripts/document_gen/templates/__init__.py
Normal file
213
scripts/document_gen/templates/calea_audio_bridge_generator.py
Normal file
213
scripts/document_gen/templates/calea_audio_bridge_generator.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
"""
|
||||
CALEA SSI Plan — Audio Bridging / Conferencing variant.
|
||||
|
||||
Audio bridging / conferencing is narrowly scoped for CALEA purposes.
|
||||
To the extent the service qualifies as an information service rather
|
||||
than as telecommunications (47 USC § 153(24) vs. § 153(53)), the CALEA
|
||||
covered-entity definition at 47 USC § 1001(8)(B)(ii) may not apply.
|
||||
For the telecommunications-service portion that does apply, intercept
|
||||
capability is provisioned at the bridge/softswitch; non-real-time
|
||||
replay of recordings is treated as a stored-record production under
|
||||
18 USC § 2703 rather than as a real-time intercept under Title III.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.calea_audio_bridge")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CALEA Audio Bridge unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "audio_bridging"
|
||||
VARIANT_LABEL = "Audio Bridging / Conferencing"
|
||||
|
||||
|
||||
def _heading(doc, text):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12); p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(text); r.bold = True; r.font.size = Pt(13); r.font.color.rgb = NAVY
|
||||
|
||||
|
||||
def _body(doc, text, bold=False):
|
||||
p = doc.add_paragraph(); p.paragraph_format.space_after = Pt(6)
|
||||
r = p.add_run(text); r.font.size = Pt(11); r.bold = bold
|
||||
|
||||
|
||||
def _bullets(doc, items):
|
||||
for it in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.clear(); r = p.add_run(it); r.font.size = Pt(11)
|
||||
|
||||
|
||||
def generate_calea_audio_bridge(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
law_enforcement_contact: Optional[dict] = None,
|
||||
cpni_protection_officer: Optional[dict] = None,
|
||||
network_infrastructure_summary: str = "",
|
||||
interception_support_method: str = "",
|
||||
reporting_year: int = 0,
|
||||
signatory_name: str = "",
|
||||
signatory_title: str = "Chief Executive Officer",
|
||||
effective_date: str = "",
|
||||
next_review_date: str = "",
|
||||
reviewer_name: str = "Justin Hannah",
|
||||
reviewer_company: str = "Performance West Inc.",
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
le = law_enforcement_contact or {}
|
||||
cpni = cpni_protection_officer or {}
|
||||
today = date.today()
|
||||
effective = effective_date or today.strftime("%m/%d/%Y")
|
||||
next_review = next_review_date or today.replace(year=today.year + 1).strftime("%m/%d/%Y")
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
title = doc.add_paragraph(); title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
tr = title.add_run("System Security and Integrity (SSI) Plan")
|
||||
tr.font.size = Pt(15); tr.bold = True; tr.font.color.rgb = NAVY
|
||||
sub = doc.add_paragraph(); sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sr = sub.add_run(entity_name); sr.font.size = Pt(13); sr.bold = True
|
||||
vsub = doc.add_paragraph(); vsub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
vr = vsub.add_run(f"Variant: {VARIANT_LABEL}")
|
||||
vr.font.size = Pt(11); vr.italic = True
|
||||
cite = doc.add_paragraph(); cite.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
cr = cite.add_run("Pursuant to 47 U.S.C. \u00a7 229 and 47 CFR \u00a7 1.20003")
|
||||
cr.font.size = Pt(10); cr.italic = True
|
||||
cite.paragraph_format.space_after = Pt(18)
|
||||
|
||||
_heading(doc, "1. Purpose and Scope Note")
|
||||
_body(doc, (
|
||||
f"This SSI Plan governs {entity_name}'s compliance with CALEA to "
|
||||
f"the extent {entity_name}'s audio bridging / conferencing "
|
||||
f"offerings constitute telecommunications service under 47 USC "
|
||||
f"\u00a7 153(53) or are otherwise within the CALEA covered-entity "
|
||||
f"definition at 47 USC \u00a7 1001(8). Portions of the service "
|
||||
f"that constitute information service under 47 USC \u00a7 153(24) "
|
||||
f"are outside the scope of CALEA."
|
||||
))
|
||||
|
||||
_heading(doc, "2. Scope and Applicability")
|
||||
_body(doc, (
|
||||
f"{entity_name} operates conference-bridge / softswitch elements "
|
||||
f"and ingress / egress SIP trunks connecting to the PSTN. For "
|
||||
f"the telecommunications-service portion of the offering, CALEA "
|
||||
f"obligations attach; for any information-service portion "
|
||||
f"(non-real-time recorded replay, data-only collaboration "
|
||||
f"features), CALEA does not apply."
|
||||
))
|
||||
|
||||
_heading(doc, "3. Designated Law Enforcement Contact (24-hour)")
|
||||
_body(doc, (
|
||||
f"Per 47 CFR \u00a7 1.20003(a)(1), {entity_name} designates the "
|
||||
f"following senior officer as 24-hour contact for law enforcement "
|
||||
f"service of process (court orders, pen register / trap-and-trace, "
|
||||
f"Title III)."
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {le.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {le.get('title') or ''}",
|
||||
f"Phone (24-hour): {le.get('phone') or ''}",
|
||||
f"Email (24-hour): {le.get('email_24h') or ''}",
|
||||
f"Backup contact: {le.get('backup_name') or '[TO BE POPULATED]'}",
|
||||
])
|
||||
|
||||
_heading(doc, "4. Network Architecture and Interception Capability")
|
||||
_body(doc, network_infrastructure_summary or (
|
||||
f"{entity_name} operates a conference-bridge softswitch with SIP "
|
||||
"trunks for inbound / outbound PSTN connectivity. Participant "
|
||||
"identities are captured via caller-ID plus dial-in PIN."
|
||||
))
|
||||
_body(doc, interception_support_method or (
|
||||
f"For real-time intercept orders directed at an identified "
|
||||
f"participant or conference, {entity_name} provisions LI at the "
|
||||
f"conference-bridge softswitch, mirroring content and "
|
||||
f"call-identifying information to the requesting law-enforcement "
|
||||
f"agency in a CALEA-safe-harbor-compliant format. Non-real-time "
|
||||
f"productions (recorded conferences lawfully requested under "
|
||||
f"18 USC \u00a7 2703) are handled as stored-record productions "
|
||||
f"under the separate subpoena-response procedure and are not "
|
||||
f"treated as real-time intercepts."
|
||||
))
|
||||
|
||||
_heading(doc, "5. CPNI Safeguards")
|
||||
_body(doc, (
|
||||
f"{entity_name} maintains a separate CPNI procedure statement. "
|
||||
f"The CPNI Protection Officer is:"
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {cpni.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {cpni.get('title') or 'CPNI Protection Officer'}",
|
||||
])
|
||||
|
||||
_heading(doc, "6. Personnel Vetting and Training")
|
||||
_bullets(doc, [
|
||||
"Annual CALEA + CPNI training for personnel with bridge-admin or "
|
||||
"subpoena-response duties.",
|
||||
"Background checks performed prior to grant of access.",
|
||||
"Access revoked within 24 hours of termination.",
|
||||
"Bridge-admin and LI actions attributed to authenticated named "
|
||||
"users.",
|
||||
])
|
||||
|
||||
_heading(doc, "7. Supervisory Review")
|
||||
_body(doc, (
|
||||
f"The {le.get('title') or 'Designated Senior Officer'} reviews LI "
|
||||
f"and subpoena-response activity at least quarterly."
|
||||
))
|
||||
|
||||
_heading(doc, "8. Records Retention")
|
||||
_body(doc, (
|
||||
"LI provisioning and service-of-process records retained ten (10) "
|
||||
"years per 47 CFR \u00a7 1.20003(b); CPNI access logs retained at "
|
||||
"least two (2) years per 47 CFR \u00a7 64.2009."
|
||||
))
|
||||
|
||||
_heading(doc, "9. Annual Review")
|
||||
_body(doc, (
|
||||
f"Reviewed at least annually. Next scheduled review: {next_review}."
|
||||
))
|
||||
|
||||
_heading(doc, "10. Certification")
|
||||
_body(doc, (
|
||||
f"I, {signatory_name or '[Authorized Officer]'}, as "
|
||||
f"{signatory_title} of {entity_name}, certify that I have "
|
||||
f"reviewed this SSI Plan and that {entity_name} complies with 47 "
|
||||
f"U.S.C. \u00a7 229 and 47 CFR \u00a7 1.20003 with respect to "
|
||||
f"the telecommunications-service portion of its offerings."
|
||||
))
|
||||
_body(doc, "")
|
||||
doc.add_paragraph("_" * 45)
|
||||
_body(doc, signatory_name or "[Authorized Officer]", bold=True)
|
||||
_body(doc, f"{signatory_title}, {entity_name}")
|
||||
_body(doc, f"Effective Date: {effective}")
|
||||
if frn: _body(doc, f"FRN: {frn}")
|
||||
_body(doc, f"Reviewed By: {reviewer_name}, {reviewer_company}")
|
||||
_body(doc, f"Next Review Date: {next_review}")
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CALEA Audio Bridge SSI plan generated: %s", out)
|
||||
return str(out)
|
||||
250
scripts/document_gen/templates/calea_clec_ss7_generator.py
Normal file
250
scripts/document_gen/templates/calea_clec_ss7_generator.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
"""
|
||||
CALEA System Security and Integrity (SSI) Plan — CLEC SS7 / facilities.
|
||||
|
||||
Tailored variant of the generic CALEA SSI plan for a Competitive Local
|
||||
Exchange Carrier that operates its own TDM / SS7 / SIGTRAN switching
|
||||
infrastructure. The lawful-intercept method is provisioned at the Class 5
|
||||
softswitch and at the SS7 / SIGTRAN STPs using the industry-standard
|
||||
ATIS J-STD-025 interface. CALEA scope covers both local-exchange
|
||||
switching and resold access transport.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.calea_clec_ss7")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CALEA CLEC SS7 unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "clec_ss7"
|
||||
VARIANT_LABEL = "Competitive Local Exchange Carrier — SS7 / SIGTRAN"
|
||||
|
||||
|
||||
def _heading(doc, text):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(text); r.bold = True; r.font.size = Pt(13)
|
||||
r.font.color.rgb = NAVY
|
||||
|
||||
|
||||
def _body(doc, text, bold=False):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
r = p.add_run(text); r.font.size = Pt(11); r.bold = bold
|
||||
|
||||
|
||||
def _bullets(doc, items):
|
||||
for it in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.clear()
|
||||
r = p.add_run(it); r.font.size = Pt(11)
|
||||
|
||||
|
||||
def generate_calea_clec_ss7(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
law_enforcement_contact: Optional[dict] = None,
|
||||
cpni_protection_officer: Optional[dict] = None,
|
||||
network_infrastructure_summary: str = "",
|
||||
interception_support_method: str = "",
|
||||
reporting_year: int = 0,
|
||||
signatory_name: str = "",
|
||||
signatory_title: str = "Chief Executive Officer",
|
||||
effective_date: str = "",
|
||||
next_review_date: str = "",
|
||||
reviewer_name: str = "Justin Hannah",
|
||||
reviewer_company: str = "Performance West Inc.",
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
le = law_enforcement_contact or {}
|
||||
cpni = cpni_protection_officer or {}
|
||||
today = date.today()
|
||||
effective = effective_date or today.strftime("%m/%d/%Y")
|
||||
next_review = next_review_date or today.replace(year=today.year + 1).strftime("%m/%d/%Y")
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
title = doc.add_paragraph(); title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
tr = title.add_run("System Security and Integrity (SSI) Plan")
|
||||
tr.font.size = Pt(15); tr.bold = True; tr.font.color.rgb = NAVY
|
||||
|
||||
sub = doc.add_paragraph(); sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sr = sub.add_run(entity_name)
|
||||
sr.font.size = Pt(13); sr.bold = True
|
||||
|
||||
vsub = doc.add_paragraph(); vsub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
vr = vsub.add_run(f"Variant: {VARIANT_LABEL}")
|
||||
vr.font.size = Pt(11); vr.italic = True
|
||||
|
||||
cite = doc.add_paragraph(); cite.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
cr = cite.add_run(
|
||||
"Pursuant to 47 U.S.C. \u00a7 229 and 47 CFR \u00a7 1.20003"
|
||||
)
|
||||
cr.font.size = Pt(10); cr.italic = True
|
||||
cite.paragraph_format.space_after = Pt(18)
|
||||
|
||||
_heading(doc, "1. Purpose")
|
||||
_body(doc, (
|
||||
f"This System Security and Integrity (SSI) Plan governs {entity_name}'s "
|
||||
f"compliance with the Communications Assistance for Law Enforcement "
|
||||
f"Act (CALEA), 47 U.S.C. \u00a7\u00a7 1001\u20131010, and the "
|
||||
f"Commission's rules at 47 CFR Part 1 Subpart Z, as applied to "
|
||||
f"{entity_name}'s operations as a Competitive Local Exchange Carrier "
|
||||
f"(CLEC) with SS7 / SIGTRAN switching infrastructure."
|
||||
))
|
||||
|
||||
_heading(doc, "2. Scope and Applicability")
|
||||
_body(doc, (
|
||||
f"{entity_name} is subject to CALEA as a facilities-based provider "
|
||||
f"of common-carrier local exchange service. Its covered equipment "
|
||||
f"includes Class 5 softswitch(es), trunk gateways, SS7 / SIGTRAN "
|
||||
f"STPs, and signaling-link interconnections to interexchange "
|
||||
f"carriers and to the public switched telephone network."
|
||||
))
|
||||
|
||||
_heading(doc, "3. Designated Law Enforcement Contact (24-hour)")
|
||||
_body(doc, (
|
||||
f"Per 47 CFR \u00a7 1.20003(a)(1), {entity_name} designates the "
|
||||
f"following senior officer as point of contact for law enforcement "
|
||||
f"inquiries, court orders, pen register / trap-and-trace orders, "
|
||||
f"and Title III wiretap orders. This contact is staffed 24 hours "
|
||||
f"a day, 365 days a year."
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {le.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {le.get('title') or ''}",
|
||||
f"Phone (24-hour): {le.get('phone') or ''}",
|
||||
f"Email (24-hour): {le.get('email_24h') or ''}",
|
||||
f"Backup contact: {le.get('backup_name') or '[TO BE POPULATED]'}",
|
||||
])
|
||||
_body(doc, (
|
||||
f"Service of process may be made on the above designee by "
|
||||
f"telephone, email, or in person. {entity_name} commits to "
|
||||
f"acknowledging any intercept or traffic-capture order within "
|
||||
f"two (2) business hours of receipt."
|
||||
))
|
||||
|
||||
_heading(doc, "4. Network Architecture and Interception Capability")
|
||||
_body(doc, network_infrastructure_summary or (
|
||||
f"{entity_name} operates a Class 5 softswitch (or TDM Class 5 "
|
||||
"switch where retained) supported by redundant SS7 / SIGTRAN "
|
||||
"signaling through owned or leased STPs. Customer access is "
|
||||
"provided via copper loops, fiber, and resold UNE-P/loop "
|
||||
"facilities where applicable. Interconnection with the PSTN is "
|
||||
"by SS7 trunks to the relevant tandems."
|
||||
))
|
||||
_body(doc, interception_support_method or (
|
||||
f"Lawful intercept is provisioned at the Class 5 softswitch and "
|
||||
"at the SS7 / SIGTRAN STP in accordance with ATIS J-STD-025-B "
|
||||
"(TIA/ANSI-41/GSM LAES). Call content is delivered to the "
|
||||
"requesting law-enforcement agency via a Call Content Channel "
|
||||
"(CCC) and call-identifying information via a Call Data Channel "
|
||||
"(CDC), following the safe-harbor industry standard adopted by "
|
||||
"the FCC under 47 CFR Part 1 Subpart Z. The Designated Senior "
|
||||
"Officer coordinates provisioning, validates the court order, "
|
||||
"and certifies activation to law enforcement."
|
||||
))
|
||||
_body(doc, (
|
||||
f"{entity_name} retains copies of ATIS J-STD-025 compliance "
|
||||
f"attestations from its switch and SS7 vendors, and maintains "
|
||||
f"interconnection agreements with its tandem provider(s) that "
|
||||
f"address CALEA responsibilities."
|
||||
))
|
||||
|
||||
_heading(doc, "5. CPNI Safeguards")
|
||||
_body(doc, (
|
||||
f"{entity_name} maintains a separate, written CPNI procedure "
|
||||
f"statement under 47 CFR \u00a7\u00a7 64.2001\u201364.2011. The "
|
||||
f"CPNI Protection Officer is:"
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {cpni.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {cpni.get('title') or 'CPNI Protection Officer'}",
|
||||
])
|
||||
_body(doc, (
|
||||
"SS7 / SIGTRAN LIDB access, PIC records, and intercept "
|
||||
"provisioning are all within the CPNI Protection Officer's "
|
||||
"oversight scope."
|
||||
))
|
||||
|
||||
_heading(doc, "6. Personnel Vetting and Training")
|
||||
_bullets(doc, [
|
||||
f"All {entity_name} personnel with access to intercept "
|
||||
"provisioning interfaces complete annual CALEA and CPNI training.",
|
||||
"Background checks are performed prior to granting access.",
|
||||
"Access is revoked within 24 hours of termination.",
|
||||
"All intercept-related actions are attributed to named "
|
||||
"individuals via authenticated logins (no shared credentials).",
|
||||
])
|
||||
|
||||
_heading(doc, "7. Supervisory Review")
|
||||
_body(doc, (
|
||||
f"The {le.get('title') or 'Designated Senior Officer'} reviews "
|
||||
f"intercept-related activity at least quarterly. Anomalies "
|
||||
f"(unauthorized access attempts, tampering, missed response SLAs) "
|
||||
f"are escalated to the CEO within one business day of detection."
|
||||
))
|
||||
|
||||
_heading(doc, "8. Records Retention")
|
||||
_body(doc, (
|
||||
"Records of intercept provisioning, service of process, "
|
||||
"acknowledgments, and termination are retained for a minimum of "
|
||||
"ten (10) years per 47 CFR \u00a7 1.20003(b). CPNI access logs "
|
||||
"are retained at least two (2) years per 47 CFR \u00a7 64.2009."
|
||||
))
|
||||
|
||||
_heading(doc, "9. Annual Review")
|
||||
_body(doc, (
|
||||
f"This Plan is reviewed at least annually and updated upon "
|
||||
f"(i) material change to the switching infrastructure, "
|
||||
f"(ii) change of upstream tandem or IXC interconnection, "
|
||||
f"(iii) new Commission / DOJ guidance, or (iv) a material breach "
|
||||
f"or near-miss. Next scheduled review: {next_review}."
|
||||
))
|
||||
|
||||
_heading(doc, "10. Certification")
|
||||
_body(doc, (
|
||||
f"I, {signatory_name or '[Authorized Officer]'}, as "
|
||||
f"{signatory_title} of {entity_name}, certify that I have "
|
||||
f"reviewed this SSI Plan and that {entity_name} has implemented "
|
||||
f"the policies, procedures, and technical measures described "
|
||||
f"herein, and complies with 47 U.S.C. \u00a7 229 and 47 CFR "
|
||||
f"\u00a7 1.20003."
|
||||
))
|
||||
_body(doc, "")
|
||||
doc.add_paragraph("_" * 45)
|
||||
_body(doc, signatory_name or "[Authorized Officer]", bold=True)
|
||||
_body(doc, f"{signatory_title}, {entity_name}")
|
||||
_body(doc, f"Effective Date: {effective}")
|
||||
if frn:
|
||||
_body(doc, f"FRN: {frn}")
|
||||
_body(doc, f"Reviewed By: {reviewer_name}, {reviewer_company}")
|
||||
_body(doc, f"Next Review Date: {next_review}")
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CALEA CLEC SS7 SSI plan generated: %s", out)
|
||||
return str(out)
|
||||
219
scripts/document_gen/templates/calea_ixc_ss7_generator.py
Normal file
219
scripts/document_gen/templates/calea_ixc_ss7_generator.py
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
"""
|
||||
CALEA System Security and Integrity (SSI) Plan — IXC SS7.
|
||||
|
||||
Interexchange-carrier variant of the CALEA SSI plan. The lawful-
|
||||
intercept method is provisioned at IXC tandem / Class 4 switching
|
||||
elements (or softswitch equivalents) using SS7 signaling, and covers
|
||||
both content and call-identifying information for pen-register /
|
||||
trap-and-trace and Title III orders directed at toll calls.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.calea_ixc_ss7")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CALEA IXC SS7 unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "ixc_ss7"
|
||||
VARIANT_LABEL = "Interexchange Carrier — SS7 / SIGTRAN"
|
||||
|
||||
|
||||
def _heading(doc, text):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12); p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(text); r.bold = True; r.font.size = Pt(13); r.font.color.rgb = NAVY
|
||||
|
||||
|
||||
def _body(doc, text, bold=False):
|
||||
p = doc.add_paragraph(); p.paragraph_format.space_after = Pt(6)
|
||||
r = p.add_run(text); r.font.size = Pt(11); r.bold = bold
|
||||
|
||||
|
||||
def _bullets(doc, items):
|
||||
for it in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.clear()
|
||||
r = p.add_run(it); r.font.size = Pt(11)
|
||||
|
||||
|
||||
def generate_calea_ixc_ss7(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
law_enforcement_contact: Optional[dict] = None,
|
||||
cpni_protection_officer: Optional[dict] = None,
|
||||
network_infrastructure_summary: str = "",
|
||||
interception_support_method: str = "",
|
||||
reporting_year: int = 0,
|
||||
signatory_name: str = "",
|
||||
signatory_title: str = "Chief Executive Officer",
|
||||
effective_date: str = "",
|
||||
next_review_date: str = "",
|
||||
reviewer_name: str = "Justin Hannah",
|
||||
reviewer_company: str = "Performance West Inc.",
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
le = law_enforcement_contact or {}
|
||||
cpni = cpni_protection_officer or {}
|
||||
today = date.today()
|
||||
effective = effective_date or today.strftime("%m/%d/%Y")
|
||||
next_review = next_review_date or today.replace(year=today.year + 1).strftime("%m/%d/%Y")
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
title = doc.add_paragraph(); title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
tr = title.add_run("System Security and Integrity (SSI) Plan")
|
||||
tr.font.size = Pt(15); tr.bold = True; tr.font.color.rgb = NAVY
|
||||
|
||||
sub = doc.add_paragraph(); sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sr = sub.add_run(entity_name); sr.font.size = Pt(13); sr.bold = True
|
||||
|
||||
vsub = doc.add_paragraph(); vsub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
vr = vsub.add_run(f"Variant: {VARIANT_LABEL}")
|
||||
vr.font.size = Pt(11); vr.italic = True
|
||||
|
||||
cite = doc.add_paragraph(); cite.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
cr = cite.add_run("Pursuant to 47 U.S.C. \u00a7 229 and 47 CFR \u00a7 1.20003")
|
||||
cr.font.size = Pt(10); cr.italic = True
|
||||
cite.paragraph_format.space_after = Pt(18)
|
||||
|
||||
_heading(doc, "1. Purpose")
|
||||
_body(doc, (
|
||||
f"This SSI Plan governs {entity_name}'s compliance with CALEA and "
|
||||
f"the Commission's implementing rules as applied to {entity_name}'s "
|
||||
f"operations as an interexchange (toll) carrier utilizing SS7 / "
|
||||
f"SIGTRAN signaling."
|
||||
))
|
||||
|
||||
_heading(doc, "2. Scope and Applicability")
|
||||
_body(doc, (
|
||||
f"{entity_name} is a provider of interexchange (toll) service "
|
||||
f"subject to CALEA. Its covered equipment includes IXC tandem / "
|
||||
f"Class 4 switching elements or softswitch equivalents, SS7 / "
|
||||
f"SIGTRAN signaling, billing-record systems, and trunk "
|
||||
f"interconnections with IXC peers, LECs, and wireless carriers."
|
||||
))
|
||||
|
||||
_heading(doc, "3. Designated Law Enforcement Contact (24-hour)")
|
||||
_body(doc, (
|
||||
f"Per 47 CFR \u00a7 1.20003(a)(1), {entity_name} designates the "
|
||||
f"following 24-hour point of contact for court orders, pen "
|
||||
f"register / trap-and-trace orders, and Title III wiretap orders."
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {le.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {le.get('title') or ''}",
|
||||
f"Phone (24-hour): {le.get('phone') or ''}",
|
||||
f"Email (24-hour): {le.get('email_24h') or ''}",
|
||||
f"Backup contact: {le.get('backup_name') or '[TO BE POPULATED]'}",
|
||||
])
|
||||
_body(doc, (
|
||||
f"{entity_name} commits to acknowledging any order within two (2) "
|
||||
f"business hours of receipt."
|
||||
))
|
||||
|
||||
_heading(doc, "4. Network Architecture and Interception Capability")
|
||||
_body(doc, network_infrastructure_summary or (
|
||||
f"{entity_name} operates softswitch / Class 4 tandem elements "
|
||||
"with redundant SS7 / SIGTRAN signaling for toll call control. "
|
||||
"Trunk peering with LECs, other IXCs, and wireless carriers is "
|
||||
"established via SS7 trunks."
|
||||
))
|
||||
_body(doc, interception_support_method or (
|
||||
f"Toll-call lawful intercept is provisioned at {entity_name}'s "
|
||||
"softswitch / tandem under ATIS J-STD-025-B. Call content is "
|
||||
"delivered via Call Content Channel (CCC); call-identifying "
|
||||
"information via Call Data Channel (CDC). The Designated Senior "
|
||||
"Officer validates the court order, coordinates provisioning, "
|
||||
"and certifies activation to the requesting law-enforcement "
|
||||
"agency."
|
||||
))
|
||||
|
||||
_heading(doc, "5. CPNI Safeguards")
|
||||
_body(doc, (
|
||||
f"{entity_name} maintains a separate CPNI procedure statement "
|
||||
f"under 47 CFR \u00a7\u00a7 64.2001\u201364.2011. The CPNI "
|
||||
f"Protection Officer is:"
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {cpni.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {cpni.get('title') or 'CPNI Protection Officer'}",
|
||||
])
|
||||
_body(doc, (
|
||||
"Toll call-record databases, PIC-administration interfaces, and "
|
||||
"intercept provisioning are all within the CPNI Protection "
|
||||
"Officer's oversight scope."
|
||||
))
|
||||
|
||||
_heading(doc, "6. Personnel Vetting and Training")
|
||||
_bullets(doc, [
|
||||
"Annual CALEA + CPNI training for all personnel with intercept "
|
||||
"or CPNI access.",
|
||||
"Background checks prior to grant of access.",
|
||||
"Access revoked within 24 hours of termination.",
|
||||
"All intercept actions attributed to authenticated named users.",
|
||||
])
|
||||
|
||||
_heading(doc, "7. Supervisory Review")
|
||||
_body(doc, (
|
||||
f"The {le.get('title') or 'Designated Senior Officer'} reviews "
|
||||
f"intercept activity at least quarterly. Anomalies are escalated "
|
||||
f"to the CEO within one business day."
|
||||
))
|
||||
|
||||
_heading(doc, "8. Records Retention")
|
||||
_body(doc, (
|
||||
"Intercept provisioning and service-of-process records retained "
|
||||
"ten (10) years per 47 CFR \u00a7 1.20003(b); CPNI access logs "
|
||||
"retained at least two (2) years per 47 CFR \u00a7 64.2009."
|
||||
))
|
||||
|
||||
_heading(doc, "9. Annual Review")
|
||||
_body(doc, (
|
||||
f"This Plan is reviewed at least annually and upon material "
|
||||
f"infrastructure / interconnection change. Next scheduled review: "
|
||||
f"{next_review}."
|
||||
))
|
||||
|
||||
_heading(doc, "10. Certification")
|
||||
_body(doc, (
|
||||
f"I, {signatory_name or '[Authorized Officer]'}, as "
|
||||
f"{signatory_title} of {entity_name}, certify that I have "
|
||||
f"reviewed this SSI Plan and that {entity_name} complies with 47 "
|
||||
f"U.S.C. \u00a7 229 and 47 CFR \u00a7 1.20003."
|
||||
))
|
||||
_body(doc, "")
|
||||
doc.add_paragraph("_" * 45)
|
||||
_body(doc, signatory_name or "[Authorized Officer]", bold=True)
|
||||
_body(doc, f"{signatory_title}, {entity_name}")
|
||||
_body(doc, f"Effective Date: {effective}")
|
||||
if frn: _body(doc, f"FRN: {frn}")
|
||||
_body(doc, f"Reviewed By: {reviewer_name}, {reviewer_company}")
|
||||
_body(doc, f"Next Review Date: {next_review}")
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CALEA IXC SS7 SSI plan generated: %s", out)
|
||||
return str(out)
|
||||
220
scripts/document_gen/templates/calea_satellite_generator.py
Normal file
220
scripts/document_gen/templates/calea_satellite_generator.py
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
"""
|
||||
CALEA SSI Plan — Satellite (MSS / FSS) variant.
|
||||
|
||||
Satellite carriers that provide telecommunications service (MSS) or
|
||||
facilitate it (certain FSS transport arrangements) are subject to CALEA.
|
||||
Intercept capability is generally provisioned at the ground segment —
|
||||
i.e., the earth station / NOC / IP gateway — where subscriber sessions
|
||||
terminate before hand-off to the PSTN or public Internet. This variant
|
||||
documents the ground-segment intercept model, the Part 25 physical-
|
||||
security controls, and the delegation to terrestrial-network partners
|
||||
where applicable.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.calea_satellite")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CALEA Satellite unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "satellite"
|
||||
VARIANT_LABEL = "Satellite (MSS / FSS)"
|
||||
|
||||
|
||||
def _heading(doc, text):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12); p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(text); r.bold = True; r.font.size = Pt(13); r.font.color.rgb = NAVY
|
||||
|
||||
|
||||
def _body(doc, text, bold=False):
|
||||
p = doc.add_paragraph(); p.paragraph_format.space_after = Pt(6)
|
||||
r = p.add_run(text); r.font.size = Pt(11); r.bold = bold
|
||||
|
||||
|
||||
def _bullets(doc, items):
|
||||
for it in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.clear(); r = p.add_run(it); r.font.size = Pt(11)
|
||||
|
||||
|
||||
def generate_calea_satellite(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
law_enforcement_contact: Optional[dict] = None,
|
||||
cpni_protection_officer: Optional[dict] = None,
|
||||
network_infrastructure_summary: str = "",
|
||||
interception_support_method: str = "",
|
||||
reporting_year: int = 0,
|
||||
signatory_name: str = "",
|
||||
signatory_title: str = "Chief Executive Officer",
|
||||
effective_date: str = "",
|
||||
next_review_date: str = "",
|
||||
reviewer_name: str = "Justin Hannah",
|
||||
reviewer_company: str = "Performance West Inc.",
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
le = law_enforcement_contact or {}
|
||||
cpni = cpni_protection_officer or {}
|
||||
today = date.today()
|
||||
effective = effective_date or today.strftime("%m/%d/%Y")
|
||||
next_review = next_review_date or today.replace(year=today.year + 1).strftime("%m/%d/%Y")
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
title = doc.add_paragraph(); title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
tr = title.add_run("System Security and Integrity (SSI) Plan")
|
||||
tr.font.size = Pt(15); tr.bold = True; tr.font.color.rgb = NAVY
|
||||
sub = doc.add_paragraph(); sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sr = sub.add_run(entity_name); sr.font.size = Pt(13); sr.bold = True
|
||||
vsub = doc.add_paragraph(); vsub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
vr = vsub.add_run(f"Variant: {VARIANT_LABEL}")
|
||||
vr.font.size = Pt(11); vr.italic = True
|
||||
cite = doc.add_paragraph(); cite.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
cr = cite.add_run(
|
||||
"Pursuant to 47 U.S.C. \u00a7 229, 47 CFR \u00a7 1.20003, and "
|
||||
"47 CFR Part 25"
|
||||
)
|
||||
cr.font.size = Pt(10); cr.italic = True
|
||||
cite.paragraph_format.space_after = Pt(18)
|
||||
|
||||
_heading(doc, "1. Purpose")
|
||||
_body(doc, (
|
||||
f"This SSI Plan governs {entity_name}'s compliance with CALEA and "
|
||||
f"47 CFR \u00a7 1.20003 as applied to {entity_name}'s operations "
|
||||
f"as a provider of Mobile Satellite Service and/or Fixed Satellite "
|
||||
f"Service telecommunications."
|
||||
))
|
||||
|
||||
_heading(doc, "2. Scope and Applicability")
|
||||
_body(doc, (
|
||||
f"{entity_name} operates (or leases capacity from) a space segment "
|
||||
f"and ground-segment infrastructure including earth stations, "
|
||||
f"gateway facilities, and a network operations center (NOC). "
|
||||
f"Subscriber sessions originate at user terminals, traverse the "
|
||||
f"satellite, and terminate at ground-segment gateways before "
|
||||
f"hand-off to terrestrial networks. CALEA obligations apply to "
|
||||
f"the telecommunications-service portions of this traffic."
|
||||
))
|
||||
|
||||
_heading(doc, "3. Designated Law Enforcement Contact (24-hour)")
|
||||
_body(doc, (
|
||||
f"Per 47 CFR \u00a7 1.20003(a)(1), {entity_name} designates the "
|
||||
f"following senior officer as 24-hour contact for law enforcement "
|
||||
f"service of process."
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {le.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {le.get('title') or ''}",
|
||||
f"Phone (24-hour): {le.get('phone') or ''}",
|
||||
f"Email (24-hour): {le.get('email_24h') or ''}",
|
||||
f"Backup contact: {le.get('backup_name') or '[TO BE POPULATED]'}",
|
||||
])
|
||||
|
||||
_heading(doc, "4. Network Architecture and Interception Capability")
|
||||
_body(doc, network_infrastructure_summary or (
|
||||
f"{entity_name}'s ground segment comprises earth-station antennas, "
|
||||
"baseband modems, gateway routing / softswitch elements, and NOC "
|
||||
"monitoring. Physical access to these facilities is controlled "
|
||||
"under the Part 25 earth-station license conditions."
|
||||
))
|
||||
_body(doc, interception_support_method or (
|
||||
f"Lawful intercept is provisioned at the ground segment where "
|
||||
"subscriber sessions are decrypted / de-encapsulated for hand-off. "
|
||||
"Content and call-identifying information are delivered to the "
|
||||
"requesting law-enforcement agency using the CALEA safe-harbor "
|
||||
"interfaces (ATIS J-STD-025 for voice / TIA TR-45 equivalents for "
|
||||
"data) or a mutually agreed alternative acceptable under 47 CFR "
|
||||
"Part 1 Subpart Z. Where {entity_name} hands off traffic to a "
|
||||
"terrestrial partner for switching or transport, the partner "
|
||||
"supports intercept under its own CALEA plan and the plans are "
|
||||
"coordinated."
|
||||
).replace("{entity_name}", entity_name))
|
||||
|
||||
_heading(doc, "5. CPNI Safeguards")
|
||||
_body(doc, (
|
||||
f"{entity_name} maintains separate CPNI procedures under 47 CFR "
|
||||
f"\u00a7\u00a7 64.2001\u201364.2011. Customer activation records, "
|
||||
f"beam / transponder assignments, and NOC operator logs are "
|
||||
f"within the CPNI Protection Officer's oversight. The CPNI "
|
||||
f"Protection Officer is:"
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {cpni.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {cpni.get('title') or 'CPNI Protection Officer'}",
|
||||
])
|
||||
|
||||
_heading(doc, "6. Personnel Vetting and Training")
|
||||
_bullets(doc, [
|
||||
"Annual CALEA + CPNI training for NOC operators and LI-provisioning "
|
||||
"personnel.",
|
||||
"Background checks performed prior to granting access to ground-"
|
||||
"segment provisioning systems.",
|
||||
"Physical access to earth-station / NOC facilities controlled per "
|
||||
"Part 25 license conditions.",
|
||||
"LI provisioning attributed to named authenticated users.",
|
||||
])
|
||||
|
||||
_heading(doc, "7. Supervisory Review")
|
||||
_body(doc, (
|
||||
f"The {le.get('title') or 'Designated Senior Officer'} reviews LI "
|
||||
f"activity and NOC operator access logs at least quarterly."
|
||||
))
|
||||
|
||||
_heading(doc, "8. Records Retention")
|
||||
_body(doc, (
|
||||
"LI provisioning and service-of-process records retained ten (10) "
|
||||
"years per 47 CFR \u00a7 1.20003(b); CPNI access logs retained at "
|
||||
"least two (2) years per 47 CFR \u00a7 64.2009."
|
||||
))
|
||||
|
||||
_heading(doc, "9. Annual Review")
|
||||
_body(doc, (
|
||||
f"Reviewed at least annually and upon material change to the "
|
||||
f"ground segment, earth-station license, or terrestrial hand-off "
|
||||
f"arrangement. Next scheduled review: {next_review}."
|
||||
))
|
||||
|
||||
_heading(doc, "10. Certification")
|
||||
_body(doc, (
|
||||
f"I, {signatory_name or '[Authorized Officer]'}, as "
|
||||
f"{signatory_title} of {entity_name}, certify that I have "
|
||||
f"reviewed this SSI Plan and that {entity_name} complies with 47 "
|
||||
f"U.S.C. \u00a7 229 and 47 CFR \u00a7 1.20003."
|
||||
))
|
||||
_body(doc, "")
|
||||
doc.add_paragraph("_" * 45)
|
||||
_body(doc, signatory_name or "[Authorized Officer]", bold=True)
|
||||
_body(doc, f"{signatory_title}, {entity_name}")
|
||||
_body(doc, f"Effective Date: {effective}")
|
||||
if frn: _body(doc, f"FRN: {frn}")
|
||||
_body(doc, f"Reviewed By: {reviewer_name}, {reviewer_company}")
|
||||
_body(doc, f"Next Review Date: {next_review}")
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CALEA Satellite SSI plan generated: %s", out)
|
||||
return str(out)
|
||||
308
scripts/document_gen/templates/calea_ssi_generator.py
Normal file
308
scripts/document_gen/templates/calea_ssi_generator.py
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
"""
|
||||
CALEA System Security and Integrity (SSI) Plan generator.
|
||||
|
||||
Under 47 USC § 229 and 47 CFR § 1.20003 every telecommunications carrier
|
||||
(including interconnected VoIP providers) must maintain — and review
|
||||
annually — a System Security and Integrity policy covering lawful-
|
||||
intercept capability, CPNI safeguards, personnel vetting, supervisory
|
||||
review, and records retention. The SSI plan is kept internally. It's
|
||||
produced for DOJ on subpoena (28 CFR § 100.10) — not routinely filed
|
||||
with the FCC.
|
||||
|
||||
We generate a carrier-specific, signable DOCX that follows the 10-section
|
||||
outline expected by DOJ / FCC Enforcement reviewers. Customer-specific
|
||||
substitutions come from ``intake_data["calea_ssi"]``.
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.calea_ssi_generator import (
|
||||
generate_calea_ssi_plan,
|
||||
)
|
||||
path = generate_calea_ssi_plan(
|
||||
entity_name="Falcon Broadband LLC",
|
||||
law_enforcement_contact={"name":"Jane Doe","title":"General Counsel",
|
||||
"phone":"555-123-4567","email_24h":"le-contact@falcon.example.com"},
|
||||
cpni_protection_officer={"name":"John Roe","title":"VP Operations"},
|
||||
network_infrastructure_summary="FreeSWITCH cluster + Ribbon SBC; "
|
||||
"trunking via Bandwidth.com + Inteliquent",
|
||||
interception_support_method="CALEA intercept provided by our upstream "
|
||||
"provider Bandwidth.com under the standard "
|
||||
"CALEA Reference Model for VoIP",
|
||||
output_path="/tmp/calea_ssi.docx",
|
||||
)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.calea_ssi")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CALEA SSI generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
|
||||
def _heading(doc, text: str) -> None:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
run = p.add_run(text)
|
||||
run.bold = True
|
||||
run.font.size = Pt(13)
|
||||
run.font.color.rgb = NAVY
|
||||
|
||||
|
||||
def _body(doc, text: str, bold: bool = False) -> None:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
run = p.add_run(text)
|
||||
run.font.size = Pt(11)
|
||||
run.bold = bold
|
||||
|
||||
|
||||
def _bullets(doc, items: list[str]) -> None:
|
||||
for it in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.clear()
|
||||
run = p.add_run(it)
|
||||
run.font.size = Pt(11)
|
||||
|
||||
|
||||
def generate_calea_ssi_plan(
|
||||
# Carrier identity
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
# Law enforcement designated 24-hour contact (47 CFR § 1.20003(a)(1))
|
||||
law_enforcement_contact: Optional[dict] = None,
|
||||
# CPNI protection officer (47 CFR § 64.2009(d))
|
||||
cpni_protection_officer: Optional[dict] = None,
|
||||
# Network / infrastructure
|
||||
network_infrastructure_summary: str = "",
|
||||
interception_support_method: str = "",
|
||||
# Operational scope
|
||||
is_interconnected_voip: bool = True,
|
||||
is_wholesale: bool = False,
|
||||
has_retail_customers: bool = True,
|
||||
# Signatory (typically the officer named on the CPNI cert)
|
||||
signatory_name: str = "",
|
||||
signatory_title: str = "Chief Executive Officer",
|
||||
# Dates
|
||||
effective_date: str = "",
|
||||
next_review_date: str = "",
|
||||
# Reviewer (PW compliance team)
|
||||
reviewer_name: str = "Justin Hannah",
|
||||
reviewer_company: str = "Performance West Inc.",
|
||||
# Output
|
||||
output_path: str = "/tmp/calea_ssi_plan.docx",
|
||||
) -> Optional[str]:
|
||||
"""Produce the 10-section CALEA SSI Plan as a DOCX."""
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
le = law_enforcement_contact or {}
|
||||
cpni = cpni_protection_officer or {}
|
||||
today = date.today()
|
||||
effective = effective_date or today.strftime("%m/%d/%Y")
|
||||
next_review = next_review_date or today.replace(year=today.year + 1).strftime("%m/%d/%Y")
|
||||
|
||||
doc = Document()
|
||||
for section in doc.sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1.25)
|
||||
section.right_margin = Inches(1.25)
|
||||
|
||||
# Title
|
||||
title = doc.add_paragraph()
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
tr = title.add_run("System Security and Integrity (SSI) Plan")
|
||||
tr.font.size = Pt(15); tr.bold = True; tr.font.color.rgb = NAVY
|
||||
|
||||
sub = doc.add_paragraph()
|
||||
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sr = sub.add_run(entity_name)
|
||||
sr.font.size = Pt(13); sr.bold = True
|
||||
|
||||
cite = doc.add_paragraph()
|
||||
cite.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
cr = cite.add_run(
|
||||
"Pursuant to 47 U.S.C. \u00a7 229 and 47 CFR \u00a7 1.20003"
|
||||
)
|
||||
cr.font.size = Pt(10); cr.italic = True
|
||||
cite.paragraph_format.space_after = Pt(18)
|
||||
|
||||
# ── 1. Purpose ──────────────────────────────────────────────────
|
||||
_heading(doc, "1. Purpose")
|
||||
_body(doc, (
|
||||
f"This System Security and Integrity (SSI) Plan governs {entity_name}'s "
|
||||
f"compliance with the Communications Assistance for Law Enforcement "
|
||||
f"Act (CALEA), 47 U.S.C. \u00a7\u00a7 1001\u20131010, and the Federal "
|
||||
f"Communications Commission's implementing rules at 47 CFR Part 1 "
|
||||
f"Subpart Z. It defines the procedures {entity_name} uses to "
|
||||
f"support lawful electronic surveillance of its telecommunications "
|
||||
f"and interconnected VoIP services while protecting customer "
|
||||
f"privacy and the integrity of company operations."
|
||||
))
|
||||
|
||||
# ── 2. Scope and Applicability ──────────────────────────────────
|
||||
_heading(doc, "2. Scope and Applicability")
|
||||
scope_bits = [f"{entity_name} is subject to CALEA as a provider of "]
|
||||
if is_interconnected_voip:
|
||||
scope_bits.append("interconnected Voice over Internet Protocol services ")
|
||||
scope_bits.append("and has designed and implemented the systems described "
|
||||
"herein to support lawful intercept obligations.")
|
||||
_body(doc, "".join(scope_bits))
|
||||
if has_retail_customers:
|
||||
_body(doc, (
|
||||
f"{entity_name} maintains retail customer relationships subject "
|
||||
f"to the CPNI safeguards defined in Section 5."
|
||||
))
|
||||
if is_wholesale:
|
||||
_body(doc, (
|
||||
f"{entity_name} also operates in a wholesale capacity. Wholesale "
|
||||
f"intercept requests are coordinated with the downstream service "
|
||||
f"provider per the CALEA Reference Model."
|
||||
))
|
||||
|
||||
# ── 3. Designated Law Enforcement Contact ──────────────────────
|
||||
_heading(doc, "3. Designated Law Enforcement Contact (24-hour)")
|
||||
_body(doc, (
|
||||
f"Per 47 CFR \u00a7 1.20003(a)(1), {entity_name} designates the following "
|
||||
f"senior officer as point of contact for law enforcement inquiries, "
|
||||
f"court orders, pen register / trap-and-trace orders, and Title III "
|
||||
f"wiretap orders. This contact is staffed 24 hours a day."
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {le.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {le.get('title') or ''}",
|
||||
f"Phone (24-hour): {le.get('phone') or ''}",
|
||||
f"Email (24-hour): {le.get('email_24h') or ''}",
|
||||
f"Backup contact: {le.get('backup_name') or '[TO BE POPULATED]'}",
|
||||
])
|
||||
_body(doc, (
|
||||
f"Law enforcement officers may effect service of process on the "
|
||||
f"above designee by telephone, email, or in person at the address "
|
||||
f"of record. {entity_name} commits to acknowledging any intercept "
|
||||
f"or traffic-capture order within two (2) business hours of receipt."
|
||||
))
|
||||
|
||||
# ── 4. Network Architecture ────────────────────────────────────
|
||||
_heading(doc, "4. Network Architecture and Interception Capability")
|
||||
_body(doc, network_infrastructure_summary or (
|
||||
f"{entity_name} operates a VoIP network consisting of session "
|
||||
"border controllers (SBCs) for signaling, softswitch(es) for "
|
||||
"call control, and DID origination / termination via "
|
||||
"commercial-grade upstream providers."
|
||||
))
|
||||
_body(doc, interception_support_method or (
|
||||
f"CALEA intercept capability is provided through {entity_name}'s "
|
||||
"upstream voice service provider(s) under the standard CALEA "
|
||||
"Reference Model for interconnected VoIP. Upon receipt of a valid "
|
||||
"court order, the designated law enforcement contact coordinates "
|
||||
"with the upstream provider's CALEA team to provision the intercept "
|
||||
"at the upstream switching element."
|
||||
))
|
||||
_body(doc, (
|
||||
f"{entity_name} retains documentation of CALEA implementation "
|
||||
f"capability including upstream provider CALEA attestations, "
|
||||
f"interconnection agreements, and ATIS J-STD-025 / TIA-J-STD-025 "
|
||||
f"compliance references."
|
||||
))
|
||||
|
||||
# ── 5. CPNI Safeguards ─────────────────────────────────────────
|
||||
_heading(doc, "5. Customer Proprietary Network Information (CPNI) Safeguards")
|
||||
_body(doc, (
|
||||
f"{entity_name} maintains separate, written CPNI procedures under "
|
||||
f"47 CFR \u00a7\u00a7 64.2001\u201364.2011. The CPNI Protection Officer is:"
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {cpni.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {cpni.get('title') or 'CPNI Protection Officer'}",
|
||||
])
|
||||
_body(doc, (
|
||||
"Access to CPNI is authorized only for legitimate business "
|
||||
"purposes, supervised by the CPNI Protection Officer, and logged "
|
||||
"for supervisory review. See the company's separate CPNI "
|
||||
"Procedure Statement for detailed controls."
|
||||
))
|
||||
|
||||
# ── 6. Personnel Vetting and Training ─────────────────────────
|
||||
_heading(doc, "6. Personnel Vetting and Training")
|
||||
_bullets(doc, [
|
||||
f"All {entity_name} personnel with access to intercept systems or "
|
||||
"CPNI complete annual CALEA and CPNI training.",
|
||||
"Background checks are performed on all personnel prior to being "
|
||||
"granted access to intercept provisioning interfaces.",
|
||||
"Access is revoked within 24 hours of termination of employment.",
|
||||
"All intercept-related actions are attributed to named individuals "
|
||||
"via authenticated logins (no shared credentials).",
|
||||
])
|
||||
|
||||
# ── 7. Supervisory Review ─────────────────────────────────────
|
||||
_heading(doc, "7. Supervisory Review")
|
||||
_body(doc, (
|
||||
f"The {le.get('title') or 'Designated Senior Officer'} reviews all "
|
||||
f"intercept-related activity no less than quarterly. Any anomaly "
|
||||
f"(unauthorized access attempt, tampering, missed response SLA) "
|
||||
f"is escalated to the CEO within one business day of detection."
|
||||
))
|
||||
|
||||
# ── 8. Records Retention ──────────────────────────────────────
|
||||
_heading(doc, "8. Records Retention")
|
||||
_body(doc, (
|
||||
"Records of intercept provisioning, service of process, "
|
||||
"acknowledgments, and termination are retained for a minimum of "
|
||||
"ten (10) years per 47 CFR \u00a7 1.20003(b). CPNI access logs are "
|
||||
"retained for at least two (2) years per 47 CFR \u00a7 64.2009(c)."
|
||||
))
|
||||
|
||||
# ── 9. Annual Review ──────────────────────────────────────────
|
||||
_heading(doc, "9. Annual Review")
|
||||
_body(doc, (
|
||||
f"This Plan is reviewed at least annually by the designated senior "
|
||||
f"officer and updated when: (i) a new class of service is offered, "
|
||||
f"(ii) an upstream provider material to CALEA intercept capability "
|
||||
f"changes, (iii) the FCC or DOJ issues new guidance, or (iv) a "
|
||||
f"material breach or near-miss is identified. Next scheduled "
|
||||
f"review: {next_review}."
|
||||
))
|
||||
|
||||
# ── 10. Certification and Signature ───────────────────────────
|
||||
_heading(doc, "10. Certification")
|
||||
_body(doc, (
|
||||
f"I, {signatory_name or '[Authorized Officer]'}, as "
|
||||
f"{signatory_title} of {entity_name}, certify that I have reviewed "
|
||||
f"this System Security and Integrity Plan and that {entity_name} "
|
||||
f"has implemented the policies, procedures, and technical measures "
|
||||
f"described herein. I further certify that {entity_name} complies "
|
||||
f"with 47 U.S.C. \u00a7 229 and 47 CFR \u00a7 1.20003, and that "
|
||||
f"{entity_name} will make this Plan available to the Commission "
|
||||
f"and the Department of Justice on request."
|
||||
))
|
||||
_body(doc, "")
|
||||
doc.add_paragraph("_" * 45)
|
||||
_body(doc, signatory_name or "[Authorized Officer]", bold=True)
|
||||
_body(doc, f"{signatory_title}, {entity_name}")
|
||||
_body(doc, f"Effective Date: {effective}")
|
||||
if frn:
|
||||
_body(doc, f"FRN: {frn}")
|
||||
_body(doc, f"Reviewed By: {reviewer_name}, {reviewer_company}")
|
||||
_body(doc, f"Next Review Date: {next_review}")
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CALEA SSI plan generated: %s", out)
|
||||
return str(out)
|
||||
231
scripts/document_gen/templates/calea_wireless_generator.py
Normal file
231
scripts/document_gen/templates/calea_wireless_generator.py
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
"""
|
||||
CALEA System Security and Integrity (SSI) Plan — Wireless (CMRS) variant.
|
||||
|
||||
Facilities-based wireless carrier SSI plan. LAES (Lawfully Authorized
|
||||
Electronic Surveillance) capability is provisioned at the Mobile
|
||||
Switching Center (MSC) / 4G EPC / 5G Core per 47 CFR § 20.13 and the
|
||||
ATIS/3GPP LI standards. Content and call-identifying information are
|
||||
delivered to law enforcement over the standardized LI interfaces (X1 /
|
||||
X2 / X3 for 3GPP). The Plan also addresses per-device location data
|
||||
as a CPNI safeguard integration point.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.calea_wireless")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CALEA Wireless unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "wireless"
|
||||
VARIANT_LABEL = "Wireless (CMRS) Facilities"
|
||||
|
||||
|
||||
def _heading(doc, text):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12); p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(text); r.bold = True; r.font.size = Pt(13); r.font.color.rgb = NAVY
|
||||
|
||||
|
||||
def _body(doc, text, bold=False):
|
||||
p = doc.add_paragraph(); p.paragraph_format.space_after = Pt(6)
|
||||
r = p.add_run(text); r.font.size = Pt(11); r.bold = bold
|
||||
|
||||
|
||||
def _bullets(doc, items):
|
||||
for it in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.clear(); r = p.add_run(it); r.font.size = Pt(11)
|
||||
|
||||
|
||||
def generate_calea_wireless(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
law_enforcement_contact: Optional[dict] = None,
|
||||
cpni_protection_officer: Optional[dict] = None,
|
||||
network_infrastructure_summary: str = "",
|
||||
interception_support_method: str = "",
|
||||
reporting_year: int = 0,
|
||||
signatory_name: str = "",
|
||||
signatory_title: str = "Chief Executive Officer",
|
||||
effective_date: str = "",
|
||||
next_review_date: str = "",
|
||||
reviewer_name: str = "Justin Hannah",
|
||||
reviewer_company: str = "Performance West Inc.",
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
le = law_enforcement_contact or {}
|
||||
cpni = cpni_protection_officer or {}
|
||||
today = date.today()
|
||||
effective = effective_date or today.strftime("%m/%d/%Y")
|
||||
next_review = next_review_date or today.replace(year=today.year + 1).strftime("%m/%d/%Y")
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
title = doc.add_paragraph(); title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
tr = title.add_run("System Security and Integrity (SSI) Plan")
|
||||
tr.font.size = Pt(15); tr.bold = True; tr.font.color.rgb = NAVY
|
||||
|
||||
sub = doc.add_paragraph(); sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sr = sub.add_run(entity_name); sr.font.size = Pt(13); sr.bold = True
|
||||
|
||||
vsub = doc.add_paragraph(); vsub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
vr = vsub.add_run(f"Variant: {VARIANT_LABEL}")
|
||||
vr.font.size = Pt(11); vr.italic = True
|
||||
|
||||
cite = doc.add_paragraph(); cite.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
cr = cite.add_run(
|
||||
"Pursuant to 47 U.S.C. \u00a7 229, 47 CFR \u00a7 1.20003, "
|
||||
"and 47 CFR \u00a7 20.13"
|
||||
)
|
||||
cr.font.size = Pt(10); cr.italic = True
|
||||
cite.paragraph_format.space_after = Pt(18)
|
||||
|
||||
_heading(doc, "1. Purpose")
|
||||
_body(doc, (
|
||||
f"This SSI Plan governs {entity_name}'s compliance with CALEA "
|
||||
f"(47 U.S.C. \u00a7\u00a7 1001\u20131010) and the Commission's "
|
||||
f"rules at 47 CFR Part 1 Subpart Z and 47 CFR \u00a7 20.13 as "
|
||||
f"applied to {entity_name}'s operations as a facilities-based "
|
||||
f"Commercial Mobile Radio Service (CMRS) provider."
|
||||
))
|
||||
|
||||
_heading(doc, "2. Scope and Applicability")
|
||||
_body(doc, (
|
||||
f"{entity_name} is a facilities-based CMRS provider subject to "
|
||||
f"the Lawfully Authorized Electronic Surveillance (LAES) "
|
||||
f"obligations of 47 CFR \u00a7 20.13. Its covered equipment "
|
||||
f"includes the Mobile Switching Center (MSC), 4G Evolved Packet "
|
||||
f"Core (EPC), 5G Core, HSS / UDM, SMS-C, and the associated "
|
||||
f"radio-access network (eNB / gNB) provisioning systems."
|
||||
))
|
||||
|
||||
_heading(doc, "3. Designated Law Enforcement Contact (24-hour)")
|
||||
_body(doc, (
|
||||
f"Per 47 CFR \u00a7 1.20003(a)(1), {entity_name} designates the "
|
||||
f"following senior officer as 24-hour point of contact for court "
|
||||
f"orders, pen-register/trap-and-trace orders, Title III wiretap "
|
||||
f"orders, and location-information orders."
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {le.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {le.get('title') or ''}",
|
||||
f"Phone (24-hour): {le.get('phone') or ''}",
|
||||
f"Email (24-hour): {le.get('email_24h') or ''}",
|
||||
f"Backup contact: {le.get('backup_name') or '[TO BE POPULATED]'}",
|
||||
])
|
||||
_body(doc, (
|
||||
f"{entity_name} commits to acknowledging any order within two (2) "
|
||||
f"business hours of receipt."
|
||||
))
|
||||
|
||||
_heading(doc, "4. Network Architecture and Interception Capability")
|
||||
_body(doc, network_infrastructure_summary or (
|
||||
f"{entity_name} operates a radio-access network (eNB / gNB), a 4G "
|
||||
"EPC with MME / S-GW / P-GW elements, and where deployed a 5G "
|
||||
"Core with AMF / SMF / UPF. Subscriber identity and location are "
|
||||
"held in the HSS / UDM. Voice service is delivered via IMS / "
|
||||
"VoLTE or via circuit-switched fallback."
|
||||
))
|
||||
_body(doc, interception_support_method or (
|
||||
f"Lawful intercept (LAES) is provisioned at {entity_name}'s MSC / "
|
||||
"EPC / 5GC elements using the 3GPP-standardized LI interfaces "
|
||||
"(X1 for provisioning / administration, X2 for intercept-related "
|
||||
"information, X3 for content-of-communications) per 3GPP TS "
|
||||
"33.126 / 33.127 / 33.128 and ATIS T1.724 / J-STD-025. Call "
|
||||
"content and call-identifying information (including cell-site "
|
||||
"/ E911 / handover location data where lawfully ordered) are "
|
||||
"delivered to the requesting agency through these standard "
|
||||
"interfaces."
|
||||
))
|
||||
|
||||
_heading(doc, "5. CPNI Safeguards")
|
||||
_body(doc, (
|
||||
f"{entity_name} maintains a separate CPNI procedure statement "
|
||||
f"under 47 CFR \u00a7\u00a7 64.2001\u201364.2011. Device-level "
|
||||
f"location data is treated as CPNI, consistent with the "
|
||||
f"Commission's 2020 LocationSmart Consent Decree (DA 20-299) "
|
||||
f"and 2024 NAL against unauthorized third-party location sharing. "
|
||||
f"The CPNI Protection Officer is:"
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {cpni.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {cpni.get('title') or 'CPNI Protection Officer'}",
|
||||
])
|
||||
|
||||
_heading(doc, "6. Personnel Vetting and Training")
|
||||
_bullets(doc, [
|
||||
"Annual CALEA + CPNI training for all personnel with LI or CPNI "
|
||||
"access.",
|
||||
"Background checks performed prior to granting access to LI "
|
||||
"provisioning or HSS / UDM systems.",
|
||||
"Access revoked within 24 hours of termination.",
|
||||
"All LI actions attributed to authenticated named users; no "
|
||||
"shared credentials.",
|
||||
])
|
||||
|
||||
_heading(doc, "7. Supervisory Review")
|
||||
_body(doc, (
|
||||
f"The {le.get('title') or 'Designated Senior Officer'} reviews "
|
||||
f"LI activity logs at least quarterly. Anomalies are escalated "
|
||||
f"to the CEO within one business day."
|
||||
))
|
||||
|
||||
_heading(doc, "8. Records Retention")
|
||||
_body(doc, (
|
||||
"LI provisioning and service-of-process records retained ten (10) "
|
||||
"years per 47 CFR \u00a7 1.20003(b); CPNI access logs retained at "
|
||||
"least two (2) years per 47 CFR \u00a7 64.2009."
|
||||
))
|
||||
|
||||
_heading(doc, "9. Annual Review")
|
||||
_body(doc, (
|
||||
f"This Plan is reviewed at least annually and upon (i) material "
|
||||
f"core or RAN network change, (ii) new 3GPP LI release adoption, "
|
||||
f"(iii) new Commission / DOJ guidance, or (iv) a material breach. "
|
||||
f"Next scheduled review: {next_review}."
|
||||
))
|
||||
|
||||
_heading(doc, "10. Certification")
|
||||
_body(doc, (
|
||||
f"I, {signatory_name or '[Authorized Officer]'}, as "
|
||||
f"{signatory_title} of {entity_name}, certify that I have "
|
||||
f"reviewed this SSI Plan and that {entity_name} complies with "
|
||||
f"47 U.S.C. \u00a7 229, 47 CFR \u00a7 1.20003, and 47 CFR "
|
||||
f"\u00a7 20.13."
|
||||
))
|
||||
_body(doc, "")
|
||||
doc.add_paragraph("_" * 45)
|
||||
_body(doc, signatory_name or "[Authorized Officer]", bold=True)
|
||||
_body(doc, f"{signatory_title}, {entity_name}")
|
||||
_body(doc, f"Effective Date: {effective}")
|
||||
if frn: _body(doc, f"FRN: {frn}")
|
||||
_body(doc, f"Reviewed By: {reviewer_name}, {reviewer_company}")
|
||||
_body(doc, f"Next Review Date: {next_review}")
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CALEA Wireless SSI plan generated: %s", out)
|
||||
return str(out)
|
||||
229
scripts/document_gen/templates/calea_wireless_mvno_generator.py
Normal file
229
scripts/document_gen/templates/calea_wireless_mvno_generator.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
"""
|
||||
CALEA SSI Plan — Wireless MVNO variant.
|
||||
|
||||
A Mobile Virtual Network Operator has no radio-access network and no
|
||||
core-network switching of its own. Under the CALEA Reference Model, the
|
||||
host MNO is responsible for actual lawful-intercept provisioning and
|
||||
delivery; the MVNO's SSI Plan documents the division of responsibility,
|
||||
the designated point of contact for law enforcement service of process,
|
||||
and the contractual flow-down terms that obligate the host MNO to
|
||||
support intercepts initiated against the MVNO's subscribers.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.calea_wireless_mvno")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CALEA Wireless MVNO unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "wireless_mvno"
|
||||
VARIANT_LABEL = "Wireless (CMRS) — MVNO"
|
||||
|
||||
|
||||
def _heading(doc, text):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12); p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(text); r.bold = True; r.font.size = Pt(13); r.font.color.rgb = NAVY
|
||||
|
||||
|
||||
def _body(doc, text, bold=False):
|
||||
p = doc.add_paragraph(); p.paragraph_format.space_after = Pt(6)
|
||||
r = p.add_run(text); r.font.size = Pt(11); r.bold = bold
|
||||
|
||||
|
||||
def _bullets(doc, items):
|
||||
for it in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.clear(); r = p.add_run(it); r.font.size = Pt(11)
|
||||
|
||||
|
||||
def generate_calea_wireless_mvno(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
law_enforcement_contact: Optional[dict] = None,
|
||||
cpni_protection_officer: Optional[dict] = None,
|
||||
network_infrastructure_summary: str = "",
|
||||
interception_support_method: str = "",
|
||||
reporting_year: int = 0,
|
||||
host_mno_name: str = "",
|
||||
signatory_name: str = "",
|
||||
signatory_title: str = "Chief Executive Officer",
|
||||
effective_date: str = "",
|
||||
next_review_date: str = "",
|
||||
reviewer_name: str = "Justin Hannah",
|
||||
reviewer_company: str = "Performance West Inc.",
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
le = law_enforcement_contact or {}
|
||||
cpni = cpni_protection_officer or {}
|
||||
today = date.today()
|
||||
effective = effective_date or today.strftime("%m/%d/%Y")
|
||||
next_review = next_review_date or today.replace(year=today.year + 1).strftime("%m/%d/%Y")
|
||||
host = host_mno_name or "its host Mobile Network Operator"
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
title = doc.add_paragraph(); title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
tr = title.add_run("System Security and Integrity (SSI) Plan")
|
||||
tr.font.size = Pt(15); tr.bold = True; tr.font.color.rgb = NAVY
|
||||
sub = doc.add_paragraph(); sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sr = sub.add_run(entity_name); sr.font.size = Pt(13); sr.bold = True
|
||||
vsub = doc.add_paragraph(); vsub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
vr = vsub.add_run(f"Variant: {VARIANT_LABEL}")
|
||||
vr.font.size = Pt(11); vr.italic = True
|
||||
cite = doc.add_paragraph(); cite.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
cr = cite.add_run("Pursuant to 47 U.S.C. \u00a7 229 and 47 CFR \u00a7 1.20003")
|
||||
cr.font.size = Pt(10); cr.italic = True
|
||||
cite.paragraph_format.space_after = Pt(18)
|
||||
|
||||
_heading(doc, "1. Purpose")
|
||||
_body(doc, (
|
||||
f"This SSI Plan governs {entity_name}'s compliance with CALEA and "
|
||||
f"its implementing rules as applied to {entity_name}'s operations "
|
||||
f"as a Mobile Virtual Network Operator (MVNO) that resells "
|
||||
f"wireless service provided by {host}."
|
||||
))
|
||||
|
||||
_heading(doc, "2. Scope and Applicability — Division of Responsibility")
|
||||
_body(doc, (
|
||||
f"{entity_name} does not own or operate radio-access equipment, "
|
||||
f"a Mobile Switching Center, an EPC / 5GC, or HSS / UDM. Under "
|
||||
f"the CALEA Reference Model and the MVNO wholesale agreement "
|
||||
f"between {entity_name} and {host}, intercept capability is "
|
||||
f"provisioned and operated by {host}. {entity_name}'s SSI "
|
||||
f"responsibility is limited to: (i) maintaining a designated "
|
||||
f"24-hour law-enforcement point of contact, (ii) coordinating "
|
||||
f"service of process between law enforcement and {host}, "
|
||||
f"(iii) ensuring contractual flow-down of CALEA obligations to "
|
||||
f"{host}, and (iv) protecting its own customer records."
|
||||
))
|
||||
|
||||
_heading(doc, "3. Designated Law Enforcement Contact (24-hour)")
|
||||
_body(doc, (
|
||||
f"Per 47 CFR \u00a7 1.20003(a)(1), {entity_name} designates the "
|
||||
f"following senior officer as 24-hour contact for law enforcement."
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {le.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {le.get('title') or ''}",
|
||||
f"Phone (24-hour): {le.get('phone') or ''}",
|
||||
f"Email (24-hour): {le.get('email_24h') or ''}",
|
||||
f"Backup contact: {le.get('backup_name') or '[TO BE POPULATED]'}",
|
||||
])
|
||||
_body(doc, (
|
||||
f"Upon receipt of a valid court order, {entity_name}'s designated "
|
||||
f"officer (a) acknowledges service within two (2) business hours, "
|
||||
f"(b) confirms that the subscriber is provisioned on {host}'s "
|
||||
f"network, and (c) coordinates with {host}'s CALEA / LAES team "
|
||||
f"to effect intercept provisioning, providing the ordering "
|
||||
f"agency with the appropriate host-MNO CALEA contact as "
|
||||
f"required."
|
||||
))
|
||||
|
||||
_heading(doc, "4. Network / Interception Capability (Host MNO)")
|
||||
_body(doc, network_infrastructure_summary or (
|
||||
f"Voice, SMS, and data services consumed by {entity_name}'s "
|
||||
f"subscribers traverse {host}'s radio-access network and core. "
|
||||
f"Authentication is performed against {host}'s HSS / UDM."
|
||||
))
|
||||
_body(doc, interception_support_method or (
|
||||
f"Lawful intercept is provisioned by {host} using the standardized "
|
||||
f"3GPP LI interfaces (X1 / X2 / X3) at {host}'s MSC / EPC / 5GC, "
|
||||
f"per ATIS T1.724 / J-STD-025 and 3GPP TS 33.126 / 33.127 / "
|
||||
f"33.128. {host} is responsible for delivering content and "
|
||||
f"call-identifying information to the requesting law-enforcement "
|
||||
f"agency."
|
||||
))
|
||||
_body(doc, (
|
||||
f"{entity_name} retains an executed copy of the MVNO wholesale "
|
||||
f"agreement with {host}, including the CALEA flow-down clauses, "
|
||||
f"and a current copy of {host}'s CALEA attestation on file."
|
||||
))
|
||||
|
||||
_heading(doc, "5. CPNI Safeguards")
|
||||
_body(doc, (
|
||||
f"{entity_name} maintains separate CPNI procedures under 47 CFR "
|
||||
f"\u00a7\u00a7 64.2001\u201364.2011 with respect to retail "
|
||||
f"customer records, billing data, and support interactions that "
|
||||
f"{entity_name} directly controls. The CPNI Protection Officer is:"
|
||||
))
|
||||
_bullets(doc, [
|
||||
f"Name: {cpni.get('name') or '[TO BE POPULATED]'}",
|
||||
f"Title: {cpni.get('title') or 'CPNI Protection Officer'}",
|
||||
])
|
||||
|
||||
_heading(doc, "6. Personnel Vetting and Training")
|
||||
_bullets(doc, [
|
||||
"Annual CALEA + CPNI training for personnel handling customer "
|
||||
"records or law-enforcement service of process.",
|
||||
"Background checks prior to granting access.",
|
||||
"Access revoked within 24 hours of termination.",
|
||||
"All service-of-process and CPNI actions attributed to named "
|
||||
"authenticated users.",
|
||||
])
|
||||
|
||||
_heading(doc, "7. Supervisory Review")
|
||||
_body(doc, (
|
||||
f"The {le.get('title') or 'Designated Senior Officer'} reviews "
|
||||
f"service-of-process logs and MVNO-host coordination records at "
|
||||
f"least quarterly."
|
||||
))
|
||||
|
||||
_heading(doc, "8. Records Retention")
|
||||
_body(doc, (
|
||||
"Service-of-process coordination records retained ten (10) years "
|
||||
"per 47 CFR \u00a7 1.20003(b); CPNI access logs retained at least "
|
||||
"two (2) years per 47 CFR \u00a7 64.2009."
|
||||
))
|
||||
|
||||
_heading(doc, "9. Annual Review")
|
||||
_body(doc, (
|
||||
f"This Plan is reviewed at least annually and upon any change to "
|
||||
f"the MVNO wholesale agreement or to {host}'s CALEA attestation. "
|
||||
f"Next scheduled review: {next_review}."
|
||||
))
|
||||
|
||||
_heading(doc, "10. Certification")
|
||||
_body(doc, (
|
||||
f"I, {signatory_name or '[Authorized Officer]'}, as "
|
||||
f"{signatory_title} of {entity_name}, certify that I have "
|
||||
f"reviewed this SSI Plan and that {entity_name} complies with 47 "
|
||||
f"U.S.C. \u00a7 229 and 47 CFR \u00a7 1.20003 through its MVNO "
|
||||
f"wholesale arrangement with {host}."
|
||||
))
|
||||
_body(doc, "")
|
||||
doc.add_paragraph("_" * 45)
|
||||
_body(doc, signatory_name or "[Authorized Officer]", bold=True)
|
||||
_body(doc, f"{signatory_title}, {entity_name}")
|
||||
_body(doc, f"Effective Date: {effective}")
|
||||
if frn: _body(doc, f"FRN: {frn}")
|
||||
_body(doc, f"Reviewed By: {reviewer_name}, {reviewer_company}")
|
||||
_body(doc, f"Next Review Date: {next_review}")
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CALEA Wireless MVNO SSI plan generated: %s", out)
|
||||
return str(out)
|
||||
270
scripts/document_gen/templates/cdr_traffic_study_generator.py
Normal file
270
scripts/document_gen/templates/cdr_traffic_study_generator.py
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
"""Traffic Study generator — PDF + XLSX deliverable.
|
||||
|
||||
Takes a fully-rolled ``cdr_traffic_studies`` row and produces:
|
||||
* a signed-ready DOCX (converted to PDF downstream) for the customer's
|
||||
audit file, with methodology statement + both Block 5 regional
|
||||
tables + revenue-vs-minutes cross-check
|
||||
* an XLSX "working doc" with per-period rollups and the same cells
|
||||
that will drop into the 499-A E-File session
|
||||
|
||||
Produced by ``CDRAnalysisHandler`` at the end of a reporting period.
|
||||
Pre-existing infrastructure reused:
|
||||
* python-docx for the DOCX
|
||||
* openpyxl for the XLSX
|
||||
* scripts.document_gen.templates.base_handler pattern for styling
|
||||
|
||||
No classification happens here — this module only formats numbers that
|
||||
the ingester + classifier already wrote into cdr_calls + cdr_traffic_studies.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cdr_traffic_study")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — traffic study generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Border, Side
|
||||
except ImportError:
|
||||
LOG.warning("openpyxl not installed — xlsx export unavailable")
|
||||
Workbook = None # type: ignore[assignment,misc]
|
||||
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
|
||||
def _pct(value) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{float(value):.2f}%"
|
||||
|
||||
|
||||
def _dollars(cents: Optional[int]) -> str:
|
||||
if cents is None:
|
||||
return "—"
|
||||
return f"${cents/100:,.2f}"
|
||||
|
||||
|
||||
def _minutes(seconds: Optional[int]) -> str:
|
||||
if seconds is None:
|
||||
return "—"
|
||||
return f"{seconds/60:,.0f}"
|
||||
|
||||
|
||||
# ─── DOCX ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def generate_traffic_study_docx(
|
||||
*,
|
||||
study: dict,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
output_path: str,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
doc = Document()
|
||||
for section in doc.sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1.25)
|
||||
section.right_margin = Inches(1.25)
|
||||
|
||||
# Title
|
||||
title = doc.add_paragraph()
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r = title.add_run(f"Telecommunications Traffic Study — {study['reporting_year']} {study['reporting_period']}")
|
||||
r.font.size = Pt(14)
|
||||
r.bold = True
|
||||
r.font.color.rgb = NAVY
|
||||
|
||||
sub = doc.add_paragraph()
|
||||
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sub_r = sub.add_run(entity_name)
|
||||
sub_r.font.size = Pt(12)
|
||||
sub_r.bold = True
|
||||
|
||||
info = doc.add_paragraph()
|
||||
info_r = info.add_run(
|
||||
f"FRN: {frn or 'N/A'} | 499 Filer ID: {filer_id_499 or 'N/A'} | "
|
||||
f"Generated: {datetime.now().strftime('%B %d, %Y')}"
|
||||
)
|
||||
info_r.font.size = Pt(9)
|
||||
info_r.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
|
||||
doc.add_paragraph()
|
||||
|
||||
# Methodology
|
||||
doc.add_heading("Methodology", level=1)
|
||||
method_paragraphs = [
|
||||
(
|
||||
f"This study analyzes {study.get('total_calls', 0):,} call detail records "
|
||||
f"covering {study['reporting_year']} {study['reporting_period']}. "
|
||||
"Each call was classified by endpoint geography using NANP area-code "
|
||||
"records and FCC country-code assignments (47 CFR § 54.706 definitions). "
|
||||
"Jurisdictional buckets are: interstate, intrastate, international, and "
|
||||
"indeterminate (records where one or both endpoints could not be "
|
||||
"resolved to a country/state)."
|
||||
),
|
||||
(
|
||||
"Revenue-based weighting is used where the source CDR carries per-call "
|
||||
"billing amounts. Minutes-weighted percentages are provided as a "
|
||||
"cross-check. Records are five-year retained per 47 CFR § 54.711(a) "
|
||||
"and available for USAC audit on request."
|
||||
),
|
||||
(study.get("methodology") or ""),
|
||||
]
|
||||
for text in method_paragraphs:
|
||||
if text:
|
||||
doc.add_paragraph(text)
|
||||
|
||||
# Jurisdictional table
|
||||
doc.add_heading("Jurisdictional Breakdown", level=1)
|
||||
juris_table = doc.add_table(rows=1, cols=3)
|
||||
juris_table.style = "Table Grid"
|
||||
hdr = juris_table.rows[0].cells
|
||||
hdr[0].text = "Category"
|
||||
hdr[1].text = "Revenue-weighted"
|
||||
hdr[2].text = "Minutes-weighted"
|
||||
for label, key_rev, key_min in [
|
||||
("Interstate", "interstate_pct", "interstate_pct_minutes"),
|
||||
("Intrastate", "intrastate_pct", "intrastate_pct_minutes"),
|
||||
("International", "international_pct", "international_pct_minutes"),
|
||||
("Indeterminate", "indeterminate_pct", "indeterminate_pct_minutes"),
|
||||
]:
|
||||
row = juris_table.add_row().cells
|
||||
row[0].text = label
|
||||
row[1].text = _pct(study.get(key_rev))
|
||||
row[2].text = _pct(study.get(key_min))
|
||||
|
||||
# Wholesale vs retail
|
||||
doc.add_heading("Block 3 vs. Block 4-A Allocation", level=1)
|
||||
w_min = study.get("wholesale_minutes") or 0
|
||||
r_min = study.get("retail_minutes") or 0
|
||||
doc.add_paragraph(
|
||||
f"Wholesale (carrier-to-carrier, Block 3): {w_min/60:,.0f} minutes\n"
|
||||
f"Retail (end-user, Block 4-A): {r_min/60:,.0f} minutes"
|
||||
)
|
||||
|
||||
# Block 5 regional — BOTH reports
|
||||
for label, key in [
|
||||
("Block 5 — by originating state of caller", "orig_state_regions_json"),
|
||||
("Block 5 — by customer billing-address state", "billing_state_regions_json"),
|
||||
]:
|
||||
doc.add_heading(label, level=1)
|
||||
regions = (study.get(key) or {})
|
||||
if not regions:
|
||||
doc.add_paragraph("(no data for this view)")
|
||||
continue
|
||||
table = doc.add_table(rows=1, cols=2)
|
||||
table.style = "Table Grid"
|
||||
h = table.rows[0].cells
|
||||
h[0].text = "Region"
|
||||
h[1].text = "% of Total"
|
||||
for region_name, pct_val in sorted(regions.items()):
|
||||
row = table.add_row().cells
|
||||
row[0].text = region_name
|
||||
row[1].text = _pct(pct_val)
|
||||
|
||||
doc.add_heading("Certification", level=1)
|
||||
doc.add_paragraph(
|
||||
f"I certify that this traffic study accurately reflects the "
|
||||
f"telecommunications usage of {entity_name} during the reporting "
|
||||
f"period. The underlying CDRs are retained for five years and "
|
||||
f"available on request."
|
||||
)
|
||||
for _ in range(2):
|
||||
doc.add_paragraph()
|
||||
doc.add_paragraph("_" * 45)
|
||||
doc.add_paragraph("Authorized Officer")
|
||||
doc.add_paragraph(entity_name)
|
||||
doc.add_paragraph(f"Date: {datetime.now().strftime('%B %d, %Y')}")
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
return str(out)
|
||||
|
||||
|
||||
# ─── XLSX (admin working doc) ───────────────────────────────────────────
|
||||
|
||||
|
||||
def generate_traffic_study_xlsx(
|
||||
*,
|
||||
study: dict,
|
||||
entity_name: str,
|
||||
output_path: str,
|
||||
) -> Optional[str]:
|
||||
if Workbook is None:
|
||||
LOG.error("openpyxl not installed")
|
||||
return None
|
||||
|
||||
wb = Workbook()
|
||||
default = wb.active
|
||||
wb.remove(default)
|
||||
|
||||
# Summary
|
||||
ws = wb.create_sheet("Summary")
|
||||
ws["A1"] = f"Traffic Study — {entity_name}"
|
||||
ws["A1"].font = Font(bold=True, size=14, color="1A2744")
|
||||
ws["A2"] = f"{study['reporting_year']} {study['reporting_period']}"
|
||||
rows = [
|
||||
("Total calls", study.get("total_calls") or 0),
|
||||
("Total minutes", (study.get("total_minutes") or 0)),
|
||||
("Total revenue (cents)", study.get("total_revenue_cents") or 0),
|
||||
("", ""),
|
||||
("Interstate % (revenue-weighted)", study.get("interstate_pct")),
|
||||
("Intrastate % (revenue-weighted)", study.get("intrastate_pct")),
|
||||
("International % (revenue-weighted)", study.get("international_pct")),
|
||||
("Indeterminate % (revenue-weighted)", study.get("indeterminate_pct")),
|
||||
("", ""),
|
||||
("Interstate % (minutes-weighted)", study.get("interstate_pct_minutes")),
|
||||
("Intrastate % (minutes-weighted)", study.get("intrastate_pct_minutes")),
|
||||
("International % (minutes-weighted)", study.get("international_pct_minutes")),
|
||||
("Indeterminate % (minutes-weighted)", study.get("indeterminate_pct_minutes")),
|
||||
("", ""),
|
||||
("Wholesale minutes (Block 3)", (study.get("wholesale_minutes") or 0) / 60),
|
||||
("Retail minutes (Block 4-A)", (study.get("retail_minutes") or 0) / 60),
|
||||
]
|
||||
for i, (label, value) in enumerate(rows, start=4):
|
||||
ws.cell(row=i, column=1, value=label)
|
||||
ws.cell(row=i, column=2, value=value)
|
||||
ws.column_dimensions["A"].width = 45
|
||||
ws.column_dimensions["B"].width = 22
|
||||
|
||||
# Regional breakdowns
|
||||
for sheet_name, key in [
|
||||
("Block 5 — Orig State", "orig_state_regions_json"),
|
||||
("Block 5 — Billing State", "billing_state_regions_json"),
|
||||
]:
|
||||
rs = wb.create_sheet(sheet_name)
|
||||
rs.cell(row=1, column=1, value="Region").font = Font(bold=True)
|
||||
rs.cell(row=1, column=2, value="% of Total").font = Font(bold=True)
|
||||
regions = study.get(key) or {}
|
||||
for i, (name, pct) in enumerate(sorted(regions.items()), start=2):
|
||||
rs.cell(row=i, column=1, value=name)
|
||||
rs.cell(row=i, column=2, value=float(pct) if pct is not None else None)
|
||||
rs.cell(row=i, column=2).number_format = '0.00"%"'
|
||||
rs.column_dimensions["A"].width = 25
|
||||
rs.column_dimensions["B"].width = 15
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
wb.save(str(out))
|
||||
return str(out)
|
||||
263
scripts/document_gen/templates/cpni_audio_bridge_generator.py
Normal file
263
scripts/document_gen/templates/cpni_audio_bridge_generator.py
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
"""
|
||||
Generate the FCC CPNI Annual Certification Letter — Audio Bridging variant.
|
||||
|
||||
Audio bridging / conferencing providers have a narrower CPNI scope than
|
||||
ordinary carriers: CPNI is generally limited to participant dial-in
|
||||
records, scheduled conference metadata, and enterprise billing records.
|
||||
Per the Commission's longstanding treatment of non-real-time conferencing
|
||||
services, some categories of conference metadata may fall outside the
|
||||
definition of CPNI where the service is not "telecommunications service"
|
||||
as defined in 47 USC § 153(53).
|
||||
|
||||
This certification addresses the CPNI {entity_name} does hold and states
|
||||
expressly where non-real-time or information-service exceptions apply.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_audio_bridge")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI Audio Bridge generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "audio_bridging"
|
||||
VARIANT_LABEL = "Audio Bridging / Conferencing"
|
||||
|
||||
MAX_FORFEITURE_PER_VIOLATION = "$251,322"
|
||||
MAX_FORFEITURE_CAP = "$2,513,215"
|
||||
|
||||
|
||||
def _sp(p, after=6, before=0):
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
if before:
|
||||
p.paragraph_format.space_before = Pt(before)
|
||||
|
||||
|
||||
def _h(doc, text):
|
||||
p = doc.add_paragraph(); r = p.add_run(text)
|
||||
r.font.size = Pt(12); r.bold = True; r.font.color.rgb = _NAVY
|
||||
_sp(p, after=4, before=8)
|
||||
|
||||
|
||||
def _b(doc, text, bold=False, size=10):
|
||||
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(text); r.font.size = Pt(size); r.bold = bold
|
||||
_sp(p, after=6)
|
||||
|
||||
|
||||
def _cb(doc, text, checked=True):
|
||||
mark = "\u2611" if checked else "\u2610"
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(f" {mark} {text}"); r.font.size = Pt(10)
|
||||
_sp(p, after=3)
|
||||
|
||||
|
||||
def generate_cpni_audio_bridge(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
officer_name: str = "",
|
||||
officer_title: str = "Chief Executive Officer",
|
||||
complaints_count: int = 0,
|
||||
complaints_description: str = "",
|
||||
has_data_broker_inquiries: bool = False,
|
||||
data_broker_description: str = "",
|
||||
reporting_year: int = 0,
|
||||
address_street: str = "",
|
||||
address_city: str = "",
|
||||
address_state: str = "",
|
||||
address_zip: str = "",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
breaches: list[dict] | None = None,
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
breaches = breaches or []
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
signer = officer_name or "Authorized Officer"
|
||||
title = officer_title or "Officer"
|
||||
|
||||
tp = doc.add_paragraph(); tp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
t = tp.add_run("CPNI Annual Certification Letter")
|
||||
t.font.size = Pt(14); t.bold = True; t.font.color.rgb = _NAVY
|
||||
_sp(tp, after=2)
|
||||
sp = doc.add_paragraph(); sp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
s = sp.add_run(
|
||||
f"Audio Bridging / Conferencing \u2014 47 CFR \u00a7 64.2009 "
|
||||
f"\u2014 Calendar Year {reporting_year}"
|
||||
)
|
||||
s.font.size = Pt(10); s.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_sp(sp, after=8)
|
||||
|
||||
_h(doc, "1. Provider Information")
|
||||
lines = [f"Company Name: {entity_name}", f"Variant: {VARIANT_LABEL}"]
|
||||
if frn: lines.append(f"FCC Registration Number (FRN): {frn}")
|
||||
if filer_id_499: lines.append(f"FCC Form 499 Filer ID: {filer_id_499}")
|
||||
addr = ", ".join(filter(None, [address_street, address_city]))
|
||||
if address_state or address_zip:
|
||||
addr += f", {address_state} {address_zip}".strip()
|
||||
if addr.strip(", "):
|
||||
lines.append(f"Address: {addr.strip(', ')}")
|
||||
if contact_phone: lines.append(f"Telephone: {contact_phone}")
|
||||
if contact_email: lines.append(f"Email: {contact_email}")
|
||||
lines.append(f"Certifying Officer: {signer}, {title}")
|
||||
lines.append(f"Date of Filing: {today}")
|
||||
lines.append(f"Filing Deadline: March 2, {reporting_year + 1}")
|
||||
_b(doc, "\n".join(lines))
|
||||
|
||||
_h(doc, "2. Officer Statement of Personal Knowledge")
|
||||
_b(doc, (
|
||||
f"I, {signer}, {title} of {entity_name}, state that I have personal "
|
||||
f"knowledge of the matters certified herein, including procedures "
|
||||
f"governing participant dial-in records, scheduled-conference "
|
||||
f"metadata, and enterprise billing data."
|
||||
))
|
||||
|
||||
_h(doc, "3. Scope Note and Certification of Compliance")
|
||||
_b(doc, (
|
||||
f"{entity_name} provides audio bridging / conferencing service. "
|
||||
f"Its CPNI-like holdings are narrow: participant dial-in numbers, "
|
||||
f"conference-bridge access records, and enterprise billing data. "
|
||||
f"To the extent any portion of the service is properly classified "
|
||||
f"as a non-real-time information service rather than a "
|
||||
f"telecommunications service under 47 USC \u00a7 153(53), the "
|
||||
f"Commission has recognized that such portion is not subject to "
|
||||
f"47 CFR Part 64 Subpart U. {entity_name} certifies compliance "
|
||||
f"with 47 CFR \u00a7\u00a7 64.2001 through 64.2011 with respect to "
|
||||
f"all remaining CPNI it holds for the period January 1, "
|
||||
f"{reporting_year} through December 31, {reporting_year}."
|
||||
))
|
||||
|
||||
_h(doc, "4. How Our Procedures Ensure Compliance")
|
||||
_cb(doc, (
|
||||
"Access to participant dial-in records and conference metadata is "
|
||||
"restricted to authenticated personnel performing billing, "
|
||||
"support, or abuse investigations. Authentication occurs through "
|
||||
"named-user credentials; access is logged (47 CFR \u00a7 64.2009)."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Customer authentication is required before CPNI release in "
|
||||
"response to a customer-initiated inquiry. Consumer-side "
|
||||
"authentication is by pre-established password; enterprise-side "
|
||||
"authentication is via credentials assigned in the master service "
|
||||
"agreement (47 CFR \u00a7 64.2010)."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Customer approval for use of CPNI beyond the scope of the "
|
||||
"subscribed service is obtained through written opt-in consent, "
|
||||
"documented per 47 CFR \u00a7 64.2007."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Supervisory review of CPNI access is conducted at least "
|
||||
"quarterly; retention of logs meets or exceeds two years, and "
|
||||
"certification records are retained for five years, per 47 CFR "
|
||||
"\u00a7 64.2009."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Annual training is provided to all personnel with CPNI access; "
|
||||
"breach-notification procedures comply with 47 CFR \u00a7 64.2011 "
|
||||
"as amended by FCC 23-111."
|
||||
))
|
||||
|
||||
_h(doc, "5. Customer Complaints")
|
||||
if complaints_count == 0:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any customer complaints during "
|
||||
f"the reporting period concerning the unauthorized release or "
|
||||
f"use of CPNI. Zero (0) complaints were logged."
|
||||
))
|
||||
else:
|
||||
desc = complaints_description or "Each complaint was investigated and resolved."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received {complaints_count} customer "
|
||||
f"complaint{'s' if complaints_count != 1 else ''} during the "
|
||||
f"reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "6. Data Broker Inquiries and Pretexting")
|
||||
if not has_data_broker_inquiries:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any inquiries, communications, "
|
||||
f"or attempts by data brokers or other unauthorized parties "
|
||||
f"seeking the unauthorized release of CPNI."
|
||||
))
|
||||
else:
|
||||
desc = data_broker_description or "Each was refused, documented, and escalated."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received data broker or pretexting-style "
|
||||
f"inquiries during the reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "7. Breach Log Summary")
|
||||
if not breaches:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced no CPNI breaches during the "
|
||||
f"reporting period. No 47 CFR \u00a7 64.2011 notifications "
|
||||
f"were required."
|
||||
))
|
||||
else:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced {len(breaches)} CPNI breach"
|
||||
f"{'es' if len(breaches) != 1 else ''} during the reporting "
|
||||
f"period; each was reported within 7 business days."
|
||||
))
|
||||
|
||||
_h(doc, "8. Penalties, Truthfulness, and Perjury Acknowledgment")
|
||||
_b(doc, (
|
||||
f"{entity_name} and the undersigned acknowledge that CPNI rule "
|
||||
f"violations may subject the carrier to forfeitures up to "
|
||||
f"{MAX_FORFEITURE_PER_VIOLATION} per violation and up to "
|
||||
f"{MAX_FORFEITURE_CAP} for any single act or failure to act."
|
||||
))
|
||||
_b(doc, (
|
||||
"Pursuant to 47 CFR \u00a7 1.17, the undersigned represents that no "
|
||||
"material factual information has been withheld and all statements "
|
||||
"are truthful, accurate, and complete."
|
||||
))
|
||||
_b(doc, (
|
||||
"Willful false statements are punishable under Title 18, U.S.C. "
|
||||
"\u00a7 1001, and by forfeiture under 47 U.S.C. \u00a7 503."
|
||||
))
|
||||
|
||||
_h(doc, "9. Signature of Certifying Officer")
|
||||
_b(doc, (
|
||||
"I declare under penalty of perjury under the laws of the United "
|
||||
"States of America that the foregoing is true and correct."
|
||||
))
|
||||
p = doc.add_paragraph(); _sp(p, after=0)
|
||||
sig = doc.add_paragraph(); sig.add_run("_" * 45).font.size = Pt(10); _sp(sig, after=2)
|
||||
nm = doc.add_paragraph(); nr = nm.add_run(signer); nr.bold = True
|
||||
nr.font.size = Pt(10); _sp(nm, after=2)
|
||||
tpp = doc.add_paragraph(); tpp.add_run(f"{title}, {entity_name}").font.size = Pt(10)
|
||||
_sp(tpp, after=2)
|
||||
dp = doc.add_paragraph(); dp.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
_sp(dp, after=2)
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CPNI Audio Bridge certification letter generated: %s", out)
|
||||
return str(out)
|
||||
588
scripts/document_gen/templates/cpni_cert_letter_generator.py
Normal file
588
scripts/document_gen/templates/cpni_cert_letter_generator.py
Normal file
|
|
@ -0,0 +1,588 @@
|
|||
"""
|
||||
Generate the FCC CPNI Annual Certification Letter.
|
||||
|
||||
Produces the annual certification required by 47 CFR § 64.2009 certifying
|
||||
compliance with the Customer Proprietary Network Information (CPNI) rules
|
||||
(47 CFR §§ 64.2001-64.2011), including amendments from the 2023 Data Breach
|
||||
Notification Order (FCC 23-111).
|
||||
|
||||
The letter is largely standard across carrier types. The only variation
|
||||
is wholesale-only carriers, whose CPNI obligations are limited to wholesale
|
||||
customer proprietary data rather than retail end-user CPNI.
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.cpni_cert_letter_generator import (
|
||||
generate_cpni_cert_letter,
|
||||
)
|
||||
path = generate_cpni_cert_letter(
|
||||
entity_name="Falcon Broadband LLC",
|
||||
frn="0027160886",
|
||||
filer_id_499="812345",
|
||||
reporting_year=2025,
|
||||
complaints_count=0,
|
||||
output_path="/tmp/cpni_cert.docx",
|
||||
)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_cert")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml.ns import qn
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI cert letter generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
# Navy blue used for section headings (RGB 0x1A, 0x27, 0x44)
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
# Spacing constants (in twips; 1 pt = 20 twips)
|
||||
_AFTER_6PT = Pt(6) if Document else None
|
||||
|
||||
|
||||
def generate_cpni_cert_letter(
|
||||
# ── Entity identity ───────────────────────────────────────────
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
# ── Address ───────────────────────────────────────────────────
|
||||
address_street: str = "",
|
||||
address_city: str = "",
|
||||
address_state: str = "",
|
||||
address_zip: str = "",
|
||||
# ── Contact / officer ─────────────────────────────────────────
|
||||
officer_name: str = "",
|
||||
officer_title: str = "Chief Executive Officer",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
# ── Reporting ─────────────────────────────────────────────────
|
||||
reporting_year: int = 0,
|
||||
complaints_count: int = 0,
|
||||
complaints_description: str = "",
|
||||
# ── Carrier flags ─────────────────────────────────────────────
|
||||
is_wholesale: bool = False,
|
||||
# ── Employee training ─────────────────────────────────────────
|
||||
employee_training_conducted: bool = True,
|
||||
# ── Disciplinary actions ──────────────────────────────────────
|
||||
disciplinary_actions_taken: bool = False,
|
||||
disciplinary_actions_description: str = "",
|
||||
# ── Data broker actions ───────────────────────────────────────
|
||||
data_broker_actions: str = "",
|
||||
# ── Breaches (per FCC 23-111) ─────────────────────────────────
|
||||
breaches: list[dict] | None = None,
|
||||
# ── Marketing / CPNI usage ────────────────────────────────────
|
||||
uses_cpni_for_marketing: bool = False,
|
||||
cpni_approval_method: str = "opt_in", # "opt_in" or "opt_out"
|
||||
# ── Pretexting safeguards ─────────────────────────────────────
|
||||
pretexting_safeguards: str = "",
|
||||
# ── Output ────────────────────────────────────────────────────
|
||||
output_path: str = "/tmp/cpni_certification_letter.docx",
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Generate a CPNI Annual Certification Letter as a DOCX file.
|
||||
|
||||
Compliant with 47 CFR § 64.2009, including the 2023 Data Breach
|
||||
Notification Order (FCC 23-111).
|
||||
|
||||
Returns the output file path on success, None on failure.
|
||||
"""
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
|
||||
if breaches is None:
|
||||
breaches = []
|
||||
|
||||
doc = Document()
|
||||
|
||||
# ── Page setup ────────────────────────────────────────────────
|
||||
for section in doc.sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1.25)
|
||||
section.right_margin = Inches(1.25)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
signer = officer_name or "Authorized Officer"
|
||||
title = officer_title or "Officer"
|
||||
|
||||
cpni_scope = (
|
||||
"wholesale customer proprietary data"
|
||||
if is_wholesale
|
||||
else "customer proprietary network information (CPNI)"
|
||||
)
|
||||
|
||||
# ── Helper functions ──────────────────────────────────────────
|
||||
|
||||
def _set_spacing(paragraph, after_pt=6, before_pt=0):
|
||||
"""Set paragraph spacing in points."""
|
||||
pf = paragraph.paragraph_format
|
||||
pf.space_after = Pt(after_pt)
|
||||
if before_pt:
|
||||
pf.space_before = Pt(before_pt)
|
||||
|
||||
def _heading(text: str, level: int = 1) -> None:
|
||||
"""Add a navy blue section heading."""
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run(text)
|
||||
run.font.size = Pt(12)
|
||||
run.bold = True
|
||||
run.font.color.rgb = _NAVY
|
||||
_set_spacing(p, after_pt=4, before_pt=8)
|
||||
|
||||
def _body(text: str, bold: bool = False, size: int = 10) -> None:
|
||||
"""Add body-text paragraph with 6pt spacing after."""
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
run = p.add_run(text)
|
||||
run.font.size = Pt(size)
|
||||
run.bold = bold
|
||||
_set_spacing(p, after_pt=6)
|
||||
|
||||
def _checkbox(label: str, checked: bool = True) -> None:
|
||||
"""Add a checkbox-style line item."""
|
||||
mark = "\u2611" if checked else "\u2610"
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
run = p.add_run(f" {mark} {label}")
|
||||
run.font.size = Pt(10)
|
||||
_set_spacing(p, after_pt=3)
|
||||
|
||||
def _spacer() -> None:
|
||||
p = doc.add_paragraph()
|
||||
_set_spacing(p, after_pt=0)
|
||||
|
||||
# ── Page numbers ──────────────────────────────────────────────
|
||||
for section in doc.sections:
|
||||
footer = section.footer
|
||||
footer.is_linked_to_previous = False
|
||||
fp = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
|
||||
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
# Insert PAGE field
|
||||
run = fp.add_run()
|
||||
run.font.size = Pt(8)
|
||||
run.font.color.rgb = RGBColor(0x80, 0x80, 0x80)
|
||||
fld_char_begin = run._element.makeelement(qn("w:fldChar"), {qn("w:fldCharType"): "begin"})
|
||||
run._element.append(fld_char_begin)
|
||||
run2 = fp.add_run()
|
||||
run2.font.size = Pt(8)
|
||||
run2.font.color.rgb = RGBColor(0x80, 0x80, 0x80)
|
||||
instr = run2._element.makeelement(qn("w:instrText"), {})
|
||||
instr.text = " PAGE "
|
||||
run2._element.append(instr)
|
||||
run3 = fp.add_run()
|
||||
run3.font.size = Pt(8)
|
||||
fld_char_end = run3._element.makeelement(qn("w:fldChar"), {qn("w:fldCharType"): "end"})
|
||||
run3._element.append(fld_char_end)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# TITLE
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
title_p = doc.add_paragraph()
|
||||
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
title_run = title_p.add_run("CPNI Annual Certification Letter")
|
||||
title_run.font.size = Pt(14)
|
||||
title_run.bold = True
|
||||
title_run.font.color.rgb = _NAVY
|
||||
_set_spacing(title_p, after_pt=2)
|
||||
|
||||
subtitle_p = doc.add_paragraph()
|
||||
subtitle_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sub_run = subtitle_p.add_run(
|
||||
f"Pursuant to 47 CFR \u00a7 64.2009 \u2014 Calendar Year {reporting_year}"
|
||||
)
|
||||
sub_run.font.size = Pt(10)
|
||||
sub_run.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_set_spacing(subtitle_p, after_pt=6)
|
||||
|
||||
# Horizontal rule
|
||||
rule_p = doc.add_paragraph()
|
||||
rule_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
rule_run = rule_p.add_run("\u2500" * 72)
|
||||
rule_run.font.size = Pt(6)
|
||||
rule_run.font.color.rgb = RGBColor(0xAA, 0xAA, 0xAA)
|
||||
_set_spacing(rule_p, after_pt=8)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 1: Provider Information
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("1. Provider Information")
|
||||
|
||||
info_lines = [f"Company Name: {entity_name}"]
|
||||
if frn:
|
||||
info_lines.append(f"FCC Registration Number (FRN): {frn}")
|
||||
if filer_id_499:
|
||||
info_lines.append(f"FCC Form 499 Filer ID: {filer_id_499}")
|
||||
addr = ", ".join(filter(None, [address_street, address_city]))
|
||||
if address_state or address_zip:
|
||||
addr += f", {address_state} {address_zip}".strip()
|
||||
if addr.strip(", "):
|
||||
info_lines.append(f"Address: {addr.strip(', ')}")
|
||||
if contact_phone:
|
||||
info_lines.append(f"Telephone: {contact_phone}")
|
||||
if contact_email:
|
||||
info_lines.append(f"Email: {contact_email}")
|
||||
info_lines.append(f"Certifying Officer: {signer}, {title}")
|
||||
info_lines.append(f"Date of Filing: {today}")
|
||||
info_lines.append(
|
||||
f"Filing Deadline: March 1, {reporting_year + 1}"
|
||||
)
|
||||
|
||||
_body("\n".join(info_lines))
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 2: Certification of Compliance
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("2. Certification of Compliance")
|
||||
|
||||
_body(
|
||||
f"Pursuant to 47 CFR \u00a7 64.2009(e), {entity_name} "
|
||||
f"({'FRN: ' + frn if frn else 'FRN pending'}"
|
||||
f"{', Filer ID: ' + filer_id_499 if filer_id_499 else ''}) "
|
||||
f"hereby submits its annual certification of compliance with the "
|
||||
f"Commission's Customer Proprietary Network Information (CPNI) rules "
|
||||
f"for calendar year {reporting_year}."
|
||||
)
|
||||
|
||||
_body(
|
||||
f"I, {signer}, {title} of {entity_name}, have personal knowledge "
|
||||
f"of, have reviewed, and am familiar with {entity_name}'s CPNI "
|
||||
f"compliance procedures and certify that the company has established "
|
||||
f"operating procedures that ensure compliance with the Commission's "
|
||||
f"CPNI rules set forth in 47 CFR \u00a7\u00a7 64.2001 through 64.2011. "
|
||||
f"{entity_name} has taken appropriate actions to protect the "
|
||||
f"confidentiality of {cpni_scope} and has limited access to and use "
|
||||
f"of such information in accordance with the Commission's rules."
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 3: Reporting Period
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("3. Reporting Period")
|
||||
_body(
|
||||
f"This certification covers the period from January 1, {reporting_year} "
|
||||
f"through December 31, {reporting_year}."
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 4: CPNI Safeguards
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("4. CPNI Safeguards")
|
||||
|
||||
_body(
|
||||
f"{entity_name} has implemented the following safeguards to protect "
|
||||
f"{cpni_scope}:"
|
||||
)
|
||||
|
||||
# 4a - Customer authentication
|
||||
_body("(a) Customer Authentication and Password Procedures", bold=True)
|
||||
_checkbox(
|
||||
f"{entity_name} requires customer authentication through a password "
|
||||
f"or other secure credential before disclosing CPNI in response to "
|
||||
f"customer-initiated contacts, in accordance with 47 CFR \u00a7 64.2010.",
|
||||
checked=True,
|
||||
)
|
||||
|
||||
# 4b - Employee training
|
||||
_body("(b) Employee Training", bold=True)
|
||||
_checkbox(
|
||||
f"All employees with access to CPNI have been adequately trained on "
|
||||
f"the Commission's CPNI rules, including proper handling, disclosure "
|
||||
f"limitations, and breach notification procedures.",
|
||||
checked=employee_training_conducted,
|
||||
)
|
||||
if not employee_training_conducted:
|
||||
_body(
|
||||
f"NOTE: {entity_name} is in the process of completing employee "
|
||||
f"training and anticipates full compliance within 30 days of this "
|
||||
f"filing."
|
||||
)
|
||||
|
||||
# 4c - Supervisory review
|
||||
_body("(c) Supervisory Review", bold=True)
|
||||
_checkbox(
|
||||
f"{entity_name} conducts regular supervisory reviews of CPNI access "
|
||||
f"and usage to ensure compliance with established procedures.",
|
||||
checked=True,
|
||||
)
|
||||
|
||||
# 4d - Pretexting safeguards
|
||||
_body("(d) Pretexting Safeguards", bold=True)
|
||||
if pretexting_safeguards:
|
||||
_checkbox(pretexting_safeguards, checked=True)
|
||||
else:
|
||||
_checkbox(
|
||||
f"{entity_name} has implemented safeguards to protect against "
|
||||
f"pretexting, including customer identity verification protocols, "
|
||||
f"employee awareness training on social engineering tactics, and "
|
||||
f"procedures to detect and report suspected pretexting attempts.",
|
||||
checked=True,
|
||||
)
|
||||
|
||||
# 4e - Notification of account changes
|
||||
_body("(e) Notification of Account Changes", bold=True)
|
||||
_checkbox(
|
||||
f"{entity_name} notifies customers of account changes, including "
|
||||
f"changes to passwords, address of record, or online account "
|
||||
f"credentials, through a communication to the customer's address "
|
||||
f"of record or established backup contact method, in accordance "
|
||||
f"with 47 CFR \u00a7 64.2010.",
|
||||
checked=True,
|
||||
)
|
||||
|
||||
# 4f - Record retention
|
||||
_body("(f) Record Retention", bold=True)
|
||||
_checkbox(
|
||||
f"{entity_name} maintains records of all CPNI access, disclosures, "
|
||||
f"customer complaints, and compliance actions for a minimum period "
|
||||
f"of five (5) years, as required by 47 CFR \u00a7 64.2009(e).",
|
||||
checked=True,
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 5: CPNI Complaints
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("5. CPNI Complaints")
|
||||
|
||||
if complaints_count == 0:
|
||||
_body(
|
||||
f"During the reporting period, {entity_name} received no complaints "
|
||||
f"regarding unauthorized release or use of CPNI."
|
||||
)
|
||||
else:
|
||||
desc = complaints_description or (
|
||||
f"Each complaint was investigated and resolved in accordance with "
|
||||
f"{entity_name}'s CPNI compliance procedures."
|
||||
)
|
||||
_body(
|
||||
f"During the reporting period, {entity_name} received "
|
||||
f"{complaints_count} complaint{'s' if complaints_count != 1 else ''} "
|
||||
f"regarding CPNI. {desc}"
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 6: Data Breaches
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("6. Data Breaches")
|
||||
|
||||
if not breaches:
|
||||
_body(
|
||||
f"During the reporting period, {entity_name} experienced no data "
|
||||
f"breaches involving CPNI. No breach notifications were required "
|
||||
f"to be filed with the Commission, law enforcement, or affected "
|
||||
f"customers under 47 CFR \u00a7 64.2011."
|
||||
)
|
||||
else:
|
||||
total_breaches = len(breaches)
|
||||
total_affected = sum(b.get("customers_affected", 0) for b in breaches)
|
||||
_body(
|
||||
f"During the reporting period, {entity_name} experienced "
|
||||
f"{total_breaches} data breach{'es' if total_breaches != 1 else ''} "
|
||||
f"involving CPNI, affecting a total of {total_affected:,} "
|
||||
f"customer{'s' if total_affected != 1 else ''}. Details of each "
|
||||
f"breach are provided below."
|
||||
)
|
||||
|
||||
# Breach detail table
|
||||
table = doc.add_table(rows=1, cols=5)
|
||||
table.style = "Table Grid"
|
||||
|
||||
# Header row
|
||||
headers = [
|
||||
"Breach #", "Date", "Customers\nAffected",
|
||||
"Description", "Response Actions",
|
||||
]
|
||||
hdr_cells = table.rows[0].cells
|
||||
for i, header in enumerate(headers):
|
||||
hdr_cells[i].text = ""
|
||||
p = hdr_cells[i].paragraphs[0]
|
||||
run = p.add_run(header)
|
||||
run.bold = True
|
||||
run.font.size = Pt(9)
|
||||
run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
|
||||
# Navy background
|
||||
shading = hdr_cells[i]._element.makeelement(
|
||||
qn("w:shd"),
|
||||
{
|
||||
qn("w:val"): "clear",
|
||||
qn("w:color"): "auto",
|
||||
qn("w:fill"): "1A2744",
|
||||
},
|
||||
)
|
||||
tc_pr = hdr_cells[i]._element.get_or_add_tcPr()
|
||||
tc_pr.append(shading)
|
||||
|
||||
# Data rows
|
||||
for idx, breach in enumerate(breaches, start=1):
|
||||
row_cells = table.add_row().cells
|
||||
values = [
|
||||
str(idx),
|
||||
str(breach.get("date", "N/A")),
|
||||
f"{breach.get('customers_affected', 0):,}",
|
||||
str(breach.get("description", "")),
|
||||
str(breach.get("response_actions", "")),
|
||||
]
|
||||
for i, val in enumerate(values):
|
||||
row_cells[i].text = ""
|
||||
p = row_cells[i].paragraphs[0]
|
||||
run = p.add_run(val)
|
||||
run.font.size = Pt(9)
|
||||
|
||||
_spacer()
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 7: Disciplinary Actions
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("7. Disciplinary Actions")
|
||||
|
||||
if not disciplinary_actions_taken:
|
||||
_body(
|
||||
f"During the reporting period, {entity_name} did not take any "
|
||||
f"disciplinary action against employees for violations of the "
|
||||
f"Commission's CPNI rules."
|
||||
)
|
||||
else:
|
||||
desc = disciplinary_actions_description or (
|
||||
"Disciplinary action was taken in accordance with company policy."
|
||||
)
|
||||
_body(
|
||||
f"During the reporting period, {entity_name} took disciplinary "
|
||||
f"action against one or more employees for violations of the "
|
||||
f"Commission's CPNI rules. {desc}"
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 8: Data Broker Actions
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("8. Actions Taken Against Data Brokers")
|
||||
|
||||
if data_broker_actions:
|
||||
_body(
|
||||
f"During the reporting period, {entity_name} took the following "
|
||||
f"actions against data brokers: {data_broker_actions}"
|
||||
)
|
||||
else:
|
||||
_body(
|
||||
f"During the reporting period, {entity_name} did not identify any "
|
||||
f"data brokers engaging in unauthorized access to or sale of CPNI, "
|
||||
f"and no actions against data brokers were required."
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 9: CPNI Marketing Usage
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("9. CPNI Marketing Usage")
|
||||
|
||||
if uses_cpni_for_marketing:
|
||||
method_label = (
|
||||
"opt-in" if cpni_approval_method == "opt_in" else "opt-out"
|
||||
)
|
||||
_body(
|
||||
f"{entity_name} uses CPNI for marketing purposes. Customer "
|
||||
f"approval for such use is obtained through the {method_label} "
|
||||
f"method, in accordance with 47 CFR \u00a7 64.2007."
|
||||
)
|
||||
else:
|
||||
_body(
|
||||
f"{entity_name} does not use CPNI for marketing purposes beyond "
|
||||
f"the scope of services to which the customer already subscribes. "
|
||||
f"No customer approval mechanism is required."
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 10: Breach Notification Compliance
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("10. Breach Notification Compliance")
|
||||
|
||||
_body(
|
||||
f"{entity_name} certifies that its breach notification procedures "
|
||||
f"are compliant with 47 CFR \u00a7 64.2011, as amended by the 2023 "
|
||||
f"Data Breach Notification Order (FCC 23-111). These procedures "
|
||||
f"include:"
|
||||
)
|
||||
_checkbox(
|
||||
"Notification to the FCC and, where applicable, the FBI and U.S. "
|
||||
"Secret Service, as soon as practicable and in no event later than "
|
||||
"30 days after reasonable determination of a breach.",
|
||||
checked=True,
|
||||
)
|
||||
_checkbox(
|
||||
"Notification to affected customers as soon as practicable and in "
|
||||
"no event later than 30 days after notification to law enforcement "
|
||||
"(unless a delay is requested by law enforcement).",
|
||||
checked=True,
|
||||
)
|
||||
_checkbox(
|
||||
"Breach notifications include the required content specified in "
|
||||
"\u00a7 64.2011, including a description of the breach, the categories "
|
||||
"of information compromised, and contact information for inquiries.",
|
||||
checked=True,
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SECTION 11: Officer Certification & Signature
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
_heading("11. Officer Certification and Signature")
|
||||
|
||||
_body(
|
||||
f"I, {signer}, {title} of {entity_name}, certify under penalty of "
|
||||
f"perjury that the foregoing is true and correct. I have personal "
|
||||
f"knowledge of the facts stated herein, have reviewed {entity_name}'s "
|
||||
f"CPNI compliance procedures, and am satisfied that {entity_name} has "
|
||||
f"complied with the requirements of 47 CFR \u00a7\u00a7 64.2001 through "
|
||||
f"64.2011 during calendar year {reporting_year}."
|
||||
)
|
||||
|
||||
_spacer()
|
||||
|
||||
_body("Respectfully submitted,")
|
||||
_spacer()
|
||||
_spacer()
|
||||
|
||||
# Signature line
|
||||
sig_line = doc.add_paragraph()
|
||||
sig_run = sig_line.add_run("_" * 45)
|
||||
sig_run.font.size = Pt(10)
|
||||
_set_spacing(sig_line, after_pt=2)
|
||||
|
||||
sig_name_p = doc.add_paragraph()
|
||||
name_run = sig_name_p.add_run(signer)
|
||||
name_run.font.size = Pt(10)
|
||||
name_run.bold = True
|
||||
_set_spacing(sig_name_p, after_pt=2)
|
||||
|
||||
sig_title_p = doc.add_paragraph()
|
||||
sig_title_p.add_run(f"{title}, {entity_name}").font.size = Pt(10)
|
||||
_set_spacing(sig_title_p, after_pt=2)
|
||||
|
||||
sig_date_p = doc.add_paragraph()
|
||||
sig_date_p.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
_set_spacing(sig_date_p, after_pt=2)
|
||||
|
||||
if contact_phone:
|
||||
sig_phone_p = doc.add_paragraph()
|
||||
sig_phone_p.add_run(f"Telephone: {contact_phone}").font.size = Pt(10)
|
||||
_set_spacing(sig_phone_p, after_pt=2)
|
||||
|
||||
if contact_email:
|
||||
sig_email_p = doc.add_paragraph()
|
||||
sig_email_p.add_run(f"Email: {contact_email}").font.size = Pt(10)
|
||||
_set_spacing(sig_email_p, after_pt=2)
|
||||
|
||||
# ── Save ──────────────────────────────────────────────────────
|
||||
output = Path(output_path)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(output))
|
||||
LOG.info("CPNI certification letter generated: %s", output)
|
||||
return str(output)
|
||||
366
scripts/document_gen/templates/cpni_clec_generator.py
Normal file
366
scripts/document_gen/templates/cpni_clec_generator.py
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
"""
|
||||
Generate the FCC CPNI Annual Certification Letter — CLEC (Facilities) variant.
|
||||
|
||||
Tailors the generic CPNI certification under 47 CFR § 64.2009(e) for a
|
||||
Competitive Local Exchange Carrier operating its own TDM / SS7 switching
|
||||
plant. Customer authorization for CPNI is obtained through traditional
|
||||
written / oral opt-in methods; the CPNI Protection Officer's scope of
|
||||
oversight explicitly includes SS7 / SIGTRAN intercept provisioning and
|
||||
PIC / LIDB record handling.
|
||||
|
||||
2026 amendments included:
|
||||
* Maximum forfeiture $251,322 per violation (capped $2,513,215).
|
||||
* 47 CFR § 1.17 truthfulness representation.
|
||||
* Title 18 penalty acknowledgment.
|
||||
* Explicit "has / has not" language for customer complaints + data
|
||||
broker inquiries (Report & Order FCC-25-XXX).
|
||||
* Officer statement of personal knowledge.
|
||||
* Narrative "how procedures ensure compliance" section.
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.cpni_clec_generator import (
|
||||
generate_cpni_clec,
|
||||
)
|
||||
path = generate_cpni_clec(
|
||||
output_path="/tmp/cpni_clec.docx",
|
||||
entity_name="Acme Telco LLC",
|
||||
frn="0027160886",
|
||||
filer_id_499="812345",
|
||||
officer_name="Jane Doe",
|
||||
officer_title="CEO",
|
||||
reporting_year=2025,
|
||||
)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_clec")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml.ns import qn
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI CLEC generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "clec"
|
||||
VARIANT_LABEL = "Competitive Local Exchange Carrier (CLEC)"
|
||||
|
||||
MAX_FORFEITURE_PER_VIOLATION = "$251,322"
|
||||
MAX_FORFEITURE_CAP = "$2,513,215"
|
||||
|
||||
|
||||
def _set_spacing(paragraph, after_pt=6, before_pt=0):
|
||||
pf = paragraph.paragraph_format
|
||||
pf.space_after = Pt(after_pt)
|
||||
if before_pt:
|
||||
pf.space_before = Pt(before_pt)
|
||||
|
||||
|
||||
def _heading(doc, text: str) -> None:
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run(text)
|
||||
run.font.size = Pt(12)
|
||||
run.bold = True
|
||||
run.font.color.rgb = _NAVY
|
||||
_set_spacing(p, after_pt=4, before_pt=8)
|
||||
|
||||
|
||||
def _body(doc, text: str, bold: bool = False, size: int = 10) -> None:
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
run = p.add_run(text)
|
||||
run.font.size = Pt(size)
|
||||
run.bold = bold
|
||||
_set_spacing(p, after_pt=6)
|
||||
|
||||
|
||||
def _checkbox(doc, label: str, checked: bool = True) -> None:
|
||||
mark = "\u2611" if checked else "\u2610"
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
run = p.add_run(f" {mark} {label}")
|
||||
run.font.size = Pt(10)
|
||||
_set_spacing(p, after_pt=3)
|
||||
|
||||
|
||||
def _spacer(doc) -> None:
|
||||
p = doc.add_paragraph()
|
||||
_set_spacing(p, after_pt=0)
|
||||
|
||||
|
||||
def generate_cpni_clec(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
officer_name: str = "",
|
||||
officer_title: str = "Chief Executive Officer",
|
||||
complaints_count: int = 0,
|
||||
complaints_description: str = "",
|
||||
has_data_broker_inquiries: bool = False,
|
||||
data_broker_description: str = "",
|
||||
reporting_year: int = 0,
|
||||
address_street: str = "",
|
||||
address_city: str = "",
|
||||
address_state: str = "",
|
||||
address_zip: str = "",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
breaches: list[dict] | None = None,
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
"""Generate the CLEC (facilities) CPNI Annual Certification Letter."""
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
breaches = breaches or []
|
||||
|
||||
doc = Document()
|
||||
for section in doc.sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1.25)
|
||||
section.right_margin = Inches(1.25)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
signer = officer_name or "Authorized Officer"
|
||||
title = officer_title or "Officer"
|
||||
|
||||
# ── Title ────────────────────────────────────────────────────────
|
||||
title_p = doc.add_paragraph()
|
||||
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
tr = title_p.add_run("CPNI Annual Certification Letter")
|
||||
tr.font.size = Pt(14)
|
||||
tr.bold = True
|
||||
tr.font.color.rgb = _NAVY
|
||||
_set_spacing(title_p, after_pt=2)
|
||||
|
||||
sub_p = doc.add_paragraph()
|
||||
sub_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sub = sub_p.add_run(
|
||||
f"Competitive Local Exchange Carrier \u2014 "
|
||||
f"47 CFR \u00a7 64.2009 \u2014 Calendar Year {reporting_year}"
|
||||
)
|
||||
sub.font.size = Pt(10)
|
||||
sub.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_set_spacing(sub_p, after_pt=8)
|
||||
|
||||
# ── 1. Provider Information ──────────────────────────────────────
|
||||
_heading(doc, "1. Provider Information")
|
||||
lines = [f"Company Name: {entity_name}", f"Variant: {VARIANT_LABEL}"]
|
||||
if frn:
|
||||
lines.append(f"FCC Registration Number (FRN): {frn}")
|
||||
if filer_id_499:
|
||||
lines.append(f"FCC Form 499 Filer ID: {filer_id_499}")
|
||||
addr = ", ".join(filter(None, [address_street, address_city]))
|
||||
if address_state or address_zip:
|
||||
addr += f", {address_state} {address_zip}".strip()
|
||||
if addr.strip(", "):
|
||||
lines.append(f"Address: {addr.strip(', ')}")
|
||||
if contact_phone:
|
||||
lines.append(f"Telephone: {contact_phone}")
|
||||
if contact_email:
|
||||
lines.append(f"Email: {contact_email}")
|
||||
lines.append(f"Certifying Officer: {signer}, {title}")
|
||||
lines.append(f"Date of Filing: {today}")
|
||||
lines.append(f"Filing Deadline: March 2, {reporting_year + 1}")
|
||||
_body(doc, "\n".join(lines))
|
||||
|
||||
# ── 2. Officer Statement of Personal Knowledge ───────────────────
|
||||
_heading(doc, "2. Officer Statement of Personal Knowledge")
|
||||
_body(doc, (
|
||||
f"I, {signer}, {title} of {entity_name}, state that I have personal "
|
||||
f"knowledge of the matters certified herein. I have reviewed "
|
||||
f"{entity_name}'s CPNI operating procedures, interviewed personnel "
|
||||
f"responsible for CPNI handling, and examined supervisory logs and "
|
||||
f"records covering the reporting period. The representations set "
|
||||
f"forth in this certification are based on my personal review and "
|
||||
f"are true and correct to the best of my knowledge, information, "
|
||||
f"and belief."
|
||||
))
|
||||
|
||||
# ── 3. Certification of Compliance ───────────────────────────────
|
||||
_heading(doc, "3. Certification of Compliance")
|
||||
_body(doc, (
|
||||
f"Pursuant to 47 CFR \u00a7 64.2009(e), {entity_name} hereby submits "
|
||||
f"its annual certification of compliance with the Customer Proprietary "
|
||||
f"Network Information (CPNI) rules at 47 CFR \u00a7\u00a7 64.2001 "
|
||||
f"through 64.2011 for the period January 1, {reporting_year} through "
|
||||
f"December 31, {reporting_year}. {entity_name} has established, "
|
||||
f"maintained, and adhered to operating procedures that ensure "
|
||||
f"compliance with these rules."
|
||||
))
|
||||
|
||||
# ── 4. How Procedures Ensure Compliance (narrative) ──────────────
|
||||
_heading(doc, "4. How Our Procedures Ensure Compliance")
|
||||
_body(doc, (
|
||||
f"As a competitive local exchange carrier operating on circuit-switched "
|
||||
f"(TDM) switching platforms with SS7 / SIGTRAN signaling, {entity_name} "
|
||||
f"protects CPNI throughout the complete life-cycle of a customer "
|
||||
f"relationship. Specific procedures include:"
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"Customer authentication is required before any CPNI disclosure in "
|
||||
"response to a customer-initiated contact. Authentication is by "
|
||||
"pre-established password or, for in-store visits, a photo ID plus "
|
||||
"verification of two account attributes that are not CPNI "
|
||||
"(47 CFR \u00a7 64.2010)."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"Customer approval for use of CPNI beyond the scope of the "
|
||||
"subscribed service is obtained through traditional written or oral "
|
||||
"opt-in consent, documented in the customer record per 47 CFR "
|
||||
"\u00a7 64.2007 and \u00a7 64.2008. Oral approvals are date-stamped "
|
||||
"and time-stamped with the agent's identity."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"The CPNI Protection Officer's oversight scope expressly includes "
|
||||
"SS7 / SIGTRAN intercept provisioning, LIDB access, PIC-change "
|
||||
"verification, and wholesale handoff logs, ensuring that network-"
|
||||
"element CPNI (e.g., originating-number records, call-path signaling) "
|
||||
"is governed by the same safeguards as customer-facing systems."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"PIC and account changes trigger customer notification to the "
|
||||
"address of record before taking effect, per 47 CFR \u00a7 64.2010(f)."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"All access to CPNI-bearing systems is logged, with supervisory "
|
||||
"review at least quarterly. Retention of access logs meets or "
|
||||
"exceeds two years (CPNI) and five years (certification records) "
|
||||
"per 47 CFR \u00a7 64.2009."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"Annual CPNI training is required for all personnel with CPNI "
|
||||
"access. Completion is tracked and attested to by the CPNI Protection "
|
||||
"Officer. Disciplinary procedures are documented and applied to any "
|
||||
"violation."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"Breach notification under 47 CFR \u00a7 64.2011 is implemented as "
|
||||
"amended by FCC 23-111 \u2014 notice to the Commission within 7 "
|
||||
"business days and to customers / law enforcement as soon as "
|
||||
"practicable, not later than 30 days after reasonable determination."
|
||||
))
|
||||
|
||||
# ── 5. Customer Complaints (has / has not) ───────────────────────
|
||||
_heading(doc, "5. Customer Complaints")
|
||||
if complaints_count == 0:
|
||||
_body(doc, (
|
||||
f"{entity_name} has NOT received any customer complaints during "
|
||||
f"the reporting period concerning the unauthorized release or "
|
||||
f"use of CPNI. Zero (0) complaints were logged."
|
||||
))
|
||||
else:
|
||||
desc = complaints_description or (
|
||||
"Each complaint was investigated and resolved in accordance with "
|
||||
"the company's CPNI compliance procedures."
|
||||
)
|
||||
_body(doc, (
|
||||
f"{entity_name} HAS received {complaints_count} customer "
|
||||
f"complaint{'s' if complaints_count != 1 else ''} during the "
|
||||
f"reporting period concerning the unauthorized release or use "
|
||||
f"of CPNI. {desc}"
|
||||
))
|
||||
|
||||
# ── 6. Data Broker Inquiries (has / has not) ─────────────────────
|
||||
_heading(doc, "6. Data Broker Inquiries and Pretexting")
|
||||
if not has_data_broker_inquiries:
|
||||
_body(doc, (
|
||||
f"{entity_name} has NOT received any inquiries, communications, "
|
||||
f"or attempts by data brokers or other unauthorized parties "
|
||||
f"seeking the unauthorized release of CPNI during the reporting "
|
||||
f"period."
|
||||
))
|
||||
else:
|
||||
desc = data_broker_description or (
|
||||
"Each such inquiry was refused, documented, and escalated to "
|
||||
"the CPNI Protection Officer."
|
||||
)
|
||||
_body(doc, (
|
||||
f"{entity_name} HAS received data broker or pretexting-style "
|
||||
f"inquiries during the reporting period. {desc}"
|
||||
))
|
||||
|
||||
# ── 7. Breach Log Summary ────────────────────────────────────────
|
||||
_heading(doc, "7. Breach Log Summary")
|
||||
if not breaches:
|
||||
_body(doc, (
|
||||
f"{entity_name} experienced no CPNI breaches during the "
|
||||
f"reporting period. No 47 CFR \u00a7 64.2011 notifications were "
|
||||
f"required."
|
||||
))
|
||||
else:
|
||||
_body(doc, (
|
||||
f"{entity_name} experienced {len(breaches)} CPNI breach"
|
||||
f"{'es' if len(breaches) != 1 else ''} during the reporting "
|
||||
f"period. Each was reported to the Commission via the CPNI "
|
||||
f"Breach Reporting Portal within 7 business days."
|
||||
))
|
||||
|
||||
# ── 8. Penalties and Truthfulness ────────────────────────────────
|
||||
_heading(doc, "8. Penalties, Truthfulness, and Perjury Acknowledgment")
|
||||
_body(doc, (
|
||||
f"{entity_name} and the undersigned officer acknowledge that "
|
||||
f"violations of the CPNI rules may subject the carrier to monetary "
|
||||
f"forfeitures of up to {MAX_FORFEITURE_PER_VIOLATION} per violation "
|
||||
f"and up to {MAX_FORFEITURE_CAP} for any single act or failure to "
|
||||
f"act (adjusted for inflation per 47 CFR \u00a7 1.80)."
|
||||
))
|
||||
_body(doc, (
|
||||
f"Pursuant to 47 CFR \u00a7 1.17, the undersigned represents that "
|
||||
f"no material factual information has been withheld from the "
|
||||
f"Commission and that all statements herein are truthful, accurate, "
|
||||
f"and complete to the best of the undersigned's knowledge and "
|
||||
f"belief, and are not intended to mislead the Commission."
|
||||
))
|
||||
_body(doc, (
|
||||
f"The undersigned further acknowledges that willful false statements "
|
||||
f"made in this certification are punishable by fine and/or "
|
||||
f"imprisonment under Title 18, U.S.C. \u00a7 1001, and/or by "
|
||||
f"forfeiture under 47 U.S.C. \u00a7 503."
|
||||
))
|
||||
|
||||
# ── 9. Signature ─────────────────────────────────────────────────
|
||||
_heading(doc, "9. Signature of Certifying Officer")
|
||||
_body(doc, (
|
||||
f"I declare under penalty of perjury under the laws of the "
|
||||
f"United States of America that the foregoing is true and correct."
|
||||
))
|
||||
_spacer(doc)
|
||||
|
||||
sig = doc.add_paragraph()
|
||||
sig.add_run("_" * 45).font.size = Pt(10)
|
||||
_set_spacing(sig, after_pt=2)
|
||||
|
||||
nm = doc.add_paragraph()
|
||||
nr = nm.add_run(signer)
|
||||
nr.bold = True
|
||||
nr.font.size = Pt(10)
|
||||
_set_spacing(nm, after_pt=2)
|
||||
|
||||
tp = doc.add_paragraph()
|
||||
tp.add_run(f"{title}, {entity_name}").font.size = Pt(10)
|
||||
_set_spacing(tp, after_pt=2)
|
||||
|
||||
dp = doc.add_paragraph()
|
||||
dp.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
_set_spacing(dp, after_pt=2)
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CPNI CLEC certification letter generated: %s", out)
|
||||
return str(out)
|
||||
307
scripts/document_gen/templates/cpni_clec_reseller_generator.py
Normal file
307
scripts/document_gen/templates/cpni_clec_reseller_generator.py
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
"""
|
||||
Generate the FCC CPNI Annual Certification Letter — CLEC Reseller variant.
|
||||
|
||||
Customer Approval / Safeguards language adapted for a CLEC reseller that
|
||||
purchases local exchange capacity wholesale and resells it under its own
|
||||
brand. The reseller's CPNI obligations extend both to end-user CPNI it
|
||||
directly handles AND to CPNI that flows down from the wholesale provider.
|
||||
Safeguards therefore include contractual flow-down terms.
|
||||
|
||||
See module ``cpni_clec_generator`` for the shared 2026 statutory block.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_clec_reseller")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI CLEC-Reseller generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "clec_reseller"
|
||||
VARIANT_LABEL = "Competitive Local Exchange Carrier — Reseller"
|
||||
|
||||
MAX_FORFEITURE_PER_VIOLATION = "$251,322"
|
||||
MAX_FORFEITURE_CAP = "$2,513,215"
|
||||
|
||||
|
||||
def _set_spacing(paragraph, after_pt=6, before_pt=0):
|
||||
pf = paragraph.paragraph_format
|
||||
pf.space_after = Pt(after_pt)
|
||||
if before_pt:
|
||||
pf.space_before = Pt(before_pt)
|
||||
|
||||
|
||||
def _heading(doc, text):
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(text)
|
||||
r.font.size = Pt(12)
|
||||
r.bold = True
|
||||
r.font.color.rgb = _NAVY
|
||||
_set_spacing(p, after_pt=4, before_pt=8)
|
||||
|
||||
|
||||
def _body(doc, text, bold=False, size=10):
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(text)
|
||||
r.font.size = Pt(size)
|
||||
r.bold = bold
|
||||
_set_spacing(p, after_pt=6)
|
||||
|
||||
|
||||
def _checkbox(doc, label, checked=True):
|
||||
mark = "\u2611" if checked else "\u2610"
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(f" {mark} {label}")
|
||||
r.font.size = Pt(10)
|
||||
_set_spacing(p, after_pt=3)
|
||||
|
||||
|
||||
def _spacer(doc):
|
||||
p = doc.add_paragraph()
|
||||
_set_spacing(p, after_pt=0)
|
||||
|
||||
|
||||
def generate_cpni_clec_reseller(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
officer_name: str = "",
|
||||
officer_title: str = "Chief Executive Officer",
|
||||
complaints_count: int = 0,
|
||||
complaints_description: str = "",
|
||||
has_data_broker_inquiries: bool = False,
|
||||
data_broker_description: str = "",
|
||||
reporting_year: int = 0,
|
||||
upstream_wholesale_provider: str = "",
|
||||
address_street: str = "",
|
||||
address_city: str = "",
|
||||
address_state: str = "",
|
||||
address_zip: str = "",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
breaches: list[dict] | None = None,
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
breaches = breaches or []
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
signer = officer_name or "Authorized Officer"
|
||||
title = officer_title or "Officer"
|
||||
upstream = upstream_wholesale_provider or "its wholesale underlying carrier(s)"
|
||||
|
||||
tp = doc.add_paragraph(); tp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
t = tp.add_run("CPNI Annual Certification Letter")
|
||||
t.font.size = Pt(14); t.bold = True; t.font.color.rgb = _NAVY
|
||||
_set_spacing(tp, after_pt=2)
|
||||
sp = doc.add_paragraph(); sp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
s = sp.add_run(
|
||||
f"Competitive Local Exchange Carrier — Reseller \u2014 "
|
||||
f"47 CFR \u00a7 64.2009 \u2014 Calendar Year {reporting_year}"
|
||||
)
|
||||
s.font.size = Pt(10); s.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_set_spacing(sp, after_pt=8)
|
||||
|
||||
_heading(doc, "1. Provider Information")
|
||||
lines = [f"Company Name: {entity_name}", f"Variant: {VARIANT_LABEL}"]
|
||||
if frn:
|
||||
lines.append(f"FCC Registration Number (FRN): {frn}")
|
||||
if filer_id_499:
|
||||
lines.append(f"FCC Form 499 Filer ID: {filer_id_499}")
|
||||
addr = ", ".join(filter(None, [address_street, address_city]))
|
||||
if address_state or address_zip:
|
||||
addr += f", {address_state} {address_zip}".strip()
|
||||
if addr.strip(", "):
|
||||
lines.append(f"Address: {addr.strip(', ')}")
|
||||
if contact_phone:
|
||||
lines.append(f"Telephone: {contact_phone}")
|
||||
if contact_email:
|
||||
lines.append(f"Email: {contact_email}")
|
||||
lines.append(f"Certifying Officer: {signer}, {title}")
|
||||
lines.append(f"Date of Filing: {today}")
|
||||
lines.append(f"Filing Deadline: March 2, {reporting_year + 1}")
|
||||
_body(doc, "\n".join(lines))
|
||||
|
||||
_heading(doc, "2. Officer Statement of Personal Knowledge")
|
||||
_body(doc, (
|
||||
f"I, {signer}, {title} of {entity_name}, state that I have personal "
|
||||
f"knowledge of the matters certified herein. I have reviewed "
|
||||
f"{entity_name}'s CPNI operating procedures (including reseller "
|
||||
f"flow-down terms with upstream wholesale providers), and I have "
|
||||
f"examined supervisory logs and records covering the reporting "
|
||||
f"period. The representations herein are based on my personal "
|
||||
f"review and are true and correct to the best of my knowledge, "
|
||||
f"information, and belief."
|
||||
))
|
||||
|
||||
_heading(doc, "3. Certification of Compliance")
|
||||
_body(doc, (
|
||||
f"Pursuant to 47 CFR \u00a7 64.2009(e), {entity_name} hereby submits "
|
||||
f"its annual certification of compliance with the CPNI rules at "
|
||||
f"47 CFR \u00a7\u00a7 64.2001 through 64.2011 for the period "
|
||||
f"January 1, {reporting_year} through December 31, {reporting_year}. "
|
||||
f"{entity_name} has established, maintained, and adhered to "
|
||||
f"operating procedures that ensure compliance with these rules."
|
||||
))
|
||||
|
||||
_heading(doc, "4. How Our Procedures Ensure Compliance")
|
||||
_body(doc, (
|
||||
f"As a CLEC reseller purchasing wholesale local-exchange capacity "
|
||||
f"from {upstream} and reselling it under its own brand, {entity_name} "
|
||||
f"protects CPNI at two boundaries: (1) the retail end-user interface "
|
||||
f"where it directly handles customer records, and (2) the wholesale "
|
||||
f"flow from the underlying carrier(s). Specific procedures include:"
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"End-user customer authentication is required before any CPNI "
|
||||
"disclosure; authentication uses pre-established password or "
|
||||
"verification of non-CPNI account attributes (47 CFR \u00a7 64.2010)."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"CPNI use beyond the scope of the subscribed service is permitted "
|
||||
"only after written or oral opt-in consent under 47 CFR \u00a7 64.2007, "
|
||||
"documented in the customer record."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
f"Reseller flow-down: {entity_name}'s wholesale-service agreement "
|
||||
f"with {upstream} expressly requires the upstream carrier to treat "
|
||||
f"all end-user CPNI received through {entity_name} in a manner "
|
||||
f"consistent with 47 CFR \u00a7\u00a7 64.2001\u201364.2011. "
|
||||
f"{entity_name} reviews upstream CPNI attestations annually."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"The CPNI Protection Officer has oversight authority over both "
|
||||
"retail records systems and wholesale interconnect logs, and "
|
||||
"reviews upstream carrier breach notices per 47 CFR \u00a7 64.2011."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"Changes to an end-user account (PIC, address-of-record, password) "
|
||||
"are confirmed to the customer's address of record before taking "
|
||||
"effect."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"Access logs are maintained and reviewed at least quarterly; "
|
||||
"retention meets or exceeds two years for CPNI access and five "
|
||||
"years for certification records under 47 CFR \u00a7 64.2009."
|
||||
))
|
||||
_checkbox(doc, (
|
||||
"Annual CPNI training is conducted for all personnel with CPNI "
|
||||
"access. Disciplinary procedures are documented and applied to "
|
||||
"any violation."
|
||||
))
|
||||
|
||||
_heading(doc, "5. Customer Complaints")
|
||||
if complaints_count == 0:
|
||||
_body(doc, (
|
||||
f"{entity_name} has NOT received any customer complaints during "
|
||||
f"the reporting period concerning the unauthorized release or "
|
||||
f"use of CPNI. Zero (0) complaints were logged."
|
||||
))
|
||||
else:
|
||||
desc = complaints_description or (
|
||||
"Each complaint was investigated and resolved in accordance with "
|
||||
"the company's CPNI compliance procedures."
|
||||
)
|
||||
_body(doc, (
|
||||
f"{entity_name} HAS received {complaints_count} customer "
|
||||
f"complaint{'s' if complaints_count != 1 else ''} during the "
|
||||
f"reporting period. {desc}"
|
||||
))
|
||||
|
||||
_heading(doc, "6. Data Broker Inquiries and Pretexting")
|
||||
if not has_data_broker_inquiries:
|
||||
_body(doc, (
|
||||
f"{entity_name} has NOT received any inquiries, communications, "
|
||||
f"or attempts by data brokers or other unauthorized parties "
|
||||
f"seeking the unauthorized release of CPNI."
|
||||
))
|
||||
else:
|
||||
desc = data_broker_description or (
|
||||
"Each such inquiry was refused, documented, and escalated."
|
||||
)
|
||||
_body(doc, (
|
||||
f"{entity_name} HAS received data broker or pretexting-style "
|
||||
f"inquiries during the reporting period. {desc}"
|
||||
))
|
||||
|
||||
_heading(doc, "7. Breach Log Summary")
|
||||
if not breaches:
|
||||
_body(doc, (
|
||||
f"{entity_name} experienced no CPNI breaches during the "
|
||||
f"reporting period. No 47 CFR \u00a7 64.2011 notifications were "
|
||||
f"required."
|
||||
))
|
||||
else:
|
||||
_body(doc, (
|
||||
f"{entity_name} experienced {len(breaches)} CPNI breach"
|
||||
f"{'es' if len(breaches) != 1 else ''} during the reporting "
|
||||
f"period; each was reported to the Commission via the CPNI "
|
||||
f"Breach Reporting Portal within 7 business days."
|
||||
))
|
||||
|
||||
_heading(doc, "8. Penalties, Truthfulness, and Perjury Acknowledgment")
|
||||
_body(doc, (
|
||||
f"{entity_name} and the undersigned officer acknowledge that CPNI "
|
||||
f"rule violations may subject the carrier to monetary forfeitures "
|
||||
f"of up to {MAX_FORFEITURE_PER_VIOLATION} per violation and up to "
|
||||
f"{MAX_FORFEITURE_CAP} for any single act or failure to act "
|
||||
f"(adjusted per 47 CFR \u00a7 1.80)."
|
||||
))
|
||||
_body(doc, (
|
||||
"Pursuant to 47 CFR \u00a7 1.17, the undersigned represents that "
|
||||
"no material factual information has been withheld and that all "
|
||||
"statements herein are truthful, accurate, and complete."
|
||||
))
|
||||
_body(doc, (
|
||||
"The undersigned acknowledges that willful false statements in this "
|
||||
"certification are punishable under Title 18, U.S.C. \u00a7 1001, "
|
||||
"and by forfeiture under 47 U.S.C. \u00a7 503."
|
||||
))
|
||||
|
||||
_heading(doc, "9. Signature of Certifying Officer")
|
||||
_body(doc, (
|
||||
"I declare under penalty of perjury under the laws of the United "
|
||||
"States of America that the foregoing is true and correct."
|
||||
))
|
||||
_spacer(doc)
|
||||
sig = doc.add_paragraph()
|
||||
sig.add_run("_" * 45).font.size = Pt(10)
|
||||
_set_spacing(sig, after_pt=2)
|
||||
nm = doc.add_paragraph()
|
||||
nr = nm.add_run(signer); nr.bold = True; nr.font.size = Pt(10)
|
||||
_set_spacing(nm, after_pt=2)
|
||||
tpp = doc.add_paragraph()
|
||||
tpp.add_run(f"{title}, {entity_name}").font.size = Pt(10)
|
||||
_set_spacing(tpp, after_pt=2)
|
||||
dp = doc.add_paragraph()
|
||||
dp.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
_set_spacing(dp, after_pt=2)
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CPNI CLEC-Reseller certification letter generated: %s", out)
|
||||
return str(out)
|
||||
289
scripts/document_gen/templates/cpni_ixc_generator.py
Normal file
289
scripts/document_gen/templates/cpni_ixc_generator.py
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
"""
|
||||
Generate the FCC CPNI Annual Certification Letter — IXC (Facilities) variant.
|
||||
|
||||
Tailors the generic CPNI certification under 47 CFR § 64.2009(e) for an
|
||||
Interexchange Carrier (IXC) focused on toll-call record handling. Key
|
||||
variant differences:
|
||||
|
||||
* CPNI scope centered on toll call records, PIC-change verification,
|
||||
and interexchange account authentication.
|
||||
* Customer approval for CPNI usage follows written/oral opt-in,
|
||||
documented in the toll-account record.
|
||||
* The CPNI Protection Officer's duties include PIC / LIDB / CDR
|
||||
access governance and fraud-management system controls.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_ixc")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI IXC generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "ixc"
|
||||
VARIANT_LABEL = "Interexchange Carrier (IXC)"
|
||||
|
||||
MAX_FORFEITURE_PER_VIOLATION = "$251,322"
|
||||
MAX_FORFEITURE_CAP = "$2,513,215"
|
||||
|
||||
|
||||
def _sp(p, after=6, before=0):
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
if before:
|
||||
p.paragraph_format.space_before = Pt(before)
|
||||
|
||||
|
||||
def _h(doc, text):
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(text)
|
||||
r.font.size = Pt(12); r.bold = True; r.font.color.rgb = _NAVY
|
||||
_sp(p, after=4, before=8)
|
||||
|
||||
|
||||
def _b(doc, text, bold=False, size=10):
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(text); r.font.size = Pt(size); r.bold = bold
|
||||
_sp(p, after=6)
|
||||
|
||||
|
||||
def _cb(doc, text, checked=True):
|
||||
mark = "\u2611" if checked else "\u2610"
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(f" {mark} {text}")
|
||||
r.font.size = Pt(10)
|
||||
_sp(p, after=3)
|
||||
|
||||
|
||||
def generate_cpni_ixc(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
officer_name: str = "",
|
||||
officer_title: str = "Chief Executive Officer",
|
||||
complaints_count: int = 0,
|
||||
complaints_description: str = "",
|
||||
has_data_broker_inquiries: bool = False,
|
||||
data_broker_description: str = "",
|
||||
reporting_year: int = 0,
|
||||
address_street: str = "",
|
||||
address_city: str = "",
|
||||
address_state: str = "",
|
||||
address_zip: str = "",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
breaches: list[dict] | None = None,
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
breaches = breaches or []
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
signer = officer_name or "Authorized Officer"
|
||||
title = officer_title or "Officer"
|
||||
|
||||
tp = doc.add_paragraph(); tp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
t = tp.add_run("CPNI Annual Certification Letter")
|
||||
t.font.size = Pt(14); t.bold = True; t.font.color.rgb = _NAVY
|
||||
_sp(tp, after=2)
|
||||
sp = doc.add_paragraph(); sp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
s = sp.add_run(
|
||||
f"Interexchange Carrier \u2014 47 CFR \u00a7 64.2009 "
|
||||
f"\u2014 Calendar Year {reporting_year}"
|
||||
)
|
||||
s.font.size = Pt(10); s.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_sp(sp, after=8)
|
||||
|
||||
_h(doc, "1. Provider Information")
|
||||
lines = [f"Company Name: {entity_name}", f"Variant: {VARIANT_LABEL}"]
|
||||
if frn: lines.append(f"FCC Registration Number (FRN): {frn}")
|
||||
if filer_id_499: lines.append(f"FCC Form 499 Filer ID: {filer_id_499}")
|
||||
addr = ", ".join(filter(None, [address_street, address_city]))
|
||||
if address_state or address_zip:
|
||||
addr += f", {address_state} {address_zip}".strip()
|
||||
if addr.strip(", "):
|
||||
lines.append(f"Address: {addr.strip(', ')}")
|
||||
if contact_phone: lines.append(f"Telephone: {contact_phone}")
|
||||
if contact_email: lines.append(f"Email: {contact_email}")
|
||||
lines.append(f"Certifying Officer: {signer}, {title}")
|
||||
lines.append(f"Date of Filing: {today}")
|
||||
lines.append(f"Filing Deadline: March 2, {reporting_year + 1}")
|
||||
_b(doc, "\n".join(lines))
|
||||
|
||||
_h(doc, "2. Officer Statement of Personal Knowledge")
|
||||
_b(doc, (
|
||||
f"I, {signer}, {title} of {entity_name}, state that I have personal "
|
||||
f"knowledge of the matters certified herein. I have reviewed "
|
||||
f"{entity_name}'s CPNI operating procedures, including procedures "
|
||||
f"governing toll call records, PIC-change verification, and "
|
||||
f"interexchange account authentication. I have examined supervisory "
|
||||
f"logs and records covering the reporting period. The "
|
||||
f"representations herein are based on my personal review and are "
|
||||
f"true and correct to the best of my knowledge, information, and "
|
||||
f"belief."
|
||||
))
|
||||
|
||||
_h(doc, "3. Certification of Compliance")
|
||||
_b(doc, (
|
||||
f"Pursuant to 47 CFR \u00a7 64.2009(e), {entity_name} hereby submits "
|
||||
f"its annual certification of compliance with the CPNI rules at "
|
||||
f"47 CFR \u00a7\u00a7 64.2001 through 64.2011 for the period "
|
||||
f"January 1, {reporting_year} through December 31, {reporting_year}."
|
||||
))
|
||||
|
||||
_h(doc, "4. How Our Procedures Ensure Compliance")
|
||||
_b(doc, (
|
||||
f"As an interexchange carrier, {entity_name}'s principal CPNI "
|
||||
f"holdings are toll call detail records (CDRs), PIC-change records, "
|
||||
f"and inter-carrier settlement data. Specific procedures include:"
|
||||
))
|
||||
_cb(doc, (
|
||||
"Interexchange account authentication is required before release "
|
||||
"of any toll record. Authentication is via pre-established "
|
||||
"password or the verification of two non-CPNI account attributes "
|
||||
"(47 CFR \u00a7 64.2010)."
|
||||
))
|
||||
_cb(doc, (
|
||||
"PIC changes require affirmative verification under 47 CFR "
|
||||
"\u00a7 64.1120 (Third Party Verification, Letter of Agency, or "
|
||||
"Internet LOA) and are confirmed to the customer's address of "
|
||||
"record before being implemented. Slamming-prevention controls "
|
||||
"are integrated with CPNI access logging."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Customer approval for use of toll CPNI beyond the scope of the "
|
||||
"subscribed service is obtained through written or oral opt-in "
|
||||
"consent, documented in the account record per 47 CFR \u00a7 64.2007."
|
||||
))
|
||||
_cb(doc, (
|
||||
"The CPNI Protection Officer has oversight authority over PIC "
|
||||
"administration, LIDB access, CDR archives, and fraud-management "
|
||||
"systems. Access attempts to these systems are logged to the named "
|
||||
"individual."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Supervisory reviews of CPNI access are conducted at least "
|
||||
"quarterly. Retention of access logs meets or exceeds two years "
|
||||
"(CPNI) and five years (certification records) per 47 CFR "
|
||||
"\u00a7 64.2009."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Annual CPNI training is required for all personnel with CPNI "
|
||||
"access. Completion is tracked and attested to by the CPNI "
|
||||
"Protection Officer."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Breach notification under 47 CFR \u00a7 64.2011 is implemented as "
|
||||
"amended by FCC 23-111 \u2014 notice to the Commission within 7 "
|
||||
"business days and to customers / law enforcement as soon as "
|
||||
"practicable (not later than 30 days after reasonable determination)."
|
||||
))
|
||||
|
||||
_h(doc, "5. Customer Complaints")
|
||||
if complaints_count == 0:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any customer complaints during "
|
||||
f"the reporting period concerning the unauthorized release or "
|
||||
f"use of CPNI. Zero (0) complaints were logged."
|
||||
))
|
||||
else:
|
||||
desc = complaints_description or (
|
||||
"Each complaint was investigated and resolved."
|
||||
)
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received {complaints_count} customer "
|
||||
f"complaint{'s' if complaints_count != 1 else ''} during the "
|
||||
f"reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "6. Data Broker Inquiries and Pretexting")
|
||||
if not has_data_broker_inquiries:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any inquiries, communications, "
|
||||
f"or attempts by data brokers or other unauthorized parties "
|
||||
f"seeking the unauthorized release of CPNI."
|
||||
))
|
||||
else:
|
||||
desc = data_broker_description or (
|
||||
"Each such inquiry was refused, documented, and escalated."
|
||||
)
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received data broker or pretexting-style "
|
||||
f"inquiries during the reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "7. Breach Log Summary")
|
||||
if not breaches:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced no CPNI breaches during the "
|
||||
f"reporting period. No 47 CFR \u00a7 64.2011 notifications were "
|
||||
f"required."
|
||||
))
|
||||
else:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced {len(breaches)} CPNI breach"
|
||||
f"{'es' if len(breaches) != 1 else ''} during the reporting "
|
||||
f"period; each was reported within 7 business days."
|
||||
))
|
||||
|
||||
_h(doc, "8. Penalties, Truthfulness, and Perjury Acknowledgment")
|
||||
_b(doc, (
|
||||
f"{entity_name} and the undersigned acknowledge that CPNI rule "
|
||||
f"violations may subject the carrier to forfeitures up to "
|
||||
f"{MAX_FORFEITURE_PER_VIOLATION} per violation and up to "
|
||||
f"{MAX_FORFEITURE_CAP} for any single act or failure to act "
|
||||
f"(adjusted per 47 CFR \u00a7 1.80)."
|
||||
))
|
||||
_b(doc, (
|
||||
"Pursuant to 47 CFR \u00a7 1.17, the undersigned represents that no "
|
||||
"material factual information has been withheld and all statements "
|
||||
"are truthful, accurate, and complete."
|
||||
))
|
||||
_b(doc, (
|
||||
"The undersigned acknowledges that willful false statements are "
|
||||
"punishable under Title 18, U.S.C. \u00a7 1001, and by forfeiture "
|
||||
"under 47 U.S.C. \u00a7 503."
|
||||
))
|
||||
|
||||
_h(doc, "9. Signature of Certifying Officer")
|
||||
_b(doc, (
|
||||
"I declare under penalty of perjury under the laws of the United "
|
||||
"States of America that the foregoing is true and correct."
|
||||
))
|
||||
p = doc.add_paragraph(); _sp(p, after=0)
|
||||
sig = doc.add_paragraph(); sig.add_run("_" * 45).font.size = Pt(10)
|
||||
_sp(sig, after=2)
|
||||
nm = doc.add_paragraph(); nr = nm.add_run(signer); nr.bold = True
|
||||
nr.font.size = Pt(10); _sp(nm, after=2)
|
||||
tpp = doc.add_paragraph()
|
||||
tpp.add_run(f"{title}, {entity_name}").font.size = Pt(10)
|
||||
_sp(tpp, after=2)
|
||||
dp = doc.add_paragraph(); dp.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
_sp(dp, after=2)
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CPNI IXC certification letter generated: %s", out)
|
||||
return str(out)
|
||||
270
scripts/document_gen/templates/cpni_ixc_reseller_generator.py
Normal file
270
scripts/document_gen/templates/cpni_ixc_reseller_generator.py
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
"""
|
||||
Generate the FCC CPNI Annual Certification Letter — IXC Reseller variant.
|
||||
|
||||
An IXC reseller buys wholesale toll minutes from an underlying carrier
|
||||
and resells them under its own brand. CPNI obligations therefore extend
|
||||
to both the retail end-user records the reseller maintains directly AND
|
||||
to the toll-CDR flow from the wholesale carrier. Safeguards include
|
||||
contractual flow-down terms with the upstream.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_ixc_reseller")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI IXC-Reseller generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "ixc_reseller"
|
||||
VARIANT_LABEL = "Interexchange Carrier — Reseller"
|
||||
|
||||
MAX_FORFEITURE_PER_VIOLATION = "$251,322"
|
||||
MAX_FORFEITURE_CAP = "$2,513,215"
|
||||
|
||||
|
||||
def _sp(p, after=6, before=0):
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
if before:
|
||||
p.paragraph_format.space_before = Pt(before)
|
||||
|
||||
|
||||
def _h(doc, text):
|
||||
p = doc.add_paragraph(); r = p.add_run(text)
|
||||
r.font.size = Pt(12); r.bold = True; r.font.color.rgb = _NAVY
|
||||
_sp(p, after=4, before=8)
|
||||
|
||||
|
||||
def _b(doc, text, bold=False, size=10):
|
||||
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(text); r.font.size = Pt(size); r.bold = bold
|
||||
_sp(p, after=6)
|
||||
|
||||
|
||||
def _cb(doc, text, checked=True):
|
||||
mark = "\u2611" if checked else "\u2610"
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(f" {mark} {text}"); r.font.size = Pt(10)
|
||||
_sp(p, after=3)
|
||||
|
||||
|
||||
def generate_cpni_ixc_reseller(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
officer_name: str = "",
|
||||
officer_title: str = "Chief Executive Officer",
|
||||
complaints_count: int = 0,
|
||||
complaints_description: str = "",
|
||||
has_data_broker_inquiries: bool = False,
|
||||
data_broker_description: str = "",
|
||||
reporting_year: int = 0,
|
||||
upstream_wholesale_provider: str = "",
|
||||
address_street: str = "",
|
||||
address_city: str = "",
|
||||
address_state: str = "",
|
||||
address_zip: str = "",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
breaches: list[dict] | None = None,
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
breaches = breaches or []
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
signer = officer_name or "Authorized Officer"
|
||||
title = officer_title or "Officer"
|
||||
upstream = upstream_wholesale_provider or "its wholesale underlying carrier(s)"
|
||||
|
||||
tp = doc.add_paragraph(); tp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
t = tp.add_run("CPNI Annual Certification Letter")
|
||||
t.font.size = Pt(14); t.bold = True; t.font.color.rgb = _NAVY
|
||||
_sp(tp, after=2)
|
||||
sp = doc.add_paragraph(); sp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
s = sp.add_run(
|
||||
f"Interexchange Carrier — Reseller \u2014 47 CFR \u00a7 64.2009 "
|
||||
f"\u2014 Calendar Year {reporting_year}"
|
||||
)
|
||||
s.font.size = Pt(10); s.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_sp(sp, after=8)
|
||||
|
||||
_h(doc, "1. Provider Information")
|
||||
lines = [f"Company Name: {entity_name}", f"Variant: {VARIANT_LABEL}"]
|
||||
if frn: lines.append(f"FCC Registration Number (FRN): {frn}")
|
||||
if filer_id_499: lines.append(f"FCC Form 499 Filer ID: {filer_id_499}")
|
||||
addr = ", ".join(filter(None, [address_street, address_city]))
|
||||
if address_state or address_zip:
|
||||
addr += f", {address_state} {address_zip}".strip()
|
||||
if addr.strip(", "):
|
||||
lines.append(f"Address: {addr.strip(', ')}")
|
||||
if contact_phone: lines.append(f"Telephone: {contact_phone}")
|
||||
if contact_email: lines.append(f"Email: {contact_email}")
|
||||
lines.append(f"Certifying Officer: {signer}, {title}")
|
||||
lines.append(f"Date of Filing: {today}")
|
||||
lines.append(f"Filing Deadline: March 2, {reporting_year + 1}")
|
||||
_b(doc, "\n".join(lines))
|
||||
|
||||
_h(doc, "2. Officer Statement of Personal Knowledge")
|
||||
_b(doc, (
|
||||
f"I, {signer}, {title} of {entity_name}, state that I have personal "
|
||||
f"knowledge of the matters certified herein. I have reviewed the "
|
||||
f"CPNI operating procedures of {entity_name}, including wholesale "
|
||||
f"CPNI flow-down terms with upstream toll providers, and examined "
|
||||
f"supervisory logs and records covering the reporting period."
|
||||
))
|
||||
|
||||
_h(doc, "3. Certification of Compliance")
|
||||
_b(doc, (
|
||||
f"Pursuant to 47 CFR \u00a7 64.2009(e), {entity_name} hereby submits "
|
||||
f"its annual certification of compliance with the CPNI rules at "
|
||||
f"47 CFR \u00a7\u00a7 64.2001 through 64.2011 for the period "
|
||||
f"January 1, {reporting_year} through December 31, {reporting_year}."
|
||||
))
|
||||
|
||||
_h(doc, "4. How Our Procedures Ensure Compliance")
|
||||
_b(doc, (
|
||||
f"As an IXC reseller purchasing wholesale toll minutes from "
|
||||
f"{upstream} and reselling them under its own brand, {entity_name} "
|
||||
f"protects CPNI at two boundaries: (1) the retail end-user toll-"
|
||||
f"account interface, and (2) the CDR and billing-record flow from "
|
||||
f"the underlying toll carrier(s)."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Interexchange account authentication is required before release "
|
||||
"of any toll record to a customer-initiated inquiry "
|
||||
"(47 CFR \u00a7 64.2010)."
|
||||
))
|
||||
_cb(doc, (
|
||||
"PIC-change verifications (TPV, LOA, or Internet LOA) are performed "
|
||||
"per 47 CFR \u00a7 64.1120 and confirmed to the customer's address "
|
||||
"of record prior to implementation."
|
||||
))
|
||||
_cb(doc, (
|
||||
f"Reseller flow-down: {entity_name}'s wholesale agreement with "
|
||||
f"{upstream} expressly requires the upstream toll carrier to "
|
||||
f"protect end-user CPNI received through {entity_name} consistent "
|
||||
f"with 47 CFR \u00a7\u00a7 64.2001\u201364.2011. {entity_name} "
|
||||
f"reviews upstream CPNI attestations annually."
|
||||
))
|
||||
_cb(doc, (
|
||||
"The CPNI Protection Officer has oversight authority over both "
|
||||
"retail toll-account records and wholesale CDR handoffs, and "
|
||||
"reviews upstream carrier breach notices per 47 CFR \u00a7 64.2011."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Customer approval for CPNI usage beyond the scope of the "
|
||||
"subscribed toll service is obtained through written or oral "
|
||||
"opt-in consent, documented per 47 CFR \u00a7 64.2007."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Supervisory review of CPNI access occurs at least quarterly; "
|
||||
"retention meets or exceeds two years (CPNI logs) and five years "
|
||||
"(certification records) per 47 CFR \u00a7 64.2009."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Annual CPNI training is mandatory for all personnel with CPNI "
|
||||
"access; breach notification procedures comply with 47 CFR "
|
||||
"\u00a7 64.2011 as amended by FCC 23-111."
|
||||
))
|
||||
|
||||
_h(doc, "5. Customer Complaints")
|
||||
if complaints_count == 0:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any customer complaints during "
|
||||
f"the reporting period concerning the unauthorized release or "
|
||||
f"use of CPNI. Zero (0) complaints were logged."
|
||||
))
|
||||
else:
|
||||
desc = complaints_description or "Each complaint was investigated and resolved."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received {complaints_count} customer "
|
||||
f"complaint{'s' if complaints_count != 1 else ''} during the "
|
||||
f"reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "6. Data Broker Inquiries and Pretexting")
|
||||
if not has_data_broker_inquiries:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any inquiries, communications, "
|
||||
f"or attempts by data brokers or other unauthorized parties "
|
||||
f"seeking the unauthorized release of CPNI."
|
||||
))
|
||||
else:
|
||||
desc = data_broker_description or "Each was refused, documented, and escalated."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received data broker or pretexting-style "
|
||||
f"inquiries during the reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "7. Breach Log Summary")
|
||||
if not breaches:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced no CPNI breaches during the "
|
||||
f"reporting period. No 47 CFR \u00a7 64.2011 notifications "
|
||||
f"were required."
|
||||
))
|
||||
else:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced {len(breaches)} CPNI breach"
|
||||
f"{'es' if len(breaches) != 1 else ''} during the reporting "
|
||||
f"period; each was reported within 7 business days."
|
||||
))
|
||||
|
||||
_h(doc, "8. Penalties, Truthfulness, and Perjury Acknowledgment")
|
||||
_b(doc, (
|
||||
f"{entity_name} and the undersigned officer acknowledge that CPNI "
|
||||
f"rule violations may subject the carrier to forfeitures up to "
|
||||
f"{MAX_FORFEITURE_PER_VIOLATION} per violation and up to "
|
||||
f"{MAX_FORFEITURE_CAP} for any single act or failure to act."
|
||||
))
|
||||
_b(doc, (
|
||||
"Pursuant to 47 CFR \u00a7 1.17, the undersigned represents that "
|
||||
"no material factual information has been withheld and that all "
|
||||
"statements are truthful, accurate, and complete."
|
||||
))
|
||||
_b(doc, (
|
||||
"The undersigned acknowledges that willful false statements are "
|
||||
"punishable under Title 18, U.S.C. \u00a7 1001, and by forfeiture "
|
||||
"under 47 U.S.C. \u00a7 503."
|
||||
))
|
||||
|
||||
_h(doc, "9. Signature of Certifying Officer")
|
||||
_b(doc, (
|
||||
"I declare under penalty of perjury under the laws of the United "
|
||||
"States of America that the foregoing is true and correct."
|
||||
))
|
||||
p = doc.add_paragraph(); _sp(p, after=0)
|
||||
sig = doc.add_paragraph(); sig.add_run("_" * 45).font.size = Pt(10); _sp(sig, after=2)
|
||||
nm = doc.add_paragraph(); nr = nm.add_run(signer); nr.bold = True
|
||||
nr.font.size = Pt(10); _sp(nm, after=2)
|
||||
tpp = doc.add_paragraph(); tpp.add_run(f"{title}, {entity_name}").font.size = Pt(10)
|
||||
_sp(tpp, after=2)
|
||||
dp = doc.add_paragraph(); dp.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
_sp(dp, after=2)
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CPNI IXC-Reseller certification letter generated: %s", out)
|
||||
return str(out)
|
||||
225
scripts/document_gen/templates/cpni_private_line_generator.py
Normal file
225
scripts/document_gen/templates/cpni_private_line_generator.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
"""
|
||||
Generate the FCC CPNI Annual Certification Letter — Private Line / BDS variant.
|
||||
|
||||
A private-line (point-to-point) or Business Data Service (BDS) offering
|
||||
typically holds negligible CPNI: there is no switched calling, no PIC,
|
||||
no per-call detail record, and no directory assistance. The carrier's
|
||||
records are limited to circuit identifiers, service-address endpoints,
|
||||
and enterprise billing data. These records generally fall outside the
|
||||
statutory definition of CPNI at 47 USC § 222(h)(1), which is tied to
|
||||
"telecommunications service" used by a customer.
|
||||
|
||||
This is a short, one-page certification that recites the carrier's
|
||||
status, acknowledges the limited applicability of the CPNI rules to
|
||||
its offerings, and commits to the same statutory safeguards (47 CFR
|
||||
§ 1.17 truthfulness, Title 18 perjury acknowledgment, and forfeiture
|
||||
awareness).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_private_line")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI Private Line generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "private_line"
|
||||
VARIANT_LABEL = "Private Line / Business Data Service (BDS)"
|
||||
|
||||
MAX_FORFEITURE_PER_VIOLATION = "$251,322"
|
||||
MAX_FORFEITURE_CAP = "$2,513,215"
|
||||
|
||||
|
||||
def _sp(p, after=6, before=0):
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
if before:
|
||||
p.paragraph_format.space_before = Pt(before)
|
||||
|
||||
|
||||
def _h(doc, text):
|
||||
p = doc.add_paragraph(); r = p.add_run(text)
|
||||
r.font.size = Pt(12); r.bold = True; r.font.color.rgb = _NAVY
|
||||
_sp(p, after=4, before=8)
|
||||
|
||||
|
||||
def _b(doc, text, bold=False, size=10):
|
||||
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(text); r.font.size = Pt(size); r.bold = bold
|
||||
_sp(p, after=6)
|
||||
|
||||
|
||||
def generate_cpni_private_line(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
officer_name: str = "",
|
||||
officer_title: str = "Chief Executive Officer",
|
||||
complaints_count: int = 0,
|
||||
complaints_description: str = "",
|
||||
has_data_broker_inquiries: bool = False,
|
||||
data_broker_description: str = "",
|
||||
reporting_year: int = 0,
|
||||
address_street: str = "",
|
||||
address_city: str = "",
|
||||
address_state: str = "",
|
||||
address_zip: str = "",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
breaches: list[dict] | None = None,
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
breaches = breaches or []
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(0.9); s.bottom_margin = Inches(0.9)
|
||||
s.left_margin = Inches(1); s.right_margin = Inches(1)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
signer = officer_name or "Authorized Officer"
|
||||
title = officer_title or "Officer"
|
||||
|
||||
tp = doc.add_paragraph(); tp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
t = tp.add_run("CPNI Annual Certification Letter")
|
||||
t.font.size = Pt(14); t.bold = True; t.font.color.rgb = _NAVY
|
||||
_sp(tp, after=2)
|
||||
sp = doc.add_paragraph(); sp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
s = sp.add_run(
|
||||
f"Private Line / Business Data Service \u2014 "
|
||||
f"47 CFR \u00a7 64.2009 \u2014 Calendar Year {reporting_year}"
|
||||
)
|
||||
s.font.size = Pt(10); s.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_sp(sp, after=6)
|
||||
|
||||
_h(doc, "1. Provider Information and Scope")
|
||||
lines = [f"Company Name: {entity_name}", f"Variant: {VARIANT_LABEL}"]
|
||||
if frn: lines.append(f"FCC Registration Number (FRN): {frn}")
|
||||
if filer_id_499: lines.append(f"FCC Form 499 Filer ID: {filer_id_499}")
|
||||
addr = ", ".join(filter(None, [address_street, address_city]))
|
||||
if address_state or address_zip:
|
||||
addr += f", {address_state} {address_zip}".strip()
|
||||
if addr.strip(", "):
|
||||
lines.append(f"Address: {addr.strip(', ')}")
|
||||
if contact_phone: lines.append(f"Telephone: {contact_phone}")
|
||||
if contact_email: lines.append(f"Email: {contact_email}")
|
||||
lines.append(f"Certifying Officer: {signer}, {title}")
|
||||
lines.append(f"Date of Filing: {today}")
|
||||
lines.append(f"Filing Deadline: March 2, {reporting_year + 1}")
|
||||
_b(doc, "\n".join(lines))
|
||||
|
||||
_h(doc, "2. Officer Statement of Personal Knowledge")
|
||||
_b(doc, (
|
||||
f"I, {signer}, {title} of {entity_name}, state that I have personal "
|
||||
f"knowledge of the matters certified herein and have reviewed "
|
||||
f"{entity_name}'s records-handling procedures for private-line / "
|
||||
f"Business Data Service (BDS) circuits covering the reporting "
|
||||
f"period."
|
||||
))
|
||||
|
||||
_h(doc, "3. Limited Applicability of CPNI Rules")
|
||||
_b(doc, (
|
||||
f"{entity_name}'s offerings consist principally of dedicated "
|
||||
f"point-to-point private-line and/or Business Data Service "
|
||||
f"circuits. These offerings generate no switched-call detail "
|
||||
f"records, no presubscribed interexchange carrier (PIC) "
|
||||
f"information, and no directory-assistance records. The records "
|
||||
f"{entity_name} maintains \u2014 circuit identifiers, A-end and "
|
||||
f"Z-end service addresses, and enterprise billing data \u2014 "
|
||||
f"generally fall outside the statutory definition of Customer "
|
||||
f"Proprietary Network Information at 47 USC \u00a7 222(h)(1), "
|
||||
f"which is tied to the customer's use of a telecommunications "
|
||||
f"service."
|
||||
))
|
||||
_b(doc, (
|
||||
f"To the extent any subset of these records constitutes CPNI "
|
||||
f"under the Commission's rules, {entity_name} certifies compliance "
|
||||
f"with 47 CFR \u00a7\u00a7 64.2001 through 64.2011 for the period "
|
||||
f"January 1, {reporting_year} through December 31, {reporting_year}. "
|
||||
f"Specifically, access to customer circuit records is restricted "
|
||||
f"to authenticated personnel; authentication is required before "
|
||||
f"disclosure in response to customer inquiries; annual training "
|
||||
f"is provided; and breach-notification procedures comply with "
|
||||
f"47 CFR \u00a7 64.2011 as amended by FCC 23-111."
|
||||
))
|
||||
|
||||
_h(doc, "4. Customer Complaints and Data Broker Inquiries")
|
||||
if complaints_count == 0 and not has_data_broker_inquiries:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any customer complaints "
|
||||
f"concerning the unauthorized release or use of CPNI during "
|
||||
f"the reporting period, and has NOT received any inquiries or "
|
||||
f"communications from data brokers or other unauthorized "
|
||||
f"parties seeking CPNI."
|
||||
))
|
||||
else:
|
||||
if complaints_count == 0:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any customer complaints "
|
||||
f"during the reporting period."
|
||||
))
|
||||
else:
|
||||
desc = complaints_description or "Each was investigated and resolved."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received {complaints_count} customer "
|
||||
f"complaint{'s' if complaints_count != 1 else ''}. {desc}"
|
||||
))
|
||||
if not has_data_broker_inquiries:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any data broker or "
|
||||
f"pretexting inquiries during the reporting period."
|
||||
))
|
||||
else:
|
||||
desc = data_broker_description or "Each was refused and documented."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received data broker / pretexting "
|
||||
f"inquiries. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "5. Penalties, Truthfulness, and Perjury Acknowledgment")
|
||||
_b(doc, (
|
||||
f"{entity_name} acknowledges that CPNI rule violations may subject "
|
||||
f"the carrier to forfeitures up to {MAX_FORFEITURE_PER_VIOLATION} "
|
||||
f"per violation and up to {MAX_FORFEITURE_CAP} for any single act "
|
||||
f"or failure to act. Pursuant to 47 CFR \u00a7 1.17, the "
|
||||
f"undersigned represents that no material factual information has "
|
||||
f"been withheld and all statements are truthful, accurate, and "
|
||||
f"complete. Willful false statements are punishable under Title "
|
||||
f"18, U.S.C. \u00a7 1001, and by forfeiture under 47 U.S.C. "
|
||||
f"\u00a7 503."
|
||||
))
|
||||
|
||||
_h(doc, "6. Signature of Certifying Officer")
|
||||
_b(doc, (
|
||||
"I declare under penalty of perjury under the laws of the United "
|
||||
"States of America that the foregoing is true and correct."
|
||||
))
|
||||
sig = doc.add_paragraph(); sig.add_run("_" * 45).font.size = Pt(10); _sp(sig, after=2)
|
||||
nm = doc.add_paragraph(); nr = nm.add_run(signer); nr.bold = True
|
||||
nr.font.size = Pt(10); _sp(nm, after=2)
|
||||
tpp = doc.add_paragraph(); tpp.add_run(f"{title}, {entity_name}").font.size = Pt(10)
|
||||
_sp(tpp, after=2)
|
||||
dp = doc.add_paragraph(); dp.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
_sp(dp, after=2)
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CPNI Private Line certification letter generated: %s", out)
|
||||
return str(out)
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
"""
|
||||
Generate the CPNI Procedure Statement — internal / customer-facing policy.
|
||||
|
||||
This is **not** the annual ECFS certification letter (see
|
||||
``cpni_cert_letter_generator.py`` for that). This is the 10-section policy
|
||||
document every carrier must maintain and provide to customers under
|
||||
47 CFR § 64.2008 (annual notice). Every example carrier in
|
||||
``docs/examplefilings/`` has one of these alongside their CPNI cert —
|
||||
Cloud One PBX, Fortel, VoIPFlo, Engage, Syntracom, Zingo, TIP Systems —
|
||||
all using this exact 10-section outline.
|
||||
|
||||
Canonical section outline:
|
||||
|
||||
1. Purpose
|
||||
2. Definition of CPNI
|
||||
3. Employee Training and Compliance
|
||||
4. Customer Authentication and Access Control
|
||||
5. Use of CPNI
|
||||
6. Customer Rights and Notification
|
||||
7. CPNI Breach Notification and Reporting
|
||||
8. Record Keeping and Audits
|
||||
9. Enforcement and Penalties
|
||||
10. Contact Information
|
||||
|
||||
Footer: Effective Date / Signatory / Reviewed By / Next Review Date.
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.cpni_procedure_statement_generator import (
|
||||
generate_cpni_procedure_statement,
|
||||
)
|
||||
path = generate_cpni_procedure_statement(
|
||||
entity_name="Falcon Broadband LLC",
|
||||
entity_abbr="FBL",
|
||||
support_email="support@falconbroadband.com",
|
||||
website="https://falconbroadband.com",
|
||||
signatory_name="Jane Doe",
|
||||
signatory_title="President",
|
||||
output_path="/tmp/cpni_policy.docx",
|
||||
)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_policy")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI policy generation unavailable")
|
||||
Document = None # type: ignore[assignment, misc]
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
BODY_SIZE = Pt(11) if Document else None
|
||||
HEADING_SIZE = Pt(13) if Document else None
|
||||
PARA_AFTER = Pt(6) if Document else None
|
||||
|
||||
|
||||
def _heading(doc, text: str) -> None:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
run = p.add_run(text)
|
||||
run.bold = True
|
||||
run.font.size = HEADING_SIZE
|
||||
run.font.color.rgb = NAVY
|
||||
|
||||
|
||||
def _body(doc, text: str, bold: bool = False) -> None:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = PARA_AFTER
|
||||
run = p.add_run(text)
|
||||
run.font.size = BODY_SIZE
|
||||
run.bold = bold
|
||||
|
||||
|
||||
def _bullets(doc, items: list[str]) -> None:
|
||||
for item in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.clear()
|
||||
run = p.add_run(item)
|
||||
run.font.size = BODY_SIZE
|
||||
|
||||
|
||||
def generate_cpni_procedure_statement(
|
||||
# Identity
|
||||
entity_name: str,
|
||||
entity_abbr: str = "",
|
||||
# Customer-facing contacts
|
||||
support_email: str = "",
|
||||
website: str = "",
|
||||
# Signatory (typically an officer)
|
||||
signatory_name: str = "",
|
||||
signatory_title: str = "",
|
||||
# Dates
|
||||
effective_date: str = "",
|
||||
next_review_date: str = "",
|
||||
# Reviewer (defaults to Performance West Inc.)
|
||||
reviewer_name: str = "Justin Hannah",
|
||||
reviewer_company: str = "Performance West Inc.",
|
||||
# Small wording knobs
|
||||
is_wholesale: bool = False,
|
||||
# Output
|
||||
output_path: str = "/tmp/cpni_procedure_statement.docx",
|
||||
) -> Optional[str]:
|
||||
"""Generate the 10-section CPNI Procedure Statement as a DOCX file."""
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
abbr = entity_abbr or entity_name
|
||||
|
||||
today = date.today()
|
||||
effective = effective_date or today.strftime("%m/%d/%Y")
|
||||
next_review = next_review_date or today.replace(year=today.year + 1).strftime("%m/%d/%Y")
|
||||
|
||||
doc = Document()
|
||||
for section in doc.sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1.25)
|
||||
section.right_margin = Inches(1.25)
|
||||
|
||||
# Title
|
||||
title_p = doc.add_paragraph()
|
||||
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
title_run = title_p.add_run(
|
||||
"Customer Proprietary Network Information (CPNI) Procedure Statement"
|
||||
)
|
||||
title_run.font.size = Pt(14)
|
||||
title_run.bold = True
|
||||
title_run.font.color.rgb = NAVY
|
||||
title_p.paragraph_format.space_after = Pt(2)
|
||||
|
||||
subtitle = doc.add_paragraph()
|
||||
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sub_run = subtitle.add_run(entity_name)
|
||||
sub_run.font.size = Pt(12)
|
||||
sub_run.bold = True
|
||||
subtitle.paragraph_format.space_after = Pt(18)
|
||||
|
||||
scope = (
|
||||
"wholesale customer proprietary data"
|
||||
if is_wholesale
|
||||
else "Customer Proprietary Network Information (CPNI)"
|
||||
)
|
||||
|
||||
# ── 1. Purpose ──────────────────────────────────────────────────
|
||||
_heading(doc, "1. Purpose")
|
||||
_body(doc, (
|
||||
f"{entity_name} is committed to protecting the confidentiality and "
|
||||
f"security of {scope} as required by the Federal Communications "
|
||||
f"Commission (FCC) under Section 222 of the Communications Act. "
|
||||
f"This document outlines the procedures {entity_name} follows to "
|
||||
f"ensure compliance with CPNI regulations set forth in 47 CFR "
|
||||
f"\u00a7\u00a7 64.2001 through 64.2011."
|
||||
))
|
||||
|
||||
# ── 2. Definition of CPNI ──────────────────────────────────────
|
||||
_heading(doc, "2. Definition of CPNI")
|
||||
_body(doc, (
|
||||
"CPNI includes information related to the quantity, technical "
|
||||
"configuration, type, destination, location, and amount of use of "
|
||||
"telecommunications services by customers. It does not include "
|
||||
"subscriber list information such as name, address, and telephone "
|
||||
"number."
|
||||
))
|
||||
|
||||
# ── 3. Employee Training and Compliance ────────────────────────
|
||||
_heading(doc, "3. Employee Training and Compliance")
|
||||
_bullets(doc, [
|
||||
f"{entity_name} trains all employees in the handling, protection, and authorized use of CPNI.",
|
||||
"Employees are prohibited from accessing or disclosing CPNI unless required for legitimate business purposes.",
|
||||
"Any violation of CPNI policies may result in disciplinary action, including termination.",
|
||||
])
|
||||
|
||||
# ── 4. Customer Authentication and Access Control ──────────────
|
||||
_heading(doc, "4. Customer Authentication and Access Control")
|
||||
_bullets(doc, [
|
||||
f"{entity_name} authenticates customers before disclosing CPNI via telephone, online, or in-store interactions.",
|
||||
"Telephone access to CPNI requires authentication through a pre-established password or by sending information to the customer's registered address.",
|
||||
"Online account access requires a secure login process with multi-factor authentication where applicable.",
|
||||
"In-person requests require valid government-issued identification.",
|
||||
])
|
||||
|
||||
# ── 5. Use of CPNI ─────────────────────────────────────────────
|
||||
_heading(doc, "5. Use of CPNI")
|
||||
_bullets(doc, [
|
||||
f"{entity_name} does not use CPNI for marketing purposes unless the customer provides explicit opt-in consent.",
|
||||
"CPNI may be used for billing, fraud prevention, and service-related notifications.",
|
||||
"CPNI is not shared with third parties unless required by law or with customer authorization.",
|
||||
])
|
||||
|
||||
# ── 6. Customer Rights and Notification ────────────────────────
|
||||
_heading(doc, "6. Customer Rights and Notification")
|
||||
_bullets(doc, [
|
||||
"Customers have the right to restrict the use of their CPNI for marketing purposes.",
|
||||
f"{entity_name} provides annual CPNI notices informing customers of their rights and how to manage their CPNI preferences.",
|
||||
"Customers may change their CPNI settings by contacting customer service.",
|
||||
])
|
||||
|
||||
# ── 7. CPNI Breach Notification and Reporting ──────────────────
|
||||
_heading(doc, "7. CPNI Breach Notification and Reporting")
|
||||
_bullets(doc, [
|
||||
f"In the case of a CPNI breach, {entity_name} follows FCC guidelines for reporting incidents per 47 CFR \u00a7 64.2011.",
|
||||
"Notification is made to the FCC, FBI, and U.S. Secret Service (the Federal Agencies) via the central reporting facility as soon as practicable, and no later than 30 days after reasonable determination of a breach.",
|
||||
"Customers are notified of unauthorized access as soon as practicable and in no event later than 30 days after notification to law enforcement (unless a delay is requested by law enforcement).",
|
||||
"The company maintains records of CPNI breaches and reports them to law enforcement as required.",
|
||||
])
|
||||
|
||||
# ── 8. Record Keeping and Audits ───────────────────────────────
|
||||
_heading(doc, "8. Record Keeping and Audits")
|
||||
_bullets(doc, [
|
||||
f"{entity_name} maintains records of customer CPNI approvals, marketing usage, and access logs for at least two years.",
|
||||
"The company conducts annual audits to ensure compliance with CPNI policies and regulatory requirements.",
|
||||
])
|
||||
|
||||
# ── 9. Enforcement and Penalties ───────────────────────────────
|
||||
_heading(doc, "9. Enforcement and Penalties")
|
||||
_bullets(doc, [
|
||||
"Any employee found violating CPNI policies will be subject to disciplinary actions, including possible termination.",
|
||||
f"{entity_name} complies with all regulatory enforcement actions and may be subject to fines for non-compliance. Per FCC Enforcement Advisory DA-26-139, failure to comply with the CPNI rules may subject the company to monetary forfeitures of up to $251,322 per violation (up to a maximum of $2,513,215 for continuing violations).",
|
||||
])
|
||||
|
||||
# ── 10. Contact Information ────────────────────────────────────
|
||||
_heading(doc, "10. Contact Information")
|
||||
_body(doc, (
|
||||
f"For questions or concerns regarding CPNI policies, customers may "
|
||||
f"contact {entity_name} Support:"
|
||||
))
|
||||
if support_email:
|
||||
_body(doc, f"Email: {support_email}")
|
||||
if website:
|
||||
_body(doc, f"Website: {website}")
|
||||
|
||||
# ── Footer: dates + signatory ──────────────────────────────────
|
||||
doc.add_paragraph("")
|
||||
_body(doc, f"Effective Date: {effective}")
|
||||
if signatory_name:
|
||||
title_suffix = f", {signatory_title}" if signatory_title else ""
|
||||
_body(doc, f"Signatory: {signatory_name}{title_suffix}, {entity_name}")
|
||||
if reviewer_name:
|
||||
_body(doc, f"Reviewed By: {reviewer_name}, {reviewer_company}")
|
||||
_body(doc, f"Next Review Date: {next_review}")
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CPNI Procedure Statement generated: %s", out)
|
||||
return str(out)
|
||||
272
scripts/document_gen/templates/cpni_satellite_generator.py
Normal file
272
scripts/document_gen/templates/cpni_satellite_generator.py
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
"""
|
||||
Generate the FCC CPNI Annual Certification Letter — Satellite variant.
|
||||
|
||||
Tailors the CPNI certification under 47 CFR § 64.2009(e) for a provider
|
||||
of Mobile Satellite Service (MSS) or Fixed Satellite Service (FSS)
|
||||
operating or leasing earth-station capacity to deliver telecommunications
|
||||
service. Variant specifics:
|
||||
|
||||
* Scope covers earth-station / NOC operator records, beam assignment
|
||||
logs, and per-terminal activation records.
|
||||
* Customer approval follows written / oral opt-in; many FSS
|
||||
deployments authorize CPNI usage through the master service
|
||||
agreement with the enterprise customer.
|
||||
* Physical-security controls at earth stations (per Part 25 license
|
||||
conditions) are incorporated into the CPNI safeguard narrative.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_satellite")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI Satellite generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "satellite"
|
||||
VARIANT_LABEL = "Satellite (MSS / FSS)"
|
||||
|
||||
MAX_FORFEITURE_PER_VIOLATION = "$251,322"
|
||||
MAX_FORFEITURE_CAP = "$2,513,215"
|
||||
|
||||
|
||||
def _sp(p, after=6, before=0):
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
if before:
|
||||
p.paragraph_format.space_before = Pt(before)
|
||||
|
||||
|
||||
def _h(doc, text):
|
||||
p = doc.add_paragraph(); r = p.add_run(text)
|
||||
r.font.size = Pt(12); r.bold = True; r.font.color.rgb = _NAVY
|
||||
_sp(p, after=4, before=8)
|
||||
|
||||
|
||||
def _b(doc, text, bold=False, size=10):
|
||||
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(text); r.font.size = Pt(size); r.bold = bold
|
||||
_sp(p, after=6)
|
||||
|
||||
|
||||
def _cb(doc, text, checked=True):
|
||||
mark = "\u2611" if checked else "\u2610"
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(f" {mark} {text}"); r.font.size = Pt(10)
|
||||
_sp(p, after=3)
|
||||
|
||||
|
||||
def generate_cpni_satellite(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
officer_name: str = "",
|
||||
officer_title: str = "Chief Executive Officer",
|
||||
complaints_count: int = 0,
|
||||
complaints_description: str = "",
|
||||
has_data_broker_inquiries: bool = False,
|
||||
data_broker_description: str = "",
|
||||
reporting_year: int = 0,
|
||||
address_street: str = "",
|
||||
address_city: str = "",
|
||||
address_state: str = "",
|
||||
address_zip: str = "",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
breaches: list[dict] | None = None,
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
breaches = breaches or []
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
signer = officer_name or "Authorized Officer"
|
||||
title = officer_title or "Officer"
|
||||
|
||||
tp = doc.add_paragraph(); tp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
t = tp.add_run("CPNI Annual Certification Letter")
|
||||
t.font.size = Pt(14); t.bold = True; t.font.color.rgb = _NAVY
|
||||
_sp(tp, after=2)
|
||||
sp = doc.add_paragraph(); sp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
s = sp.add_run(
|
||||
f"Satellite (MSS / FSS) \u2014 47 CFR \u00a7 64.2009 "
|
||||
f"\u2014 Calendar Year {reporting_year}"
|
||||
)
|
||||
s.font.size = Pt(10); s.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_sp(sp, after=8)
|
||||
|
||||
_h(doc, "1. Provider Information")
|
||||
lines = [f"Company Name: {entity_name}", f"Variant: {VARIANT_LABEL}"]
|
||||
if frn: lines.append(f"FCC Registration Number (FRN): {frn}")
|
||||
if filer_id_499: lines.append(f"FCC Form 499 Filer ID: {filer_id_499}")
|
||||
addr = ", ".join(filter(None, [address_street, address_city]))
|
||||
if address_state or address_zip:
|
||||
addr += f", {address_state} {address_zip}".strip()
|
||||
if addr.strip(", "):
|
||||
lines.append(f"Address: {addr.strip(', ')}")
|
||||
if contact_phone: lines.append(f"Telephone: {contact_phone}")
|
||||
if contact_email: lines.append(f"Email: {contact_email}")
|
||||
lines.append(f"Certifying Officer: {signer}, {title}")
|
||||
lines.append(f"Date of Filing: {today}")
|
||||
lines.append(f"Filing Deadline: March 2, {reporting_year + 1}")
|
||||
_b(doc, "\n".join(lines))
|
||||
|
||||
_h(doc, "2. Officer Statement of Personal Knowledge")
|
||||
_b(doc, (
|
||||
f"I, {signer}, {title} of {entity_name}, state that I have personal "
|
||||
f"knowledge of the matters certified herein, including procedures "
|
||||
f"at the network operations center (NOC) governing subscriber "
|
||||
f"terminal records, beam / transponder assignments, and enterprise "
|
||||
f"master-service-agreement data. I have reviewed supervisory logs "
|
||||
f"covering the reporting period."
|
||||
))
|
||||
|
||||
_h(doc, "3. Certification of Compliance")
|
||||
_b(doc, (
|
||||
f"Pursuant to 47 CFR \u00a7 64.2009(e), {entity_name} hereby submits "
|
||||
f"its annual certification of compliance with the CPNI rules at "
|
||||
f"47 CFR \u00a7\u00a7 64.2001 through 64.2011 for the period "
|
||||
f"January 1, {reporting_year} through December 31, {reporting_year}."
|
||||
))
|
||||
|
||||
_h(doc, "4. How Our Procedures Ensure Compliance")
|
||||
_b(doc, (
|
||||
f"As a provider of MSS and/or FSS telecommunications service, "
|
||||
f"{entity_name}'s CPNI holdings consist primarily of: subscriber "
|
||||
f"and enterprise-customer account records, per-terminal activation "
|
||||
f"/ deactivation logs, beam and transponder assignment records, "
|
||||
f"and NOC-generated usage reports. Specific procedures include:"
|
||||
))
|
||||
_cb(doc, (
|
||||
"Customer authentication is required before CPNI release. "
|
||||
"Consumer MSS subscribers authenticate via password; enterprise "
|
||||
"FSS customers authenticate through credentials assigned to named "
|
||||
"points of contact under the master service agreement "
|
||||
"(47 CFR \u00a7 64.2010)."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Customer approval for CPNI usage beyond the scope of the "
|
||||
"subscribed service is obtained through written opt-in consent, "
|
||||
"documented in the customer record per 47 CFR \u00a7 64.2007. "
|
||||
"Enterprise MSAs include the required opt-in as a standard clause."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Earth-station / NOC operator access to CPNI-bearing systems is "
|
||||
"restricted to cleared personnel. Physical access is controlled "
|
||||
"by badge and, where applicable, by the security requirements of "
|
||||
"the Part 25 earth-station license."
|
||||
))
|
||||
_cb(doc, (
|
||||
"The CPNI Protection Officer's oversight scope includes NOC "
|
||||
"operator activity, terminal provisioning workflows, and "
|
||||
"beam-assignment systems."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Supervisory review of CPNI access occurs at least quarterly. "
|
||||
"Retention of access logs meets or exceeds two years (CPNI) and "
|
||||
"five years (certification records) per 47 CFR \u00a7 64.2009."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Annual CPNI training is required for all personnel with CPNI "
|
||||
"access; breach notification procedures comply with 47 CFR "
|
||||
"\u00a7 64.2011 as amended by FCC 23-111."
|
||||
))
|
||||
|
||||
_h(doc, "5. Customer Complaints")
|
||||
if complaints_count == 0:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any customer complaints during "
|
||||
f"the reporting period concerning the unauthorized release or "
|
||||
f"use of CPNI. Zero (0) complaints were logged."
|
||||
))
|
||||
else:
|
||||
desc = complaints_description or "Each complaint was investigated and resolved."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received {complaints_count} customer "
|
||||
f"complaint{'s' if complaints_count != 1 else ''} during the "
|
||||
f"reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "6. Data Broker Inquiries and Pretexting")
|
||||
if not has_data_broker_inquiries:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any inquiries, communications, "
|
||||
f"or attempts by data brokers or other unauthorized parties "
|
||||
f"seeking the unauthorized release of CPNI."
|
||||
))
|
||||
else:
|
||||
desc = data_broker_description or "Each was refused, documented, and escalated."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received data broker or pretexting-style "
|
||||
f"inquiries during the reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "7. Breach Log Summary")
|
||||
if not breaches:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced no CPNI breaches during the "
|
||||
f"reporting period. No 47 CFR \u00a7 64.2011 notifications "
|
||||
f"were required."
|
||||
))
|
||||
else:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced {len(breaches)} CPNI breach"
|
||||
f"{'es' if len(breaches) != 1 else ''} during the reporting "
|
||||
f"period; each was reported within 7 business days."
|
||||
))
|
||||
|
||||
_h(doc, "8. Penalties, Truthfulness, and Perjury Acknowledgment")
|
||||
_b(doc, (
|
||||
f"{entity_name} and the undersigned acknowledge that CPNI rule "
|
||||
f"violations may subject the carrier to forfeitures up to "
|
||||
f"{MAX_FORFEITURE_PER_VIOLATION} per violation and up to "
|
||||
f"{MAX_FORFEITURE_CAP} for any single act or failure to act."
|
||||
))
|
||||
_b(doc, (
|
||||
"Pursuant to 47 CFR \u00a7 1.17, the undersigned represents that no "
|
||||
"material factual information has been withheld and all statements "
|
||||
"are truthful, accurate, and complete."
|
||||
))
|
||||
_b(doc, (
|
||||
"Willful false statements are punishable under Title 18, U.S.C. "
|
||||
"\u00a7 1001, and by forfeiture under 47 U.S.C. \u00a7 503."
|
||||
))
|
||||
|
||||
_h(doc, "9. Signature of Certifying Officer")
|
||||
_b(doc, (
|
||||
"I declare under penalty of perjury under the laws of the United "
|
||||
"States of America that the foregoing is true and correct."
|
||||
))
|
||||
p = doc.add_paragraph(); _sp(p, after=0)
|
||||
sig = doc.add_paragraph(); sig.add_run("_" * 45).font.size = Pt(10); _sp(sig, after=2)
|
||||
nm = doc.add_paragraph(); nr = nm.add_run(signer); nr.bold = True
|
||||
nr.font.size = Pt(10); _sp(nm, after=2)
|
||||
tpp = doc.add_paragraph(); tpp.add_run(f"{title}, {entity_name}").font.size = Pt(10)
|
||||
_sp(tpp, after=2)
|
||||
dp = doc.add_paragraph(); dp.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
_sp(dp, after=2)
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CPNI Satellite certification letter generated: %s", out)
|
||||
return str(out)
|
||||
282
scripts/document_gen/templates/cpni_wireless_generator.py
Normal file
282
scripts/document_gen/templates/cpni_wireless_generator.py
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
"""
|
||||
Generate the FCC CPNI Annual Certification Letter — Wireless (CMRS) variant.
|
||||
|
||||
Tailors the CPNI certification for a Commercial Mobile Radio Service
|
||||
provider that operates its own radio / core network. Key variant
|
||||
differences:
|
||||
|
||||
* Customer approval for CPNI use is frequently obtained through
|
||||
handset-based (one-tap) mechanisms, in addition to traditional
|
||||
opt-in.
|
||||
* CPNI scope includes roaming records, eSIM provisioning / transfer
|
||||
records, and device-level location data.
|
||||
* Mobile location information is treated as CPNI and subject to
|
||||
heightened consent safeguards consistent with the 2020 LocationSmart
|
||||
Consent Decree (DA 20-299) and the 2024 Notice of Apparent Liability
|
||||
against the Tier-1 carriers (FCC 24-40) addressing unauthorized
|
||||
location-data sharing.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_wireless")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI Wireless generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "wireless"
|
||||
VARIANT_LABEL = "Wireless (CMRS) — Facilities"
|
||||
|
||||
MAX_FORFEITURE_PER_VIOLATION = "$251,322"
|
||||
MAX_FORFEITURE_CAP = "$2,513,215"
|
||||
|
||||
|
||||
def _sp(p, after=6, before=0):
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
if before:
|
||||
p.paragraph_format.space_before = Pt(before)
|
||||
|
||||
|
||||
def _h(doc, text):
|
||||
p = doc.add_paragraph(); r = p.add_run(text)
|
||||
r.font.size = Pt(12); r.bold = True; r.font.color.rgb = _NAVY
|
||||
_sp(p, after=4, before=8)
|
||||
|
||||
|
||||
def _b(doc, text, bold=False, size=10):
|
||||
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(text); r.font.size = Pt(size); r.bold = bold
|
||||
_sp(p, after=6)
|
||||
|
||||
|
||||
def _cb(doc, text, checked=True):
|
||||
mark = "\u2611" if checked else "\u2610"
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(f" {mark} {text}"); r.font.size = Pt(10)
|
||||
_sp(p, after=3)
|
||||
|
||||
|
||||
def generate_cpni_wireless(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
officer_name: str = "",
|
||||
officer_title: str = "Chief Executive Officer",
|
||||
complaints_count: int = 0,
|
||||
complaints_description: str = "",
|
||||
has_data_broker_inquiries: bool = False,
|
||||
data_broker_description: str = "",
|
||||
reporting_year: int = 0,
|
||||
address_street: str = "",
|
||||
address_city: str = "",
|
||||
address_state: str = "",
|
||||
address_zip: str = "",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
breaches: list[dict] | None = None,
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
breaches = breaches or []
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
signer = officer_name or "Authorized Officer"
|
||||
title = officer_title or "Officer"
|
||||
|
||||
tp = doc.add_paragraph(); tp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
t = tp.add_run("CPNI Annual Certification Letter")
|
||||
t.font.size = Pt(14); t.bold = True; t.font.color.rgb = _NAVY
|
||||
_sp(tp, after=2)
|
||||
sp = doc.add_paragraph(); sp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
s = sp.add_run(
|
||||
f"Wireless (CMRS) Facilities \u2014 47 CFR \u00a7 64.2009 "
|
||||
f"\u2014 Calendar Year {reporting_year}"
|
||||
)
|
||||
s.font.size = Pt(10); s.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_sp(sp, after=8)
|
||||
|
||||
_h(doc, "1. Provider Information")
|
||||
lines = [f"Company Name: {entity_name}", f"Variant: {VARIANT_LABEL}"]
|
||||
if frn: lines.append(f"FCC Registration Number (FRN): {frn}")
|
||||
if filer_id_499: lines.append(f"FCC Form 499 Filer ID: {filer_id_499}")
|
||||
addr = ", ".join(filter(None, [address_street, address_city]))
|
||||
if address_state or address_zip:
|
||||
addr += f", {address_state} {address_zip}".strip()
|
||||
if addr.strip(", "):
|
||||
lines.append(f"Address: {addr.strip(', ')}")
|
||||
if contact_phone: lines.append(f"Telephone: {contact_phone}")
|
||||
if contact_email: lines.append(f"Email: {contact_email}")
|
||||
lines.append(f"Certifying Officer: {signer}, {title}")
|
||||
lines.append(f"Date of Filing: {today}")
|
||||
lines.append(f"Filing Deadline: March 2, {reporting_year + 1}")
|
||||
_b(doc, "\n".join(lines))
|
||||
|
||||
_h(doc, "2. Officer Statement of Personal Knowledge")
|
||||
_b(doc, (
|
||||
f"I, {signer}, {title} of {entity_name}, state that I have personal "
|
||||
f"knowledge of the matters certified herein, including procedures "
|
||||
f"governing device-level location data, roaming records, SIM / eSIM "
|
||||
f"provisioning, and handset-based customer consent flows. I have "
|
||||
f"reviewed operating procedures and supervisory logs covering the "
|
||||
f"reporting period."
|
||||
))
|
||||
|
||||
_h(doc, "3. Certification of Compliance")
|
||||
_b(doc, (
|
||||
f"Pursuant to 47 CFR \u00a7 64.2009(e), {entity_name} hereby submits "
|
||||
f"its annual certification of compliance with the CPNI rules at "
|
||||
f"47 CFR \u00a7\u00a7 64.2001 through 64.2011 for the period "
|
||||
f"January 1, {reporting_year} through December 31, {reporting_year}."
|
||||
))
|
||||
|
||||
_h(doc, "4. How Our Procedures Ensure Compliance")
|
||||
_b(doc, (
|
||||
f"As a Commercial Mobile Radio Service (CMRS) provider, "
|
||||
f"{entity_name}'s CPNI holdings include call detail records, "
|
||||
f"roaming records, SIM/eSIM provisioning and transfer logs, and "
|
||||
f"device-level location data. Specific procedures include:"
|
||||
))
|
||||
_cb(doc, (
|
||||
"Customer authentication for CPNI disclosures in response to a "
|
||||
"customer-initiated contact uses a pre-established password, or "
|
||||
"in-app / on-device verification (biometric or PIN) tied to the "
|
||||
"authenticated subscriber identity (47 CFR \u00a7 64.2010)."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Customer approval for CPNI use beyond the scope of the subscribed "
|
||||
"service may be obtained through traditional opt-in (written / oral) "
|
||||
"OR through a secure one-tap in-app consent flow that meets the "
|
||||
"FCC's 'knowing consent' standard under 47 CFR \u00a7 64.2007. "
|
||||
"Consents are timestamped and retained."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Location data consent. Consistent with the 2020 LocationSmart "
|
||||
"Consent Decree (DA 20-299) and the 2024 NAL addressing unauthorized "
|
||||
"third-party location disclosure, {entity_name} treats device "
|
||||
"location data as CPNI and requires separate, express consent for "
|
||||
"disclosure to any third party. A chain-of-consent audit is "
|
||||
"performed for each location-data aggregator relationship."
|
||||
).replace("{entity_name}", entity_name))
|
||||
_cb(doc, (
|
||||
"SIM / eSIM transfer (port-out / device-swap) requires multi-factor "
|
||||
"authentication plus customer notification to the address of record "
|
||||
"prior to completion \u2014 implementing the anti-SIM-swap rules "
|
||||
"codified at 47 CFR \u00a7 64.2010(f)\u2013(g)."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Roaming records are protected with the same safeguards as home-"
|
||||
"network CDRs; access is logged to the named individual and "
|
||||
"reviewed quarterly by the CPNI Protection Officer."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Access logs are retained for at least two years; certification "
|
||||
"records for five years; access is reviewed at least quarterly."
|
||||
))
|
||||
_cb(doc, (
|
||||
"Annual CPNI training is mandatory for all personnel with CPNI "
|
||||
"access; breach notification procedures comply with 47 CFR "
|
||||
"\u00a7 64.2011 as amended by FCC 23-111."
|
||||
))
|
||||
|
||||
_h(doc, "5. Customer Complaints")
|
||||
if complaints_count == 0:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any customer complaints during "
|
||||
f"the reporting period concerning the unauthorized release or "
|
||||
f"use of CPNI (including unauthorized location-data "
|
||||
f"disclosures). Zero (0) complaints were logged."
|
||||
))
|
||||
else:
|
||||
desc = complaints_description or "Each complaint was investigated and resolved."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received {complaints_count} customer "
|
||||
f"complaint{'s' if complaints_count != 1 else ''} during the "
|
||||
f"reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "6. Data Broker Inquiries and Pretexting")
|
||||
if not has_data_broker_inquiries:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any inquiries, communications, "
|
||||
f"or attempts by data brokers or other unauthorized parties "
|
||||
f"seeking the unauthorized release of CPNI or device location "
|
||||
f"data."
|
||||
))
|
||||
else:
|
||||
desc = data_broker_description or "Each was refused, documented, and escalated."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received data broker or pretexting-style "
|
||||
f"inquiries during the reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "7. Breach Log Summary")
|
||||
if not breaches:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced no CPNI breaches during the "
|
||||
f"reporting period. No 47 CFR \u00a7 64.2011 notifications were "
|
||||
f"required."
|
||||
))
|
||||
else:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced {len(breaches)} CPNI breach"
|
||||
f"{'es' if len(breaches) != 1 else ''} during the reporting "
|
||||
f"period; each was reported within 7 business days."
|
||||
))
|
||||
|
||||
_h(doc, "8. Penalties, Truthfulness, and Perjury Acknowledgment")
|
||||
_b(doc, (
|
||||
f"{entity_name} and the undersigned acknowledge that CPNI rule "
|
||||
f"violations may subject the carrier to forfeitures up to "
|
||||
f"{MAX_FORFEITURE_PER_VIOLATION} per violation and up to "
|
||||
f"{MAX_FORFEITURE_CAP} for any single act or failure to act."
|
||||
))
|
||||
_b(doc, (
|
||||
"Pursuant to 47 CFR \u00a7 1.17, the undersigned represents that no "
|
||||
"material factual information has been withheld and all statements "
|
||||
"are truthful, accurate, and complete."
|
||||
))
|
||||
_b(doc, (
|
||||
"Willful false statements are punishable under Title 18, U.S.C. "
|
||||
"\u00a7 1001, and by forfeiture under 47 U.S.C. \u00a7 503."
|
||||
))
|
||||
|
||||
_h(doc, "9. Signature of Certifying Officer")
|
||||
_b(doc, (
|
||||
"I declare under penalty of perjury under the laws of the United "
|
||||
"States of America that the foregoing is true and correct."
|
||||
))
|
||||
p = doc.add_paragraph(); _sp(p, after=0)
|
||||
sig = doc.add_paragraph(); sig.add_run("_" * 45).font.size = Pt(10); _sp(sig, after=2)
|
||||
nm = doc.add_paragraph(); nr = nm.add_run(signer); nr.bold = True
|
||||
nr.font.size = Pt(10); _sp(nm, after=2)
|
||||
tpp = doc.add_paragraph(); tpp.add_run(f"{title}, {entity_name}").font.size = Pt(10)
|
||||
_sp(tpp, after=2)
|
||||
dp = doc.add_paragraph(); dp.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
_sp(dp, after=2)
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CPNI Wireless certification letter generated: %s", out)
|
||||
return str(out)
|
||||
273
scripts/document_gen/templates/cpni_wireless_mvno_generator.py
Normal file
273
scripts/document_gen/templates/cpni_wireless_mvno_generator.py
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
"""
|
||||
Generate the FCC CPNI Annual Certification Letter — Wireless MVNO variant.
|
||||
|
||||
An MVNO does not own spectrum or a radio-access network; it resells a
|
||||
host MNO's wireless service under its own brand. The MVNO directly
|
||||
controls retail billing records, device-ordering records, and customer
|
||||
support authentication flows. Everything touching the radio network
|
||||
(location signaling, HLR / HSS attach records) is held by the host MNO
|
||||
under its own CPNI certification. This variant clarifies the dividing
|
||||
line and reinforces delegation + flow-down language.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.cpni_wireless_mvno")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CPNI Wireless MVNO generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
VARIANT_ID = "wireless_mvno"
|
||||
VARIANT_LABEL = "Wireless (CMRS) — MVNO"
|
||||
|
||||
MAX_FORFEITURE_PER_VIOLATION = "$251,322"
|
||||
MAX_FORFEITURE_CAP = "$2,513,215"
|
||||
|
||||
|
||||
def _sp(p, after=6, before=0):
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
if before:
|
||||
p.paragraph_format.space_before = Pt(before)
|
||||
|
||||
|
||||
def _h(doc, text):
|
||||
p = doc.add_paragraph(); r = p.add_run(text)
|
||||
r.font.size = Pt(12); r.bold = True; r.font.color.rgb = _NAVY
|
||||
_sp(p, after=4, before=8)
|
||||
|
||||
|
||||
def _b(doc, text, bold=False, size=10):
|
||||
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(text); r.font.size = Pt(size); r.bold = bold
|
||||
_sp(p, after=6)
|
||||
|
||||
|
||||
def _cb(doc, text, checked=True):
|
||||
mark = "\u2611" if checked else "\u2610"
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(f" {mark} {text}"); r.font.size = Pt(10)
|
||||
_sp(p, after=3)
|
||||
|
||||
|
||||
def generate_cpni_wireless_mvno(
|
||||
output_path: str,
|
||||
entity_name: str,
|
||||
frn: str = "",
|
||||
filer_id_499: str = "",
|
||||
officer_name: str = "",
|
||||
officer_title: str = "Chief Executive Officer",
|
||||
complaints_count: int = 0,
|
||||
complaints_description: str = "",
|
||||
has_data_broker_inquiries: bool = False,
|
||||
data_broker_description: str = "",
|
||||
reporting_year: int = 0,
|
||||
host_mno_name: str = "",
|
||||
address_street: str = "",
|
||||
address_city: str = "",
|
||||
address_state: str = "",
|
||||
address_zip: str = "",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
breaches: list[dict] | None = None,
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
breaches = breaches or []
|
||||
host = host_mno_name or "its host Mobile Network Operator"
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
signer = officer_name or "Authorized Officer"
|
||||
title = officer_title or "Officer"
|
||||
|
||||
tp = doc.add_paragraph(); tp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
t = tp.add_run("CPNI Annual Certification Letter")
|
||||
t.font.size = Pt(14); t.bold = True; t.font.color.rgb = _NAVY
|
||||
_sp(tp, after=2)
|
||||
sp = doc.add_paragraph(); sp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
s = sp.add_run(
|
||||
f"Wireless (CMRS) — MVNO \u2014 47 CFR \u00a7 64.2009 "
|
||||
f"\u2014 Calendar Year {reporting_year}"
|
||||
)
|
||||
s.font.size = Pt(10); s.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_sp(sp, after=8)
|
||||
|
||||
_h(doc, "1. Provider Information")
|
||||
lines = [f"Company Name: {entity_name}", f"Variant: {VARIANT_LABEL}"]
|
||||
if frn: lines.append(f"FCC Registration Number (FRN): {frn}")
|
||||
if filer_id_499: lines.append(f"FCC Form 499 Filer ID: {filer_id_499}")
|
||||
addr = ", ".join(filter(None, [address_street, address_city]))
|
||||
if address_state or address_zip:
|
||||
addr += f", {address_state} {address_zip}".strip()
|
||||
if addr.strip(", "):
|
||||
lines.append(f"Address: {addr.strip(', ')}")
|
||||
if contact_phone: lines.append(f"Telephone: {contact_phone}")
|
||||
if contact_email: lines.append(f"Email: {contact_email}")
|
||||
lines.append(f"Certifying Officer: {signer}, {title}")
|
||||
lines.append(f"Host MNO: {host}")
|
||||
lines.append(f"Date of Filing: {today}")
|
||||
lines.append(f"Filing Deadline: March 2, {reporting_year + 1}")
|
||||
_b(doc, "\n".join(lines))
|
||||
|
||||
_h(doc, "2. Officer Statement of Personal Knowledge")
|
||||
_b(doc, (
|
||||
f"I, {signer}, {title} of {entity_name}, state that I have personal "
|
||||
f"knowledge of the matters certified herein. I have reviewed "
|
||||
f"{entity_name}'s CPNI procedures, the CPNI-related portions of "
|
||||
f"the MVNO wholesale agreement with {host}, and supervisory logs "
|
||||
f"covering the reporting period."
|
||||
))
|
||||
|
||||
_h(doc, "3. Certification of Compliance")
|
||||
_b(doc, (
|
||||
f"Pursuant to 47 CFR \u00a7 64.2009(e), {entity_name} hereby submits "
|
||||
f"its annual certification of compliance with the CPNI rules at "
|
||||
f"47 CFR \u00a7\u00a7 64.2001 through 64.2011 for the period "
|
||||
f"January 1, {reporting_year} through December 31, {reporting_year}."
|
||||
))
|
||||
|
||||
_h(doc, "4. How Our Procedures Ensure Compliance")
|
||||
_b(doc, (
|
||||
f"As a Mobile Virtual Network Operator (MVNO) that does not own "
|
||||
f"spectrum or a radio-access network, {entity_name}'s CPNI "
|
||||
f"responsibilities divide into (A) CPNI that {entity_name} directly "
|
||||
f"controls and (B) CPNI that is held, transported, or generated by "
|
||||
f"{host} and made available to {entity_name} pursuant to the MVNO "
|
||||
f"agreement. Both are protected."
|
||||
))
|
||||
_b(doc, f"(A) CPNI directly controlled by {entity_name}:", bold=True)
|
||||
_cb(doc, (
|
||||
"Retail customer records, billing data, plan-change history, and "
|
||||
"customer-support interaction logs are held in systems owned and "
|
||||
"administered by {entity_name} and are subject to password-based "
|
||||
"authentication, opt-in consent for marketing use, quarterly "
|
||||
"supervisory review, and annual training per 47 CFR \u00a7\u00a7 "
|
||||
"64.2005\u201364.2010."
|
||||
).replace("{entity_name}", entity_name))
|
||||
_cb(doc, (
|
||||
"SIM / eSIM port-out and device-swap orders received by "
|
||||
"{entity_name} require multi-factor authentication of the "
|
||||
"subscriber plus notification to the address of record before the "
|
||||
"order is submitted to {host} for execution (47 CFR \u00a7 64.2010)."
|
||||
).replace("{entity_name}", entity_name).replace("{host}", host))
|
||||
_b(doc, f"(B) CPNI delegated to or held by {host}:", bold=True)
|
||||
_cb(doc, (
|
||||
"Radio-access network signaling, HLR / HSS / UDM records, "
|
||||
"per-device location information, and lawful-intercept records "
|
||||
"are held by the host MNO and are governed by that host MNO's own "
|
||||
"CPNI certification and safeguards. {entity_name} does NOT have "
|
||||
"direct access to these data stores."
|
||||
).replace("{entity_name}", entity_name))
|
||||
_cb(doc, (
|
||||
f"The MVNO wholesale agreement between {entity_name} and {host} "
|
||||
f"expressly requires {host} to protect all CPNI generated through "
|
||||
f"{entity_name}'s subscribers consistent with 47 CFR "
|
||||
f"\u00a7\u00a7 64.2001\u201364.2011, to provide breach notice to "
|
||||
f"{entity_name} within a commercially reasonable time, and to "
|
||||
f"limit use of such CPNI to the provision of service to "
|
||||
f"{entity_name} and its subscribers."
|
||||
))
|
||||
_cb(doc, (
|
||||
f"{entity_name} reviews {host}'s annual CPNI certification and "
|
||||
f"any published breach notices, and maintains a file of the "
|
||||
f"current executed MVNO agreement."
|
||||
))
|
||||
|
||||
_h(doc, "5. Customer Complaints")
|
||||
if complaints_count == 0:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any customer complaints during "
|
||||
f"the reporting period concerning the unauthorized release or "
|
||||
f"use of CPNI. Zero (0) complaints were logged."
|
||||
))
|
||||
else:
|
||||
desc = complaints_description or "Each complaint was investigated and resolved."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received {complaints_count} customer "
|
||||
f"complaint{'s' if complaints_count != 1 else ''} during the "
|
||||
f"reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "6. Data Broker Inquiries and Pretexting")
|
||||
if not has_data_broker_inquiries:
|
||||
_b(doc, (
|
||||
f"{entity_name} has NOT received any inquiries, communications, "
|
||||
f"or attempts by data brokers or other unauthorized parties "
|
||||
f"seeking the unauthorized release of CPNI."
|
||||
))
|
||||
else:
|
||||
desc = data_broker_description or "Each was refused, documented, and escalated."
|
||||
_b(doc, (
|
||||
f"{entity_name} HAS received data broker or pretexting-style "
|
||||
f"inquiries during the reporting period. {desc}"
|
||||
))
|
||||
|
||||
_h(doc, "7. Breach Log Summary")
|
||||
if not breaches:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced no CPNI breaches during the "
|
||||
f"reporting period. No 47 CFR \u00a7 64.2011 notifications "
|
||||
f"were required."
|
||||
))
|
||||
else:
|
||||
_b(doc, (
|
||||
f"{entity_name} experienced {len(breaches)} CPNI breach"
|
||||
f"{'es' if len(breaches) != 1 else ''} during the reporting "
|
||||
f"period; each was reported within 7 business days."
|
||||
))
|
||||
|
||||
_h(doc, "8. Penalties, Truthfulness, and Perjury Acknowledgment")
|
||||
_b(doc, (
|
||||
f"{entity_name} and the undersigned acknowledge that CPNI rule "
|
||||
f"violations may subject the carrier to forfeitures up to "
|
||||
f"{MAX_FORFEITURE_PER_VIOLATION} per violation and up to "
|
||||
f"{MAX_FORFEITURE_CAP} for any single act or failure to act."
|
||||
))
|
||||
_b(doc, (
|
||||
"Pursuant to 47 CFR \u00a7 1.17, the undersigned represents that no "
|
||||
"material factual information has been withheld and all statements "
|
||||
"are truthful, accurate, and complete."
|
||||
))
|
||||
_b(doc, (
|
||||
"Willful false statements are punishable under Title 18, U.S.C. "
|
||||
"\u00a7 1001, and by forfeiture under 47 U.S.C. \u00a7 503."
|
||||
))
|
||||
|
||||
_h(doc, "9. Signature of Certifying Officer")
|
||||
_b(doc, (
|
||||
"I declare under penalty of perjury under the laws of the United "
|
||||
"States of America that the foregoing is true and correct."
|
||||
))
|
||||
p = doc.add_paragraph(); _sp(p, after=0)
|
||||
sig = doc.add_paragraph(); sig.add_run("_" * 45).font.size = Pt(10); _sp(sig, after=2)
|
||||
nm = doc.add_paragraph(); nr = nm.add_run(signer); nr.bold = True
|
||||
nr.font.size = Pt(10); _sp(nm, after=2)
|
||||
tpp = doc.add_paragraph(); tpp.add_run(f"{title}, {entity_name}").font.size = Pt(10)
|
||||
_sp(tpp, after=2)
|
||||
dp = doc.add_paragraph(); dp.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
_sp(dp, after=2)
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("CPNI Wireless MVNO certification letter generated: %s", out)
|
||||
return str(out)
|
||||
252
scripts/document_gen/templates/crtc_letter_generator.py
Normal file
252
scripts/document_gen/templates/crtc_letter_generator.py
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
"""
|
||||
Generate the CRTC Registration Notification Letter as a DOCX file.
|
||||
|
||||
This produces a formal letter addressed to the Secretary General of the CRTC
|
||||
notifying them that a new telecommunications service provider has been
|
||||
established under a BC corporation and wishes to register as a:
|
||||
- Voice, Data & Wireless Reseller (domestic)
|
||||
- Basic International Telecommunications Service (BITS) provider (if applicable)
|
||||
|
||||
The letter follows the format specified at:
|
||||
https://crtc.gc.ca/eng/comm/telecom/registr4.htm
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.crtc_letter_generator import generate_crtc_letter
|
||||
pdf_path = generate_crtc_letter(
|
||||
entity_name="1234567 B.C. Ltd.",
|
||||
incorporation_number="1234567",
|
||||
registered_office="329 Howe St, Vancouver, BC V6C 3N2",
|
||||
services_description="Resale of voice, data, and wireless services...",
|
||||
geographic_coverage="BC and Worldwide",
|
||||
include_bits=True,
|
||||
regulatory_contact_name="Regulatory Director",
|
||||
regulatory_contact_email="regulatory@example.ca",
|
||||
regulatory_contact_phone="+16045551234",
|
||||
director_name="John Doe",
|
||||
output_path="/tmp/crtc_letter.docx",
|
||||
)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.crtc_letter")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — CRTC letter generation unavailable")
|
||||
Document = None
|
||||
|
||||
|
||||
def generate_crtc_letter(
|
||||
entity_name: str,
|
||||
incorporation_number: str,
|
||||
registered_office: str,
|
||||
services_description: str,
|
||||
geographic_coverage: str = "Canada-wide",
|
||||
include_bits: bool = True,
|
||||
regulatory_contact_name: str = "Regulatory Director",
|
||||
regulatory_contact_email: str = "",
|
||||
regulatory_contact_phone: str = "",
|
||||
director_name: str = "",
|
||||
ca_domain: str = "",
|
||||
output_path: str = "/tmp/crtc_notification_letter.docx",
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Generate a CRTC Registration Notification Letter as a DOCX file.
|
||||
|
||||
Returns the output file path on success, None on failure.
|
||||
"""
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
doc = Document()
|
||||
|
||||
# Page margins
|
||||
for section in doc.sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1.25)
|
||||
section.right_margin = Inches(1.25)
|
||||
|
||||
# ── Sender block ──────────────────────────────────────────
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
|
||||
sender = doc.add_paragraph()
|
||||
sender.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
sender_run = sender.add_run(
|
||||
f"{entity_name}\n"
|
||||
f"Incorporation No. {incorporation_number}\n"
|
||||
f"{registered_office}\n"
|
||||
)
|
||||
sender_run.font.size = Pt(10)
|
||||
if regulatory_contact_phone:
|
||||
sender.add_run(f"Tel: {regulatory_contact_phone}\n").font.size = Pt(10)
|
||||
if regulatory_contact_email:
|
||||
sender.add_run(f"Email: {regulatory_contact_email}\n").font.size = Pt(10)
|
||||
if ca_domain:
|
||||
sender.add_run(f"Web: https://{ca_domain}\n").font.size = Pt(10)
|
||||
|
||||
# Date
|
||||
date_para = doc.add_paragraph()
|
||||
date_para.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
date_run = date_para.add_run(today)
|
||||
date_run.font.size = Pt(10)
|
||||
|
||||
# ── Addressee ─────────────────────────────────────────────
|
||||
doc.add_paragraph()
|
||||
addressee = doc.add_paragraph()
|
||||
addressee_run = addressee.add_run(
|
||||
"Secretary General\n"
|
||||
"Canadian Radio-television and\n"
|
||||
"Telecommunications Commission (CRTC)\n"
|
||||
"Ottawa, Ontario\n"
|
||||
"K1A 0N2"
|
||||
)
|
||||
addressee_run.font.size = Pt(10)
|
||||
|
||||
# ── Subject line ──────────────────────────────────────────
|
||||
doc.add_paragraph()
|
||||
subject = doc.add_paragraph()
|
||||
subject_run = subject.add_run(
|
||||
f"Re: Registration as a Telecommunications Service Provider — {entity_name}"
|
||||
)
|
||||
subject_run.font.size = Pt(10)
|
||||
subject_run.bold = True
|
||||
|
||||
# ── Body ──────────────────────────────────────────────────
|
||||
doc.add_paragraph()
|
||||
|
||||
# Introduction
|
||||
intro = doc.add_paragraph()
|
||||
intro_run = intro.add_run(
|
||||
f"Dear Secretary General,\n\n"
|
||||
f"Pursuant to the Telecommunications Act, S.C. 1993, c. 38, and the "
|
||||
f"Canadian Radio-television and Telecommunications Commission's registration "
|
||||
f"requirements for telecommunications service providers, {entity_name} "
|
||||
f"(Incorporation No. {incorporation_number}) hereby notifies the Commission of its "
|
||||
f"intention to provide telecommunications services in Canada."
|
||||
)
|
||||
intro_run.font.size = Pt(10)
|
||||
|
||||
# Company information section
|
||||
doc.add_paragraph()
|
||||
info_heading = doc.add_paragraph()
|
||||
info_heading_run = info_heading.add_run("1. Company Information")
|
||||
info_heading_run.font.size = Pt(10)
|
||||
info_heading_run.bold = True
|
||||
|
||||
info = doc.add_paragraph()
|
||||
info.style.font.size = Pt(10)
|
||||
info_text = (
|
||||
f"Legal Name: {entity_name}\n"
|
||||
f"Incorporation Number: {incorporation_number}\n"
|
||||
f"Mailing Address: {registered_office}\n"
|
||||
f"Telephone: {regulatory_contact_phone}\n"
|
||||
f"Email: {regulatory_contact_email}\n"
|
||||
)
|
||||
info.add_run(info_text).font.size = Pt(10)
|
||||
|
||||
# Services section
|
||||
doc.add_paragraph()
|
||||
svc_heading = doc.add_paragraph()
|
||||
svc_heading_run = svc_heading.add_run("2. Description of Services")
|
||||
svc_heading_run.font.size = Pt(10)
|
||||
svc_heading_run.bold = True
|
||||
|
||||
svc = doc.add_paragraph()
|
||||
svc.add_run(
|
||||
f"{entity_name} intends to operate as a reseller of voice, data, and wireless "
|
||||
f"telecommunications services. Specifically:\n\n"
|
||||
f"{services_description}\n\n"
|
||||
f"Geographic Coverage: {geographic_coverage}"
|
||||
).font.size = Pt(10)
|
||||
|
||||
# Registration type
|
||||
doc.add_paragraph()
|
||||
reg_heading = doc.add_paragraph()
|
||||
reg_heading_run = reg_heading.add_run("3. Registration Category")
|
||||
reg_heading_run.font.size = Pt(10)
|
||||
reg_heading_run.bold = True
|
||||
|
||||
reg = doc.add_paragraph()
|
||||
reg_text = f"{entity_name} registers as a Voice, Data & Wireless Reseller."
|
||||
if include_bits:
|
||||
reg_text += (
|
||||
f"\n\n{entity_name} also intends to provide Basic International "
|
||||
f"Telecommunications Services (BITS) and will file a separate notification "
|
||||
f"with the Commission pursuant to CRTC Telecom Decision 98-17."
|
||||
)
|
||||
reg.add_run(reg_text).font.size = Pt(10)
|
||||
|
||||
# Response Manager
|
||||
doc.add_paragraph()
|
||||
rm_heading = doc.add_paragraph()
|
||||
rm_heading_run = rm_heading.add_run("4. Response Manager for Regulatory Matters")
|
||||
rm_heading_run.font.size = Pt(10)
|
||||
rm_heading_run.bold = True
|
||||
|
||||
rm = doc.add_paragraph()
|
||||
rm.add_run(
|
||||
f"Name: {regulatory_contact_name}\n"
|
||||
f"Title: Regulatory Director\n"
|
||||
f"Organization: {entity_name}\n"
|
||||
f"Address: {registered_office}\n"
|
||||
f"Telephone: {regulatory_contact_phone}\n"
|
||||
f"Email: {regulatory_contact_email}"
|
||||
).font.size = Pt(10)
|
||||
|
||||
# Compliance commitment
|
||||
doc.add_paragraph()
|
||||
compliance_heading = doc.add_paragraph()
|
||||
compliance_heading_run = compliance_heading.add_run("5. Compliance")
|
||||
compliance_heading_run.font.size = Pt(10)
|
||||
compliance_heading_run.bold = True
|
||||
|
||||
compliance = doc.add_paragraph()
|
||||
compliance.add_run(
|
||||
f"{entity_name} confirms that it will comply with all applicable provisions of "
|
||||
f"the Telecommunications Act, CRTC regulations, and conditions of service, "
|
||||
f"including participation in the Commission for Complaints for "
|
||||
f"Telecom-Television Services (CCTS)."
|
||||
).font.size = Pt(10)
|
||||
|
||||
# Closing
|
||||
doc.add_paragraph()
|
||||
doc.add_paragraph()
|
||||
closing = doc.add_paragraph()
|
||||
closing.add_run("Respectfully submitted,").font.size = Pt(10)
|
||||
|
||||
# Signature block (space for eSign)
|
||||
doc.add_paragraph()
|
||||
doc.add_paragraph() # Space for signature
|
||||
doc.add_paragraph()
|
||||
|
||||
sig_line = doc.add_paragraph()
|
||||
sig_line.add_run("_" * 40).font.size = Pt(10)
|
||||
|
||||
sig_name = doc.add_paragraph()
|
||||
sig_name_run = sig_name.add_run(director_name or regulatory_contact_name)
|
||||
sig_name_run.font.size = Pt(10)
|
||||
sig_name_run.bold = True
|
||||
|
||||
sig_title = doc.add_paragraph()
|
||||
sig_title.add_run(f"Director, {entity_name}").font.size = Pt(10)
|
||||
|
||||
sig_date = doc.add_paragraph()
|
||||
sig_date.add_run(f"Date: {today}").font.size = Pt(10)
|
||||
|
||||
# Save
|
||||
output = Path(output_path)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(output))
|
||||
LOG.info("CRTC letter generated: %s", output)
|
||||
return str(output)
|
||||
1326
scripts/document_gen/templates/fcc_499a_checklist_generator.py
Normal file
1326
scripts/document_gen/templates/fcc_499a_checklist_generator.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,427 @@
|
|||
"""
|
||||
Generate the 499-A Revenue Calculation Workbook (xlsx).
|
||||
|
||||
Companion workbook customers fill in with their monthly/quarterly revenue
|
||||
splits. The workbook roll-ups feed the actual 499-A Block 3, 4-A, 4-B,
|
||||
and 5 line items that the Playwright handler pushes into USAC E-File.
|
||||
|
||||
Design principles:
|
||||
|
||||
* Entirely local — formulas live in the workbook, not here. Customers open
|
||||
it in Excel/Google Sheets, plug in numbers, and the totals flip.
|
||||
* Jurisdictional splits per 2026 Instructions (section IV.C.5): intrastate
|
||||
/ interstate / international. Column D/E/F carry the split percentages
|
||||
so customers can override the VoIP safe harbor if they have a traffic
|
||||
study.
|
||||
* Every revenue row maps to a specific Form 499-A line (e.g. Line 404.1,
|
||||
Line 414, Line 418.4) so the admin can lift values straight into E-File.
|
||||
|
||||
Sheets produced:
|
||||
|
||||
1. README — explains the workbook, filing window, where values feed.
|
||||
2. Block 3 — Carrier's Carrier Revenue (Lines 301-315)
|
||||
3. Block 4-A — End-User & Non-Telecom Revenue (Lines 401-418)
|
||||
4. Block 4-B — Totals & Uncollectibles (Lines 419-423)
|
||||
5. Block 5 — Regional Percentages + TRS + Reseller Exclusions
|
||||
6. Summary — single-page dashboard of all key roll-ups
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.form_499a_revenue_workbook_generator import (
|
||||
generate_499a_revenue_workbook,
|
||||
)
|
||||
path = generate_499a_revenue_workbook(
|
||||
entity_name="Falcon Broadband LLC",
|
||||
filer_id_499="812345",
|
||||
reporting_year=2025,
|
||||
voip_safe_harbor_pct=64.9,
|
||||
output_path="/tmp/499a_workbook.xlsx",
|
||||
)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.499a_workbook")
|
||||
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
except ImportError:
|
||||
LOG.warning("openpyxl not installed — 499-A workbook generation unavailable")
|
||||
Workbook = None # type: ignore[assignment,misc]
|
||||
|
||||
|
||||
# Style constants
|
||||
NAVY_FILL = "FF1A2744"
|
||||
LIGHT_FILL = "FFF8FAFC"
|
||||
BORDER_THIN = None # set lazily (needs openpyxl)
|
||||
HEADER_FONT = None
|
||||
TITLE_FONT = None
|
||||
|
||||
|
||||
def _init_styles():
|
||||
"""Lazy style init — openpyxl objects can't be module-level if import fails."""
|
||||
global BORDER_THIN, HEADER_FONT, TITLE_FONT
|
||||
if BORDER_THIN is None:
|
||||
side = Side(style="thin", color="FFCBD5E1")
|
||||
BORDER_THIN = Border(left=side, right=side, top=side, bottom=side)
|
||||
HEADER_FONT = Font(name="Calibri", size=11, bold=True, color="FFFFFFFF")
|
||||
TITLE_FONT = Font(name="Calibri", size=14, bold=True, color="FF1A2744")
|
||||
|
||||
|
||||
# Block 3: Carrier's Carrier Revenue (Lines 301-315).
|
||||
# Each tuple is (line_num, label, is_total_row).
|
||||
_BLOCK_3_LINES = [
|
||||
("303.1", "Fixed local — UNEs", False),
|
||||
("303.2", "Fixed local — other arrangements", False),
|
||||
("304.1", "Per-minute charges — state/federal access tariff", False),
|
||||
("304.2", "Per-minute — UNEs or other arrangement", False),
|
||||
("305.1", "Local private line — for resale as telecom", False),
|
||||
("305.2", "Local private line — for resale as interconnected VoIP", False),
|
||||
("306", "Payphone compensation from toll carriers", False),
|
||||
("307", "Other local telecom service revenues", False),
|
||||
("308", "Universal service support revenues from Federal/state", False),
|
||||
("309", "Mobile — monthly/activation/message (non-toll)", False),
|
||||
("310", "Operator/toll with alternative billing", False),
|
||||
("311", "Ordinary long distance (direct-dialed MTS, toll-free, etc.)", False),
|
||||
("312", "Long distance private line services", False),
|
||||
("313", "Satellite services", False),
|
||||
("314", "All other long distance services", False),
|
||||
("315", "TOTAL revenues from resale (sum 303–314)", True),
|
||||
]
|
||||
|
||||
# Block 4-A: End-User and Non-Telecom Revenue (Lines 401-418).
|
||||
_BLOCK_4A_LINES = [
|
||||
("403", "Surcharges recovering USF contributions", False),
|
||||
("404.1", "Fixed flat-rate w/ interstate toll — local portion", False),
|
||||
("404.2", "Fixed flat-rate w/ interstate toll — toll portion", False),
|
||||
("404.3", "Fixed — no interstate toll included", False),
|
||||
("404.4", "Interconnected VoIP — with broadband connection", False),
|
||||
("404.5", "Interconnected VoIP — independent of broadband", False),
|
||||
("405", "Tariffed SLC / ARC / PICC (LEC no-PIC)", False),
|
||||
("406", "Local private line & special access (wireline broadband)", False),
|
||||
("407", "Payphone coin revenues (local + LD)", False),
|
||||
("408", "Other local telecom service revenues", False),
|
||||
("409", "Mobile — monthly and activation charges", False),
|
||||
("410", "Mobile — message / roaming / air-time (excl. separately stated toll)", False),
|
||||
("411", "Prepaid calling card (at face value)", False),
|
||||
("412", "International calls — both endpoints foreign", False),
|
||||
("413", "Operator/toll with alternative billing (non-412)", False),
|
||||
("414.1", "Ordinary LD — non-VoIP", False),
|
||||
("414.2", "Ordinary LD — interconnected VoIP", False),
|
||||
("415", "Long distance private line services", False),
|
||||
("416", "Satellite services", False),
|
||||
("417", "All other long distance services", False),
|
||||
("418.1", "Bundled — with circuit-switched local", False),
|
||||
("418.2", "Bundled — with interconnected VoIP local", False),
|
||||
("418.3", "Other bundled / non-telecom", False),
|
||||
("418.4", "Non-interconnected VoIP (not in other categories)", False),
|
||||
]
|
||||
|
||||
# Block 4-B: Total / uncollectible (Lines 419-423).
|
||||
_BLOCK_4B_LINES = [
|
||||
("419", "Gross billed revenues from all sources", False),
|
||||
("420", "Gross universal service contribution base", False),
|
||||
("421", "Uncollectible associated with Line 419", False),
|
||||
("422", "Uncollectible associated with Line 420", False),
|
||||
("423", "Net universal service contribution base (420 − 422)", True),
|
||||
]
|
||||
|
||||
# Block 5 regional breakout rows (Lines 503-510).
|
||||
_BLOCK_5_REGIONS = [
|
||||
("503", "Southeast: AL, FL, GA, KY, LA, MS, NC, PR, SC, TN, USVI"),
|
||||
("504", "Western: AK, AZ, CO, ID, IA, MN, MT, NE, NM, ND, OR, SD, UT, WA, WY"),
|
||||
("505", "West Coast: CA, HI, NV, AS, GU, Johnston, Midway, MP, Wake"),
|
||||
("506", "Mid-Atlantic: DE, DC, MD, NJ, PA, VA, WV"),
|
||||
("507", "Mid-West: IL, IN, MI, OH, WI"),
|
||||
("508", "Northeast: CT, ME, MA, NH, NY, RI, VT"),
|
||||
("509", "Southwest: AR, KS, MO, OK, TX"),
|
||||
("510", "TOTAL (must sum to 100%)"),
|
||||
]
|
||||
|
||||
|
||||
def _set_header_row(ws, row: int, headers: list[str]) -> None:
|
||||
_init_styles()
|
||||
navy = PatternFill(start_color=NAVY_FILL, end_color=NAVY_FILL, fill_type="solid")
|
||||
for col, header in enumerate(headers, start=1):
|
||||
cell = ws.cell(row=row, column=col, value=header)
|
||||
cell.font = HEADER_FONT
|
||||
cell.fill = navy
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
cell.border = BORDER_THIN
|
||||
|
||||
|
||||
def _write_revenue_block(
|
||||
ws,
|
||||
*,
|
||||
title: str,
|
||||
lines: list[tuple[str, str, bool]],
|
||||
start_row: int = 1,
|
||||
) -> int:
|
||||
"""Write a Block 3 / 4-A / 4-B style sheet. Returns next free row."""
|
||||
_init_styles()
|
||||
ws.cell(row=start_row, column=1, value=title).font = TITLE_FONT
|
||||
row = start_row + 2
|
||||
|
||||
_set_header_row(ws, row, [
|
||||
"Line",
|
||||
"Description",
|
||||
"(a) Total Revenue",
|
||||
"(b) Interstate %",
|
||||
"(c) International %",
|
||||
"(d) Interstate $",
|
||||
"(e) International $",
|
||||
])
|
||||
row += 1
|
||||
|
||||
total_row_idx = None
|
||||
for line_num, label, is_total in lines:
|
||||
if is_total:
|
||||
total_row_idx = row
|
||||
ws.cell(row=row, column=1, value=line_num).font = Font(bold=True)
|
||||
ws.cell(row=row, column=2, value=label).font = Font(bold=True)
|
||||
# TOTAL formula
|
||||
data_range = f"C{row - len(lines) + 1}:C{row - 1}"
|
||||
ws.cell(row=row, column=3, value=f"=SUM({data_range})").font = Font(bold=True)
|
||||
ws.cell(row=row, column=6, value=f"=SUM(F{row - len(lines) + 1}:F{row - 1})").font = Font(bold=True)
|
||||
ws.cell(row=row, column=7, value=f"=SUM(G{row - len(lines) + 1}:G{row - 1})").font = Font(bold=True)
|
||||
else:
|
||||
ws.cell(row=row, column=1, value=line_num)
|
||||
ws.cell(row=row, column=2, value=label)
|
||||
# Column C is the total revenue input (customer fills).
|
||||
# Columns D/E percentages feed formulas in F/G.
|
||||
ws.cell(row=row, column=6, value=f"=C{row}*D{row}/100")
|
||||
ws.cell(row=row, column=7, value=f"=C{row}*E{row}/100")
|
||||
|
||||
for col in range(1, 8):
|
||||
ws.cell(row=row, column=col).border = BORDER_THIN
|
||||
# Currency formatting
|
||||
for col in (3, 6, 7):
|
||||
ws.cell(row=row, column=col).number_format = '"$"#,##0.00'
|
||||
# Percentage columns
|
||||
for col in (4, 5):
|
||||
ws.cell(row=row, column=col).number_format = '0.00"%"'
|
||||
|
||||
row += 1
|
||||
|
||||
# Column widths
|
||||
ws.column_dimensions["A"].width = 10
|
||||
ws.column_dimensions["B"].width = 55
|
||||
for col_letter in ("C", "D", "E", "F", "G"):
|
||||
ws.column_dimensions[col_letter].width = 16
|
||||
|
||||
return row + 1
|
||||
|
||||
|
||||
def generate_499a_revenue_workbook(
|
||||
# Entity
|
||||
entity_name: str,
|
||||
filer_id_499: str = "",
|
||||
frn: str = "",
|
||||
# Reporting period
|
||||
reporting_year: int = 0,
|
||||
# VoIP safe harbor (populated in README)
|
||||
voip_safe_harbor_pct: float = 64.9,
|
||||
# Pre-fill: optional traffic study row from cdr_traffic_studies.
|
||||
# When provided, interstate / international percentages on each
|
||||
# revenue line pre-populate from the study, saving the customer
|
||||
# from typing them in — reviewer still validates before filing.
|
||||
traffic_study: Optional[dict] = None,
|
||||
# Output
|
||||
output_path: str = "/tmp/form_499a_revenue_workbook.xlsx",
|
||||
) -> Optional[str]:
|
||||
"""Produce the 499-A revenue calculation workbook.
|
||||
|
||||
When ``traffic_study`` is passed (a row from ``cdr_traffic_studies``),
|
||||
the workbook pre-fills interstate/international % cells across the
|
||||
revenue blocks using the study's computed values. Block 5 regional
|
||||
rows pre-fill with BOTH the orig-state and billing-state percentages
|
||||
so the admin chooses at submission time.
|
||||
"""
|
||||
if Workbook is None:
|
||||
LOG.error("openpyxl not installed")
|
||||
return None
|
||||
|
||||
_init_styles()
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year - 1
|
||||
interstate_pct_prefill = (traffic_study or {}).get("interstate_pct")
|
||||
international_pct_prefill = (traffic_study or {}).get("international_pct")
|
||||
orig_regions = (traffic_study or {}).get("orig_state_regions_json") or {}
|
||||
billing_regions = (traffic_study or {}).get("billing_state_regions_json") or {}
|
||||
|
||||
wb = Workbook()
|
||||
# Kill the default sheet
|
||||
default = wb.active
|
||||
wb.remove(default)
|
||||
|
||||
# ── README sheet ────────────────────────────────────────────────
|
||||
ws = wb.create_sheet("README")
|
||||
ws.cell(row=1, column=1, value=f"2026 FCC Form 499-A Revenue Calculation Workbook").font = TITLE_FONT
|
||||
ws.cell(row=2, column=1, value=f"Reporting calendar year {reporting_year}").font = Font(italic=True)
|
||||
ws.cell(row=3, column=1, value=f"Filer: {entity_name} | Filer 499 ID: {filer_id_499 or '(pending)'} | FRN: {frn or '(pending)'}")
|
||||
|
||||
readme = [
|
||||
"",
|
||||
"How to use this workbook:",
|
||||
" 1. Open each Block sheet (Block 3, Block 4-A, Block 4-B, Block 5).",
|
||||
" 2. In column C (\u201cTotal Revenue\u201d), enter each Line item's gross billed revenue for the year.",
|
||||
" 3. In columns D and E, enter the interstate % and international % for each line.",
|
||||
" If you are an interconnected VoIP provider and have not conducted a traffic study, you may",
|
||||
f" use the 2026 VoIP safe harbor of {voip_safe_harbor_pct}% for lines 404.4 / 404.5 / 414.2.",
|
||||
" 4. Columns F and G calculate automatically. Block 4-B pulls totals from Block 4-A.",
|
||||
" 5. The Summary sheet gives a one-page view of everything that feeds the filing.",
|
||||
"",
|
||||
"Important:",
|
||||
" \u2022 Report dollar amounts in whole dollars (round >$1000 to nearest thousand per Section G).",
|
||||
" \u2022 Do NOT enter negative numbers on any billed revenue line (see Lines 421/422 for uncollectibles).",
|
||||
" \u2022 The 2026 Form 499-A (reporting 2025 revenues) is due April 1, 2026.",
|
||||
" \u2022 E-File opens March 2, 2026. File online at https://forms.universalservice.org.",
|
||||
"",
|
||||
"Block assignments (2025/2026 form — structure unchanged):",
|
||||
" \u2022 Block 3 = Carrier's Carrier Revenue (Lines 301\u2013315)",
|
||||
" \u2022 Block 4-A = End-User and Non-Telecom Revenue (Lines 401\u2013418)",
|
||||
" \u2022 Block 4-B = Total Revenue + Uncollectibles (Lines 419\u2013423)",
|
||||
" \u2022 Block 5 = Regional Percentage Breakouts + TRS contribution base + Reseller Exclusions",
|
||||
"",
|
||||
"Questions: contact support@performancewest.net or 888-411-0383.",
|
||||
]
|
||||
for i, line in enumerate(readme, start=4):
|
||||
ws.cell(row=i, column=1, value=line)
|
||||
ws.column_dimensions["A"].width = 110
|
||||
|
||||
# ── Block 3 ─────────────────────────────────────────────────────
|
||||
ws3 = wb.create_sheet("Block 3")
|
||||
_write_revenue_block(
|
||||
ws3,
|
||||
title=f"Block 3 — Carrier's Carrier Revenue (Reporting {reporting_year})",
|
||||
lines=_BLOCK_3_LINES,
|
||||
)
|
||||
|
||||
# ── Block 4-A ───────────────────────────────────────────────────
|
||||
ws4a = wb.create_sheet("Block 4-A")
|
||||
_write_revenue_block(
|
||||
ws4a,
|
||||
title=f"Block 4-A — End-User and Non-Telecom Revenue (Reporting {reporting_year})",
|
||||
lines=_BLOCK_4A_LINES,
|
||||
)
|
||||
|
||||
# ── Block 4-B ───────────────────────────────────────────────────
|
||||
ws4b = wb.create_sheet("Block 4-B")
|
||||
ws4b.cell(row=1, column=1, value="Block 4-B — Total Revenue and Uncollectible Revenue").font = TITLE_FONT
|
||||
_set_header_row(ws4b, 3, ["Line", "Description", "Total Revenue", "Interstate", "International"])
|
||||
# Line 419 pulls from Block 3 Line 315 + Block 4-A total
|
||||
ws4b.cell(row=4, column=1, value="419")
|
||||
ws4b.cell(row=4, column=2, value="Gross billed revenues from all sources")
|
||||
ws4b.cell(row=4, column=3, value="='Block 3'!C" + str(3 + len(_BLOCK_3_LINES) + 1) + "+'Block 4-A'!C" + str(3 + len(_BLOCK_4A_LINES) + 1))
|
||||
# Line 420 — contribution base (lines 403-411 + 413-417 end-user)
|
||||
ws4b.cell(row=5, column=1, value="420")
|
||||
ws4b.cell(row=5, column=2, value="Gross USF contribution base amounts (fill manually; see Table 3 in 2026 instructions)")
|
||||
# Line 421
|
||||
ws4b.cell(row=6, column=1, value="421")
|
||||
ws4b.cell(row=6, column=2, value="Uncollectible associated with Line 419")
|
||||
# Line 422
|
||||
ws4b.cell(row=7, column=1, value="422")
|
||||
ws4b.cell(row=7, column=2, value="Uncollectible associated with Line 420")
|
||||
# Line 423
|
||||
ws4b.cell(row=8, column=1, value="423").font = Font(bold=True)
|
||||
ws4b.cell(row=8, column=2, value="Net USF contribution base (420 − 422)").font = Font(bold=True)
|
||||
ws4b.cell(row=8, column=3, value="=C5-C7").font = Font(bold=True)
|
||||
for row_idx in range(3, 9):
|
||||
for col in range(1, 6):
|
||||
ws4b.cell(row=row_idx, column=col).border = BORDER_THIN
|
||||
ws4b.cell(row=row_idx, column=3).number_format = '"$"#,##0.00'
|
||||
ws4b.column_dimensions["A"].width = 10
|
||||
ws4b.column_dimensions["B"].width = 55
|
||||
ws4b.column_dimensions["C"].width = 18
|
||||
|
||||
# ── Block 5 ─────────────────────────────────────────────────────
|
||||
ws5 = wb.create_sheet("Block 5")
|
||||
title_suffix = (
|
||||
" (pre-filled from traffic study — pick orig-state OR billing-state at submission)"
|
||||
if traffic_study else ""
|
||||
)
|
||||
ws5.cell(row=1, column=1,
|
||||
value=f"Block 5 — Regional Percentage Breakouts + TRS Contribution Base{title_suffix}").font = TITLE_FONT
|
||||
_set_header_row(ws5, 3, [
|
||||
"Line", "Region",
|
||||
"Block 3 (Carrier) %",
|
||||
"Block 4 (End-User) %",
|
||||
"Pre-fill: by Orig State %",
|
||||
"Pre-fill: by Billing State %",
|
||||
])
|
||||
for i, (line_num, label) in enumerate(_BLOCK_5_REGIONS, start=4):
|
||||
ws5.cell(row=i, column=1, value=line_num)
|
||||
ws5.cell(row=i, column=2, value=label)
|
||||
is_total = line_num == "510"
|
||||
if is_total:
|
||||
ws5.cell(row=i, column=3, value=f"=SUM(C{4}:C{i-1})").font = Font(bold=True)
|
||||
ws5.cell(row=i, column=4, value=f"=SUM(D{4}:D{i-1})").font = Font(bold=True)
|
||||
else:
|
||||
# Pre-fill columns 5 + 6 from the traffic study when available.
|
||||
# Admin reviewer picks one of them at filing time and copies
|
||||
# the value into columns C/D for the official submission.
|
||||
region_key = label.split(":", 1)[0].strip() if ":" in label else label
|
||||
orig_v = orig_regions.get(region_key)
|
||||
bill_v = billing_regions.get(region_key)
|
||||
if orig_v is not None:
|
||||
ws5.cell(row=i, column=5, value=float(orig_v))
|
||||
if bill_v is not None:
|
||||
ws5.cell(row=i, column=6, value=float(bill_v))
|
||||
# Format all %
|
||||
for col in (3, 4, 5, 6):
|
||||
ws5.cell(row=i, column=col).number_format = '0.00"%"'
|
||||
ws5.cell(row=i, column=col).border = BORDER_THIN
|
||||
for col in (1, 2):
|
||||
ws5.cell(row=i, column=col).border = BORDER_THIN
|
||||
# Line 511 — reseller exclusions
|
||||
row_offset = 4 + len(_BLOCK_5_REGIONS) + 1
|
||||
ws5.cell(row=row_offset, column=2, value="Line 511 — Revenues from resellers that do not contribute to USF (included in Block 4-B Line 420 but excluded from TRS/NANPA/LNP/FCC regulatory fee bases)").font = Font(italic=True)
|
||||
_set_header_row(ws5, row_offset + 1, ["", "Reseller Filer 499 ID", "(a) Total Revenue", "(b) Interstate/Int'l"])
|
||||
for r in range(row_offset + 2, row_offset + 7):
|
||||
for col in range(1, 5):
|
||||
ws5.cell(row=r, column=col).border = BORDER_THIN
|
||||
ws5.cell(row=r, column=3).number_format = '"$"#,##0.00'
|
||||
ws5.cell(row=r, column=4).number_format = '"$"#,##0.00'
|
||||
ws5.column_dimensions["A"].width = 10
|
||||
ws5.column_dimensions["B"].width = 70
|
||||
ws5.column_dimensions["C"].width = 20
|
||||
ws5.column_dimensions["D"].width = 22
|
||||
|
||||
# ── Summary ─────────────────────────────────────────────────────
|
||||
wss = wb.create_sheet("Summary")
|
||||
wss.cell(row=1, column=1, value="Summary — what feeds into USAC E-File").font = TITLE_FONT
|
||||
summary_rows = [
|
||||
("Block 3 Line 315 — Total revenues from resale", "='Block 3'!C" + str(3 + len(_BLOCK_3_LINES) + 1)),
|
||||
("Block 4-A total — End-user + non-telecom", "='Block 4-A'!C" + str(3 + len(_BLOCK_4A_LINES) + 1)),
|
||||
("Block 4-B Line 419 — Gross billed revenues", "='Block 4-B'!C4"),
|
||||
("Block 4-B Line 420 — USF contribution base", "='Block 4-B'!C5"),
|
||||
("Block 4-B Line 423 — Net USF contribution base", "='Block 4-B'!C8"),
|
||||
("Block 5 Line 510 — Regional total (must be 100%)", "='Block 5'!C" + str(4 + len(_BLOCK_5_REGIONS) - 1)),
|
||||
]
|
||||
for i, (label, formula) in enumerate(summary_rows, start=3):
|
||||
wss.cell(row=i, column=1, value=label)
|
||||
cell = wss.cell(row=i, column=2, value=formula)
|
||||
if "100%" in label or "Line 510" in label:
|
||||
cell.number_format = '0.00"%"'
|
||||
else:
|
||||
cell.number_format = '"$"#,##0.00'
|
||||
for col in (1, 2):
|
||||
wss.cell(row=i, column=col).border = BORDER_THIN
|
||||
wss.column_dimensions["A"].width = 55
|
||||
wss.column_dimensions["B"].width = 22
|
||||
|
||||
# Reorder tabs: README first, Summary last
|
||||
order = ["README", "Block 3", "Block 4-A", "Block 4-B", "Block 5", "Summary"]
|
||||
wb._sheets = [wb[name] for name in order if name in wb.sheetnames]
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
wb.save(str(out))
|
||||
LOG.info("499-A revenue workbook generated: %s", out)
|
||||
return str(out)
|
||||
146
scripts/document_gen/templates/guides/dno_list_enforcement.md
Normal file
146
scripts/document_gen/templates/guides/dno_list_enforcement.md
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# Do-Not-Originate (DNO) List Enforcement Guide
|
||||
|
||||
## Implementation Handbook for Voice Service Providers
|
||||
|
||||
**Prepared by Performance West Inc.**
|
||||
**Effective Date: 2026**
|
||||
|
||||
---
|
||||
|
||||
## 1. What Is the DNO List?
|
||||
|
||||
The FCC Do-Not-Originate (DNO) list is a database of telephone numbers that should **never** appear as the calling party number on the public switched telephone network (PSTN). These numbers are known to be used for illegal robocalling, spoofing, or fraud.
|
||||
|
||||
The DNO list is maintained by the **Industry Traceback Group (ITG)**, operated by USTelecom — the Broadband Association, and is distributed to voice service providers and gateway providers for enforcement.
|
||||
|
||||
**Regulatory basis:** 47 CFR § 64.6305 (2025 RMD Report & Order, effective February 5, 2026)
|
||||
|
||||
---
|
||||
|
||||
## 2. Who Must Enforce the DNO List?
|
||||
|
||||
All providers who file in the FCC Robocall Mitigation Database:
|
||||
- **Voice Service Providers** (originating carriers)
|
||||
- **Gateway Providers** (receiving international traffic)
|
||||
- **Intermediate Providers** (transit/tandem carriers)
|
||||
|
||||
If you have an RMD filing, you are expected to enforce the DNO list.
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Steps
|
||||
|
||||
### Step 1: Obtain the DNO List
|
||||
|
||||
**Check with your switch/platform provider first.** Many VoIP platforms, hosted PBX providers, and wholesale carriers already include DNO list enforcement as part of their service at no extra cost. If your provider handles this for you, confirm it in writing and reference it in your RMD filing — you may not need to manage the list yourself.
|
||||
|
||||
**If your platform does NOT include DNO enforcement,** you must obtain the DNO registry directly:
|
||||
|
||||
- **DNO Registry:** https://tracebacks.org/dno-registry/
|
||||
- This is the official source operated by USTelecom's Industry Traceback Group (ITG)
|
||||
- The registry provides a downloadable list of numbers that must be blocked
|
||||
- Register for access and download the current list
|
||||
- Updates are published regularly — check the registry for the current update schedule
|
||||
|
||||
**Other sources that may include DNO enforcement:**
|
||||
- Call analytics vendors (TransNexus, Neustar, Hiya) often include DNO blocking in their platforms
|
||||
- Your upstream carrier may already block DNO-listed numbers before they reach your network
|
||||
|
||||
### Step 2: Integrate into Your Call Routing
|
||||
|
||||
**For SIP/VoIP infrastructure:**
|
||||
|
||||
```
|
||||
# Example: SIP proxy rule (Kamailio/OpenSIPS/FreeSWITCH concept)
|
||||
# Block calls where the FROM or P-Asserted-Identity matches a DNO number
|
||||
|
||||
if (is_in_dno_list($fU)) {
|
||||
sl_send_reply("603", "Decline - DNO Listed Number");
|
||||
exit;
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation options by platform:**
|
||||
|
||||
| Platform | Method |
|
||||
|---|---|
|
||||
| FreeSWITCH | Load DNO list into mod_blacklist or use a Lua/Python script in dialplan |
|
||||
| Kamailio | Hash table lookup in route block, loaded from DB or file |
|
||||
| Asterisk | Use a database-backed dialplan function (ODBC or AstDB) |
|
||||
| Metaswitch | Policy rule in call control configuration |
|
||||
| BroadSoft/Cisco BroadWorks | System-level origination blocking rule |
|
||||
| Cloud UCaaS | Contact your UCaaS vendor — they should enforce DNO on your behalf |
|
||||
|
||||
### Step 3: Apply to All Call Origination Points
|
||||
|
||||
The DNO check must be applied at **every point where calls enter your network**:
|
||||
|
||||
1. **Customer-facing SIP trunks** — check the calling number before routing
|
||||
2. **Gateway interfaces** — check foreign-originated traffic at ingress
|
||||
3. **Wholesale interconnections** — check traffic received from downstream carriers
|
||||
4. **SIP registration** — optionally prevent registration of DNO-listed numbers as extensions
|
||||
|
||||
### Step 4: Establish an Update Schedule
|
||||
|
||||
| Frequency | Action |
|
||||
|---|---|
|
||||
| **Daily** (recommended) | Download latest DNO list, update blocking rules |
|
||||
| **Immediately** | If you receive a traceback request identifying a DNO number you're originating, block within 4 hours |
|
||||
| **Monthly** | Audit your blocking logs to confirm DNO enforcement is working |
|
||||
|
||||
### Step 5: Log and Report
|
||||
|
||||
Maintain logs of:
|
||||
- DNO list version/date loaded
|
||||
- Number of calls blocked per day due to DNO match
|
||||
- Any exceptions or overrides (there should be none for DNO numbers)
|
||||
- Date and time of each list update
|
||||
|
||||
These logs demonstrate compliance if the FCC or ITG requests evidence of enforcement.
|
||||
|
||||
---
|
||||
|
||||
## 4. What to Do If a Customer Complains
|
||||
|
||||
If a legitimate customer reports their number is being blocked:
|
||||
|
||||
1. Verify the number against the current DNO list
|
||||
2. If the number IS on the DNO list, the customer must contact the ITG to request removal
|
||||
3. Do NOT create exceptions to bypass DNO blocking — this violates your RMD certification
|
||||
4. Document the complaint and your response
|
||||
|
||||
---
|
||||
|
||||
## 5. Documenting DNO Enforcement in Your RMD Filing
|
||||
|
||||
Your RMD certification and/or robocall mitigation plan (Exhibit A) should include language such as:
|
||||
|
||||
> "[Company Name] immediately blocks any numbers identified on the FCC Do-Not-Originate (DNO) list. DNO list updates are applied daily to prevent origination of calls from numbers known to be used for illegal robocalling. Blocking is enforced at all network ingress points including customer SIP trunks, gateway interfaces, and wholesale interconnections."
|
||||
|
||||
---
|
||||
|
||||
## 6. Common Mistakes to Avoid
|
||||
|
||||
| Mistake | Consequence |
|
||||
|---|---|
|
||||
| Not mentioning DNO in your RMD filing | Filing flagged as deficient |
|
||||
| Updating the list monthly instead of daily | Stale data allows blocked numbers through |
|
||||
| Only blocking on one trunk, not all ingress | Partial enforcement = non-compliance |
|
||||
| Creating manual exceptions for "known good" customers | Undermines the entire program |
|
||||
| Not logging blocked calls | Cannot demonstrate compliance to FCC/ITG |
|
||||
|
||||
---
|
||||
|
||||
## 7. Resources
|
||||
|
||||
- **Industry Traceback Group:** https://tracebacks.org
|
||||
- **USTelecom:** https://www.ustelecom.org
|
||||
- **FCC RMD Portal:** https://apps.fcc.gov/rmd/
|
||||
- **47 CFR § 64.6305:** https://www.ecfr.gov/current/title-47/chapter-I/subchapter-B/part-64/subpart-CC
|
||||
- **2025 RMD Report & Order:** FCC 25-6 (effective February 5, 2026)
|
||||
|
||||
---
|
||||
|
||||
*This guide is provided for informational purposes as part of your RMD filing service. It is not legal advice. Consult with your regulatory counsel for implementation decisions specific to your network.*
|
||||
|
||||
*Performance West Inc. — performancewest.net — 1-888-411-0383*
|
||||
184
scripts/document_gen/templates/guides/kyc_procedures.md
Normal file
184
scripts/document_gen/templates/guides/kyc_procedures.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# Know Your Customer (KYC) Procedures Guide
|
||||
|
||||
## Implementation Handbook for Voice Service Providers
|
||||
|
||||
**Prepared by Performance West Inc.**
|
||||
**Effective Date: 2026**
|
||||
|
||||
---
|
||||
|
||||
## 1. What Are KYC Procedures Under the RMD?
|
||||
|
||||
The FCC's 2025 RMD Report & Order requires all voice service providers to implement **Know Your Customer (KYC) procedures** as part of their robocall mitigation program. KYC is the process of verifying the identity and legitimacy of customers before providing them with voice service — and monitoring them on an ongoing basis.
|
||||
|
||||
**Regulatory basis:** 47 CFR § 64.1200(n)(4), reinforced by the 2025 RMD Report & Order (FCC 25-6)
|
||||
|
||||
---
|
||||
|
||||
## 2. Required KYC Elements
|
||||
|
||||
Your KYC program must include:
|
||||
|
||||
### A. Information Collection at Signup
|
||||
|
||||
Collect the following from every new customer before activating service:
|
||||
|
||||
| Required Information | Purpose |
|
||||
|---|---|
|
||||
| Full legal name (individual or entity) | Identity verification |
|
||||
| Physical business address (no P.O. boxes for high-volume/toll-free) | Location verification |
|
||||
| Business identification (EIN/tax ID, or last 4 SSN for individuals) | Tax identity confirmation |
|
||||
| Government-issued photo ID | Identity authentication |
|
||||
| Business website or description of legitimate business purpose | Legitimacy assessment |
|
||||
| Contact phone and email | Communication channel |
|
||||
|
||||
### B. Verification Steps
|
||||
|
||||
For each new customer, perform these checks:
|
||||
|
||||
1. **Cross-reference business name + EIN** against your state's business registry or IRS database
|
||||
2. **Verify address** via USPS Address Verification or a third-party source (LexisNexis, Dun & Bradstreet)
|
||||
3. **Authenticate photo ID** — confirm it is genuine, not expired, and the name matches (see recommended tool below)
|
||||
4. **Open-source search** — search the customer name and principals for:
|
||||
- Prior association with illegal robocalling
|
||||
- Inclusion on the ITG's known bad-actor traceback list
|
||||
- FCC enforcement actions or complaints
|
||||
- Spoofing or fraud complaints
|
||||
|
||||
#### Recommended: Stripe Identity for ID Verification
|
||||
|
||||
For automated, reliable identity verification, we recommend **Stripe Identity** (https://stripe.com/identity). It provides:
|
||||
|
||||
- **Government-issued ID document verification** — authenticates the ID is real, not expired, and not tampered with
|
||||
- **Selfie matching with liveness detection** — confirms the person holding the ID is the person on it
|
||||
- **SSN-based ID number lookup** (US only) — cross-references against authoritative databases
|
||||
|
||||
**Pricing:**
|
||||
- **First 50 verifications: FREE** (included with any Stripe account)
|
||||
- **$1.50 per verification** after the free tier
|
||||
- **Volume discounts** available for 2,000+ verifications/month (contact Stripe)
|
||||
|
||||
This is significantly cheaper than traditional KYC vendors and integrates directly into your customer onboarding flow via API or hosted verification page. Most small-to-mid carriers will stay within the free tier (50 new customers per billing cycle). At $1.50 each after that, verifying 100 customers costs just $75.
|
||||
|
||||
**Integration:** Stripe Identity can be embedded as a link in your customer signup form — the customer clicks a link, takes a photo of their ID and a selfie, and Stripe returns a pass/fail result to your system within seconds. No manual review needed for passing verifications.
|
||||
|
||||
### C. Red-Flag Review
|
||||
|
||||
Trigger enhanced due diligence when any of the following occur:
|
||||
|
||||
- Customer is unwilling or unable to provide complete KYC information
|
||||
- Discrepancies between provided information and public records
|
||||
- Use of privacy-protected or anonymous registration services
|
||||
- Usage patterns inconsistent with stated business purpose
|
||||
- Prior complaints, tracebacks, or enforcement actions linked to the customer
|
||||
- Request for unusually high call volumes relative to stated business size
|
||||
|
||||
### D. Ongoing Monitoring
|
||||
|
||||
- **Annual re-vetting** for all customers (minimum)
|
||||
- **Immediate re-review** upon complaints, traceback requests, or anomalous traffic patterns
|
||||
- **High-volume/toll-free customers:** quarterly review
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Steps
|
||||
|
||||
### Step 1: Create Your KYC Intake Form
|
||||
|
||||
Build a customer onboarding form (paper or digital) that collects all required fields. Store responses in your CRM or customer database.
|
||||
|
||||
**Recommended fields:**
|
||||
```
|
||||
- Legal entity name
|
||||
- DBA / trade name
|
||||
- Entity type (LLC, Corp, Sole Prop, etc.)
|
||||
- EIN or Tax ID
|
||||
- State of formation
|
||||
- Physical address (street, city, state, zip)
|
||||
- Mailing address (if different)
|
||||
- Primary contact name, title, phone, email
|
||||
- Government-issued ID (upload or in-person)
|
||||
- Business website URL
|
||||
- Description of intended use of voice services
|
||||
- Expected monthly call volume
|
||||
- Authorized signatory for service agreement
|
||||
```
|
||||
|
||||
### Step 2: Build Your Verification Checklist
|
||||
|
||||
For each new customer, a team member should complete:
|
||||
|
||||
- [ ] Business name verified against state registry
|
||||
- [ ] EIN verified (IRS EIN verification letter or cross-reference)
|
||||
- [ ] Address validated via USPS or third-party
|
||||
- [ ] Photo ID reviewed and authenticated
|
||||
- [ ] Web search completed for bad-actor associations
|
||||
- [ ] ITG traceback list checked (if available)
|
||||
- [ ] FCC ECFS searched for complaints against this entity
|
||||
- [ ] No red flags identified (or enhanced due diligence completed)
|
||||
- [ ] Acceptable Use Policy signed by customer
|
||||
- [ ] Service activated
|
||||
|
||||
### Step 3: Acceptable Use Policy
|
||||
|
||||
Every customer must sign an Acceptable Use Policy (AUP) that includes:
|
||||
|
||||
- Prohibition of illegal robocalling, spoofing, and fraud
|
||||
- Prohibition of originating calls to/from DNO-listed numbers
|
||||
- Agreement to cooperate with traceback requests
|
||||
- Right to immediately suspend service for violations
|
||||
- Requirement to notify you of changes to business information
|
||||
|
||||
### Step 4: Set Up Ongoing Monitoring
|
||||
|
||||
Configure your systems to flag:
|
||||
- Customers exceeding their stated call volume by 2x or more
|
||||
- Sudden spikes in short-duration calls (potential robocall signature)
|
||||
- High Answer-Seizure Ratio (ASR) anomalies
|
||||
- Complaints received from downstream carriers or end users
|
||||
- Traceback requests from ITG or law enforcement
|
||||
|
||||
### Step 5: Document Your Process
|
||||
|
||||
Write an internal SOP document covering:
|
||||
- Who performs KYC reviews (role/title)
|
||||
- How records are stored and for how long
|
||||
- What triggers enhanced due diligence
|
||||
- How to handle customer refusals
|
||||
- Escalation procedures for red-flag findings
|
||||
|
||||
---
|
||||
|
||||
## 4. Documenting KYC in Your RMD Filing
|
||||
|
||||
Your RMD certification (Exhibit A) should include:
|
||||
|
||||
> "[Company Name] conducts internal Know Your Customer (KYC) procedures for all customers. At account signup or upon any material change in service usage, we require and collect: full legal name, physical business address, business identification (EIN or tax ID), government-issued photo ID, and a description of legitimate business purpose. We cross-reference business information against state registries, validate addresses via USPS, verify photo ID authenticity, and conduct open-source searches for prior robocalling associations. Enhanced due diligence is triggered when red flags are identified."
|
||||
|
||||
---
|
||||
|
||||
## 5. Common Mistakes to Avoid
|
||||
|
||||
| Mistake | Consequence |
|
||||
|---|---|
|
||||
| No KYC section in RMD filing | Filing flagged as deficient under 2026 requirements |
|
||||
| Collecting info but not verifying it | Non-compliance — verification is the key requirement |
|
||||
| No ongoing monitoring after signup | Fails the "continuous compliance" standard |
|
||||
| No AUP or terms of service | Cannot enforce against abusive customers |
|
||||
| Storing KYC data without security measures | Potential data breach liability |
|
||||
|
||||
---
|
||||
|
||||
## 6. Resources
|
||||
|
||||
- **FCC 47 CFR § 64.1200(n)(4):** KYC requirements for voice service providers
|
||||
- **ITG (Industry Traceback Group):** https://tracebacks.org
|
||||
- **FCC ECFS (complaints search):** https://www.fcc.gov/ecfs/
|
||||
- **USPS Address Verification:** https://tools.usps.com/zip-code-lookup.htm
|
||||
- **IRS EIN Verification:** https://www.irs.gov/businesses/small-businesses-self-employed/employer-id-numbers
|
||||
|
||||
---
|
||||
|
||||
*This guide is provided for informational purposes as part of your RMD filing service. It is not legal advice.*
|
||||
|
||||
*Performance West Inc. — performancewest.net — 1-888-411-0383*
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
# Material Change Update Procedures Guide
|
||||
|
||||
## Implementation Handbook for Voice Service Providers
|
||||
|
||||
**Prepared by Performance West Inc.**
|
||||
**Effective Date: 2026**
|
||||
|
||||
---
|
||||
|
||||
## 1. What Is the Material Change Requirement?
|
||||
|
||||
Under the FCC's 2025 RMD Report & Order (effective February 5, 2026), all providers with an RMD filing must update their certification within **10 business days** of any material change to their operations, ownership, or filing information.
|
||||
|
||||
Failure to update within 10 business days can result in a **$1,000 per day forfeiture**.
|
||||
|
||||
**Regulatory basis:** 47 CFR § 64.6305, 2025 RMD Report & Order (FCC 25-6)
|
||||
|
||||
---
|
||||
|
||||
## 2. What Counts as a Material Change?
|
||||
|
||||
| Category | Examples |
|
||||
|---|---|
|
||||
| **Ownership** | Change in controlling interest, merger, acquisition, new parent company |
|
||||
| **Corporate identity** | Legal name change, DBA change, new EIN |
|
||||
| **Contact information** | New robocall mitigation contact person, email, phone, address |
|
||||
| **STIR/SHAKEN status** | Change from partial to full implementation, switch to a different STI-CA, loss of SPC token |
|
||||
| **Provider classification** | Adding gateway operations, ceasing to be a VSP, becoming an intermediate provider |
|
||||
| **Upstream provider** | Changing the upstream carrier that provides STIR/SHAKEN signing |
|
||||
| **Robocall mitigation program** | Significant changes to KYC procedures, analytics vendors, blocking policies |
|
||||
| **Trade names / DBAs** | Adding or removing names under which you operate |
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Steps
|
||||
|
||||
### Step 1: Designate a Compliance Officer
|
||||
|
||||
Assign one person (and a backup) as the **RMD Compliance Officer** responsible for:
|
||||
- Monitoring for material changes
|
||||
- Initiating the update process within 10 business days
|
||||
- Maintaining an audit trail of updates
|
||||
|
||||
**Recommended:** The same person who is listed as the RMD contact on your filing.
|
||||
|
||||
### Step 2: Create a Material Change Checklist
|
||||
|
||||
Post this checklist where operations, legal, and management teams can see it:
|
||||
|
||||
**When ANY of the following occur, notify the RMD Compliance Officer immediately:**
|
||||
|
||||
- [ ] Company name or DBA changed
|
||||
- [ ] Ownership or controlling interest changed
|
||||
- [ ] New parent company, merger, or acquisition
|
||||
- [ ] RMD contact person changed (name, email, phone)
|
||||
- [ ] Principal office address changed
|
||||
- [ ] STIR/SHAKEN certificate authority changed
|
||||
- [ ] SPC token renewed, revoked, or transferred
|
||||
- [ ] Upstream provider changed
|
||||
- [ ] Provider type changed (added/removed VSP/gateway/intermediate)
|
||||
- [ ] Robocall mitigation program materially revised
|
||||
- [ ] New analytics vendor deployed or existing vendor discontinued
|
||||
- [ ] Trade names added or removed
|
||||
|
||||
### Step 3: Establish an Internal Notification Process
|
||||
|
||||
Create a simple workflow:
|
||||
|
||||
```
|
||||
Change occurs (e.g., new upstream provider signed)
|
||||
↓
|
||||
Department head notifies RMD Compliance Officer (email/ticket)
|
||||
<20><><EFBFBD>
|
||||
Compliance Officer logs the change in the tracking spreadsheet
|
||||
↓
|
||||
Within 5 business days: prepare updated RMD filing content
|
||||
↓
|
||||
Within 10 business days: submit update to FCC RMD portal
|
||||
↓
|
||||
Confirm update accepted, save confirmation screenshot
|
||||
```
|
||||
|
||||
### Step 4: Update the RMD Portal
|
||||
|
||||
To submit an update:
|
||||
|
||||
1. Log in to the FCC RMD portal at https://apps.fcc.gov/rmd/ (MFA required since Feb 5, 2026)
|
||||
2. Navigate to your existing certification
|
||||
3. Click "Update" or "Edit"
|
||||
4. Modify the relevant fields
|
||||
5. Re-upload your certification letter/Exhibit A if the mitigation plan changed
|
||||
6. Submit and save the confirmation page
|
||||
|
||||
**Or:** Contact Performance West — we can submit the update on your behalf as your authorized filing agent.
|
||||
|
||||
### Step 5: Maintain an Audit Trail
|
||||
|
||||
Keep a log of all material changes and RMD updates:
|
||||
|
||||
| Date | Change Description | Notified By | Updated in RMD | Confirmation # |
|
||||
|---|---|---|---|---|
|
||||
| 2026-03-15 | New upstream provider (ABC Telecom) | VP Engineering | 2026-03-18 | RMD-UPD-12345 |
|
||||
| 2026-04-01 | Contact email changed to new@company.com | HR | 2026-04-03 | RMD-UPD-12346 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Documenting Material Change Procedures in Your RMD Filing
|
||||
|
||||
Your RMD certification should include:
|
||||
|
||||
> "[Company Name] updates its RMD certification and CORES registration within 10 business days of any material change, including but not limited to changes in ownership, contacts, STIR/SHAKEN posture, upstream provider, or trade names, per 47 CFR § 64.6305. A designated compliance officer monitors for material changes and maintains an audit trail of all updates."
|
||||
|
||||
---
|
||||
|
||||
## 5. Penalties for Non-Compliance
|
||||
|
||||
| Violation | Penalty |
|
||||
|---|---|
|
||||
| Failure to update within 10 business days | $1,000/day forfeiture |
|
||||
| False or inaccurate information in filing | $10,000 base forfeiture |
|
||||
| Failure to maintain compliant certification | Removal from RMD (downstream carriers must block your traffic within 30 days) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Common Mistakes to Avoid
|
||||
|
||||
| Mistake | Consequence |
|
||||
|---|---|
|
||||
| No material change language in RMD filing | Filing flagged as deficient |
|
||||
| Updating only annually during recertification | Misses the 10-day requirement for mid-year changes |
|
||||
| No internal notification process | Changes happen without anyone updating the RMD |
|
||||
| Updating the filing but not the Exhibit A | Inconsistency between certification and plan |
|
||||
| No audit trail | Cannot demonstrate timely compliance if audited |
|
||||
|
||||
---
|
||||
|
||||
*This guide is provided for informational purposes as part of your RMD filing service. It is not legal advice.*
|
||||
|
||||
*Performance West Inc. — performancewest.net — 1-888-411-0383*
|
||||
137
scripts/document_gen/templates/guides/traceback_response.md
Normal file
137
scripts/document_gen/templates/guides/traceback_response.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Traceback Response Procedures Guide
|
||||
|
||||
## Implementation Handbook for Voice Service Providers
|
||||
|
||||
**Prepared by Performance West Inc.**
|
||||
**Effective Date: 2026**
|
||||
|
||||
---
|
||||
|
||||
## 1. What Is a Traceback Request?
|
||||
|
||||
A traceback is the process of identifying the originating source of an illegal robocall by tracing the call path backward through the network — from the terminating carrier, through intermediate/transit providers, back to the originating carrier and ultimately the customer who placed the call.
|
||||
|
||||
Traceback requests are issued by:
|
||||
- **Industry Traceback Group (ITG)** — operated by USTelecom
|
||||
- **FCC Enforcement Bureau**
|
||||
- **State attorneys general**
|
||||
- **Federal and state law enforcement**
|
||||
|
||||
**Your obligation:** Respond fully and completely within **24 hours**.
|
||||
|
||||
**Regulatory basis:** 47 CFR § 64.6305(d)(2)(iii), (e)(2)(iii), (f)(2)(iii)
|
||||
|
||||
---
|
||||
|
||||
## 2. Implementation Steps
|
||||
|
||||
### Step 1: Designate a 24/7 Traceback Contact
|
||||
|
||||
You MUST have someone available to respond to traceback requests within 24 hours, including weekends and holidays.
|
||||
|
||||
**Recommended structure:**
|
||||
- **Primary contact:** Your RMD compliance officer or NOC manager
|
||||
- **Backup contact:** A second person with access to CDR systems
|
||||
- **Email alias:** Create a dedicated email like traceback@yourcompany.com that forwards to both
|
||||
- **Phone:** A direct line or on-call number (not a general IVR)
|
||||
|
||||
Register your traceback contact with the ITG at https://tracebacks.org
|
||||
|
||||
### Step 2: Ensure CDR Access
|
||||
|
||||
Your traceback contact must be able to:
|
||||
- Search Call Detail Records (CDRs) by called number, calling number, and date/time
|
||||
- Identify which customer or trunk group originated a specific call
|
||||
- Export relevant CDR data for the requesting party
|
||||
- Access records going back at least 18 months
|
||||
|
||||
**Systems to prepare:**
|
||||
- CDR database or data warehouse with search capability
|
||||
- SIP/SS7 log access (if available)
|
||||
- Customer account lookup by trunk/SIP registration
|
||||
|
||||
### Step 3: Create a Traceback Response Template
|
||||
|
||||
When you receive a traceback request, respond with:
|
||||
|
||||
```
|
||||
TRACEBACK RESPONSE
|
||||
Date: [today]
|
||||
Request Reference: [ITG/FCC reference number]
|
||||
Responding Provider: [Your company name]
|
||||
FRN: [Your FRN]
|
||||
Contact: [Name, email, phone]
|
||||
|
||||
CALL DETAILS REQUESTED:
|
||||
Called Number: [number from request]
|
||||
Calling Number: [number from request]
|
||||
Date/Time: [from request]
|
||||
|
||||
FINDINGS:
|
||||
Call found in our records: YES / NO
|
||||
Originating customer/trunk: [customer name or trunk ID]
|
||||
Customer account number: [if applicable]
|
||||
Upstream source (if transit): [provider name, trunk ID]
|
||||
|
||||
CDR excerpt attached: YES / NO
|
||||
|
||||
ACTION TAKEN:
|
||||
[e.g., "Customer notified of violation", "Traffic blocked",
|
||||
"Account suspended pending investigation", "Referred to upstream provider"]
|
||||
|
||||
Signed: [Name, Title]
|
||||
```
|
||||
|
||||
### Step 4: Establish Response SLA
|
||||
|
||||
| Timeline | Action |
|
||||
|---|---|
|
||||
| **0–1 hour** | Acknowledge receipt of traceback request |
|
||||
| **1–4 hours** | Search CDRs, identify the source |
|
||||
| **4–12 hours** | Prepare response with CDR evidence |
|
||||
| **Within 24 hours** | Send complete response to the requesting party |
|
||||
| **Immediately** | If the source is a known bad actor, block the traffic |
|
||||
|
||||
### Step 5: Take Enforcement Action
|
||||
|
||||
After identifying the source:
|
||||
1. **Notify the customer** that they are the subject of a traceback
|
||||
2. **Review the customer's account** for patterns of abuse
|
||||
3. **If abuse is confirmed:** suspend or terminate service per your AUP
|
||||
4. **If the call was transit traffic:** forward the traceback to your upstream provider
|
||||
5. **Document everything** — enforcement actions, customer communications, blocking orders
|
||||
|
||||
---
|
||||
|
||||
## 3. Documenting Traceback Procedures in Your RMD Filing
|
||||
|
||||
Your RMD certification should include:
|
||||
|
||||
> "[Company Name] commits to respond fully and completely to all traceback requests from the Commission, civil and criminal law enforcement, and the industry traceback consortium, and to do so within 24 hours of receipt. [Company Name] cooperates with the Industry Traceback Group operated by USTelecom and provides requested call detail records and tracing information necessary to identify the origin of suspected illegal robocalls."
|
||||
|
||||
---
|
||||
|
||||
## 4. What Happens If You Don't Respond
|
||||
|
||||
| Failure | Consequence |
|
||||
|---|---|
|
||||
| No response within 24 hours | Reported to FCC as non-responsive provider |
|
||||
| Pattern of non-response | FCC enforcement action, potential RMD removal |
|
||||
| RMD removal | Downstream carriers must block your traffic within 30 days |
|
||||
|
||||
---
|
||||
|
||||
## 5. Common Mistakes to Avoid
|
||||
|
||||
| Mistake | Consequence |
|
||||
|---|---|
|
||||
| No 24/7 contact registered with ITG | Traceback requests go unanswered |
|
||||
| CDRs not retained long enough | Cannot trace historical calls |
|
||||
| Responding to ITG but not taking action against the customer | FCC views this as insufficient mitigation |
|
||||
| No traceback language in RMD filing | Filing flagged as deficient |
|
||||
|
||||
---
|
||||
|
||||
*This guide is provided for informational purposes as part of your RMD filing service. It is not legal advice.*
|
||||
|
||||
*Performance West Inc. — performancewest.net — 1-888-411-0383*
|
||||
324
scripts/document_gen/templates/ocn_request_form_generator.py
Normal file
324
scripts/document_gen/templates/ocn_request_form_generator.py
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
"""
|
||||
Generate the NECA Company Code (OCN) Request Form packet.
|
||||
|
||||
NECA has not revised the Company Code Request Form since August 2023;
|
||||
the current form is still in use in 2026. Standard processing is
|
||||
$550 / 10 business days; expedited is $675 / 5 business days. Payment is
|
||||
made directly to NECA (Performance West does not collect the NECA fee
|
||||
separately — it is a pass-through).
|
||||
|
||||
Required supporting documentation per NECA (varies by service category):
|
||||
|
||||
* Legal document proving existence (Articles of Incorporation with
|
||||
state seal, state registration, etc.) — always required
|
||||
* For CLEC, ULEC, CAP, Local Reseller: state PUC certification
|
||||
* For Interexchange Carrier: state PUC approval where applicable
|
||||
* For ETHX: signed customer contracts + service description
|
||||
* For IPES (VoIP): signed interconnection agreements (or an approved
|
||||
interconnection order) + end-user contractual agreements or invoices
|
||||
* For Wireless/PCS: FCC radio/PCS license
|
||||
* For Wireless/PCS resellers: interconnection agreement with carrier
|
||||
|
||||
This generator produces:
|
||||
1. A cover letter introducing the request on Performance West letterhead
|
||||
2. A filled-in version of the 2-page NECA form (replicating the fields;
|
||||
the customer prints, signs, and submits OR we fax/email on their behalf)
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.ocn_request_form_generator import (
|
||||
generate_ocn_request_packet,
|
||||
)
|
||||
path = generate_ocn_request_packet(
|
||||
entity_name="Falcon Broadband LLC",
|
||||
service_category="IPES",
|
||||
operating_states=["CA","NY","TX"],
|
||||
expedited=True,
|
||||
...,
|
||||
)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.ocn_request")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — OCN request generation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
BODY_SIZE = Pt(10) if Document else None
|
||||
HEADING_SIZE = Pt(12) if Document else None
|
||||
|
||||
|
||||
# NECA contact block — unchanged since 2023 form revision.
|
||||
NECA_ADDRESS = (
|
||||
"NECA Company Code Administrator\n"
|
||||
"60 Columbia Road, Building A \u2014 2nd Floor\n"
|
||||
"Morristown, NJ 07960\n"
|
||||
"Phone: 973-884-8105 | Fax: 973-993-1063 | Email: ccfees@neca.org"
|
||||
)
|
||||
|
||||
NECA_FEES = {
|
||||
"standard": {"cents": 55000, "processing_days": 10, "label": "Standard (10 business days)"},
|
||||
"expedited": {"cents": 67500, "processing_days": 5, "label": "Expedited (5 business days)"},
|
||||
}
|
||||
|
||||
SERVICE_CATEGORIES = {
|
||||
"CAP": "Competitive Access Provider",
|
||||
"ETHX": "Ethernet Exchange",
|
||||
"CLEC": "Competitive Local Exchange Carrier",
|
||||
"IC": "Interexchange Carrier",
|
||||
"IPES": "Internet Protocol Enabled Services (VoIP)",
|
||||
"LRSL": "Local Exchange Reseller",
|
||||
"PCS": "Personal Communications Service",
|
||||
"PCSR": "PCS Reseller",
|
||||
"ULEC": "Unbundled Local Exchange Carrier",
|
||||
"WIRE": "Wireless Carrier",
|
||||
"WRSL": "Wireless Reseller",
|
||||
}
|
||||
|
||||
# Required documentation by category. Lifted directly from the NECA form
|
||||
# (page 2, "REQUIRED DOCUMENTATION" section).
|
||||
REQUIRED_DOCS_BY_CATEGORY = {
|
||||
"IPES": [
|
||||
"Legal document (e.g., Articles of Incorporation with state seal) as proof of existence.",
|
||||
"Signed interconnection agreements (or evidence of an interconnection order pursuant to an approved tariff).",
|
||||
"Signed contractual agreements OR an invoice with end-user customers showing proof of customer.",
|
||||
"Detailed description of the type of IPES service being provided including areas served.",
|
||||
],
|
||||
"CLEC": [
|
||||
"Legal document (e.g., Articles of Incorporation with state seal) as proof of existence.",
|
||||
"Copy of the certification by the state Public Utility Commission.",
|
||||
],
|
||||
"ULEC": [
|
||||
"Legal document proving existence.",
|
||||
"Copy of the certification by the state Public Utility Commission.",
|
||||
],
|
||||
"CAP": [
|
||||
"Legal document proving existence.",
|
||||
"Copy of the certification by the state Public Utility Commission.",
|
||||
],
|
||||
"LRSL": [
|
||||
"Legal document proving existence.",
|
||||
"Copy of the certification by the state Public Utility Commission.",
|
||||
],
|
||||
"IC": [
|
||||
"Legal document proving existence.",
|
||||
"State PUC approval where applicable.",
|
||||
],
|
||||
"ETHX": [
|
||||
"Legal document proving existence.",
|
||||
"Proof of service and customers (contractual agreements + service description).",
|
||||
],
|
||||
"WIRE": [
|
||||
"Legal document proving existence.",
|
||||
"Copy of the company's FCC radio/PCS license.",
|
||||
],
|
||||
"PCS": [
|
||||
"Legal document proving existence.",
|
||||
"Copy of the company's FCC radio/PCS license.",
|
||||
],
|
||||
"WRSL": [
|
||||
"Legal document proving existence.",
|
||||
"Copy of the interconnection agreement with the wireless carrier.",
|
||||
],
|
||||
"PCSR": [
|
||||
"Legal document proving existence.",
|
||||
"Copy of the interconnection agreement with the wireless carrier.",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _heading(doc, text: str, level: int = 1) -> None:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12 if level == 1 else 8)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
run = p.add_run(text)
|
||||
run.bold = True
|
||||
run.font.size = HEADING_SIZE if level == 1 else Pt(11)
|
||||
run.font.color.rgb = NAVY
|
||||
|
||||
|
||||
def _body(doc, text: str, bold: bool = False) -> None:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
run = p.add_run(text)
|
||||
run.font.size = BODY_SIZE
|
||||
run.bold = bold
|
||||
|
||||
|
||||
def _field_line(doc, label: str, value: str) -> None:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
run_l = p.add_run(f"{label}: ")
|
||||
run_l.bold = True
|
||||
run_l.font.size = BODY_SIZE
|
||||
run_v = p.add_run(value or "_______________________")
|
||||
run_v.font.size = BODY_SIZE
|
||||
|
||||
|
||||
def generate_ocn_request_packet(
|
||||
# Entity identity
|
||||
entity_name: str,
|
||||
legal_entity_full: str = "",
|
||||
# Requestor (the person filling out the form — defaults to PW)
|
||||
requestor_name: str = "Justin Hannah",
|
||||
requestor_employer: str = "Performance West Inc.",
|
||||
requestor_voice: str = "888-411-0383",
|
||||
requestor_fax: str = "",
|
||||
requestor_mailing_address: str = "30 N Gould St, Ste N, Sheridan, WY 82801",
|
||||
requestor_email: str = "filings@performancewest.net",
|
||||
# Company contact (client-side)
|
||||
company_contact_name: str = "",
|
||||
company_contact_voice: str = "",
|
||||
company_contact_fax: str = "",
|
||||
company_contact_email: str = "",
|
||||
company_contact_address: str = "",
|
||||
# Service category — key from SERVICE_CATEGORIES (default IPES for VoIP)
|
||||
service_category: str = "IPES",
|
||||
operating_states: list[str] | None = None,
|
||||
expedited: bool = False,
|
||||
# Output
|
||||
output_path: str = "/tmp/ocn_request_packet.docx",
|
||||
) -> Optional[str]:
|
||||
"""Generate the NECA OCN request packet DOCX (cover letter + filled form)."""
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
operating_states = operating_states or []
|
||||
category = service_category.upper()
|
||||
if category not in SERVICE_CATEGORIES:
|
||||
LOG.warning(
|
||||
"generate_ocn_request_packet: unknown service_category %r, "
|
||||
"defaulting to IPES",
|
||||
service_category,
|
||||
)
|
||||
category = "IPES"
|
||||
|
||||
fee = NECA_FEES["expedited"] if expedited else NECA_FEES["standard"]
|
||||
legal_entity_full = legal_entity_full or entity_name
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
|
||||
doc = Document()
|
||||
for section in doc.sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1)
|
||||
section.right_margin = Inches(1)
|
||||
|
||||
# ── Page 1: Cover letter ────────────────────────────────────────
|
||||
title_p = doc.add_paragraph()
|
||||
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
title_run = title_p.add_run("NECA Company Code (OCN) Request Packet")
|
||||
title_run.font.size = Pt(14)
|
||||
title_run.bold = True
|
||||
title_run.font.color.rgb = NAVY
|
||||
|
||||
_body(doc, f"Date: {today}")
|
||||
_body(doc, "")
|
||||
_body(doc, "To:")
|
||||
_body(doc, NECA_ADDRESS)
|
||||
_body(doc, "")
|
||||
_body(doc, (
|
||||
f"Dear NECA Company Code Administrator,\n\n"
|
||||
f"Please find enclosed a Company Code Request Form and supporting "
|
||||
f"documentation for {legal_entity_full}. We are submitting this "
|
||||
f"request in the {category} \u2014 "
|
||||
f"{SERVICE_CATEGORIES[category]} category."
|
||||
))
|
||||
_body(doc, (
|
||||
f"Processing type requested: {fee['label']}.\n"
|
||||
f"Payment of ${fee['cents']/100:.2f} will accompany this filing."
|
||||
))
|
||||
if operating_states and category in ("CLEC", "ULEC"):
|
||||
_body(doc, (
|
||||
f"States of operation (multiple codes will be assigned for "
|
||||
f"CLEC/ULEC): {', '.join(operating_states)}"
|
||||
))
|
||||
_body(doc, (
|
||||
"If you have questions or need additional information, please "
|
||||
f"contact me directly at {requestor_voice} or {requestor_email}."
|
||||
))
|
||||
_body(doc, "")
|
||||
_body(doc, "Sincerely,")
|
||||
_body(doc, "")
|
||||
_body(doc, "")
|
||||
_body(doc, requestor_name, bold=True)
|
||||
_body(doc, requestor_employer)
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
# ── Page 2: Company Code Request Form (replicated fields) ──────
|
||||
_heading(doc, "COMPANY CODE REQUEST FORM")
|
||||
_body(doc, f"Issued: August 2023 | Date of Request: {today}")
|
||||
|
||||
_heading(doc, "REQUESTOR INFORMATION", level=2)
|
||||
_field_line(doc, "Requestor's Name", requestor_name)
|
||||
_field_line(doc, "Employer", requestor_employer)
|
||||
_field_line(doc, "Mailing Address", requestor_mailing_address)
|
||||
_field_line(doc, "Voice Number", requestor_voice)
|
||||
_field_line(doc, "Fax Number", requestor_fax)
|
||||
_field_line(doc, "Email Address", requestor_email)
|
||||
_body(doc, (
|
||||
"Note: This contact will also be listed in iconectiv's routing "
|
||||
"products as \"Agent for Service of Process\". To use a different "
|
||||
"contact, notify iconectiv TruOps TRA at 732-699-6700."
|
||||
))
|
||||
|
||||
_heading(doc, "COMPANY INFORMATION", level=2)
|
||||
_field_line(doc, "Company Name / Full Legal Entity Name", legal_entity_full)
|
||||
_field_line(doc, "Company Contact", company_contact_name)
|
||||
_field_line(doc, "Voice Number", company_contact_voice)
|
||||
_field_line(doc, "Fax Number", company_contact_fax)
|
||||
_field_line(doc, "Email Address", company_contact_email)
|
||||
_field_line(doc, "Company Contact Mailing Address", company_contact_address)
|
||||
|
||||
_heading(doc, "SERVICE CATEGORY REQUESTED", level=2)
|
||||
for code, label in SERVICE_CATEGORIES.items():
|
||||
mark = "\u2611" if code == category else "\u2610"
|
||||
states = ""
|
||||
if code == category and operating_states and code in ("CLEC", "ULEC"):
|
||||
states = " Operating States: " + ", ".join(operating_states)
|
||||
expedited_mark = "\u2611 EXPEDITED" if (code == category and expedited) else ""
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
run = p.add_run(f" {mark} {code} \u2014 {label}{states} {expedited_mark}")
|
||||
run.font.size = BODY_SIZE
|
||||
|
||||
_heading(doc, "REQUIRED DOCUMENTATION (attach to submission)", level=2)
|
||||
for doc_item in REQUIRED_DOCS_BY_CATEGORY.get(category, []):
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.clear()
|
||||
run = p.add_run(doc_item)
|
||||
run.font.size = BODY_SIZE
|
||||
|
||||
_heading(doc, "PRICING AND PAYMENT", level=2)
|
||||
_body(doc, (
|
||||
f"Submit ${fee['cents']/100:.2f} for this "
|
||||
f"{'expedited' if expedited else 'standard'} request. Code requests "
|
||||
f"are processed within {fee['processing_days']} business days of "
|
||||
f"receipt of all required documentation including payment."
|
||||
))
|
||||
|
||||
_heading(doc, "SUBMISSION", level=2)
|
||||
_body(doc, (
|
||||
"Fax the completed form + documentation to +1 973-993-1063, OR "
|
||||
"email to ccfees@neca.org, OR mail to:"
|
||||
))
|
||||
_body(doc, NECA_ADDRESS)
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("NECA OCN request packet generated: %s", out)
|
||||
return str(out)
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
"""
|
||||
Generate the FCC Form 499-A Reseller Certification Attestation.
|
||||
|
||||
Produces a DOCX that a filer sends to its reseller customer for the
|
||||
reseller's authorized officer to sign. The signed attestation
|
||||
establishes that the purchased service is being resold into a
|
||||
telecommunications or interconnected VoIP offering, and that USF
|
||||
contribution is flowing (per 2026 Form 499-A Section IV.C.4).
|
||||
|
||||
The sample certification text is reproduced verbatim from
|
||||
``site/src/lib/fcc_constants.ts::RESELLER_CERTIFICATION_SAMPLE_TEXT``
|
||||
and mirrored in the constant ``RESELLER_CERTIFICATION_SAMPLE_TEXT``
|
||||
below. If upstream source text changes, update both locations.
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.reseller_cert_attestation_generator import (
|
||||
generate_reseller_cert_attestation,
|
||||
)
|
||||
path = generate_reseller_cert_attestation(
|
||||
output_path="/tmp/reseller_cert.docx",
|
||||
filer_legal_name="Acme Telco LLC",
|
||||
filer_filer_id_499="812345",
|
||||
reseller_legal_name="Beta Reseller Corp",
|
||||
reseller_filer_id_499="812999",
|
||||
reporting_year=2025,
|
||||
)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.reseller_cert_attestation")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — reseller cert attestation unavailable")
|
||||
Document = None # type: ignore[assignment,misc]
|
||||
|
||||
_NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
|
||||
# Verbatim from site/src/lib/fcc_constants.ts (RESELLER_CERTIFICATION_SAMPLE_TEXT).
|
||||
RESELLER_CERTIFICATION_SAMPLE_TEXT = (
|
||||
"I certify under penalty of perjury that the company is purchasing "
|
||||
"service(s) for resale, at least in part, and that the company is "
|
||||
"incorporating the purchased services into its own offerings which "
|
||||
"are, at least in part, assessable U.S. telecommunications or "
|
||||
"interconnected Voice over Internet Protocol services. I also "
|
||||
"certify under penalty of perjury that the company either directly "
|
||||
"contributes or has a reasonable expectation that another entity in "
|
||||
"the downstream chain of resellers directly contributes to the "
|
||||
"federal universal service support mechanisms on the assessable "
|
||||
"portion of revenues from offerings that incorporate the purchased "
|
||||
"services."
|
||||
)
|
||||
|
||||
|
||||
def _sp(p, after=6, before=0):
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
if before:
|
||||
p.paragraph_format.space_before = Pt(before)
|
||||
|
||||
|
||||
def _h(doc, text):
|
||||
p = doc.add_paragraph(); r = p.add_run(text)
|
||||
r.font.size = Pt(12); r.bold = True; r.font.color.rgb = _NAVY
|
||||
_sp(p, after=4, before=8)
|
||||
|
||||
|
||||
def _b(doc, text, bold=False, size=10):
|
||||
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(text); r.font.size = Pt(size); r.bold = bold
|
||||
_sp(p, after=6)
|
||||
|
||||
|
||||
def generate_reseller_cert_attestation(
|
||||
output_path: str,
|
||||
filer_legal_name: str,
|
||||
filer_filer_id_499: str,
|
||||
reseller_legal_name: str,
|
||||
reseller_filer_id_499: str = "",
|
||||
reporting_year: int = 0,
|
||||
effective_date: str = "",
|
||||
filer_contact_name: str = "",
|
||||
filer_contact_email: str = "",
|
||||
filer_contact_phone: str = "",
|
||||
**_: dict,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Produce the Reseller Certification Attestation DOCX for signature
|
||||
by the reseller's authorized officer.
|
||||
"""
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
if reporting_year == 0:
|
||||
reporting_year = datetime.now().year
|
||||
effective = effective_date or datetime.now().strftime("%B %d, %Y")
|
||||
|
||||
doc = Document()
|
||||
for s in doc.sections:
|
||||
s.top_margin = Inches(1); s.bottom_margin = Inches(1)
|
||||
s.left_margin = Inches(1.25); s.right_margin = Inches(1.25)
|
||||
|
||||
# Title
|
||||
tp = doc.add_paragraph(); tp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
t = tp.add_run("Reseller Certification Attestation")
|
||||
t.font.size = Pt(15); t.bold = True; t.font.color.rgb = _NAVY
|
||||
_sp(tp, after=2)
|
||||
|
||||
sp = doc.add_paragraph(); sp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
s = sp.add_run(
|
||||
f"FCC Form 499-A Section IV.C.4 \u2014 Reporting Year {reporting_year}"
|
||||
)
|
||||
s.font.size = Pt(10); s.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
|
||||
_sp(sp, after=8)
|
||||
|
||||
# ── 1. Parties ────────────────────────────────────────────────
|
||||
_h(doc, "1. Parties")
|
||||
_b(doc, (
|
||||
f"Filer (upstream wholesaler): {filer_legal_name}"
|
||||
+ (f" | Filer 499 ID: {filer_filer_id_499}" if filer_filer_id_499 else "")
|
||||
))
|
||||
_b(doc, (
|
||||
f"Reseller (purchaser): {reseller_legal_name}"
|
||||
+ (f" | Filer 499 ID: {reseller_filer_id_499}"
|
||||
if reseller_filer_id_499 else " | Filer 499 ID: _______________")
|
||||
))
|
||||
_b(doc, f"Effective Reporting Year: {reporting_year}")
|
||||
_b(doc, f"Date of Attestation: {effective}")
|
||||
|
||||
# ── 2. Purpose ────────────────────────────────────────────────
|
||||
_h(doc, "2. Purpose")
|
||||
_b(doc, (
|
||||
f"Pursuant to Section IV.C.4 of the FCC Form 499-A Instructions "
|
||||
f"({reporting_year}), {filer_legal_name} is required to obtain from "
|
||||
f"{reseller_legal_name} a signed certification that the services "
|
||||
f"purchased by the reseller are being incorporated into assessable "
|
||||
f"U.S. telecommunications or interconnected Voice over Internet "
|
||||
f"Protocol offerings, and that federal universal-service "
|
||||
f"contributions are flowing on those resold services. This "
|
||||
f"Attestation satisfies that requirement."
|
||||
))
|
||||
|
||||
# ── 3. Certification Language (verbatim) ──────────────────────
|
||||
_h(doc, "3. Certification")
|
||||
_b(doc, RESELLER_CERTIFICATION_SAMPLE_TEXT)
|
||||
|
||||
# ── 4. Annual Renewal Notice ──────────────────────────────────
|
||||
_h(doc, "4. Annual Renewal")
|
||||
_b(doc, (
|
||||
"This Attestation is effective for the reporting year stated "
|
||||
f"above and must be renewed annually. {filer_legal_name} will "
|
||||
f"request an updated, signed Attestation from {reseller_legal_name} "
|
||||
f"on or about January 1 of each subsequent reporting year. A "
|
||||
f"current signed Attestation must be on file at the time "
|
||||
f"{filer_legal_name} submits its Form 499-A for the reporting "
|
||||
f"year."
|
||||
))
|
||||
|
||||
# ── 5. Reseller Authorized Officer Signature ──────────────────
|
||||
_h(doc, "5. Signature — Reseller Authorized Officer")
|
||||
_b(doc, (
|
||||
f"By signing below, the undersigned officer of {reseller_legal_name} "
|
||||
f"certifies that he or she is an authorized officer of the company, "
|
||||
f"has personal knowledge of the matters certified above, and "
|
||||
f"executes this Attestation on behalf of the company."
|
||||
))
|
||||
|
||||
# Blank signature block
|
||||
sig = doc.add_paragraph(); sig.add_run("_" * 55).font.size = Pt(10)
|
||||
_sp(sig, after=2)
|
||||
|
||||
nm = doc.add_paragraph()
|
||||
nm.add_run("Name (printed): ").font.size = Pt(10)
|
||||
nm.add_run("_" * 40).font.size = Pt(10)
|
||||
_sp(nm, after=4)
|
||||
|
||||
tt = doc.add_paragraph()
|
||||
tt.add_run("Title: ").font.size = Pt(10)
|
||||
tt.add_run("_" * 48).font.size = Pt(10)
|
||||
_sp(tt, after=4)
|
||||
|
||||
co = doc.add_paragraph()
|
||||
co.add_run("Company: ").font.size = Pt(10)
|
||||
co.add_run(reseller_legal_name).font.size = Pt(10)
|
||||
_sp(co, after=4)
|
||||
|
||||
dt = doc.add_paragraph()
|
||||
dt.add_run("Date: ").font.size = Pt(10)
|
||||
dt.add_run("_" * 20).font.size = Pt(10)
|
||||
_sp(dt, after=10)
|
||||
|
||||
# ── 6. Filer Contact (for returning the signed form) ──────────
|
||||
_h(doc, "6. Return Signed Attestation To")
|
||||
_b(doc, filer_legal_name)
|
||||
if filer_contact_name:
|
||||
_b(doc, f"Attention: {filer_contact_name}")
|
||||
if filer_contact_email:
|
||||
_b(doc, f"Email: {filer_contact_email}")
|
||||
if filer_contact_phone:
|
||||
_b(doc, f"Phone: {filer_contact_phone}")
|
||||
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(out))
|
||||
LOG.info("Reseller Certification Attestation generated: %s", out)
|
||||
return str(out)
|
||||
602
scripts/document_gen/templates/rmd_exhibit_a_generator.py
Normal file
602
scripts/document_gen/templates/rmd_exhibit_a_generator.py
Normal file
|
|
@ -0,0 +1,602 @@
|
|||
"""
|
||||
Generate the Robocall Mitigation Plan (Exhibit A to RMD filing).
|
||||
|
||||
Follows the Performance West canonical 7-section outline observed across
|
||||
all example filings in docs/examplefilings/ (Engage, Franklin, Zingo,
|
||||
Syntracom, VoIPFlo, Fortel). This is a deterministic, template-driven
|
||||
generator — no LLM — with role-specific paragraphs for each carrier
|
||||
category.
|
||||
|
||||
2026 Updates (2025 RMD R&O effective Feb 5, 2026):
|
||||
* References recertification due March 2, 2026 (March 1 is Sunday)
|
||||
* Multi-factor authentication on RMD portal
|
||||
* $10,000 false-info forfeiture and $1,000/day late-update forfeiture
|
||||
* 10-business-day material-change update deadline
|
||||
* Explicit DNO list blocking + 4-hour high-risk alert review SLA
|
||||
* Penalty-of-perjury declaration at the end
|
||||
|
||||
Canonical section outline (mirrors docs/examplefilings/):
|
||||
|
||||
Introduction (scope narrative)
|
||||
1. Contact Information (+ Principals/Affiliates, past-2-years affirmation)
|
||||
2. Implementation of STIR/SHAKEN Framework (Option 1/2/3, named upstream)
|
||||
3. Robocall Monitoring and Mitigation
|
||||
3.5 Know Your Customer (KYC) Procedures (Performed In-House)
|
||||
4. Call Analytics and Upstream Provider Procedures
|
||||
5. Compliance with FCC Requirements
|
||||
6. Future Enhancements
|
||||
7. Commitment to Correct Deficiencies
|
||||
Conclusion
|
||||
Perjury declaration + signature block
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.rmd_exhibit_a_generator import (
|
||||
generate_exhibit_a,
|
||||
)
|
||||
path = generate_exhibit_a(
|
||||
entity_name="Falcon Broadband LLC",
|
||||
entity_abbr="FBL",
|
||||
frn="0027160886",
|
||||
address="123 Example St, City, ST 00000",
|
||||
contact_name="Jane Doe",
|
||||
contact_title="President",
|
||||
contact_email="jane@falconbroadband.com",
|
||||
contact_phone="555-123-4567",
|
||||
principals=["Jane Doe"],
|
||||
carrier_role="ucaas",
|
||||
upstream_provider_name="VoIP Innovations",
|
||||
rmd_option="option2", # "option1" | "option2" | "option3"
|
||||
scope_narrative="small UCaaS provider serving retail end-users",
|
||||
output_path="/tmp/rmd_plan.docx",
|
||||
)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Awaitable, Callable, Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.rmd_exhibit_a")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — RMD plan generation unavailable")
|
||||
Document = None # type: ignore[assignment, misc]
|
||||
|
||||
NAVY_BLUE = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
HEADING_SIZE = Pt(12)
|
||||
BODY_SIZE = Pt(10)
|
||||
PARA_SPACING_AFTER = Pt(6)
|
||||
|
||||
|
||||
# ── RMD option labels per FCC 2025 R&O ────────────────────────────────────
|
||||
# Option 1: complete STIR/SHAKEN implementation
|
||||
# Option 2: partial STIR/SHAKEN — IP portions only, relying on upstream
|
||||
# Option 3: robocall-mitigation-only (no STIR/SHAKEN signing at all)
|
||||
RMD_OPTION_LABELS = {
|
||||
"option1": "Option 1 — Complete STIR/SHAKEN Implementation",
|
||||
"option2": "Option 2 — Partial STIR/SHAKEN Implementation",
|
||||
"option3": "Option 3 — Robocall Mitigation Only (no STIR/SHAKEN signing)",
|
||||
}
|
||||
|
||||
|
||||
# ── Role-specific STIR/SHAKEN language ────────────────────────────────────
|
||||
|
||||
|
||||
def _stir_shaken_paragraph(
|
||||
*,
|
||||
entity_name: str,
|
||||
entity_abbr: str,
|
||||
rmd_option: str,
|
||||
upstream_provider_name: str,
|
||||
carrier_role: str,
|
||||
ocn: str,
|
||||
) -> list[str]:
|
||||
"""Return a list of paragraphs describing STIR/SHAKEN implementation."""
|
||||
option = rmd_option.lower()
|
||||
upstream = upstream_provider_name or "its underlying carrier"
|
||||
|
||||
paras: list[str] = []
|
||||
|
||||
if option == "option1":
|
||||
paras.append(
|
||||
f"{entity_name} complies with the STIR/SHAKEN call authentication "
|
||||
f"framework and signs all outbound calls originated from its "
|
||||
f"network using its own STI certificate. {entity_abbr} certifies "
|
||||
f"complete STIR/SHAKEN implementation (Option 1 in the RMD)."
|
||||
)
|
||||
if ocn:
|
||||
paras.append(
|
||||
f"All calls are attested at level A, B, or C as appropriate "
|
||||
f"using {entity_abbr}'s OCN {ocn} under its STI-CA-issued "
|
||||
f"certificate."
|
||||
)
|
||||
elif option == "option2":
|
||||
paras.append(
|
||||
f"{entity_name} complies with FCC STIR/SHAKEN caller authentication "
|
||||
f"requirements through its partnership with {upstream}. This "
|
||||
f"partnership ensures that all outbound calls are attested under "
|
||||
f"the STIR/SHAKEN framework; calls originating from the network "
|
||||
f"are validated to verify caller identity and detect spoofed "
|
||||
f"calls."
|
||||
)
|
||||
paras.append(
|
||||
f"{entity_abbr} certifies partial STIR/SHAKEN implementation "
|
||||
f"(Option 2 in the RMD) for IP portions of the network, relying "
|
||||
f"on {upstream} for technical attestation/signing. {entity_abbr} "
|
||||
f"makes all attestation-level decisions based on verified customer "
|
||||
f"right-to-use of DIDs and provides this information to the "
|
||||
f"upstream carrier for signing. {entity_abbr} does not maintain "
|
||||
f"its own SPC token or certificate, as this is unnecessary and "
|
||||
f"disproportionate for a small provider without wholesale, "
|
||||
f"high-volume origination, or facilities-based IP origination "
|
||||
f"infrastructure."
|
||||
)
|
||||
elif option == "option3":
|
||||
paras.append(
|
||||
f"{entity_name} certifies no STIR/SHAKEN signing implementation "
|
||||
f"(Option 3 in the RMD). {entity_abbr} does not originate "
|
||||
f"outbound calls that require its own STIR/SHAKEN signing and "
|
||||
f"does not maintain an SPC token or certificate. Inbound call "
|
||||
f"authentication is verified using STIR/SHAKEN attestation "
|
||||
f"results provided by upstream carriers."
|
||||
)
|
||||
else:
|
||||
paras.append(
|
||||
f"{entity_name} complies with FCC STIR/SHAKEN requirements in "
|
||||
f"accordance with its filing option on the Robocall Mitigation "
|
||||
f"Database."
|
||||
)
|
||||
|
||||
paras.append(
|
||||
f"{entity_abbr} confirms that no previous certification has been "
|
||||
f"removed by Commission action."
|
||||
)
|
||||
return paras
|
||||
|
||||
|
||||
# ── Role-specific scope language ──────────────────────────────────────────
|
||||
|
||||
|
||||
_ROLE_SCOPE_DEFAULTS = {
|
||||
"ucaas": "small UCaaS provider serving end-users",
|
||||
"facilities": "facilities-based voice service provider serving end-users",
|
||||
"reseller": "voice service reseller",
|
||||
"wholesale_domestic": "domestic wholesale voice provider",
|
||||
"gateway": "international gateway provider",
|
||||
"international_only": "carrier handling exclusively international voice traffic",
|
||||
}
|
||||
|
||||
|
||||
def _scope_paragraph(
|
||||
*,
|
||||
entity_name: str,
|
||||
entity_abbr: str,
|
||||
carrier_role: str,
|
||||
scope_narrative: str,
|
||||
is_wholesale: bool,
|
||||
is_gateway: bool,
|
||||
foreign_traffic: bool,
|
||||
) -> str:
|
||||
default_role = _ROLE_SCOPE_DEFAULTS.get(carrier_role, "voice service provider")
|
||||
narrative = scope_narrative or default_role
|
||||
|
||||
parts = [
|
||||
f"{entity_name} (\"{entity_abbr}\"), a {narrative}, is committed to "
|
||||
f"mitigating unlawful robocalls and complying with Federal "
|
||||
f"Communications Commission (FCC) regulations."
|
||||
]
|
||||
if not is_wholesale and carrier_role not in ("wholesale_domestic", "gateway"):
|
||||
parts.append(
|
||||
f"{entity_abbr} does not provide wholesale services, SIP trunking, "
|
||||
f"origination for resellers, or act as an intermediate/gateway "
|
||||
f"provider in any call path."
|
||||
)
|
||||
if not foreign_traffic:
|
||||
parts.append(
|
||||
f"{entity_abbr} does not accept foreign-originated traffic and "
|
||||
f"operates solely with domestic U.S. NANP resources."
|
||||
)
|
||||
parts.append(
|
||||
f"As a small provider without its own Class 4 switch or outbound "
|
||||
f"origination platform, {entity_abbr} relies on trusted underlying "
|
||||
f"carriers for DID origination, call termination, and STIR/SHAKEN "
|
||||
f"attestation/signing where applicable."
|
||||
)
|
||||
parts.append(
|
||||
f"This Robocall Mitigation Plan outlines {entity_abbr}'s measures to "
|
||||
f"detect, prevent, and mitigate unlawful robocalls in compliance "
|
||||
f"with FCC regulations, including 47 CFR \u00a7 64.6305 and the updated "
|
||||
f"requirements from the 2025 Robocall Mitigation Database Report "
|
||||
f"and Order (effective February 5, 2026, with first annual "
|
||||
f"recertification due March 2, 2026 because March 1, 2026 falls on "
|
||||
f"a Sunday)."
|
||||
)
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
# ── Doc helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _add_heading(doc, text: str, level: int = 1) -> None:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_before = Pt(12 if level == 1 else 8)
|
||||
p.paragraph_format.space_after = PARA_SPACING_AFTER
|
||||
run = p.add_run(text)
|
||||
run.bold = True
|
||||
run.font.size = HEADING_SIZE if level == 1 else Pt(11)
|
||||
run.font.color.rgb = NAVY_BLUE
|
||||
|
||||
|
||||
def _add_body(doc, text: str, bold: bool = False) -> None:
|
||||
for chunk in text.split("\n\n"):
|
||||
chunk = chunk.strip()
|
||||
if not chunk:
|
||||
continue
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = PARA_SPACING_AFTER
|
||||
run = p.add_run(chunk)
|
||||
run.font.size = BODY_SIZE
|
||||
run.bold = bold
|
||||
|
||||
|
||||
def _add_bullets(doc, items: list[str], *, indent: float = 0.25) -> None:
|
||||
for item in items:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(indent)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
p.clear()
|
||||
run = p.add_run(item)
|
||||
run.font.size = BODY_SIZE
|
||||
|
||||
|
||||
def _add_page_number_footer(doc) -> None:
|
||||
for section in doc.sections:
|
||||
footer = section.footer
|
||||
footer.is_linked_to_previous = False
|
||||
p = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = p.add_run()
|
||||
for field_char in ("begin",):
|
||||
fc = OxmlElement("w:fldChar")
|
||||
fc.set(qn("w:fldCharType"), field_char)
|
||||
run._element.append(fc)
|
||||
r2 = p.add_run()
|
||||
instr = OxmlElement("w:instrText")
|
||||
instr.set(qn("xml:space"), "preserve")
|
||||
instr.text = " PAGE "
|
||||
r2._element.append(instr)
|
||||
r3 = p.add_run()
|
||||
fc2 = OxmlElement("w:fldChar")
|
||||
fc2.set(qn("w:fldCharType"), "end")
|
||||
r3._element.append(fc2)
|
||||
|
||||
|
||||
# ── Main generator ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def generate_exhibit_a(
|
||||
# Identity
|
||||
entity_name: str,
|
||||
entity_abbr: str = "",
|
||||
frn: str = "",
|
||||
ocn: str = "",
|
||||
address: str = "",
|
||||
# Contact
|
||||
contact_name: str = "",
|
||||
contact_title: str = "",
|
||||
contact_email: str = "",
|
||||
contact_phone: str = "",
|
||||
# Principals / affiliates (list of names or short descriptions)
|
||||
principals: list[str] | None = None,
|
||||
affiliates: list[str] | None = None,
|
||||
# Classification
|
||||
carrier_role: str = "facilities",
|
||||
carrier_metadata: dict | None = None,
|
||||
upstream_provider_name: str = "",
|
||||
is_wholesale: bool = False,
|
||||
is_gateway: bool = False,
|
||||
foreign_traffic: bool = False,
|
||||
# RMD filing option — "option1", "option2", "option3"
|
||||
rmd_option: str = "option2",
|
||||
# Operational narrative
|
||||
scope_narrative: str = "",
|
||||
high_risk_alert_sla_hours: int = 4,
|
||||
# Analytics vendors / systems (optional)
|
||||
analytics_systems: list[str] | None = None,
|
||||
third_party_vendors: list[str] | None = None,
|
||||
# Signature
|
||||
signer_name: str = "",
|
||||
signer_title: str = "",
|
||||
# Legacy LLM hook kept for backwards compat — ignored (no-op)
|
||||
llm_generate: Callable[[str, str], Awaitable[str]] | None = None,
|
||||
# Output
|
||||
output_path: str = "/tmp/rmd_plan.docx",
|
||||
) -> Optional[str]:
|
||||
"""Generate the PW canonical Robocall Mitigation Plan as a DOCX file."""
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
entity_abbr = entity_abbr or _derive_abbr(entity_name)
|
||||
principals = principals or []
|
||||
affiliates = affiliates or []
|
||||
signer_name = signer_name or contact_name
|
||||
signer_title = signer_title or contact_title
|
||||
|
||||
doc = Document()
|
||||
for section in doc.sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1.25)
|
||||
section.right_margin = Inches(1.25)
|
||||
_add_page_number_footer(doc)
|
||||
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
|
||||
# Title + "Updated as of"
|
||||
title_p = doc.add_paragraph()
|
||||
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
title_run = title_p.add_run(f"Robocall Mitigation Plan for {entity_name}")
|
||||
title_run.font.size = Pt(14)
|
||||
title_run.bold = True
|
||||
title_run.font.color.rgb = NAVY_BLUE
|
||||
title_p.paragraph_format.space_after = Pt(2)
|
||||
|
||||
subtitle_p = doc.add_paragraph()
|
||||
subtitle_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
sub_run = subtitle_p.add_run(f"Updated as of {today}")
|
||||
sub_run.font.size = Pt(10)
|
||||
sub_run.italic = True
|
||||
subtitle_p.paragraph_format.space_after = Pt(12)
|
||||
|
||||
# ── Introduction ──
|
||||
_add_heading(doc, "Introduction")
|
||||
_add_body(doc, _scope_paragraph(
|
||||
entity_name=entity_name, entity_abbr=entity_abbr,
|
||||
carrier_role=carrier_role, scope_narrative=scope_narrative,
|
||||
is_wholesale=is_wholesale, is_gateway=is_gateway,
|
||||
foreign_traffic=foreign_traffic,
|
||||
))
|
||||
|
||||
# ── 1. Contact Information ──
|
||||
_add_heading(doc, "1. Contact Information")
|
||||
_add_body(doc, entity_name)
|
||||
if address:
|
||||
_add_body(doc, f"Address: {address}")
|
||||
if contact_name:
|
||||
title_suffix = f", {contact_title}" if contact_title else ""
|
||||
_add_body(doc, f"Primary Contact: {contact_name}{title_suffix}")
|
||||
if contact_email:
|
||||
_add_body(doc, f"Email Address: {contact_email}")
|
||||
if contact_phone:
|
||||
_add_body(doc, f"Phone: {contact_phone}")
|
||||
if frn:
|
||||
_add_body(doc, f"FRN: {frn}")
|
||||
if ocn:
|
||||
_add_body(doc, f"OCN: {ocn}")
|
||||
else:
|
||||
_add_body(doc, (
|
||||
f"{entity_abbr} does not possess an Operating Company Number "
|
||||
f"(OCN). Per FCC guidance, no OCN is required for a small "
|
||||
f"retail provider without local exchange carrier status; "
|
||||
f"\"No\" is selected on the RMD form."
|
||||
))
|
||||
|
||||
_add_body(doc, "Principals, Affiliates, Subsidiaries, and Parent Companies:", bold=True)
|
||||
_add_bullets(doc, principals or [f"{contact_name or entity_name} (sole principal)"])
|
||||
if affiliates:
|
||||
_add_body(doc, "Affiliates:")
|
||||
_add_bullets(doc, affiliates)
|
||||
|
||||
_add_body(doc, (
|
||||
f"{entity_abbr} affirms that neither it nor any affiliated entity "
|
||||
f"has been subject to FCC or law enforcement action related to "
|
||||
f"illegal robocalling, spoofing, or RMD deficiencies in the past "
|
||||
f"two years."
|
||||
))
|
||||
|
||||
# ── 2. STIR/SHAKEN ──
|
||||
_add_heading(doc, "2. Implementation of STIR/SHAKEN Framework")
|
||||
stir_paras = _stir_shaken_paragraph(
|
||||
entity_name=entity_name, entity_abbr=entity_abbr,
|
||||
rmd_option=rmd_option,
|
||||
upstream_provider_name=upstream_provider_name,
|
||||
carrier_role=carrier_role,
|
||||
ocn=ocn,
|
||||
)
|
||||
for para in stir_paras:
|
||||
_add_body(doc, para)
|
||||
_add_body(doc, f"RMD filing option selected: {RMD_OPTION_LABELS.get(rmd_option.lower(), rmd_option)}", bold=True)
|
||||
|
||||
# ── 3. Robocall Monitoring and Mitigation ──
|
||||
_add_heading(doc, "3. Robocall Monitoring and Mitigation")
|
||||
_add_body(doc, (
|
||||
f"{entity_abbr} actively works to prevent illegal robocalls from "
|
||||
f"originating or transiting through its network. The program applies "
|
||||
f"to all voice traffic that {entity_abbr} originates, transits, or "
|
||||
f"terminates, and includes the following elements:"
|
||||
))
|
||||
_add_body(doc, "Traffic Monitoring:", bold=True)
|
||||
_add_bullets(doc, [
|
||||
"Monitoring call patterns for anomalies \u2014 high call volumes to specific destinations, short-duration calls, ASR/ACD deviations, call velocity, and snowshoeing.",
|
||||
f"Receiving and reviewing all high-risk alerts within {high_risk_alert_sla_hours} hours and taking immediate action.",
|
||||
"Investigating and addressing suspicious activity promptly.",
|
||||
])
|
||||
_add_body(doc, "Customer Vetting:", bold=True)
|
||||
_add_bullets(doc, [
|
||||
"Verifying the identity of new customers and assessing the legitimacy of their intended usage.",
|
||||
"Ensuring customers agree to terms prohibiting illegal robocalling.",
|
||||
"Immediately blocking any numbers identified on the FCC Do-Not-Originate (DNO) list via upstream enforcement.",
|
||||
"Taking reasonable steps to prevent new and renewing customers from originating illegal robocalls.",
|
||||
])
|
||||
_add_body(doc, "Complaint Resolution:", bold=True)
|
||||
_add_bullets(doc, [
|
||||
"Providing a clear process for individuals to report suspected robocalls (including email to the support address above).",
|
||||
"Investigating complaints and taking corrective actions, including termination of services for violators.",
|
||||
])
|
||||
|
||||
# ── 3.5 KYC (In-House) ──
|
||||
_add_heading(doc, "3.5. Know Your Customer (KYC) Procedures (Performed In-House)", level=2)
|
||||
_add_body(doc, (
|
||||
f"{entity_name} conducts its own internal Know Your Customer (KYC) "
|
||||
f"process for all new customers and customer renewals to ensure "
|
||||
f"that only legitimate entities and individuals are granted access "
|
||||
f"to services capable of making outbound calls or using numbering "
|
||||
f"resources."
|
||||
))
|
||||
_add_body(doc, "Collection and Verification of Customer Information. At account signup or upon any material change in service usage, we require and collect:", bold=True)
|
||||
_add_bullets(doc, [
|
||||
"Full legal name of the individual or entity",
|
||||
"Physical business address (no P.O. boxes accepted for high-volume or toll-free services)",
|
||||
"Business identification number (EIN, or equivalent tax ID for non-U.S. entities) or, for individuals, the last four digits of SSN or government-issued ID number",
|
||||
"At least one valid government-issued photo ID for the account owner or authorized officer",
|
||||
"Business website, or if none exists, a description of the legitimate business purpose for the service",
|
||||
])
|
||||
_add_body(doc, "Verification Steps Performed In-House. Staff manually verify the provided information by:", bold=True)
|
||||
_add_bullets(doc, [
|
||||
"Cross-referencing business name and EIN against public state business registry databases or IRS records where available",
|
||||
"Confirming the provided physical address via USPS address validation tools and third-party data sources (e.g., Google Maps satellite/street view confirmation)",
|
||||
"Verifying that the submitted photo ID matches the name and appears authentic",
|
||||
"Conducting an open-source and web search for the customer and principals to identify any prior association with illegal robocalling, call spoofing, or inclusion on the Industry Traceback Group's known bad-actor list",
|
||||
])
|
||||
_add_body(doc, "Red-Flag Review and Enhanced Due Diligence. If any of the following risk indicators are present, we perform enhanced in-house due diligence before activating or continuing service:", bold=True)
|
||||
_add_bullets(doc, [
|
||||
"Customer is unwilling or unable to provide complete KYC information",
|
||||
"Discrepancies between provided information and public records",
|
||||
"Use of privacy-protected or anonymous registration services for domains/websites",
|
||||
"Requested usage patterns inconsistent with stated business purpose",
|
||||
"Prior complaints or traceback involvement linked to the customer or its principals",
|
||||
])
|
||||
_add_body(doc, (
|
||||
"Acceptance of Terms Prohibiting Illegal Activity. All customers "
|
||||
"must electronically acknowledge and agree to our Acceptable Use "
|
||||
"Policy and Robocall Policy, which explicitly prohibit the "
|
||||
"origination or facilitation of illegal robocalls, unlawful caller "
|
||||
"ID spoofing, or any violation of the Telephone Consumer Protection "
|
||||
"Act (TCPA) or Telemarketing Sales Rule (TSR)."
|
||||
))
|
||||
_add_body(doc, (
|
||||
"Ongoing Monitoring and Re-Vetting. Existing customers are subject "
|
||||
"to periodic re-vetting (at least annually for high-volume or "
|
||||
"toll-free customers) and immediate re-review upon receipt of "
|
||||
"complaints, traceback requests, or detected anomalous traffic "
|
||||
"patterns."
|
||||
))
|
||||
|
||||
# ── 4. Call Analytics and Upstream Provider Procedures ──
|
||||
_add_heading(doc, "4. Call Analytics and Upstream Provider Procedures")
|
||||
_add_body(doc, "Call Analytics:", bold=True)
|
||||
if analytics_systems or third_party_vendors:
|
||||
items: list[str] = []
|
||||
if analytics_systems:
|
||||
items.append("Analytics systems deployed: " + ", ".join(analytics_systems))
|
||||
if third_party_vendors:
|
||||
items.append("Third-party analytics vendors: " + ", ".join(third_party_vendors))
|
||||
items.append("Systems analyze call patterns in real time to identify potential violations.")
|
||||
_add_bullets(doc, items)
|
||||
else:
|
||||
_add_bullets(doc, [
|
||||
f"{entity_abbr} utilizes the underlying carriers' real-time call analytics platforms, which monitor ASR, ACD, call velocity, short-duration patterns, and snowshoeing.",
|
||||
f"{entity_abbr} receives and reviews all high-risk alerts within {high_risk_alert_sla_hours} hours and takes immediate action.",
|
||||
"As a small provider without its own switching platform, independent analytics are unnecessary and disproportionate to risk.",
|
||||
])
|
||||
_add_body(doc, "Upstream Provider Procedures:", bold=True)
|
||||
_add_bullets(doc, [
|
||||
"Prior to contracting and annually thereafter, we verify each upstream provider's active RMD filing, STIR/SHAKEN status, and robocall mitigation plan via the RMD portal before routing any traffic.",
|
||||
"Periodic reviews confirm ongoing compliance.",
|
||||
])
|
||||
|
||||
# ── 5. Compliance with FCC Requirements ──
|
||||
_add_heading(doc, "5. Compliance with FCC Requirements")
|
||||
_add_body(doc, f"{entity_abbr} fully complies with FCC robocall mitigation rules:")
|
||||
_add_bullets(doc, [
|
||||
"Maintains an active filing in the FCC's Robocall Mitigation Database (RMD), including role (voice service provider serving end-users), STIR/SHAKEN status, and mitigation plan.",
|
||||
"Annual Recertification: Recertifies RMD filing annually by March 1 each year (for 2026, by March 2 because March 1 is a Sunday; window opened February 1, 2026). Recertification involves logging into the RMD portal with multi-factor authentication (required effective February 5, 2026), verifying accuracy of all information, updating if needed, and submitting via the \"Recertify\" button.",
|
||||
"Prompt Updates: Updates RMD and CORES registration within 10 business days of any material change (e.g., ownership, contacts, STIR/SHAKEN posture, upstream provider, trade names), per 47 CFR \u00a7 64.6305.",
|
||||
"Responds fully to Industry Traceback Group (ITG) traceback requests within 24 hours.",
|
||||
"Pays any required filing fees to the FCC.",
|
||||
"Responds promptly to FCC deficiency notices, curing issues within specified timeframes to avoid RMD removal or traffic blocking.",
|
||||
])
|
||||
_add_body(doc, (
|
||||
"Noncompliance risks under the 2025 RMD R&O include a base forfeiture "
|
||||
"of $10,000 for false or inaccurate information and $1,000 per day "
|
||||
"until cured for failure to update RMD information within 10 "
|
||||
"business days of a material change."
|
||||
))
|
||||
|
||||
# ── 6. Future Enhancements ──
|
||||
_add_heading(doc, "6. Future Enhancements")
|
||||
_add_body(doc, f"{entity_abbr} commits to ongoing improvement:")
|
||||
_add_bullets(doc, [
|
||||
"Monitoring upstream carrier enhancements for better analytics/blocking.",
|
||||
"Educating customers on robocall awareness and reporting.",
|
||||
"Adapting to emerging threats (e.g., AI voice cloning) via upstream partnerships and FCC guidance.",
|
||||
"Strengthening partnerships with industry organizations and regulators to stay ahead of emerging robocall trends.",
|
||||
])
|
||||
|
||||
# ── 7. Commitment to Correct Deficiencies ──
|
||||
_add_heading(doc, "7. Commitment to Correct Deficiencies")
|
||||
_add_body(doc, (
|
||||
f"{entity_abbr} will respond promptly to any FCC notice of deficiency "
|
||||
f"in its RMD certification. This includes:"
|
||||
))
|
||||
_add_bullets(doc, [
|
||||
"Updating RMD certifications and robocall mitigation plans to cure identified deficiencies.",
|
||||
"Providing detailed explanations to the FCC regarding corrective actions taken.",
|
||||
"Ensuring compliance within the specified timeframe to avoid removal from the RMD.",
|
||||
])
|
||||
|
||||
# ── Conclusion ──
|
||||
_add_heading(doc, "Conclusion")
|
||||
_add_body(doc, (
|
||||
f"{entity_name} is dedicated to protecting its customers and the "
|
||||
f"public from the harm caused by illegal robocalls. Through "
|
||||
f"reliable upstream partnerships, customer-focused controls, and "
|
||||
f"full adherence to updated FCC requirements (including 2026 annual "
|
||||
f"recertification by March 2, 2026), {entity_abbr} provides secure, "
|
||||
f"compliant voice services."
|
||||
))
|
||||
|
||||
# ── Perjury declaration + signature ──
|
||||
_add_heading(doc, " ", level=2) # spacer
|
||||
_add_body(doc, (
|
||||
"I declare under penalty of perjury under the laws of the United "
|
||||
"States of America that to the best of my knowledge the foregoing "
|
||||
"is true and correct."
|
||||
))
|
||||
_add_body(doc, "")
|
||||
sig = doc.add_paragraph()
|
||||
sig.add_run("_" * 45).font.size = BODY_SIZE
|
||||
_add_body(doc, signer_name or "[Authorized Signer]", bold=True)
|
||||
_add_body(doc, signer_title or "[Title]")
|
||||
_add_body(doc, entity_name)
|
||||
_add_body(doc, f"Date: {today}")
|
||||
|
||||
# Save
|
||||
output = Path(output_path)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(output))
|
||||
LOG.info("RMD Plan (Exhibit A) generated: %s", output)
|
||||
return str(output)
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _derive_abbr(entity_name: str) -> str:
|
||||
"""Build a 2-4 letter abbreviation from the legal name."""
|
||||
words = [w for w in entity_name.replace(",", "").split() if w.lower() not in (
|
||||
"inc", "inc.", "llc", "llc.", "corporation", "corp", "corp.", "co", "co.",
|
||||
"ltd", "ltd.", "company", "the", "a", "of",
|
||||
)]
|
||||
if not words:
|
||||
return entity_name[:3].upper()
|
||||
letters = "".join(w[0].upper() for w in words[:3])
|
||||
return letters if len(letters) >= 2 else entity_name[:3].upper()
|
||||
1110
scripts/document_gen/templates/rmd_letter_generator.py
Normal file
1110
scripts/document_gen/templates/rmd_letter_generator.py
Normal file
File diff suppressed because it is too large
Load diff
303
scripts/document_gen/traffic_study_stamper.py
Normal file
303
scripts/document_gen/traffic_study_stamper.py
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
"""
|
||||
Traffic Study Page Stamper for FCC Form 499-A filings.
|
||||
|
||||
2026 Form 499-A Section IV.C.5.h requires carriers that submit a
|
||||
traffic study (in lieu of electing a safe-harbor allocation) to stamp
|
||||
every page of the study with a one-line header identifying the Filer
|
||||
ID, Company Name, and Affiliated Filers Name. USAC uses this header
|
||||
to match the study back to the 499-A submission and to verify
|
||||
consistency across affiliated filers.
|
||||
|
||||
Primary path
|
||||
------------
|
||||
Try to generate a text overlay via ``reportlab`` and merge it onto
|
||||
each page of the source PDF with ``pypdf``. Each overlay PDF matches
|
||||
the media-box size of its corresponding source page so that the
|
||||
merge is geometrically correct.
|
||||
|
||||
Fallback
|
||||
--------
|
||||
If ``reportlab`` is not installed, attempt a best-effort stamping
|
||||
using a pypdf-authored PageObject with a small content-stream
|
||||
annotation. If that also fails, copy the source PDF to the output
|
||||
path unchanged, log a warning, and return the output path (the 499-A
|
||||
submission plan requires the filing to proceed regardless).
|
||||
|
||||
Usage
|
||||
-----
|
||||
from scripts.document_gen.traffic_study_stamper import stamp_pages
|
||||
out = stamp_pages(
|
||||
pdf_path="/data/traffic_study.pdf",
|
||||
output_path="/data/traffic_study.stamped.pdf",
|
||||
filer_id="812345",
|
||||
company_name="Acme Telco LLC",
|
||||
affiliated_filers_name="Acme Holdings",
|
||||
)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.traffic_study_stamper")
|
||||
|
||||
# ── PDF core (required) ─────────────────────────────────────────────
|
||||
try:
|
||||
from pypdf import PdfReader, PdfWriter, PageObject
|
||||
from pypdf.generic import (
|
||||
ContentStream,
|
||||
NameObject,
|
||||
NumberObject,
|
||||
TextStringObject,
|
||||
)
|
||||
_HAS_PYPDF = True
|
||||
except ImportError:
|
||||
LOG.warning("pypdf not installed — traffic study stamping unavailable")
|
||||
PdfReader = None # type: ignore[assignment,misc]
|
||||
PdfWriter = None # type: ignore[assignment,misc]
|
||||
PageObject = None # type: ignore[assignment,misc]
|
||||
_HAS_PYPDF = False
|
||||
|
||||
# ── Reportlab (preferred overlay path; optional) ────────────────────
|
||||
try:
|
||||
from reportlab.pdfgen import canvas as _rl_canvas # type: ignore
|
||||
_HAS_REPORTLAB = True
|
||||
except ImportError:
|
||||
_rl_canvas = None # type: ignore[assignment]
|
||||
_HAS_REPORTLAB = False
|
||||
|
||||
STAMP_FONT_PT = 8
|
||||
STAMP_MARGIN_Y_PT = 20 # distance from top of page
|
||||
STAMP_MARGIN_X_PT = 36 # 0.5 inch from left
|
||||
|
||||
|
||||
def _format_stamp(filer_id: str, company_name: str, affiliated_filers_name: str) -> str:
|
||||
"""Build the stamp-text one-liner per Form 499-A Section IV.C.5.h."""
|
||||
return (
|
||||
f"Filer ID {filer_id or '\u2014'} | "
|
||||
f"{company_name or '\u2014'} | "
|
||||
f"Affiliated Filers: {affiliated_filers_name or '\u2014'}"
|
||||
)
|
||||
|
||||
|
||||
def _overlay_reportlab(
|
||||
stamp_text: str, width: float, height: float
|
||||
) -> Optional[bytes]:
|
||||
"""Build a one-page overlay PDF (bytes) sized (width, height) with the
|
||||
stamp drawn at the top-left. Returns None if reportlab can't be used."""
|
||||
if not _HAS_REPORTLAB or _rl_canvas is None:
|
||||
return None
|
||||
try:
|
||||
buf = io.BytesIO()
|
||||
c = _rl_canvas.Canvas(buf, pagesize=(width, height))
|
||||
c.setFont("Helvetica", STAMP_FONT_PT)
|
||||
# y measured from bottom of page; header sits near top
|
||||
y = height - STAMP_MARGIN_Y_PT
|
||||
c.drawString(STAMP_MARGIN_X_PT, y, stamp_text)
|
||||
c.showPage()
|
||||
c.save()
|
||||
return buf.getvalue()
|
||||
except Exception as exc: # pragma: no cover
|
||||
LOG.warning("reportlab overlay build failed: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def _apply_overlay_via_pypdf(
|
||||
page: "PageObject", # type: ignore[name-defined]
|
||||
overlay_pdf_bytes: bytes,
|
||||
) -> None:
|
||||
"""Merge a single-page overlay PDF onto the given source page."""
|
||||
from pypdf import PdfReader as _Reader
|
||||
overlay_reader = _Reader(io.BytesIO(overlay_pdf_bytes))
|
||||
if not overlay_reader.pages:
|
||||
return
|
||||
page.merge_page(overlay_reader.pages[0])
|
||||
|
||||
|
||||
def _stamp_via_content_stream(
|
||||
page: "PageObject", # type: ignore[name-defined]
|
||||
stamp_text: str,
|
||||
page_height: float,
|
||||
) -> bool:
|
||||
"""
|
||||
Fallback stamping when reportlab is unavailable.
|
||||
|
||||
Appends a minimal PDF content stream to draw ``stamp_text`` at the
|
||||
top of ``page``. Returns True on success, False on any exception.
|
||||
"""
|
||||
try:
|
||||
# Escape parentheses / backslashes per PDF string encoding.
|
||||
safe = (
|
||||
stamp_text.replace("\\", "\\\\")
|
||||
.replace("(", "\\(")
|
||||
.replace(")", "\\)")
|
||||
)
|
||||
y = page_height - STAMP_MARGIN_Y_PT
|
||||
# Use Helvetica (F1) at STAMP_FONT_PT. We add an /F1 resource
|
||||
# reference if missing.
|
||||
stream = (
|
||||
f"q BT /F1 {STAMP_FONT_PT} Tf "
|
||||
f"{STAMP_MARGIN_X_PT} {y} Td ({safe}) Tj ET Q"
|
||||
).encode("latin-1", errors="replace")
|
||||
|
||||
existing = page.get_contents()
|
||||
from pypdf.generic import ByteStringObject, ArrayObject
|
||||
|
||||
new_cs = ContentStream(None, None)
|
||||
new_cs.set_data(stream)
|
||||
|
||||
# Ensure /Font /F1 exists in the page resources.
|
||||
from pypdf.generic import DictionaryObject, IndirectObject
|
||||
resources = page.get("/Resources")
|
||||
if isinstance(resources, IndirectObject):
|
||||
resources = resources.get_object()
|
||||
if resources is None:
|
||||
resources = DictionaryObject()
|
||||
page[NameObject("/Resources")] = resources
|
||||
fonts = resources.get("/Font")
|
||||
if isinstance(fonts, IndirectObject):
|
||||
fonts = fonts.get_object()
|
||||
if fonts is None:
|
||||
fonts = DictionaryObject()
|
||||
resources[NameObject("/Font")] = fonts
|
||||
if "/F1" not in fonts:
|
||||
helv = DictionaryObject(
|
||||
{
|
||||
NameObject("/Type"): NameObject("/Font"),
|
||||
NameObject("/Subtype"): NameObject("/Type1"),
|
||||
NameObject("/BaseFont"): NameObject("/Helvetica"),
|
||||
}
|
||||
)
|
||||
fonts[NameObject("/F1")] = helv
|
||||
|
||||
# Append the new content stream. If existing /Contents is an
|
||||
# array, append. Otherwise, wrap both into an array.
|
||||
if existing is None:
|
||||
page[NameObject("/Contents")] = new_cs
|
||||
else:
|
||||
# merge_page would normally handle this; we emulate the
|
||||
# simplest case by concatenating streams.
|
||||
try:
|
||||
combined = ContentStream(None, None)
|
||||
combined.set_data(existing.get_data() + b"\n" + stream)
|
||||
page[NameObject("/Contents")] = combined
|
||||
except Exception:
|
||||
# Last-ditch: prepend via an array.
|
||||
page[NameObject("/Contents")] = ArrayObject([existing, new_cs])
|
||||
return True
|
||||
except Exception as exc:
|
||||
LOG.warning("pypdf content-stream stamping failed: %s", exc)
|
||||
return False
|
||||
|
||||
|
||||
def stamp_pages(
|
||||
pdf_path: str,
|
||||
output_path: str,
|
||||
filer_id: str,
|
||||
company_name: str,
|
||||
affiliated_filers_name: str = "\u2014",
|
||||
) -> str:
|
||||
"""
|
||||
Stamp every page of ``pdf_path`` with a one-line header containing
|
||||
the Filer ID, Company Name, and Affiliated Filers Name. Write the
|
||||
result to ``output_path``. Return ``output_path``.
|
||||
|
||||
This function is best-effort by design. The Form 499-A filing plan
|
||||
requires that the submission proceed even when fancy stamping fails
|
||||
(e.g., in a constrained environment missing ``reportlab``). On any
|
||||
unrecoverable error the source PDF is copied verbatim to the
|
||||
output path and a warning is logged.
|
||||
"""
|
||||
out = Path(output_path)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
src = Path(pdf_path)
|
||||
if not src.exists():
|
||||
raise FileNotFoundError(f"source PDF not found: {pdf_path}")
|
||||
|
||||
stamp_text = _format_stamp(filer_id, company_name, affiliated_filers_name)
|
||||
|
||||
if not _HAS_PYPDF:
|
||||
LOG.warning(
|
||||
"pypdf unavailable — copying source PDF unchanged to %s", out
|
||||
)
|
||||
shutil.copyfile(src, out)
|
||||
return str(out)
|
||||
|
||||
try:
|
||||
reader = PdfReader(str(src))
|
||||
writer = PdfWriter()
|
||||
overlay_mode = "reportlab" if _HAS_REPORTLAB else "content_stream"
|
||||
|
||||
for page in reader.pages:
|
||||
mb = page.mediabox
|
||||
width = float(mb.width)
|
||||
height = float(mb.height)
|
||||
|
||||
stamped = False
|
||||
if overlay_mode == "reportlab":
|
||||
overlay_bytes = _overlay_reportlab(stamp_text, width, height)
|
||||
if overlay_bytes:
|
||||
try:
|
||||
_apply_overlay_via_pypdf(page, overlay_bytes)
|
||||
stamped = True
|
||||
except Exception as exc:
|
||||
LOG.warning(
|
||||
"overlay merge failed on page; "
|
||||
"falling back to content stream: %s", exc
|
||||
)
|
||||
|
||||
if not stamped:
|
||||
_stamp_via_content_stream(page, stamp_text, height)
|
||||
|
||||
writer.add_page(page)
|
||||
|
||||
with out.open("wb") as fh:
|
||||
writer.write(fh)
|
||||
|
||||
if overlay_mode != "reportlab":
|
||||
LOG.warning(
|
||||
"reportlab not available — used pypdf content-stream fallback "
|
||||
"to stamp %s (filer=%s).", out, filer_id
|
||||
)
|
||||
else:
|
||||
LOG.info(
|
||||
"Traffic study stamped via reportlab overlay: %s (filer=%s)",
|
||||
out, filer_id,
|
||||
)
|
||||
return str(out)
|
||||
|
||||
except Exception as exc:
|
||||
LOG.warning(
|
||||
"traffic-study stamping failed (%s); copying source unchanged "
|
||||
"to preserve filing timeline.", exc
|
||||
)
|
||||
try:
|
||||
shutil.copyfile(src, out)
|
||||
except Exception as exc2: # pragma: no cover
|
||||
LOG.error("fallback copy also failed: %s", exc2)
|
||||
raise
|
||||
return str(out)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import argparse
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
|
||||
ap.add_argument("source_pdf")
|
||||
ap.add_argument("output_pdf")
|
||||
ap.add_argument("--filer-id", required=True)
|
||||
ap.add_argument("--company-name", required=True)
|
||||
ap.add_argument("--affiliated-filers-name", default="\u2014")
|
||||
args = ap.parse_args()
|
||||
p = stamp_pages(
|
||||
pdf_path=args.source_pdf,
|
||||
output_path=args.output_pdf,
|
||||
filer_id=args.filer_id,
|
||||
company_name=args.company_name,
|
||||
affiliated_filers_name=args.affiliated_filers_name,
|
||||
)
|
||||
print(p)
|
||||
Loading…
Add table
Add a link
Reference in a new issue