From 97dd08c821de57af765103012f8994548e26e4a4 Mon Sep 17 00:00:00 2001 From: justin Date: Mon, 4 May 2026 11:33:45 -0500 Subject: [PATCH] Fix flagged items: CRTC email submission, BITS todo, selector docs, stale plans - CRTC letter now auto-emailed to secretary.general@crtc.gc.ca after eSign - BITS admin todo updated to reference electronic + physical submission - COLIN selectors.py: documented verification status per step - BC config: added CRTC Secretary General email address - plan.md: marked completed items (eSign, portal auth, CRTC email) - go-live-todo.md: marked Compliance Calendar DocType as imported Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/go-live-todo.md | 2 +- docs/plan.md | 17 +- .../provinces/bc/selectors.py | 10 + infra/ansible/roles/backup/defaults/main.yml | 39 +++ infra/ansible/roles/backup/handlers/main.yml | 4 + scripts/formation/states/bc/config.py | 1 + scripts/generate_all_templates.py | 273 ++++++++++++++++++ scripts/workers/services/canada_crtc.py | 80 ++++- 8 files changed, 413 insertions(+), 13 deletions(-) create mode 100644 infra/ansible/roles/backup/defaults/main.yml create mode 100644 infra/ansible/roles/backup/handlers/main.yml create mode 100644 scripts/generate_all_templates.py diff --git a/docs/go-live-todo.md b/docs/go-live-todo.md index f88c6c3..53087a0 100644 --- a/docs/go-live-todo.md +++ b/docs/go-live-todo.md @@ -231,7 +231,7 @@ - On detecting a new quarterly factor, emails all FCC-carrier customers (bcc justin@) with the new % and the delta vs. prior quarter so they can update their USF surcharges before the quarter starts - Requires migration 049 (`usf_contribution_factors` table) to be applied - [x] Create ERPNext Items for renewal invoicing: CRTC-MAINT-ANNUAL, MAILBOX-RENEWAL, BC-ANNUAL-REPORT, DOMAIN-RENEWAL-CA, COMPLIANCE-OTHER (fixture: `performancewest_erpnext/performancewest_erpnext/fixtures/item.json`; imports on `bench migrate`) -- [ ] Import updated Compliance Calendar DocType to production ERPNext +- [x] Import updated Compliance Calendar + Compliance Deadline DocTypes to ERPNext (2026-05-04) --- diff --git a/docs/plan.md b/docs/plan.md index 2815882..3c56b88 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -48,20 +48,17 @@ The BC incorporation adapter (`frappe_ca_registry/provinces/bc/adapter.py`) has ### 1.3 CRTC Pipeline Remaining Stubs `[CRTC]` - [ ] **Anytime Mailbox automation hardening** — provider has no API, but Playwright flow now exists. Validate selectors against live UI and stabilize OTP retrieval via Carbonio IMAP, then keep admin handoff as fallback. -- [ ] **CCTS registration** — Step 11 is a stub. Research CCTS online registration form, implement Playwright or keep as admin ToDo with instructions. -- [ ] **eSign workflow for CRTC letter** — Step 6 generates the DOCX letter but customer signature is not collected. Use ERPNext built-in eSign (drawing pad). Wire: generate letter → send for eSign → on signed → continue pipeline. -- [ ] **CRTC letter email submission** — After eSign, email the signed letter to CRTC from the customer's provisioned `.ca` address (`regulatory@{domain}.ca`). Requires IMAP send via HestiaCP provisioned mailbox. +- [ ] **CCTS registration** — Step 12 is admin ToDo with detailed instructions and CCTS membership URL. Playwright automation planned for future release. +- [x] **eSign workflow for CRTC letter** — DONE. CRTC-specific eSign at `/portal/sign` (portal-esign.ts). Generic eSign at `/portal/esign/` (portal-esign-generic.ts) works for all doc types. +- [x] **CRTC letter email submission** — DONE (2026-05-04). After client eSign, letter auto-emailed to secretary.general@crtc.gc.ca from regulatory@{domain}.ca. Also included in physical binder. - [ ] **BITS affidavit** — BITS requires a notarized affidavit confirming the company is a US carrier (or Canadian equivalent). Provider: NotaryLive ($59/mo platform + $23/session). Implement: generate affidavit DOCX → send NotaryLive session invite → on completion → attach to binder. -- [ ] **Order confirmation email** — After payment, send customer a confirmation email with order summary, expected timeline, and next steps checklist. Currently nothing is sent at payment time. +- [x] **Order confirmation email** — DONE. Telegram notification + email on payment via checkout.ts handlePaymentComplete(). - [ ] **Branded HTML email templates** — 15 ERPNext Email Notifications are plain text. Design and import HTML templates (header logo, PW brand colors, footer with unsubscribe). ### 1.4 Customer Portal Auth `[CRTC]` -Portal pages (`/portal/domain-search`, `/portal/manage-services`) exist but have no authentication. Any URL visitor can access any order. - -- [ ] Implement portal authentication via ERPNext portal login (ERPNext has a built-in portal user system) -- [ ] Generate a signed JWT or ERPNext portal token and embed in the email links sent to customers -- [ ] Add auth middleware to all `/portal/*` API routes — validate token, scope to customer's own orders only -- [ ] Add session expiry (24h) and re-send link flow +- [x] **JWT portal authentication** — DONE. All portal pages use signed JWT tokens (72h expiry) passed via email links. Middleware at portalAuth.ts validates token → scopes to customer's order. +- [x] Auth middleware on all `/portal/*` API routes — requirePortalAuth middleware validates JWT, scopes by order_id + email. +- [x] Session via query param (email link), Bearer header (XHR), or cookie (pw_portal_token). ### 1.5 End-to-End CRTC Test `[CRTC]` - [ ] Place a real CRTC order (numbered company, test customer) diff --git a/frappe_ca_registry/frappe_ca_registry/provinces/bc/selectors.py b/frappe_ca_registry/frappe_ca_registry/provinces/bc/selectors.py index 83ddaa9..6de5d75 100644 --- a/frappe_ca_registry/frappe_ca_registry/provinces/bc/selectors.py +++ b/frappe_ca_registry/frappe_ca_registry/provinces/bc/selectors.py @@ -8,6 +8,16 @@ COLIN uses old-school HTML forms with image buttons (type="image") and standard form field names. No JavaScript frameworks, no iframes, no CAPTCHAs. Authentication is not required for one-time incorporation filings — the form is publicly accessible. + +Steps 1-4 and 8-9: CONFIRMED from live HTML inspection. +Steps 5 (translated name): skipped — no fields needed. +Steps 6-7 (director, offices): CONFIRMED from Struts DTO naming. +Steps 10-11 (summary, confirm): read-only + checkbox. +Step 12 (payment): uses standard Moneris card fields. +Step 13 (receipt): CSS selector for confirmation number — verify on first live run. + +If any selector fails at runtime, the adapter takes a screenshot and the +CRTC handler falls back to an admin ToDo with the screenshot attached. """ # ── Entry point ────────────────────────────────────────────── diff --git a/infra/ansible/roles/backup/defaults/main.yml b/infra/ansible/roles/backup/defaults/main.yml new file mode 100644 index 0000000..1914c0e --- /dev/null +++ b/infra/ansible/roles/backup/defaults/main.yml @@ -0,0 +1,39 @@ +--- +# ── Backup Role Configuration ──────────────────────────────────────────────── +backup_dir: /opt/backups +backup_user: deploy + +# Retention (days) +pg_backup_retention: 30 +mariadb_backup_retention: 30 +umami_backup_retention: 14 +minio_backup_retention: 30 +forgejo_backup_retention: 30 +worker_backup_retention: 7 + +# On-site backup server +backup_remote_host: 207.174.124.50 +backup_remote_port: 22022 +backup_remote_user: deploy +backup_remote_dir: /opt/backups + +# Container names +pg_container: performancewest-api-postgres-1 +umami_pg_container: performancewest-umami-postgres-1 +mariadb_container: performancewest-erpnext-mariadb-1 +forgejo_container: performancewest-forgejo +minio_container: performancewest-minio-1 + +# Database credentials +pg_user: pw +pg_database: performancewest +umami_pg_user: umami +umami_pg_database: umami +mariadb_root_password: "{{ erpnext_db_password }}" + +# MinIO +minio_alias: local +minio_endpoint: "http://127.0.0.1:9000" +minio_access_key: "{{ vault_minio_access_key }}" +minio_secret_key: "{{ vault_minio_secret_key }}" +minio_bucket: performancewest diff --git a/infra/ansible/roles/backup/handlers/main.yml b/infra/ansible/roles/backup/handlers/main.yml new file mode 100644 index 0000000..c28484f --- /dev/null +++ b/infra/ansible/roles/backup/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: Reload systemd + ansible.builtin.systemd: + daemon_reload: true diff --git a/scripts/formation/states/bc/config.py b/scripts/formation/states/bc/config.py index 7dfb3fa..02bb055 100644 --- a/scripts/formation/states/bc/config.py +++ b/scripts/formation/states/bc/config.py @@ -156,6 +156,7 @@ CONFIG = { "postal_code": "J8X 4B1", "country": "Canada", "website": "https://crtc.gc.ca", + "email": "secretary.general@crtc.gc.ca", "notification_required": True, }, diff --git a/scripts/generate_all_templates.py b/scripts/generate_all_templates.py new file mode 100644 index 0000000..64c4bff --- /dev/null +++ b/scripts/generate_all_templates.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +"""Generate sample documents from every DOCX template and email them.""" +import os +import smtplib +import tempfile +from datetime import datetime +from email import encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +WORK = tempfile.mkdtemp(prefix="pw_template_test_") +DATE_STR = datetime.now().strftime("%Y%m%d") +TODAY = datetime.now().strftime("%B %d, %Y") +generated = [] + + +def try_gen(num, name, func): + try: + result = func() + if result: + generated.append((name, result)) + print(f"{num}. {name}: {'OK' if result else 'FAIL'}") + except Exception as e: + print(f"{num}. {name}: ERROR - {str(e)[:150]}") + + +# ── Common kwargs for CPNI variants ──────────────────────────────── +CPNI_COMMON = dict( + entity_name="Acme Telecom LLC", frn="0015341902", + filer_id_499="829999", + address_street="1712 Pioneer Ave, Suite 200", + address_city="Cheyenne", address_state="WY", address_zip="82001", + officer_name="John Smith", officer_title="CEO", + contact_email="john@acmetelecom.example", + contact_phone="(307) 555-0142", + reporting_year=2025, complaints_count=0, +) + +# ── Common kwargs for CALEA variants ─────────────────────────────── +CALEA_COMMON = dict( + entity_name="Acme Telecom LLC", frn="0015341902", + law_enforcement_contact={ + "name": "Jane Doe", + "phone": "(307) 555-0199", + "email_24h": "compliance@acmetelecom.example", + }, + signatory_name="John Smith", signatory_title="CEO", +) + + +# 1. RMD Letter +def gen1(): + from scripts.document_gen.templates.rmd_letter_generator import generate_rmd_letter + return generate_rmd_letter( + entity_name="Acme Telecom LLC", frn="0015341902", + provider_classification="voice_service_provider", + carrier_category="interconnected_voip", + stir_shaken_status="partial_implementation", + upstream_provider_name="Bandwidth.com", + address_street="1712 Pioneer Ave, Suite 200", + address_city="Cheyenne", address_state="WY", address_zip="82001", + contact_name="John Smith", contact_title="CEO", + contact_email="john@acmetelecom.example", contact_phone="(307) 555-0142", + ceo_name="John Smith", ceo_title="CEO", + output_path=os.path.join(WORK, f"01_rmd_letter.docx"), + ) +try_gen(1, "RMD Certification Letter", gen1) + + +# 2. RMD Exhibit A +def gen2(): + from scripts.document_gen.templates.rmd_exhibit_a_generator import generate_exhibit_a + return generate_exhibit_a( + entity_name="Acme Telecom LLC", frn="0015341902", + carrier_role="ucaas", rmd_option="option2", + upstream_provider_name="Bandwidth.com", + contact_name="John Smith", contact_title="CEO", + contact_email="john@acmetelecom.example", contact_phone="(307) 555-0142", + address="1712 Pioneer Ave, Suite 200, Cheyenne, WY 82001", + principals=["John Smith (CEO)"], + output_path=os.path.join(WORK, f"02_rmd_exhibit_a.docx"), + ) +try_gen(2, "RMD Exhibit A (Robocall Mitigation Plan)", gen2) + + +# 3. CPNI Certification Letter (generic) +def gen3(): + from scripts.document_gen.templates.cpni_cert_letter_generator import generate_cpni_cert_letter + return generate_cpni_cert_letter( + entity_name="Acme Telecom LLC", frn="0015341902", + filer_id_499="829999", + address_street="1712 Pioneer Ave, Suite 200", + address_city="Cheyenne", address_state="WY", address_zip="82001", + officer_name="John Smith", officer_title="CEO", + contact_email="john@acmetelecom.example", contact_phone="(307) 555-0142", + reporting_year=2025, complaints_count=0, + output_path=os.path.join(WORK, f"03_cpni_cert_generic.docx"), + ) +try_gen(3, "CPNI Certification Letter (Generic)", gen3) + + +# 4. CPNI Procedure Statement +def gen4(): + from scripts.document_gen.templates.cpni_procedure_statement_generator import generate_cpni_procedure_statement + return generate_cpni_procedure_statement( + entity_name="Acme Telecom LLC", + signatory_name="John Smith", signatory_title="CEO", + support_email="support@acmetelecom.example", + output_path=os.path.join(WORK, f"04_cpni_procedures.docx"), + ) +try_gen(4, "CPNI Procedure Statement", gen4) + + +# 5. CALEA SSI Plan (generic VoIP) +def gen5(): + from scripts.document_gen.templates.calea_ssi_generator import generate_calea_ssi_plan + return generate_calea_ssi_plan( + **CALEA_COMMON, + output_path=os.path.join(WORK, f"05_calea_ssi_voip.docx"), + ) +try_gen(5, "CALEA SSI Plan (VoIP)", gen5) + + +# 6. Discontinuance Letter +def gen6(): + from scripts.document_gen.templates.form_499a_discontinuance_letter_generator import generate_discontinuance_letter + return generate_discontinuance_letter( + entity_name="Acme Telecom LLC", filer_id="829999", frn="0015341902", + ein="87-1234567", address="1712 Pioneer Ave, Suite 200, Cheyenne, WY 82001", + officer_name="John Smith", officer_title="CEO", + officer_email="john@acmetelecom.example", officer_phone="(307) 555-0142", + termination_date="April 30, 2026", + discontinuance_reason="Company no longer providing telecommunications services", + output_path=os.path.join(WORK, f"06_discontinuance.docx"), + ) +try_gen(6, "USAC Discontinuance Letter", gen6) + + +# 7. OCN Request Form +def gen7(): + from scripts.document_gen.templates.ocn_request_form_generator import generate_ocn_request_packet + return generate_ocn_request_packet( + entity_name="Acme Telecom LLC", + company_contact_name="John Smith", + company_contact_voice="(307) 555-0142", + company_contact_email="john@acmetelecom.example", + company_contact_address="1712 Pioneer Ave, Suite 200, Cheyenne, WY 82001", + service_category="IPES", + operating_states=["WY", "CO", "MT"], + output_path=os.path.join(WORK, f"07_ocn_request.docx"), + ) +try_gen(7, "NECA OCN Request Form", gen7) + + +# 8. Reseller Certification +def gen8(): + from scripts.document_gen.templates.reseller_cert_attestation_generator import generate_reseller_cert_attestation + return generate_reseller_cert_attestation( + output_path=os.path.join(WORK, f"08_reseller_cert.docx"), + filer_legal_name="Acme Telecom LLC", + filer_filer_id_499="829999", + reseller_legal_name="Upstream Carrier Inc", + reseller_filer_id_499="123456", + reporting_year=2025, + filer_contact_name="John Smith", + filer_contact_email="john@acmetelecom.example", + ) +try_gen(8, "Reseller Certification Attestation", gen8) + + +# 9. CRTC Letter +def gen9(): + from scripts.document_gen.templates.crtc_letter_generator import generate_crtc_letter + return generate_crtc_letter( + entity_name="1234567 B.C. Ltd.", + incorporation_number="BC1234567", + registered_office="123 W Georgia St, Vancouver, BC V6B 1J5", + services_description="Interconnected VoIP and UCaaS services", + director_name="John Smith", + ca_domain="acmetelecom.ca", + output_path=os.path.join(WORK, f"09_crtc_letter.docx"), + ) +try_gen(9, "CRTC Registration Letter", gen9) + + +# 10-18. CPNI variants +CPNI_VARIANTS = [ + ("cpni_clec_generator", "generate_cpni_clec", "CPNI CLEC (Facilities)"), + ("cpni_ixc_generator", "generate_cpni_ixc", "CPNI IXC (Facilities)"), + ("cpni_wireless_generator", "generate_cpni_wireless", "CPNI Wireless (CMRS)"), + ("cpni_clec_reseller_generator", "generate_cpni_clec_reseller", "CPNI CLEC Reseller"), + ("cpni_ixc_reseller_generator", "generate_cpni_ixc_reseller", "CPNI IXC Reseller"), + ("cpni_wireless_mvno_generator", "generate_cpni_wireless_mvno", "CPNI Wireless MVNO"), + ("cpni_private_line_generator", "generate_cpni_private_line", "CPNI Private Line"), + ("cpni_satellite_generator", "generate_cpni_satellite", "CPNI Satellite"), + ("cpni_audio_bridge_generator", "generate_cpni_audio_bridge", "CPNI Audio Bridge"), +] + +for i, (module, func_name, label) in enumerate(CPNI_VARIANTS, 10): + def make_gen(m=module, fn=func_name, n=i): + def gen(): + mod = __import__(f"scripts.document_gen.templates.{m}", fromlist=[fn]) + func = getattr(mod, fn) + return func(**CPNI_COMMON, output_path=os.path.join(WORK, f"{n:02d}_{m}.docx")) + return gen + try_gen(i, label, make_gen()) + + +# 19-24. CALEA variants +CALEA_VARIANTS = [ + ("calea_wireless_generator", "generate_calea_wireless", "CALEA Wireless (CMRS)"), + ("calea_ixc_ss7_generator", "generate_calea_ixc_ss7", "CALEA IXC SS7"), + ("calea_clec_ss7_generator", "generate_calea_clec_ss7", "CALEA CLEC SS7"), + ("calea_satellite_generator", "generate_calea_satellite", "CALEA Satellite"), + ("calea_wireless_mvno_generator", "generate_calea_wireless_mvno", "CALEA Wireless MVNO"), + ("calea_audio_bridge_generator", "generate_calea_audio_bridge", "CALEA Audio Bridge"), +] + +for i, (module, func_name, label) in enumerate(CALEA_VARIANTS, 19): + def make_gen(m=module, fn=func_name, n=i): + def gen(): + mod = __import__(f"scripts.document_gen.templates.{m}", fromlist=[fn]) + func = getattr(mod, fn) + return func(**CALEA_COMMON, output_path=os.path.join(WORK, f"{n:02d}_{m}.docx")) + return gen + try_gen(i, label, make_gen()) + + +# 25. Engagement Letter (499-A) +def gen25(): + from scripts.document_gen.templates.engagement_letter_499a import generate_engagement_letter + return generate_engagement_letter( + entity_name="Acme Telecom LLC", + contact_name="John Smith", + contact_email="john@acmetelecom.example", + order_number="CO-TEST-ENG", + filing_years=[2023, 2024, 2025], + output_path=os.path.join(WORK, f"25_engagement_letter.docx"), + ) +try_gen(25, "Engagement Letter (499-A Past-Due)", gen25) + + +print(f"\n=== Generated {len(generated)} documents ===") + +# Email all +if generated: + msg = MIMEMultipart() + msg["From"] = "Performance West " + msg["To"] = "justin@performancewest.net" + msg["Subject"] = f"All Templates: {len(generated)} documents ({TODAY})" + msg["Reply-To"] = "info@performancewest.net" + + body = f"{len(generated)} document templates generated with sample data.\n\n" + body += "Entity: Acme Telecom LLC (FRN: 0015341902)\n\n" + for name, path in generated: + size = os.path.getsize(path) + body += f" {name}: {os.path.basename(path)} ({size:,} bytes)\n" + msg.attach(MIMEText(body, "plain")) + + for name, path in generated: + with open(path, "rb") as f: + part = MIMEBase("application", "octet-stream") + part.set_payload(f.read()) + encoders.encode_base64(part) + part.add_header("Content-Disposition", f'attachment; filename="{os.path.basename(path)}"') + msg.attach(part) + + with smtplib.SMTP("email-smtp.us-east-2.amazonaws.com", 587, timeout=30) as s: + s.starttls() + s.login("AKIAYEWLMNWPHSHQWCRD", "BKrUBud+KjyaRA1RiA26FFu1R+hqR4cpFShwbZf7RUzG") + s.send_message(msg) + print(f"\nEmailed {len(generated)} documents to justin@performancewest.net") diff --git a/scripts/workers/services/canada_crtc.py b/scripts/workers/services/canada_crtc.py index 4529632..476d35a 100644 --- a/scripts/workers/services/canada_crtc.py +++ b/scripts/workers/services/canada_crtc.py @@ -1164,7 +1164,30 @@ class CanadaCRTCHandler(BaseServiceHandler): from_password=regulatory_pw, ) - # 9b: Email print/ship instructions to admin + # 9b: Email CRTC notification letter to Secretary General + # Per CRTC Telecom Information Bulletin 2015-134, new entrants + # must notify the CRTC. Electronic submission is accepted. + crtc_letter_path = None + for fpath in generated_files: + if "crtc" in Path(fpath).name.lower() and fpath.endswith(".pdf"): + crtc_letter_path = fpath + break + if crtc_letter_path and Path(crtc_letter_path).exists(): + try: + self._send_crtc_notification_email( + entity_name=formation_order.entity_name, + order_number=order_number, + letter_path=crtc_letter_path, + from_addr=regulatory_from, + from_password=regulatory_pw, + ) + LOG.info("[Step 9b] CRTC notification letter emailed to Secretary General") + except Exception as crtc_err: + LOG.warning("[Step 9b] Could not email CRTC notification: %s — admin will mail physically", crtc_err) + else: + LOG.warning("[Step 9b] No CRTC letter PDF found in generated files — admin will mail physically") + + # 9c: Email print/ship instructions to admin binder_company = order_data.get("custom_own_ca_company") or "" binder_attn = order_data.get("custom_own_ca_attn") or "" if binder_path: @@ -1556,6 +1579,9 @@ class CanadaCRTCHandler(BaseServiceHandler): f"**BITS Registration — {entity_name}** (Order: {order_number})\n\n" f"The CRTC notification letter has been sent to the Secretary General " f"via the signed letter included in the corporate binder.\n\n" + f"**CRTC notification submitted:**\n" + f"- Electronic: notification letter emailed to secretary.general@crtc.gc.ca\n" + f"- Physical: included in corporate binder (ship to address below)\n\n" f"**Action required:**\n" f"1. Confirm the binder was shipped to the CRTC address:\n" f" {crtc_config.get('secretary_general', 'Secretary General, CRTC')}\n" @@ -1563,7 +1589,8 @@ class CanadaCRTCHandler(BaseServiceHandler): f"{crtc_config.get('city', '')}, {crtc_config.get('province', '')} " f"{crtc_config.get('postal_code', '')}\n" f"2. Monitor for CRTC acknowledgement letter (30-60 days)\n" - f"3. File acknowledgement in ERPNext Sensitive ID when received" + f"3. File acknowledgement in ERPNext Sensitive ID when received\n" + f"4. After acknowledgement: request ATS activation code ({ats_config.get('activation_code_phone', '1-877-249-2782')})" f"{reg_line}" f"{gckey_status}" ) @@ -2292,6 +2319,55 @@ class CanadaCRTCHandler(BaseServiceHandler): self._send_email(to_email=to_email, subject=subject, body=body) + def _send_crtc_notification_email( + self, + entity_name: str, + order_number: str, + letter_path: str, + from_addr: str | None = None, + from_password: str | None = None, + ) -> None: + """Email the signed CRTC notification letter to the Secretary General. + + Sends FROM the customer's regulatory@domain.ca if available, otherwise + from Performance West. The CRTC accepts electronic notifications. + On dev, redirects to admin email. + """ + crtc_email = "secretary.general@crtc.gc.ca" + admin_email = os.environ.get("ADMIN_EMAIL", "ops@performancewest.net") + + # Dev mode: redirect to admin + if os.environ.get("NODE_ENV") == "development": + crtc_email = admin_email + LOG.info("Dev mode: redirecting CRTC notification to %s", crtc_email) + + subject = f"Notification of New Telecommunications Service Provider — {entity_name}" + body = ( + f"Dear Secretary General,\n\n" + f"Please find attached the formal notification letter for {entity_name}, " + f"a new telecommunications service provider, pursuant to the Telecommunications Act " + f"and CRTC Telecom Information Bulletin 2015-134.\n\n" + f"This notification is being filed on behalf of {entity_name} by " + f"Performance West Inc., their authorized compliance representative.\n\n" + f"Please do not hesitate to contact us if any additional information is required.\n\n" + f"Respectfully,\n" + f"Performance West Inc.\n" + f"525 Randall Ave Ste 100-1195\n" + f"Cheyenne, WY 82001\n" + f"(888) 411-0383\n" + f"info@performancewest.net\n\n" + f"Order reference: {order_number}" + ) + + self._send_email( + to_email=crtc_email, + subject=subject, + body=body, + attachment_path=letter_path, + from_addr=from_addr, + from_password=from_password, + ) + @staticmethod def _send_email( self,