diff --git a/scripts/document_gen/templates/crtc_vendor_guide_generator.py b/scripts/document_gen/templates/crtc_vendor_guide_generator.py new file mode 100644 index 0000000..8ac36a4 --- /dev/null +++ b/scripts/document_gen/templates/crtc_vendor_guide_generator.py @@ -0,0 +1,238 @@ +""" +Generate a Canadian Wholesale Vendor Reference Guide for CRTC corporate binders. + +Lists recommended upstream voice, data, DID, and UCaaS providers for +Canadian telecom resellers. Included in the "Miscellaneous" section of +the corporate binder delivered to CRTC carrier package clients. + +Usage: + from scripts.document_gen.templates.crtc_vendor_guide_generator import generate_vendor_guide + path = generate_vendor_guide( + entity_name="1234567 B.C. Ltd.", + output_path="/tmp/vendor_guide.docx", + ) +""" +from __future__ import annotations + +import logging +from datetime import datetime +from pathlib import Path + +LOG = logging.getLogger("document_gen.crtc_vendor_guide") + +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 — vendor guide generation unavailable") + Document = None + +NAVY = RGBColor(0x1A, 0x27, 0x44) if RGBColor else None +GRAY = RGBColor(0x64, 0x74, 0x8B) if RGBColor else None +GREEN = RGBColor(0x05, 0x96, 0x69) if RGBColor else None + +VENDORS = [ + { + "name": "Fibernetics", + "location": "Cambridge, ON", + "services": "Wholesale SIP trunking, hosted PBX white-label, DID origination (Canada & US), " + "wholesale voice termination, fax-over-IP. Full API for provisioning.", + "website": "fibernetics.ca", + "notes": "Popular choice for Canadian resellers. Offers white-label UCaaS platform. " + "Competitive wholesale voice rates. Canadian-owned and operated.", + }, + { + "name": "Iristel", + "location": "Markham, ON", + "services": "Wholesale voice termination (70+ countries), international DID numbers, " + "MVNO solutions, SIP trunking, SMS/MMS, STIR/SHAKEN attestation services.", + "website": "iristel.com", + "notes": "One of Canada's largest independent carriers. Operates in 70+ countries. " + "Has US operations and participates in the FCC RMD. Good for international voice.", + }, + { + "name": "Flowroute (Intrado)", + "location": "Seattle, WA (serves Canada)", + "services": "US and Canadian DID provisioning (number-level API), SIP trunking, " + "SMS/MMS API, E911, CNAM. Per-minute and per-channel pricing.", + "website": "flowroute.com", + "notes": "Performance West's default DID provider for CRTC packages. Excellent API. " + "Canadian numbers available in BC, ON, AB, QC area codes. US-based but serves " + "Canadian carriers seamlessly under the shared +1 numbering plan.", + }, + { + "name": "VoIP.ms", + "location": "Montreal, QC", + "services": "Canadian and US DIDs, SIP trunking, pay-per-minute voice, toll-free numbers, " + "SMS, fax, E911. Self-serve portal with instant provisioning.", + "website": "voip.ms", + "notes": "Developer-friendly with extensive API. Very competitive per-minute rates. " + "Popular with small-to-mid carriers and UCaaS resellers. Canadian-owned.", + }, + { + "name": "Telnyx", + "location": "Chicago, IL (global network)", + "services": "Global SIP trunking, DID provisioning (40+ countries), SMS/MMS API, " + "wireless (eSIM), storage, identity verification. Mission control portal.", + "website": "telnyx.com", + "notes": "Full-stack communications platform. Private global network with PoPs in " + "Toronto and Vancouver. Strong API-first approach. Good for carriers building " + "programmable voice/messaging products.", + }, + { + "name": "SkySwitch (Sangoma)", + "location": "Huntsville, AL / Toronto, ON", + "services": "White-label UCaaS platform, hosted PBX, contact center, SIP trunking, " + "SD-WAN. Complete reseller portal with billing integration.", + "website": "skyswitch.com", + "notes": "Full white-label UC platform — you sell under your brand, they handle the " + "infrastructure. Popular with ISPs and MSPs adding voice services. " + "Owned by Sangoma (Canadian company, TSX:STC).", + }, + { + "name": "Distributel", + "location": "Toronto, ON", + "services": "Wholesale internet (DSL, cable, fibre), wholesale voice, SIP trunking. " + "CLEC with TPIA access to incumbent networks.", + "website": "distributel.ca", + "notes": "One of Canada's original competitive carriers. Strong wholesale internet " + "offering. Good option if your Canadian entity needs to resell broadband " + "in addition to voice.", + }, + { + "name": "Allstream (Zayo Canada)", + "location": "Toronto, ON", + "services": "Enterprise SIP trunking, MPLS, dedicated internet, SD-WAN, " + "unified communications. Fibre network across major Canadian cities.", + "website": "allstream.com", + "notes": "Enterprise-grade wholesale provider. Extensive Canadian fibre network. " + "Better suited for larger carriers or those needing dedicated circuits. " + "Owned by Zayo Group.", + }, +] + + +def generate_vendor_guide( + entity_name: str, + output_path: str, +) -> str | None: + """Generate the vendor reference guide DOCX.""" + if Document is None: + LOG.error("python-docx not installed") + return None + + doc = Document() + + # Page setup + 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) + + # Title + title = doc.add_paragraph() + title.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = title.add_run("Canadian Wholesale Vendor Reference Guide") + run.font.size = Pt(18) + run.font.color.rgb = NAVY + run.font.bold = True + + # Subtitle + sub = doc.add_paragraph() + sub.alignment = WD_ALIGN_PARAGRAPH.CENTER + sr = sub.add_run(f"Prepared for {entity_name}") + sr.font.size = Pt(11) + sr.font.color.rgb = GRAY + + date_p = doc.add_paragraph() + date_p.alignment = WD_ALIGN_PARAGRAPH.CENTER + dr = date_p.add_run(datetime.now().strftime("%B %Y")) + dr.font.size = Pt(10) + dr.font.color.rgb = GRAY + + # Intro + doc.add_paragraph() + intro = doc.add_paragraph() + ir = intro.add_run( + "As a newly registered Canadian telecommunications service provider, you will need " + "upstream wholesale partners to provide voice termination, DID numbers, SIP trunking, " + "and potentially white-label UCaaS or broadband services. This guide lists vendors " + "commonly used by Canadian telecom resellers." + ) + ir.font.size = Pt(10) + ir.font.color.rgb = RGBColor(0x37, 0x41, 0x51) + + intro2 = doc.add_paragraph() + ir2 = intro2.add_run( + "Performance West does not endorse or guarantee any of these vendors. This list is " + "provided for reference only. You should evaluate each provider based on your specific " + "business requirements, traffic volume, geographic coverage needs, and pricing." + ) + ir2.font.size = Pt(9) + ir2.font.color.rgb = GRAY + ir2.font.italic = True + + # Vendors + for vendor in VENDORS: + doc.add_paragraph() + + # Vendor name + name_p = doc.add_paragraph() + nr = name_p.add_run(vendor["name"]) + nr.font.size = Pt(13) + nr.font.color.rgb = NAVY + nr.font.bold = True + + loc_r = name_p.add_run(f" — {vendor['location']}") + loc_r.font.size = Pt(10) + loc_r.font.color.rgb = GRAY + + # Website + web_p = doc.add_paragraph() + wr = web_p.add_run(vendor["website"]) + wr.font.size = Pt(9) + wr.font.color.rgb = RGBColor(0x1E, 0x40, 0xAF) + wr.font.underline = True + + # Services + svc_label = doc.add_paragraph() + sl = svc_label.add_run("Services: ") + sl.font.size = Pt(9) + sl.font.bold = True + sl.font.color.rgb = RGBColor(0x37, 0x41, 0x51) + sv = svc_label.add_run(vendor["services"]) + sv.font.size = Pt(9) + sv.font.color.rgb = RGBColor(0x37, 0x41, 0x51) + + # Notes + notes_p = doc.add_paragraph() + nl = notes_p.add_run("Notes: ") + nl.font.size = Pt(9) + nl.font.bold = True + nl.font.color.rgb = GRAY + nn = notes_p.add_run(vendor["notes"]) + nn.font.size = Pt(9) + nn.font.color.rgb = GRAY + + # Footer disclaimer + doc.add_paragraph() + doc.add_paragraph() + disc = doc.add_paragraph() + disc_r = disc.add_run( + "Document prepared by Performance West Inc., a regulatory compliance consulting firm. " + "Performance West Inc. is not a law firm and this document does not constitute legal advice " + "or legal representation. Vendor information is current as of the date shown above and " + "may change without notice." + ) + disc_r.font.size = Pt(7) + disc_r.font.color.rgb = RGBColor(0x99, 0x99, 0x99) + disc_r.font.italic = True + + # Save + out = Path(output_path) + out.parent.mkdir(parents=True, exist_ok=True) + doc.save(str(out)) + LOG.info("Vendor guide generated: %s", out) + return str(out) diff --git a/scripts/workers/binder_compiler.py b/scripts/workers/binder_compiler.py index 75dff82..f4e5037 100644 --- a/scripts/workers/binder_compiler.py +++ b/scripts/workers/binder_compiler.py @@ -49,6 +49,7 @@ DEFAULT_SECTIONS = [ "Director Consent(s)", "Share Structure", "Corporate Bylaws", + "Wholesale Vendor Reference Guide", "Miscellaneous", ] diff --git a/scripts/workers/services/canada_crtc.py b/scripts/workers/services/canada_crtc.py index d2545d6..88dd551 100644 --- a/scripts/workers/services/canada_crtc.py +++ b/scripts/workers/services/canada_crtc.py @@ -933,6 +933,20 @@ class CanadaCRTCHandler(BaseServiceHandler): else: LOG.warning("CRTC letter generation failed — will need manual creation") + # Step 6a: Generate vendor reference guide (included in binder) + LOG.info("[Step 6a] Generating wholesale vendor reference guide") + try: + from scripts.document_gen.templates.crtc_vendor_guide_generator import generate_vendor_guide + vendor_guide_path = generate_vendor_guide( + entity_name=formation_order.entity_name or f"{formation_order.state_filing_number} B.C. Ltd.", + output_path=os.path.join(work_dir, f"vendor_reference_guide_{order_number}.docx"), + ) + if vendor_guide_path: + generated_files.append(vendor_guide_path) + LOG.info("Vendor guide generated: %s", vendor_guide_path) + except Exception as exc: + LOG.warning("Vendor guide generation failed (non-fatal): %s", exc) + # ---------------------------------------------------------- # # Step 6b: eSign pause # Upload the CRTC letter PDF to MinIO, store the object key on