- checkout.ts: generalize ensureCompliancePortalUser -> ensurePortalUser and call it in the CRTC post-payment path so PayPal/crypto/webhook-confirmed CRTC orders always get an ERPNext Customer + Website User (the single source of truth for portal login/password), matching the compliance fix from the PayPal incident. Also flip portal_user_created for canada_crtc/formation. - canada-crtc.ts: enforce discount active+start/expiry windows, global usage limit and applies_to scope server-side at checkout (was active-only), so a promo like CANADA200 actually stops working after its expiry. - scripts/generate_canada_carrier_guide_pdf.py: render the public Canadian wholesale carrier/vendor guide PDF (reuses the canonical VENDORS list) to site/public/guides/canada-carrier-guide.pdf for the CRTC campaign lead magnet.
180 lines
8.5 KiB
Python
180 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate the public "Canadian Wholesale Carrier & Vendor Reference Guide" PDF.
|
|
|
|
This is the lead-magnet attached (as a hosted download link) to the CRTC
|
|
USF-increase campaign. It is the marketing-facing, generic version of the
|
|
per-client vendor guide in
|
|
scripts/document_gen/templates/crtc_vendor_guide_generator.py -- it reuses the
|
|
SAME vetted vendor list (one source of truth) but renders a branded, standalone
|
|
PDF with reportlab (no per-client entity name) suitable for hosting at
|
|
site/public/guides/canada-carrier-guide.pdf.
|
|
|
|
Usage:
|
|
python3 scripts/generate_canada_carrier_guide_pdf.py
|
|
python3 scripts/generate_canada_carrier_guide_pdf.py --out path/to/file.pdf
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
from reportlab.lib.colors import HexColor
|
|
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
|
from reportlab.lib.pagesizes import letter
|
|
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
|
from reportlab.lib.units import inch
|
|
from reportlab.platypus import (
|
|
HRFlowable,
|
|
Image,
|
|
ListFlowable,
|
|
ListItem,
|
|
Paragraph,
|
|
SimpleDocTemplate,
|
|
Spacer,
|
|
)
|
|
|
|
# Reuse the canonical vendor list so the public guide and the per-client binder
|
|
# guide never drift. Load the module BY PATH to avoid importing the
|
|
# scripts.document_gen package __init__ (which pulls in jinja2/docx and other
|
|
# heavy deps we don't need just for the VENDORS list).
|
|
import importlib.util as _ilu
|
|
|
|
_VENDOR_MODULE = os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
"document_gen", "templates", "crtc_vendor_guide_generator.py",
|
|
)
|
|
_spec = _ilu.spec_from_file_location("_crtc_vendor_guide", _VENDOR_MODULE)
|
|
_mod = _ilu.module_from_spec(_spec) # type: ignore[arg-type]
|
|
_spec.loader.exec_module(_mod) # type: ignore[union-attr]
|
|
VENDORS = _mod.VENDORS
|
|
|
|
NAVY = HexColor("#1A2744")
|
|
GRAY = HexColor("#64748B")
|
|
SLATE = HexColor("#374151")
|
|
LINK = HexColor("#1E40AF")
|
|
RED = HexColor("#E63F2A")
|
|
GREEN = HexColor("#059669")
|
|
|
|
DEFAULT_OUT = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"site", "public", "guides", "canada-carrier-guide.pdf",
|
|
)
|
|
LOGO = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"site", "public", "images", "logo.png",
|
|
)
|
|
|
|
|
|
def _styles() -> dict:
|
|
base = getSampleStyleSheet()
|
|
return {
|
|
"title": ParagraphStyle("title", parent=base["Title"], fontName="Helvetica-Bold",
|
|
fontSize=20, textColor=NAVY, alignment=TA_CENTER, spaceAfter=4),
|
|
"subtitle": ParagraphStyle("subtitle", parent=base["Normal"], fontName="Helvetica",
|
|
fontSize=11, textColor=GRAY, alignment=TA_CENTER, spaceAfter=2),
|
|
"date": ParagraphStyle("date", parent=base["Normal"], fontName="Helvetica",
|
|
fontSize=10, textColor=GRAY, alignment=TA_CENTER, spaceAfter=14),
|
|
"h2": ParagraphStyle("h2", parent=base["Heading2"], fontName="Helvetica-Bold",
|
|
fontSize=14, textColor=NAVY, spaceBefore=10, spaceAfter=6),
|
|
"body": ParagraphStyle("body", parent=base["Normal"], fontName="Helvetica",
|
|
fontSize=10, textColor=SLATE, leading=15, spaceAfter=8, alignment=TA_LEFT),
|
|
"intro_small": ParagraphStyle("intro_small", parent=base["Normal"], fontName="Helvetica-Oblique",
|
|
fontSize=9, textColor=GRAY, leading=13, spaceAfter=8),
|
|
"vname": ParagraphStyle("vname", parent=base["Normal"], fontName="Helvetica-Bold",
|
|
fontSize=13, textColor=NAVY, spaceBefore=12, spaceAfter=1),
|
|
"vweb": ParagraphStyle("vweb", parent=base["Normal"], fontName="Helvetica",
|
|
fontSize=9, textColor=LINK, spaceAfter=3),
|
|
"vsvc": ParagraphStyle("vsvc", parent=base["Normal"], fontName="Helvetica",
|
|
fontSize=9.5, textColor=SLATE, leading=13, spaceAfter=2),
|
|
"vnotes": ParagraphStyle("vnotes", parent=base["Normal"], fontName="Helvetica",
|
|
fontSize=9, textColor=GRAY, leading=12, spaceAfter=2),
|
|
"disc": ParagraphStyle("disc", parent=base["Normal"], fontName="Helvetica-Oblique",
|
|
fontSize=7, textColor=HexColor("#999999"), leading=10, spaceBefore=14),
|
|
}
|
|
|
|
|
|
def build_guide(out_path: str) -> str:
|
|
s = _styles()
|
|
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
|
doc = SimpleDocTemplate(
|
|
out_path, pagesize=letter,
|
|
topMargin=0.85 * inch, bottomMargin=0.85 * inch,
|
|
leftMargin=1 * inch, rightMargin=1 * inch,
|
|
title="Canadian Wholesale Carrier & Vendor Reference Guide",
|
|
author="Performance West Inc.",
|
|
)
|
|
story: list = []
|
|
|
|
if os.path.exists(LOGO):
|
|
try:
|
|
img = Image(LOGO, width=1.4 * inch, height=1.18 * inch, kind="proportional")
|
|
img.hAlign = "CENTER"
|
|
story.append(img)
|
|
story.append(Spacer(1, 8))
|
|
except Exception:
|
|
pass
|
|
|
|
story.append(Paragraph("Canadian Wholesale Carrier & Vendor Reference Guide", s["title"]))
|
|
story.append(Paragraph("Upstream voice, DID, SIP & UCaaS partners for CRTC-registered carriers", s["subtitle"]))
|
|
story.append(Paragraph(datetime.now().strftime("%B %Y"), s["date"]))
|
|
story.append(HRFlowable(width="100%", thickness=2, color=RED, spaceAfter=12))
|
|
|
|
story.append(Paragraph(
|
|
"As a CRTC-registered Canadian telecommunications service provider, you will need "
|
|
"upstream wholesale partners for voice termination, DID numbers, SIP trunking, and "
|
|
"(optionally) white-label UCaaS or broadband. This guide lists vendors commonly used "
|
|
"by Canadian telecom resellers so you can hit the ground running once your carrier "
|
|
"entity is live.", s["body"]))
|
|
story.append(Paragraph(
|
|
"Many large US carriers and wholesale providers (Lumen, Bandwidth, Telnyx, Sinch, etc.) "
|
|
"will board CRTC-registered Canadian carriers for cross-border voice and data. If you "
|
|
"have a specific US carrier you want to work with, contact them directly and ask about "
|
|
"their requirements for onboarding a Canadian wholesale partner -- some require an RMD "
|
|
"filing, others only need a CRTC registration letter and proof of incorporation.", s["body"]))
|
|
story.append(Paragraph(
|
|
"Performance West does not endorse or guarantee any of these vendors. This list is "
|
|
"provided for reference only. Evaluate each provider against your own traffic volume, "
|
|
"coverage, and pricing requirements.", s["intro_small"]))
|
|
|
|
story.append(HRFlowable(width="100%", thickness=0.75, color=HexColor("#E5E7EB"), spaceBefore=6, spaceAfter=2))
|
|
story.append(Paragraph("Recommended Canadian Wholesale Partners", s["h2"]))
|
|
|
|
for v in VENDORS:
|
|
story.append(Paragraph(f'{v["name"]} — <font color="#64748B">{v["location"]}</font>', s["vname"]))
|
|
story.append(Paragraph(v["website"], s["vweb"]))
|
|
story.append(Paragraph(f'<b>Services:</b> {v["services"]}', s["vsvc"]))
|
|
story.append(Paragraph(f'<b>Notes:</b> {v["notes"]}', s["vnotes"]))
|
|
|
|
story.append(Spacer(1, 10))
|
|
story.append(HRFlowable(width="100%", thickness=2, color=NAVY, spaceBefore=6, spaceAfter=8))
|
|
story.append(Paragraph(
|
|
'<b>Ready to launch your Canadian carrier?</b> Performance West handles the complete '
|
|
'turnkey setup -- BC incorporation, CRTC domestic + BITS registration, registered '
|
|
'office, .ca domain, Canadian DID, and corporate binder. Learn more at '
|
|
'<font color="#1E40AF">performancewest.net/order/canada-crtc</font> or call '
|
|
'1-888-411-0383.', s["body"]))
|
|
|
|
story.append(Paragraph(
|
|
"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.", s["disc"]))
|
|
|
|
doc.build(story)
|
|
print(f"[guide] wrote {out_path} ({os.path.getsize(out_path)} bytes, {len(VENDORS)} vendors)")
|
|
return out_path
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="Generate the public Canada carrier vendor guide PDF")
|
|
ap.add_argument("--out", default=DEFAULT_OUT, help=f"output path (default: {DEFAULT_OUT})")
|
|
args = ap.parse_args()
|
|
build_guide(args.out)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|