CRTC: ERPNext as portal source of truth + harden discount expiry + carrier guide PDF
- 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.
This commit is contained in:
parent
eed5e4a258
commit
e379e2b10f
4 changed files with 366 additions and 14 deletions
180
scripts/generate_canada_carrier_guide_pdf.py
Normal file
180
scripts/generate_canada_carrier_guide_pdf.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
#!/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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue