diff --git a/api/src/routes/canada-crtc.ts b/api/src/routes/canada-crtc.ts index 7a0773e..57d4e1e 100644 --- a/api/src/routes/canada-crtc.ts +++ b/api/src/routes/canada-crtc.ts @@ -226,17 +226,34 @@ router.post("/api/v1/canada-crtc/orders", submitLimiter, async (req, res) => { : company_type === "numbered_tradename" ? TRADE_NAME_SERVICE_FEE : 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; if (discount_code) { const dcResult = await pool.query("SELECT * FROM discount_codes WHERE code = $1 AND active = TRUE", [discount_code.toUpperCase()]); if (dcResult.rows.length > 0) { const dc = dcResult.rows[0]; - const discountableAmount = SERVICE_FEE + typeAddon; // discount applies to full service fee - if (dc.discount_type === "percent") { - discountCents = Math.round((discountableAmount * dc.discount_value) / 100); + const now = new Date(); + const notStarted = dc.starts_at && new Date(dc.starts_at) > now; + 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 { - 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); + } } } } diff --git a/api/src/routes/checkout.ts b/api/src/routes/checkout.ts index 3e24e4b..719d37f 100644 --- a/api/src/routes/checkout.ts +++ b/api/src/routes/checkout.ts @@ -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. * - * 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, * but PayPal / crypto / remediation-pipeline orders reach handlePaymentComplete * 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 - * portal account regardless of how it was created or paid. Fully idempotent: - * ensureWebsiteUser no-ops if the User already exists, and we only flip the - * flag (which gates the delivery worker's set-password invite) the first time. + * runs in that shared post-payment path so EVERY paid order (compliance, + * canada_crtc, formation) gets an ERPNext Customer + Website User regardless of + * how it was created or paid. Fully idempotent: ensureWebsiteUser no-ops if the + * 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, orderType: string, rows: Record[], @@ -241,7 +243,11 @@ async function ensureCompliancePortalUser( if (portalUserCreated) { const table = orderType === "compliance_batch" || orderType === "compliance" ? "compliance_orders" - : null; + : orderType === "canada_crtc" + ? "canada_crtc_orders" + : orderType === "formation" + ? "formation_orders" + : null; if (table) { try { 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 // customers who paid via PayPal being unable to log in). Idempotent. try { - await ensureCompliancePortalUser(order_id, order_type, updated.rows); + await ensurePortalUser(order_id, order_type, updated.rows); } catch (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") { 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" // Stripe card/ACH/Klarna: advance to "Awaiting Funds" — balance.available webhook handles next step // Crypto: advance to "Awaiting Funds" — manual admin step later diff --git a/scripts/generate_canada_carrier_guide_pdf.py b/scripts/generate_canada_carrier_guide_pdf.py new file mode 100644 index 0000000..4cc1bf0 --- /dev/null +++ b/scripts/generate_canada_carrier_guide_pdf.py @@ -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"]}  —  {v["location"]}', s["vname"])) + story.append(Paragraph(v["website"], s["vweb"])) + story.append(Paragraph(f'Services: {v["services"]}', s["vsvc"])) + story.append(Paragraph(f'Notes: {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( + 'Ready to launch your Canadian carrier? 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 ' + 'performancewest.net/order/canada-crtc 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()) diff --git a/site/public/guides/canada-carrier-guide.pdf b/site/public/guides/canada-carrier-guide.pdf new file mode 100644 index 0000000..508d675 --- /dev/null +++ b/site/public/guides/canada-carrier-guide.pdf @@ -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.;UI%7`TeZV?J!b8.b[NijLeZp^-B3e!i_@YU2N#bW74593O'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"RtfTuG!4L2_rjL!^BJDWqUE%QrZ+mr?#_90RS3([[ieE59Fu=F7mckendstream +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[]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)Bu3ULV: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<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,Tj1IDS7jGAYu(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$1udMOp+/#.IaKf8B!D+";d%DUT)'T\X\[:p!Lu4(URRh<@oQVCWU!/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)eMZqB_Ie"cq#s!;fX\@cqE$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[DFpotQYjq*]#!&'.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*)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""%S+%L2WcF].bU&Q'hPeY"_:*jVS1FCMW0t0EhZ8+JI$i3N];%:b#6TK22OJ,2HKT\2eig07j#s)tj+VdO:6'Q^\;cuE+WJ$F9@&ep\Wg_I++S/Pu?E6a3.6_GQW[Tf*ABO7u[k^[_BEsZoV8^r0ZQsfj.LME9o@ApeAqZFoNNOM?_MT%&!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]ahsiendstream +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-[!2+^'?ENA,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;kU9]dWP^ooq#`hk#I^g%!MO$7>"3jC992pG;93gDIHDF>JNUi+B1=Ua6-4MMbG6#*IJCHnFoDeJ&(EAPUpK*B+U(C6Fn1LK:^M8cc,"O-_[AuU*':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'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(r52#=AO/cia]B=&rdpjdLLE7ElHn$WmZFD,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$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*4gBLt\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'LNgn6*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@=G3d9PM77G@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$oR4HQ\[4h/PM)V5V5TCdN"KgXqXA>mC-lqZfd_(D#jdD`](Z)][eXJIQeA3?jX_!`bPU]Q\N@23F#Dja.f^kq/9-s/#1e;mSO.i"erGf[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,c#]5PQ6E3bD4RPU8jB5cuBtNH+oF@M&$kh!]Q@c91T]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 +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 11 0 R +/Root 10 0 R +/Size 16 +>> +startxref +11157 +%%EOF