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:
justin 2026-06-17 23:34:13 -05:00
parent eed5e4a258
commit e379e2b10f
4 changed files with 366 additions and 14 deletions

View file

@ -226,17 +226,34 @@ router.post("/api/v1/canada-crtc/orders", submitLimiter, async (req, res) => {
: company_type === "numbered_tradename" ? TRADE_NAME_SERVICE_FEE : company_type === "numbered_tradename" ? TRADE_NAME_SERVICE_FEE
: 0; : 0;
// Discount code (applies to service fee only, not gov fees) // Discount code (applies to service fee only, not gov fees).
// Enforce active + start/expiry windows + global usage limit + scope
// server-side so promotional expiry (e.g. "expires Friday") is honored at
// checkout, not just in the front-end validator. Mirrors GET
// /api/v1/discount/:code in routes/discounts.ts.
let discountCents = 0; let discountCents = 0;
if (discount_code) { if (discount_code) {
const dcResult = await pool.query("SELECT * FROM discount_codes WHERE code = $1 AND active = TRUE", [discount_code.toUpperCase()]); const dcResult = await pool.query("SELECT * FROM discount_codes WHERE code = $1 AND active = TRUE", [discount_code.toUpperCase()]);
if (dcResult.rows.length > 0) { if (dcResult.rows.length > 0) {
const dc = dcResult.rows[0]; const dc = dcResult.rows[0];
const discountableAmount = SERVICE_FEE + typeAddon; // discount applies to full service fee const now = new Date();
if (dc.discount_type === "percent") { const notStarted = dc.starts_at && new Date(dc.starts_at) > now;
discountCents = Math.round((discountableAmount * dc.discount_value) / 100); const expired = dc.expires_at && new Date(dc.expires_at) < now;
const usedUp = dc.max_uses !== null && dc.current_uses >= dc.max_uses;
const outOfScope = (() => {
if (!dc.applies_to) return false; // NULL = all services
const allowed = String(dc.applies_to).split(",").map((s: string) => s.trim().toLowerCase());
return !allowed.includes("canada-crtc");
})();
if (notStarted || expired || usedUp || outOfScope) {
console.warn(`[canada-crtc] discount ${dc.code} rejected at checkout`, { notStarted, expired, usedUp, outOfScope });
} else { } else {
discountCents = Math.min(dc.discount_value, discountableAmount); const discountableAmount = SERVICE_FEE + typeAddon; // discount applies to full service fee
if (dc.discount_type === "percent") {
discountCents = Math.round((discountableAmount * dc.discount_value) / 100);
} else {
discountCents = Math.min(dc.discount_value, discountableAmount);
}
} }
} }
} }

View file

@ -167,19 +167,21 @@ async function findOrCreateCustomer(
} }
/** /**
* Ensure a compliance customer has an ERPNext portal account and persist the * Ensure a paid customer has an ERPNext portal account and persist the
* `portal_user_created` flag on their order rows. * `portal_user_created` flag on their order rows.
* *
* ERPNext (portal.performancewest.net) is the single customer portal. Normal * ERPNext (portal.performancewest.net) is the SINGLE source of truth for the
* customer portal login, password, and the customer/order records. Normal
* Stripe checkout provisions the Website User up-front via findOrCreateCustomer, * Stripe checkout provisions the Website User up-front via findOrCreateCustomer,
* but PayPal / crypto / remediation-pipeline orders reach handlePaymentComplete * but PayPal / crypto / remediation-pipeline orders reach handlePaymentComplete
* without ever creating one leaving those customers unable to log in. This * without ever creating one leaving those customers unable to log in. This
* runs in that shared post-payment path so EVERY paid compliance order gets a * runs in that shared post-payment path so EVERY paid order (compliance,
* portal account regardless of how it was created or paid. Fully idempotent: * canada_crtc, formation) gets an ERPNext Customer + Website User regardless of
* ensureWebsiteUser no-ops if the User already exists, and we only flip the * how it was created or paid. Fully idempotent: ensureWebsiteUser no-ops if the
* flag (which gates the delivery worker's set-password invite) the first time. * User already exists, and we only flip the flag (which gates the delivery
* worker's set-password invite) the first time.
*/ */
async function ensureCompliancePortalUser( async function ensurePortalUser(
orderId: string, orderId: string,
orderType: string, orderType: string,
rows: Record<string, unknown>[], rows: Record<string, unknown>[],
@ -241,7 +243,11 @@ async function ensureCompliancePortalUser(
if (portalUserCreated) { if (portalUserCreated) {
const table = orderType === "compliance_batch" || orderType === "compliance" const table = orderType === "compliance_batch" || orderType === "compliance"
? "compliance_orders" ? "compliance_orders"
: null; : orderType === "canada_crtc"
? "canada_crtc_orders"
: orderType === "formation"
? "formation_orders"
: null;
if (table) { if (table) {
try { try {
if (orderType === "compliance_batch") { if (orderType === "compliance_batch") {
@ -1825,7 +1831,7 @@ export async function handlePaymentComplete(
// come straight here and would otherwise skip it (this was the cause of // come straight here and would otherwise skip it (this was the cause of
// customers who paid via PayPal being unable to log in). Idempotent. // customers who paid via PayPal being unable to log in). Idempotent.
try { try {
await ensureCompliancePortalUser(order_id, order_type, updated.rows); await ensurePortalUser(order_id, order_type, updated.rows);
} catch (portalErr) { } catch (portalErr) {
console.error("[checkout] Compliance portal-user provisioning failed (non-fatal):", portalErr); console.error("[checkout] Compliance portal-user provisioning failed (non-fatal):", portalErr);
} }
@ -1874,6 +1880,17 @@ export async function handlePaymentComplete(
if (order_type === "canada_crtc") { if (order_type === "canada_crtc") {
const soName = (order.erpnext_sales_order as string) || null; const soName = (order.erpnext_sales_order as string) || null;
// Ensure the customer has an ERPNext portal account (the single source of
// truth for login/password). create-session provisions it up-front for
// card/ACH, but PayPal / crypto / webhook-confirmed orders reach here
// directly and would otherwise skip it — the same gap that left PayPal
// compliance customers unable to log in. Idempotent. See ensurePortalUser.
try {
await ensurePortalUser(order_id, order_type, updated.rows);
} catch (portalErr) {
console.error("[checkout] CRTC portal-user provisioning failed (non-fatal):", portalErr);
}
// PayPal: funds are instant — advance directly to "Client Selection" // PayPal: funds are instant — advance directly to "Client Selection"
// Stripe card/ACH/Klarna: advance to "Awaiting Funds" — balance.available webhook handles next step // Stripe card/ACH/Klarna: advance to "Awaiting Funds" — balance.available webhook handles next step
// Crypto: advance to "Awaiting Funds" — manual admin step later // Crypto: advance to "Awaiting Funds" — manual admin step later

View 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 &amp; Vendor Reference Guide", s["title"]))
story.append(Paragraph("Upstream voice, DID, SIP &amp; 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"]} &nbsp;&mdash;&nbsp; <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())

View file

@ -0,0 +1,138 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 5 0 R /F3 6 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 70 /Length 1296 /SMask 4 0 R
/Subtype /Image /Type /XObject /Width 83
>>
stream
Gb"/hb>/d;*65Ml>%j7P5]-qa]Ejti9tc@DGjq@B:F%9A"@s5_,h`Ah*f>hNU-p4:Gt*a!e7eKI#!IM77t03lmBSf04#8t)ma?P?LUK;9qgLd.2RHmEX/`2"d]GZ<5rZ_ZJ._n?AaG)+cR#kEG,e5T-]oPH/mrn^Yb2-.`C3`GEU`b/=!fnb`ChJj1]$+;8:eg>]e[EP6(Bo4S"hN#&6\MdZWAK`3/-(DVG'oYP<BoN#dRMY\:"(ihX8nPCKPfm=*FDlFU.cK-\gtd2mMiP:LhL!<V4#nL83?,1-&&Mj$d41'=_#hD11)t@4*p0Z20JhCkr>.;UI%7`TeZV?J!b8.b[NijLeZp^-B3e!i_@YU2N#bW745<D)dmsX?K<,'eKSA=+)p(PKc8IUA<;,e0FeSRsQ?qc7cc"?F2@C&bTPGqKBX=9oEBN0m_6!e3pAE)"(>93O'QnGX^p"03@$F3kKdeZID;?BtW_*Lg'GeI[aQK!]:R>-;7iBY?nEF7CW%.D%O?ZI1*0'6f@d7#CMF>6;8^\^JOlZ]6[a\.QS\0Oe<;_qVY7$3b32UofB&C73BiI`6^eE,\;jp4/remhUIfO1glr?-s@iQOd%_t,&jf52506b?J2(=00QZh<`gklB.a)AS(A!#iY(Ds`_'=ig8i"q]DlXBaIm@sRI_)Kd3VigRKr;fYCAL6D0t[>+td:+o,ScU_Jn2nJH-61E&e<3:63ocB'IEUYGPsH2Qm%Q;O?%@!SaiHrk@K+@A+6+[WL3uWLdg=i8iBL;Da0?b[.4h8#W1j@@CAc4`bVk)IN1RSU19h'J0eiWlICtp#F4L1/0>$IeHf_BYe!HrO82K?Y(!;C,c%D2ND:'HgL5'lqE$>ZIeco\3H2'523,BZIE3(%)F@DE7gSU(1FE8SD)B3^%KVIL,42gn7)(^)]63IgT>P>1c:mgHYtAhZ<6O(qNJUu'ni$pNj0F-rcT5`KXGN/%X@C!b@6XHlO@o"]n$os=mT7pJ.ZdSFU6o)p@RIe`M/uRZr]/b;>#MWWo&IMnO3H,e3qUXR0s[bZ),D"RtfT<XH:TS7%CM9Kf&oSL1tDG$%<(CdcP;:I'As*70fV8iXCT"![%]aQj1&!Z)=eLeA(k*8JM:d=bqV_VQ#F=c:=_'TLPo;I#S[AO`]8V<Rn7;m(]Db]1;++,c7Z`6,nf+$1/c[bOiQ"LNA*2j(gAe?OEB9>uG!4L2_rjL!^BJDWqUE%QrZ+mr?#_90RS3([[ieE59Fu=F7mck<BCWmQ6;;XTi[Js),/`C&tN<1@uAa3lHX~>endstream
endobj
4 0 obj
<<
/BitsPerComponent 8 /ColorSpace /DeviceGray /Decode [ 0 1 ] /Filter [ /ASCII85Decode /FlateDecode ] /Height 70 /Length 1642
/Subtype /Image /Type /XObject /Width 83
>>
stream
Gb"/fD+ml0(k<.([%J.q@JC,HYj!DM&>DDoL_CtE5h(dDJ]=e0+<\8beW;?:P3Kgo*+MGlOj%R\d&"qq<'WVo;dpMuj#\J&(sp235S*rFU[<K]EbTlIcFqR+J!TL#e,,a.]MI8<qjqj]6N7:']OSNpfbib#G11TQq\rsc*i@dP>]6&PG2<4q*$E:@I2"r\IcGDJG[O(Vf-SJhC\2qPZ`)^E6o_#9?@&a"B\Ngh8>EN$V52L^?-;L[$.RAUGBB.a*i&`t/Q"+UB>F_Ac24^#B\rj6OePV^)6sbL2(ORn%T)B</&nr0Xi>u3ULV:E+oZOmW]@RIU[nbYiWqkJN-&TI9iSr*J./,Ebe.C:`ajt+(q8NgA`cstog\CA0T2k8o6f!@=sL5mg]3[5Y^n-mQ-*?*!6S/d,hG(eJ-m+6jnOW]^`&tr(LB>ej>;ONF]m0]?fT]%8;&2;puhn)NDUV,\55CpLlZ;F/2_;km_iUdIU)@Z+!9<RTs><7I,d^mpq=LV.P3HRi,s"i0Qi,;9!Q=EI0qX.2sq*_p%[=lp=5/2c$7dKVeO_'*1q(SI4*1XCT%BuF970BkH/@O6aVis*0B.NW]_jO*P1cu+\-UjE=aM/*EH,Tj1IDS7j<a9.UL46Xi9h68E`1]BVW+t/k"I$$V3n:N`2_.a3L/646OeAq9IsS9eN1X-fHmDGa\WOllat>GAYu(S%CZ%hF,$R3&g!9om8g@E:uKM:*de-l55j"TjpJ(8P(=lBUe6Y@jKJrU1n-VVcA;X;:n86jebj,59ql('qcTkVR13uL:)dG<"..]VJ%*<,]D[k)l(nNQ#]2i/>j$LWEbGaOU2X0E)CFIVN$1u<JN<.0OiG&6c&U)DgTJ2(Re:@cFXe:[]p\^MC0H'?$'I5;V8ee;=X!BKZZlR>dMOp+/#.IaKf8B!D+";d%DUT)'T\X\[:p!Lu4(URRh<@o<i*XAp:OH4(ca:Q![\\c*t"fFt1B<l<`RG(pq)/laYNh3X2r&['C:;ZmoQ63+%l_'IG;H2<008p`bjANfcD@Z)#OnGr$)CDFQ`iI-K2#$'e,Zggs0_r7u(D12cVdPcJXj+^11+>QVCWU!/TYCE@q8M:,,QMA4b^]1g3pQ-V,R#,T7lC(Phk?i&?)lKVKO-Sqj(/dmroS568iWN!GuSuAAT^*BSf7de;Yf3#sEW%6I?@Vq^Lk)]5PhQ%4SH`cuU9"8s>A-i)S[]D$E^^m':b9nr9T^tP9YKqtOH6B_9qJt7u(\Fcr$*IR(Oi9W$qPPRbI5-YC4hm[@]+-RF3U0<4[.)2qO&XbBlSn&O0Q:].WU'JLqUsV5O!G:r6V04NMo.j9f7M.OKL4[:D'$08SQ!Hmg`J+ni/9'$mQBmeQ)0*7!aenYf"4lZNKBj(I/%[e8.7_q,X)e<r$j9UQ<*PmY/\=UK<h?@"=6q1Q#/+F5e'WKT96-<1^(V[3C3$hI-RfHQ^[cRs4N]p:m4e,O\?5_H%B"jC@1pL=-GB?)J<aF?L?a)%l>MZqB_Ie"cq#s!;fX\@cqE<DoY8LnQYi]GO%K1F0bsl9ALTZpRs2'dToD,KVC9YV)smZJs4Wa_`gs?E>$utT7HXsj94cV;P])1Y;B=:*5p9(:@WUtSo1q"?P3;<2t1X$~>endstream
endobj
5 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
6 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject <<
/FormXob.4ad94e19863e704c886468add7da1aa5 3 0 R
>>
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 14 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 612 792 ] /Parent 12 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/PageMode /UseNone /Pages 12 0 R /Type /Catalog
>>
endobj
11 0 obj
<<
/Author (Performance West Inc.) /CreationDate (D:20260617232550-05'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617232550-05'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Canadian Wholesale Carrier & Vendor Reference Guide) /Trapped /False
>>
endobj
12 0 obj
<<
/Count 3 /Kids [ 7 0 R 8 0 R 9 0 R ] /Type /Pages
>>
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1981
>>
stream
GauHK=a/Rh'Roe[\5+\oNN@PBT55JpUqrp_/.[1IU]I?Y1h9(-nF(^:7L1]&1d3pZF(,B6k2($eM]K%lbL-?e"i0dJ/oMbL>Sp8QnLYj&+5kE=m-C(gS90D!$9^J%cqCIma1'PcgU8:Zp@Od*\!!u+na,h4MrG,:=?ah>T/oO`H8Mr+1lapfN(K=()0#ogi.dm&NF1F2;2d6"-H<@s5od;K0k[D<iF9e1plUgEpqLeQY,`[MhR_Y!$C'Z):(fV_4`D&V^UkZgM*T:Xdj$0\(b+G8SEEk7S3i8B_5Mlki"I[u0$[o'%ubu?KfFl_Ls?*n;F/Kj_V:MjFE=>FpotQYjq*]#!&'.uQVf^\6u9D*o=!,@5\V3t_Fn,]LqrQ`5I"Lu!^EoQ=JPZ$OHlK7J0ehW]muUObls(&6cAK!8&trO6rYHI&uJo!mpY#;$9D\&64=a-#-_o0m&':sLgVaPn[+meV!r(T=qcP6[='QA<9q3#98]@O@@P=*itq=V[q*(S_FO(@XJ?H_oclOh;"P<K#iXb0=fL:HQ367!*(G]BqVg#a(?,\]&8u<FGts$:*?YaVB@$\`X`Ge>*)PJFXeIgMGu<';91d*I.7$k:7.jq2p_#/)/muq>=KsuDP2a)r,P$S\cDn;=T"NbZ(dJe:e5pEM5Znm:4CW\8M7S(Djm3%>RDEBg0@W:NkNg00=Nsu;]/c;+]T$K=KXX_[-eSX[Z=#e-&n];cO<[!s93j1`'JNkUCT&QA4]rlB#!jQ[F-X#k$>(5)`BtI`W]>d$i&,,%WKro$E)gb"<br0P[&dB;#0\9(958;rH-%OB*<*1HBYBac*KY:N3>"%S+%L2<BHhX7Bge1VmYABH.)Ij5=L"rr?bi&kjn]og_9*Jl9<6Q?]ITs/S;=8Cj/@I]YY+FbE%/C4/hgZX^WIlJ'sXDreNTFc-%62A5uT[Qkt^M\0PraH,5E3Z>WcF].bU&Q'hPeY"_:<RZpJJ,!$\%c9R*\30nH?RXmnEGX\<3:e8UXuA;b/SiP<4Tm@k[0]"&*!^r,(7VC^>*jVS1FCMW0t0EhZ8+JI$i3N];%:b#6TK22OJ,2HKT\2eig07j#</^B33?T^ld>s)tj+VdO:6'Q^\;cuE+WJ$F9@&ep\Wg_I++S/Pu?E6a3.6_GQW[Tf*ABO7u[k^[_BEsZoV8^r0ZQsfj.LME9o@ApeAqZFoNNOM?_MT%<Wr0*^3SIH3]qg8(W9F]u_,@,dY-OsNo6h!*"iFOYCmoC7`3tHG33e)q7KsU$@IbM0ZKHnq4-.\XI4]gkKo6n0efr[K4QrApV!aPsG4H052L5_'S%*;jHP*l9JiiAfU#\GOHe^Ama<drj.KOY8cD0\pk,7@*;cCG$MMY`/dd$i"ABn&X6_J.:f_j5^(,eJqFaD?*[qU94G$9*egOu\nS;\<;0=-=0lE4lZ6h-D>&!3`b9c5u:1E#9r3i('D][YUHDh"k53HiUTL=]dUa-i?K>d.?e)R2A>*tYh/N?6D!G++S3Uhr9^c-n-;el?k@m11eYXg)_M%K!^VFeMAhKNXfr'\E#=0GNmN./D`jGK6aIC()ga^8Q$B5s!Wh5js+?a1'OLK)=e3[F?4W2B-.$.b2!Zkj`,5RTDIXO0XBYQ/ZYM#[QdJW]ahsi<uY,AuEbTO4`R7MlJG[XNY$O:]ho5k+qcY3B?l(aNl7^Al?%@-_1Y^`;b$5*NY?)-^"?h%YqWsE+b94;Ad\0@31V[J&NCT]"?!ST%A:peLTgkOP1Tn%7Z\:_$GN-pE)"Xn#lY9EsDiG\#`9MAg9K&E?+F<6iKZ2FH=2%i'92;i`BhP5;e7:MCd7*_1cU0NF=sf]1[M[H]2+sN_$R#R=`d)5Lr=tMct[Neh/j6Ws/:,f2MR-"`HC[dsdp1/^nESlm(A4`O*e`$5[a<-ltJ%]n,/HGLkB1fncBD3DQ^&=51@de7p4O6hKF7.9K12F6;-[dL.?,BE&%sVlD3~>endstream
endobj
14 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2255
>>
stream
GauHL>BAdn'7JbFd;Q:r+Br)FhJDQ7:h@:M+mW+M07u*s`0,l!8?dKVr]<86!kbr]];W-<N6u2hVu-*H4[,Fsf6;^sR-89OB:jSB)`3tFhssr-\YYe;LOVP`q30VBlLZ^A$TsHMGht*/n2jsG-8G<jId??!K6hoR7UEuNG:krOCq"`R%J#s:\'sW*RFB8[R["5UrU#grs1$2BQVQbj0o70O^.:Yj6pfLXCK_Xi;HE$M-$+Al8[<Rj9g+Xi):@PZn8MR+nR+baac].VJoAQsfl/q5Rl.eAq0pR_2#3D3qKJ8nO%+@Y<_8TL2qnsPK7ui/%(J,D^`ZtR#(ZIN^P?3=cCY98d</8B8gkYjboWPAP1Rj-BXc^Y]aG(`SPB\lfOBB]c*W<r&kKU#2Z:Tm-ta`^dN'=$1k^X_s)lbfV$J-]>[!2+^'?ENA<QN?]cE&9bUeU9:m%,7Z33QAFOdSBQb+/A?Ie8#4p`G;"Zk`7mdut?3ftR[2gB42?P82,Lu@/a)-i^M*Rc-&N<o3pWCIq2Bm9.Q_AIu`bNZ)BE?qZG2O#N#M0'o_/d)^YdEgFdEF)*YW=D8$o5#!'ra??t1D'fjh!]B5I't!-(-WZXr+sjPRi[O$fFkaZ:*(=5me4En8lZ"L)Om';@cT"?gLqA'p&$BT^^M:'"%sQg2F]d`8!CDZB#SpX-S)<b=DHA[m9]+V0XZio1C1?VC>,RR>`)7(\`4T/+35@s3f<2Y,A!"'Nj1I1M=4Z[Q>stCdB%4GWG&8*.O&&sF?qC]WBcX8=,+k._$6M:V0s&t??7^H][IbH4UI8,DOp?F.MJkeH:Q"odVQ_#GUZRVo$om>N(B@U,JD;F@CEAo=9,="j-d-)UnH3QpV%2K1=@rDNSQ>'mT<\Q8aH1?GrpR(1.jI^ho25e63Ui$CG`Oi?("3/MAR]r?#0',E*BgMVLK"DV/GZAU/F#)H/kfalLPdA6P_h[JZ\O)8>H[%7nN"LE*r1eW6JRXnXpj4L4%mUg-?j*63m`dOG+WWU#+'+KG9&c1fTcR_;H/77lflbo;L2f^(;BC>G!Zi!-A_mR6#>RjD2.S&Le)6=C]hfr1V%+fog$E7hu8\-JDj'g<o%%ZrLbk3>;kU9]dWP^ooq#`hk#I^g%!MO$7>"3jC992pG;93gDIHDF>JNUi+B1=Ua6-4MMbG6#*IJCHnFoDeJ&(EAPUpK*B+U(<Kh%io0N8V#26iAQK3b"f;:K(YGueW8(%PG77*ta,P3\H&\(@s*5]0Cf]CG<%@5':%3qmMHcl_N>C6Fn1LK:^M8cc,"O-_[A<W7Q!W)20CM`3+TRWuG8@8.hZjcQ"oOlY$:>uU*':4*BO8=oqo<1iXUfA!5p/oTg?=K?+1\Sm=$omg[aj$jD$^,%"A.87WFTUo"YrB4)>Q\m#34+V/i$Rb\3M6[HufuHRto.]082@0'-=-fo:Ea#,b#]H`ddqFT(Lu;.fk%6eA%Q1f*Zk';lG$70)GN:[P_:FA$@Q?Xk!kJjb3WZklL5h)X8Uml&sqsRNnocj'*(RlR\!hRMs;J[P"2rXlVb,^pMjcr\g`&UVS75+CJ&?oj75'YQKTCbU'$q<]>=X\:EUYN-d'<gKHRfVGA-7*C3!D[u?"F(!<q>6&Co!c!1pI9:&t*ocQ,5UtiAp"]9:bO@U6E3k:daH*mpUbWr*So?WDJq[Vq,TO+kWDs,P['No=X:h3?#()rg>o1i`kO3D@GgW*]f.+\BdN\q_.32e&8l\6'`Bp=lYUhMg"7K4+FOM-nfb=jBRX+m>YPDD;U(<iOL9Z8c`$KEE6UVX<>r52#=AO/cia]B=&rdpjdLLE7ElHn$WmZFD,<T,`uU1l,ZmKu]d,<WQ#.E3O#.0b^$+k19[pC*eQd4Bu=OH*Qs?os;LAHE:E04HQM59$.F.m``50\gSZf+lrO<["M]&pWl>e5q0uk)1/6oiZ[W:AT)XkKjpb7ljn+X4oVKF-M<>BsElR\E5`"cLK!STepb!Ao"RkDseX+e;+VfC"H+qeSgp>.,XHCj?W3`aR;57bLEX\jokk?llB$[f`!n:TYl)MhId=Wgf(eMm1+Si6'j=E/.cV2f?C?6VQM=!,A5#;lL&KK""S__3Q;LC(F)(9BnB6A)#UKnI/@"#lUsFPN;2?%C#TJ%c9;2H24,M,)7XM4s704de]uQBL@#Qh0,j5%bZ9?0+Fl?a4n`q+Al1/rTN6K;UF__$4!/qdD[&o^.h&LRId0ljV5XeOO3$P&rrI9.P'[~>endstream
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1736
>>
stream
Gau0BhfId8&:Vr4Z&b[cNb)^QqpJ'Wh$oo#QQGd>%s\bq^hR]-Q/kd<f68M[#80?a2n>$dfO!s+Dti!l!^;'hqbBe7M`I;U,!Z3s07aPF+j@.Jj8A!,'@KL5`\8Tf2b%e7MXmQA0d&/'h:X=.&c`6?(6A59M$[L`+;s/rM",BdC'D<8pZ^`Y-F,1<_,G*Y'H5mDk'o\c4pXX&YP*eh-38rmi!lGiiWnC1_oPWn:=3;r.?*W2QQ%;6s2B<_#CcA/O-=,?\OX^h*hp*Y.IlD;*/l(@[NAF]6aDe!%M9L]O8jjXHDrHVg)Bbl;'N7]=eu5Xo19o,GdDQi24u8h/Y[unU.a(N-J5PBf*4<HlB.u<s6@fikF+b$\BAI"1bBP,8e_<D<69gr[L]_//JqKG#;0&*Qr$():'FIV_4NNnS6kJE>gBLt\A2U+),O(.+"AQeWEHY=f=*C,Y.re?nR-D1E<9onEk%L^B0^6r9?$pR-G?2LKSQTV(q&Q=6H0M)Y+T#0Ng7G?1jcG`TQH<:Og0f&QS=M!V)VD5lRiH[W2[1W@g&"!o*NFOinC?./j9!8>V`-ar=!1_+JX\ZUSHQhJWV'LNg<k,N<P!$'a<H>n6*mBA7^b9;XVW.WA'\hTNq[&\J?aNnXuW!Lc1-mrS@VCmJ,8/1Skgln:DV)o^UH+j*)^B-VI)?o*mKkW?-L<];$Wt%sBU1T1/(OQdVX0j7l??-da=f5DSG%Qde[B:^gZ,4."Z/7GV9ta8U$<\3%Iqp?VL/q&KWoMl>V/cR&6^VD\&t5Xm>?bluSm!$OJFnj106VGR7@=G3d9P<IC1f:]QJUkZXf&/T2en3H1'QEO?e'q0;X4-gtDfcRpA=6F?-8cB8eB,K>M77G@AMDW;?Ic#L,mJZ:S.#&%R&*b&L_HW4L(a#>3>K4An8T0l!.9`$ZdERR?gSmBX;EJA1\OKIW*m_!%B@kbA4qaD)I9op%GiTa+DMWKOZK46DrNW$LnC51d5=K"Xpp/_SqG9`Y:fg01B_]_F(FJUfep5Q6TPDHQJ]TL?1Ku"!CYrtp%FUsRMA$V+=RT$o<QG=qMTnf&G$X?hWI*G-'>R4HQ\[4h/PM)V5V5TCdN"<X/qBcrDbs`m9QBdI$<\N:NKaTEqni3A8*raiM[X`N-EGrJVu%Xr]<"be#1Ob4Zfg%]Y`mEj7hEbos+&.)@8O_Q5=MK<)W8nA)qOi02<9.=42b([F5'B'Ce6_#J?>KgXqXA>mC-lqZfd_(D#jdD`](Z)][eXJIQeA3?jX_!`bPU]Q\N@23F#Dja.f^kq/9-s/#1e;mSO.i"erGf[<gh/S!r6GP%td*/<I<3YU_H<Z\_)oZGLKi\][rP0Z;Vn8j'<eBP-N:#(mEFM[?iQ<2rQjQ:>a>b[eg8dloSjEHaq>_bP?s,n^b:/?TNW&ERAVprN70hL*up@^*'=9mL]sW"Mp!bV:tl$\#38@Mq?5]12f_*$f/pTq\mCg8'@#RC#h3"DMhO?5,X/j7.@J,\5hgj7$_JL&TQPKj*=4o4C_NDB7_KD7d4E](I@El-bUZ8hh<ELtQ8/12BgNH@>,c#]5PQ6E3bD4RPU8jB5cuBtNH+oF@M&$kh!]Q@c91T]<Z+nTi]3dA8(OXHBs&%6'T0"^<-aBkD^T"f]%&\*Y4i9\XL"i>R;$\SrV"KcU]![8JnTlt^Y+MD);]ei>AS5Ss!rRK"SP^p$4.bV_;8]aC;qHdC)pPQ(WUh.Y@~>endstream
endobj
xref
0 16
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000001717 00000 n
0000003565 00000 n
0000003677 00000 n
0000003792 00000 n
0000004050 00000 n
0000004245 00000 n
0000004440 00000 n
0000004510 00000 n
0000004837 00000 n
0000004909 00000 n
0000006982 00000 n
0000009329 00000 n
trailer
<<
/ID
[<dc6b7564b74779a92e78c9be042d812c><dc6b7564b74779a92e78c9be042d812c>]
% ReportLab generated PDF document -- digest (opensource)
/Info 11 0 R
/Root 10 0 R
/Size 16
>>
startxref
11157
%%EOF