diff --git a/api/migrations/074_engagement_columns.sql b/api/migrations/074_engagement_columns.sql new file mode 100644 index 0000000..2b4d7b2 --- /dev/null +++ b/api/migrations/074_engagement_columns.sql @@ -0,0 +1,12 @@ +-- 074_engagement_columns.sql +-- Store client engagement authorization consent for compliance orders. +-- Part 1: clickwrap checkbox consent for all orders. +-- Part 2: eSign engagement letter for 499-A past-due/multi-year refiling. + +ALTER TABLE compliance_orders + ADD COLUMN IF NOT EXISTS engagement_accepted_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS engagement_accepted_ip TEXT, + ADD COLUMN IF NOT EXISTS engagement_version TEXT, + ADD COLUMN IF NOT EXISTS engagement_esign_required BOOLEAN DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS engagement_esign_signed_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS engagement_letter_minio_key TEXT; diff --git a/api/src/routes/checkout.ts b/api/src/routes/checkout.ts index 6b3a31e..194bcf0 100644 --- a/api/src/routes/checkout.ts +++ b/api/src/routes/checkout.ts @@ -1462,7 +1462,11 @@ export async function handlePaymentComplete( } // ── Send order confirmation email ────────────────────────────────────── - try { + // Skip for compliance_batch — sendComplianceIntakeEmail already sent + // a combined confirmation + intake email above. + if (order_type === "compliance_batch") { + console.log(`[checkout] Skipping generic confirmation for ${order_id} — intake email already sent`); + } else try { await sendOrderConfirmationEmail({ order_id, order_type, diff --git a/api/src/routes/compliance-orders.ts b/api/src/routes/compliance-orders.ts index 686909a..4b599a7 100644 --- a/api/src/routes/compliance-orders.ts +++ b/api/src/routes/compliance-orders.ts @@ -701,6 +701,8 @@ router.post("/api/v1/compliance-orders", async (req, res) => { // Multi-year catch-up (migration 060) — array of reporting years. // 2+ years gets the 15% multi-year discount. multi_year_filings, + // Engagement authorization (migration 074) + engagement_accepted, } = req.body ?? {}; if (!service_slug || !customer_email || !customer_name) { @@ -844,8 +846,9 @@ router.post("/api/v1/compliance-orders", async (req, res) => { discount_code, discount_cents, notes, intake_data, filing_mode, form_year_override, revises_order_number, revised_reason, waive_deminimis_exemption, waive_deminimis_reason, - multi_year_filings, multi_year_discount_pct - ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22) + multi_year_filings, multi_year_discount_pct, + engagement_accepted_at, engagement_accepted_ip, engagement_version + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25) RETURNING *`, [ order_number, @@ -870,6 +873,9 @@ router.post("/api/v1/compliance-orders", async (req, res) => { waive_deminimis_reason || null, myf && myf.length > 0 ? myf : null, multi_year_discount_pct || null, + engagement_accepted ? new Date().toISOString() : null, + engagement_accepted ? (req.ip || req.headers["x-forwarded-for"] || null) : null, + engagement_accepted ? "v1-2026-04" : null, ], ); @@ -898,6 +904,7 @@ router.post("/api/v1/compliance-orders/batch", async (req, res) => { customer_phone, discount_code, intake_data, + engagement_accepted, } = req.body ?? {}; if (!rawServices || !Array.isArray(rawServices) || rawServices.length === 0) { @@ -997,8 +1004,9 @@ router.post("/api/v1/compliance-orders/batch", async (req, res) => { order_number, batch_id, service_slug, service_name, service_fee_cents, gov_fee_cents, gov_fee_label, customer_email, customer_name, customer_phone, - discount_code, discount_cents, intake_data - ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) + discount_code, discount_cents, intake_data, + engagement_accepted_at, engagement_accepted_ip, engagement_version + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) RETURNING *`, [ orderNumber, @@ -1014,6 +1022,9 @@ router.post("/api/v1/compliance-orders/batch", async (req, res) => { discount_code || null, svcDiscount, intake_data ? JSON.stringify(intake_data) : "{}", + engagement_accepted ? new Date().toISOString() : null, + engagement_accepted ? (req.ip || req.headers["x-forwarded-for"] || null) : null, + engagement_accepted ? "v1-2026-04" : null, ], ); orders.push(result.rows[0]); diff --git a/docs/usac-past-due-fee-negotiation.md b/docs/usac-past-due-fee-negotiation.md new file mode 100644 index 0000000..62fcea8 --- /dev/null +++ b/docs/usac-past-due-fee-negotiation.md @@ -0,0 +1,179 @@ +# USAC Past-Due Filing Fee Negotiation & Contingency Fee Service + +## Context + +Carriers with past-due USAC filing obligations (Form 499-A late filings) face compounding penalties. This document covers how the fees work, viable methods to reduce them, whether we can charge on a contingency basis, and when the client needs a lawyer. + +--- + +## 1. How USAC Late Filing Fees Work + +### Fee Calculation +Late filing fees are assessed when a carrier fails to file Form 499-A or 499-Q within **30 days** of the due date. The fee is the **greater** of: +- **$100/month flat**, OR +- **Monthly USF obligation × (U.S. prime rate + 3.5%) / 365 × days late** + +Fees accrue monthly until the form is submitted. For a carrier with meaningful revenue, this compounds quickly. + +### Additional Consequences +- **Interest**: Accrues daily at prime + 3.5% from delinquency date +- **91+ days past due**: Additional 6% annual penalty applied retroactively +- **Red Light status**: Triggered after 1 day delinquent — all USF disbursements withheld, TIN-linked entities affected +- **DCIA referral**: Debt referred to U.S. Treasury for collection after 90+ days +- **FCC enforcement**: Additional collection costs under 31 U.S.C. § 3717 + +### Key Policy +> "Late payment fees will not be reversed unless USAC has made an error." + +USAC's official position is that penalties are only reversed for USAC billing errors, not for carrier negligence or hardship. + +--- + +## 2. Viable Methods to Reduce Past-Due Fees + +### Method 1: USAC Billing Dispute (Email) +- **How**: Email CustomerSupport@usac.org with subject "Billing Inquiry or Dispute" +- **Include**: Requestor name, Filer ID, invoice date, statement IDs, dispute description +- **Response time**: 24 hours +- **Success rate**: LOW — only reverses fees for documented USAC errors +- **Best for**: Carriers who believe USAC miscalculated their obligation, billed the wrong entity, or double-counted contributions + +### Method 2: Payment Plan (Installment Agreement) +- **How**: Submit written request to USAC at 700 12th Street NW, Suite 900, Washington DC 20005 +- **Requirement**: Must demonstrate financial inability to pay in one lump sum (verified per 31 CFR § 901.8) +- **Terms**: Promissory note, additional interest, admin fees, possible audit obligations +- **Invoices**: Separate "PMTP" invoices, due by 15th of following month +- **Effect**: Stops Red Light status, prevents Treasury referral, allows structured payoff +- **Does NOT reduce the total amount** — just spreads it out + +### Method 3: USAC Appeal (Contributor Division) +- **How**: Email ContributorAppeals@usac.org within 60 days of USAC decision +- **Include**: Filer ID, USAC decision letter, supporting docs, explanation of relief sought +- **Timeline**: Written acknowledgment upon receipt, then written decision +- **Best for**: Challenging the underlying assessment (revenue classification errors, incorrect contribution base, de minimis eligibility disputes) +- **Pending appeals protect disputed debt from Treasury referral and Red Light status** + +### Method 4: FCC Appeal / Waiver Request +- **How**: File with FCC via ECFS (Electronic Comment Filing System), docket WC 06-122 +- **Deadline**: 60 days after USAC decision on appeal +- **Standard**: Must demonstrate "good cause" and "special circumstances" warranting deviation from the general rule, and that the deviation serves the public interest +- **Reality**: FCC has maintained an **extremely strict standard**. In February 2026, the FCC denied **every single one** of 20+ waiver requests for FY 2023-2024 regulatory fee penalties. The FCC cited that "relief is reserved for rare, extraordinary circumstances outside a payer's control." Examples of denied reasons: administrative oversight, delayed invoice awareness, CORES navigation difficulties, bank transaction limits, contractor mistakes. +- **Historical precedent for grants**: Post-9/11 relief is cited as the benchmark. Only truly extraordinary external events have qualified. +- **Requires an attorney** — 47 CFR § 1.23 limits practice before the FCC to bar-admitted attorneys + +### Method 5: Revenue Reclassification / Revised Filing +- **How**: File revised 499-A forms that correctly classify revenue +- **Effect**: If original filings overcounted contributable revenue (e.g., classified broadband revenue as telecom, included exempt revenue), the revised filing reduces the contribution base, which reduces the USF obligation, which reduces the late fees proportionally +- **This is the most practical path for many carriers** — filing accurate revised forms that reduce the assessment base +- **We can do this** — it's form preparation, not legal representation + +### Method 6: De Minimis Exemption / Discontinuance +- **How**: If carrier qualifies as de minimis (< $10,000 in contributable telecom revenue), file for de minimis status retroactively or file 499-A Discontinuance +- **Effect**: Eliminates or dramatically reduces the contribution obligation, and therefore the late fees on that obligation +- **We already offer this**: 499-A Discontinuance service ($299) + +--- + +## 3. Contingency Fee Legality + +### The Core Question +Can Performance West charge 15% of savings from reducing a client's USAC bill? + +### Answer: YES for form preparation / consulting; NO for legal representation + +**What we CAN do (not practicing law):** +- Prepare and file revised 499-A forms with correct revenue classifications +- Calculate de minimis eligibility and file discontinuance +- Identify revenue reclassification opportunities (broadband vs. telecom allocation) +- Prepare billing dispute letters for USAC (factual, not legal arguments) +- Advise on the USAC payment plan process +- Charge a flat fee, hourly rate, or percentage of savings for these consulting services + +**What we CANNOT do (practicing law):** +- File appeals with the FCC on behalf of the client (47 CFR § 1.23 — attorneys only) +- Negotiate settlements directly with USAC on legal grounds +- Provide legal opinions on whether penalties are lawfully assessed +- Represent the client in any FCC proceeding or hearing + +### Contingency Fee Structure — Safe Harbor +A **percentage-of-savings** model is legally permissible as a **consulting fee** when: +1. We are performing form preparation, revenue analysis, and classification work (not legal advocacy) +2. We do not hold ourselves out as attorneys or provide legal advice +3. The fee is tied to the result of our consulting work (revised filings that reduce the assessment), not to the outcome of a legal proceeding +4. We have a clear engagement letter stating we are compliance consultants, not lawyers + +This is analogous to how tax preparation firms charge based on refund amounts, or how energy consultants charge a percentage of utility bill reductions. The key is that we're doing the analytical/filing work, not the legal representation. + +### When They Need a Lawyer +- Filing an appeal with the FCC (mandatory — 47 CFR § 1.23) +- Challenging the constitutionality or legality of the USF assessment +- Responding to FCC enforcement actions or forfeiture proceedings +- Contesting Treasury collection actions (DCIA referral) +- Any proceeding before an administrative law judge + +**Recommended telecom law firms**: CommLaw Group, iCommLaw, Kelley Drye & Warren LLP (telecom practice) + +--- + +## 4. Recommended Service Offering + +### "USAC Past-Due Assessment Review" — Consulting Service + +**What we deliver:** +1. **Revenue audit**: Review the carrier's past 499-A filings for revenue misclassification. Many small carriers over-report because they include broadband revenue, non-telecom revenue, or exempt services in their contribution base. +2. **Revised filing preparation**: File corrected 499-A forms for the delinquent years with proper revenue classification. +3. **De minimis / discontinuance evaluation**: Determine if the carrier qualifies for de minimis exemption or should file a discontinuance. +4. **Payment plan assistance**: Help the carrier prepare the financial documentation needed for a USAC installment agreement. +5. **Billing dispute letter**: If USAC made a calculation error, prepare the dispute email. + +**Pricing model options:** +- **Option A**: Flat fee per year of remediation ($499/year) +- **Option B**: 15% of documented reduction in USF obligation (contingency) +- **Option C**: Hybrid — $299 minimum + 10% of savings above $2,000 + +**Engagement letter must clearly state:** +- We are compliance consultants, not attorneys +- We prepare forms and provide regulatory compliance consulting +- If the client needs legal representation before the FCC, we will refer to a telecom attorney +- The fee is for consulting services (form preparation, revenue analysis, classification) + +--- + +## 5. Public Examples / Precedent + +### FCC Waiver Denials (DA-26-184, February 2026) +The FCC denied 20+ requests to waive the 25% late penalty on FY 2023/2024 regulatory fees. Every request was denied. The FCC emphasized that waivers require "extraordinary circumstances outside the payer's control" — not administrative oversight, not bank issues, not contractor errors. This sets a very high bar for any FCC-level appeal. + +### Fifth Circuit USF Challenge +In 2024, the Fifth Circuit ruled the USF contribution scheme violates the legislative vesting clause (non-delegation doctrine). While this is being appealed and doesn't directly help with past-due fees, it creates uncertainty about the long-term viability of the USF contribution mechanism and could be cited in waiver requests as a mitigating factor. + +### deltathree, Inc. (DA-16-432) +The FCC granted deltathree's request for review of revised quarterly filings submitted 45+ days late. This is a rare example of the FCC allowing late revised filings, but the circumstances were unusual. + +--- + +## 6. Summary — What to Tell Clients + +| Approach | Who Does It | Success Rate | Cost to Client | +|----------|-------------|-------------|----------------| +| Revenue reclassification + revised 499-A | **Us** | HIGH (if revenue was misclassified) | $499/year or 15% of savings | +| De minimis / discontinuance filing | **Us** | HIGH (if eligible) | $299 | +| USAC billing dispute | **Us** (prepare letter) | LOW (only for USAC errors) | Included | +| USAC payment plan | **Us** (prepare docs) | MEDIUM (restructures, doesn't reduce) | Included | +| USAC appeal (47 CFR 54.719) | **Us** (prepare) + attorney (file) | MEDIUM | Attorney fees | +| FCC waiver request | **Attorney only** | VERY LOW (nearly all denied) | $5,000-15,000+ | + +**Bottom line**: The most effective path is NOT trying to negotiate the penalties away — it's reducing the underlying assessment through accurate revenue reclassification, which proportionally reduces all associated late fees and interest. This is consulting work we can do legally on a contingency basis. + +--- + +## Sources +- [USAC Late Filing Fees](https://www.usac.org/service-providers/making-payments/invoices/late-filing-sanction/) +- [USAC Billing Disputes](https://www.usac.org/service-providers/making-payments/billing-disputes/) +- [USAC Payment Plans](https://www.usac.org/service-providers/making-payments/how-to-pay/payment-plans/) +- [USAC Appeals Process](https://www.usac.org/about/appeals-audits/appeals/) +- [USAC Late Payments / DCIA / Red Light](https://www.usac.org/service-providers/making-payments/late-payments-dcia-red-light/) +- [47 CFR § 1.23 — Practice Before the FCC](https://www.law.cornell.edu/cfr/text/47/1.23) +- [FCC Contribution Methodology](https://www.fcc.gov/general/contribution-methodology-administrative-filings) +- [FCC Regulatory Fee Penalty Denials (2026)](https://radioink.com/2026/02/26/fcc-holds-line-on-25-regulatory-fee-penalty-for-late-payments/) +- [USF Due Process Violations (CGO)](https://www.thecgo.org/research/an-alphabet-soup-of-due-process-violations/) diff --git a/scripts/document_gen/templates/engagement_letter_499a.py b/scripts/document_gen/templates/engagement_letter_499a.py new file mode 100644 index 0000000..e0f5126 --- /dev/null +++ b/scripts/document_gen/templates/engagement_letter_499a.py @@ -0,0 +1,193 @@ +"""Generate a 499-A engagement letter for past-due/multi-year refiling. + +Produces a DOCX engagement letter that the client must eSign before +we begin work on revised 499-A filings for prior years. +""" +from __future__ import annotations + +import os +from datetime import date +from typing import Optional + + +def generate_engagement_letter( + *, + entity_name: str, + frn: str = "", + contact_name: str = "", + contact_email: str = "", + filing_years: list[int] | None = None, + fee_description: str = "", + order_number: str = "", + output_path: str, +) -> str: + """Generate the engagement letter DOCX. Returns the output path.""" + from docx import Document + from docx.shared import Pt, Inches, RGBColor + from docx.enum.text import WD_ALIGN_PARAGRAPH + + NAVY = RGBColor(0x1A, 0x27, 0x44) + GREEN = RGBColor(0x05, 0x96, 0x69) + today = date.today() + today_str = today.strftime("%B %d, %Y") + years_str = ", ".join(str(y) for y in (filing_years or [today.year])) + + doc = Document() + for s in doc.sections: + s.top_margin = Inches(1) + s.bottom_margin = Inches(1) + s.left_margin = Inches(1.25) + s.right_margin = Inches(1.25) + + # Header + tp = doc.add_paragraph() + tp.alignment = WD_ALIGN_PARAGRAPH.CENTER + tr = tp.add_run("Engagement Letter") + tr.font.size = Pt(16) + tr.bold = True + tr.font.color.rgb = NAVY + + sp = doc.add_paragraph() + sp.alignment = WD_ALIGN_PARAGRAPH.CENTER + sr = sp.add_run("FCC Form 499-A Revenue Audit & Revised Filing") + sr.font.size = Pt(11) + sr.italic = True + sp.paragraph_format.space_after = Pt(16) + + def _p(text: str, bold: bool = False, size: int = 11) -> None: + p = doc.add_paragraph() + p.paragraph_format.space_after = Pt(6) + r = p.add_run(text) + r.font.size = Pt(size) + r.bold = bold + + def _h(text: str) -> None: + p = doc.add_paragraph() + p.paragraph_format.space_before = Pt(12) + p.paragraph_format.space_after = Pt(4) + r = p.add_run(text) + r.font.size = Pt(12) + r.bold = True + r.font.color.rgb = NAVY + + _p(f"Date: {today_str}") + _p(f"Client: {entity_name}", bold=True) + if frn: + _p(f"FCC Registration Number (FRN): {frn}") + if contact_name: + _p(f"Contact: {contact_name}" + (f" ({contact_email})" if contact_email else "")) + if order_number: + _p(f"Order Reference: {order_number}") + + _h("1. Scope of Services") + _p( + f"Performance West Inc. (\"PW\") will provide the following compliance " + f"consulting services for {entity_name} (\"Client\"):" + ) + _p( + f" a) Revenue audit of Client's FCC Form 499-A filings for calendar " + f"year(s) {years_str} to identify potential revenue misclassifications " + f"affecting the Universal Service Fund (USF) contribution base." + ) + _p( + f" b) Preparation and submission of revised FCC Form 499-A filings " + f"for the identified calendar year(s) with corrected revenue classifications." + ) + _p( + f" c) Evaluation of Client's eligibility for de minimis exemption " + f"under 47 CFR \u00a7 54.708." + ) + _p( + f" d) Assistance with USAC billing dispute documentation and " + f"payment plan applications, if applicable." + ) + + _h("2. Fee Structure") + if fee_description: + _p(fee_description) + else: + _p( + f"PW's fee for these services is $499 per calendar year of revised " + f"filing, totaling ${499 * len(filing_years or [today.year])} for " + f"{len(filing_years or [today.year])} year(s)." + ) + _p( + "Payment is due upon engagement. This fee covers the revenue audit, " + "revised form preparation, and submission. Government filing fees, " + "if any, are the Client's responsibility and will be disclosed " + "before submission." + ) + + _h("3. Authorization") + _p( + f"Client hereby authorizes PW to prepare and submit revised FCC " + f"Form 499-A filings on Client's behalf for the calendar year(s) " + f"identified above. Client understands that PW will submit these " + f"forms to the Universal Service Administrative Company (USAC) " + f"using the Client's Filer ID and FRN." + ) + + _h("4. Not Legal Advice") + _p( + "PW is a compliance consulting firm, not a law firm. The services " + "described in this letter constitute regulatory form preparation " + "and compliance consulting, not legal advice or legal representation. " + "PW does not provide legal opinions, represent clients before the " + "FCC in adjudicatory proceedings, or create an attorney-client " + "relationship." + ) + _p( + "If Client requires legal representation before the FCC (e.g., " + "appeals under 47 CFR \u00a7 54.719, waiver requests, or enforcement " + "proceedings), PW will refer Client to a qualified telecommunications " + "attorney." + ) + + _h("5. Client Responsibilities") + _p( + "Client certifies that all information provided to PW is accurate " + "and complete to the best of Client's knowledge. Client understands " + "that FCC Form 499-A filings carry a certification of accuracy and " + "that false statements may result in penalties under 18 U.S.C. \u00a7 1001." + ) + _p( + "Client agrees to provide PW with access to revenue records, " + "financial statements, and other documentation reasonably necessary " + "to perform the revenue audit and prepare revised filings." + ) + + _h("6. Limitation of Liability") + _p( + "PW's liability under this engagement is limited to the fees paid " + "by Client for the services described herein. PW is not responsible " + "for penalties, interest, or other charges assessed by USAC or the " + "FCC, whether arising from prior filings, revised filings, or any " + "other cause." + ) + + _h("7. Term") + _p( + "This engagement begins upon signature below and continues until " + "the revised filings have been submitted to USAC, or until " + "terminated by either party with written notice." + ) + + # Signature block + doc.add_paragraph() + _p("AGREED AND ACCEPTED:", bold=True) + doc.add_paragraph() + + _p(f"Client: {entity_name}") + _p("Signature: ____________________________________") + _p("Name: ____________________________________") + _p("Title: ____________________________________") + _p(f"Date: {today_str}") + + doc.add_paragraph() + + _p("Performance West Inc.") + _p("525 Randall Ave Ste 100-1195, Cheyenne, WY 82001") + _p("1-888-411-0383 | info@performancewest.net") + + doc.save(output_path) + return output_path diff --git a/scripts/workers/job_server.py b/scripts/workers/job_server.py index 29d8606..d2401da 100644 --- a/scripts/workers/job_server.py +++ b/scripts/workers/job_server.py @@ -1032,6 +1032,8 @@ def handle_process_compliance_service(payload: dict) -> dict: order["customer_name"] = row.get("customer_name") order["customer_phone"] = row.get("customer_phone") order["batch_id"] = row.get("batch_id") + order["engagement_esign_signed_at"] = row.get("engagement_esign_signed_at") + order["engagement_esign_required"] = row.get("engagement_esign_required") order["filing_mode"] = row.get("filing_mode") order["form_year_override"] = row.get("form_year_override") order["revises_order_number"] = row.get("revises_order_number") diff --git a/scripts/workers/services/form_499a.py b/scripts/workers/services/form_499a.py index 9a5f59b..78b3fa8 100644 --- a/scripts/workers/services/form_499a.py +++ b/scripts/workers/services/form_499a.py @@ -170,11 +170,36 @@ class Form499AHandler(BaseServiceHandler): date_str = datetime.now().strftime("%Y%m%d") generated: list[str] = [] + # Engagement letter gate: past-due or multi-year (2+) refiling orders + # require a signed engagement letter before we begin work. + filing_mode = order_data.get("filing_mode") or "current" + multi_year = order_data.get("multi_year_filings") or [] + needs_engagement = ( + filing_mode == "past_due" + or (multi_year and len(multi_year) >= 2) + ) + if needs_engagement: + esign_signed = order_data.get("engagement_esign_signed_at") + if not esign_signed: + # Check if we already generated the letter (avoid re-sending on re-dispatch) + already_required = order_data.get("engagement_esign_required") + if not already_required: + logger.info( + "Form499AHandler: %s requires engagement letter eSign (mode=%s, years=%s) — generating + pausing", + order_number, filing_mode, multi_year, + ) + self._generate_and_send_engagement_letter(order_data) + else: + logger.info( + "Form499AHandler: %s still waiting for engagement eSign — skipping", + order_number, + ) + return [] + # Multi-year mode (migration 060): when multi_year_filings has 2+ # years, run this handler once per year with each year pinned as # form_year_override. Persist per-year confirmations to # compliance_orders.multi_year_confirmations. - multi_year = order_data.get("multi_year_filings") or [] if multi_year and len(multi_year) >= 2: all_generated: list[str] = [] year_conf_records: list[dict] = [] @@ -1410,6 +1435,150 @@ class Form499AHandler(BaseServiceHandler): except Exception as exc: logger.error("Could not create admin ToDo: %s", exc) + def _generate_and_send_engagement_letter(self, order_data: dict) -> None: + """Generate engagement letter PDF and email client a signing link.""" + order_number = order_data["name"] + entity = order_data.get("entity", {}) or {} + intake = order_data.get("intake_data") or {} + multi_year = order_data.get("multi_year_filings") or [] + customer_email = order_data.get("customer_email", "") + customer_name = order_data.get("customer_name", "") + + import tempfile + work_dir = tempfile.mkdtemp(prefix="engagement_") + docx_path = os.path.join(work_dir, f"engagement_{order_number}.docx") + + try: + from scripts.document_gen.templates.engagement_letter_499a import ( + generate_engagement_letter, + ) + generate_engagement_letter( + entity_name=entity.get("legal_name") or intake.get("entity_legal_name", ""), + frn=entity.get("frn") or intake.get("frn", ""), + contact_name=customer_name, + contact_email=customer_email, + filing_years=multi_year if multi_year else None, + order_number=order_number, + output_path=docx_path, + ) + + # Convert to PDF + pdf_path = self._convert_to_pdf(docx_path) + + # Upload to MinIO + from scripts.document_gen import MinioStorage + storage = MinioStorage() + minio_key = f"engagement/{order_number}/engagement_letter.pdf" + storage.upload_file(pdf_path or docx_path, minio_key) + + # Update order with engagement letter path + mark eSign required + try: + conn = psycopg2.connect(os.environ.get("DATABASE_URL", "")) + cur = conn.cursor() + cur.execute( + """UPDATE compliance_orders + SET engagement_esign_required = TRUE, + engagement_letter_minio_key = %s, + payment_status = 'pending_esign' + WHERE order_number = %s""", + (minio_key, order_number), + ) + conn.commit() + cur.close() + conn.close() + except Exception as exc: + logger.warning("Could not update engagement status: %s", exc) + + # Email client the engagement signing link + if customer_email: + try: + try: + import jwt as pyjwt + except ImportError: + import PyJWT as pyjwt + secret = os.environ.get("CUSTOMER_JWT_SECRET", "changeme") + domain = os.environ.get("DOMAIN", "performancewest.net") + token = pyjwt.encode( + {"order_id": order_number, "order_type": "compliance", "email": customer_email}, + secret, algorithm="HS256", + ) + sign_url = f"https://{domain}/portal/engagement-sign?token={token}" + + import smtplib + from email.mime.text import MIMEText + from email.mime.multipart import MIMEMultipart + + first_name = customer_name.split(" ")[0] if customer_name else "there" + years_str = ", ".join(str(y) for y in multi_year) if multi_year else "current year" + subject = f"Engagement Letter — 499-A Revenue Audit for {entity.get('legal_name', order_number)}" + body = ( + f"

Engagement Letter Ready for Signature

" + f"

Hi {first_name},

" + f"

Before we begin your FCC Form 499-A revenue audit and revised filing " + f"for calendar year(s) {years_str}, we need your signature " + f"on the engagement letter.

" + f"

Please review and sign the letter by clicking below:

" + f"

" + f"Review & Sign Engagement Letter

" + f"

Order: {order_number}

" + f"

" + f"Performance West Inc. | 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001 | 1-888-411-0383

" + ) + + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = os.environ.get("SMTP_FROM", "Performance West ") + msg["To"] = customer_email + msg.attach(MIMEText(body, "html")) + + smtp_host = os.environ.get("SMTP_HOST", "co.carrierone.com") + smtp_port = int(os.environ.get("SMTP_PORT", "587")) + smtp_user = os.environ.get("SMTP_USER", "") + smtp_pass = os.environ.get("SMTP_PASS", "") + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.starttls() + if smtp_user and smtp_pass: + server.login(smtp_user, smtp_pass) + server.send_message(msg) + logger.info("Engagement letter email sent to %s for %s", customer_email, order_number) + except Exception as exc: + logger.warning("Could not send engagement email for %s: %s", order_number, exc) + + # Create admin todo + try: + from scripts.workers.erpnext_client import ERPNextClient + ERPNextClient().create_resource("ToDo", { + "description": ( + f"[fcc-499a] {order_number}\n\n" + f"Engagement letter generated and sent for 499-A past-due/multi-year refiling.\n" + f"Entity: {entity.get('legal_name', '')}\n" + f"Years: {years_str}\n" + f"Waiting for client eSign before processing begins." + ), + "priority": "Medium", + "role": "Accounting Advisor", + }) + except Exception as exc: + logger.warning("Could not create engagement admin ToDo: %s", exc) + + except Exception as exc: + logger.error("Engagement letter generation failed for %s: %s", order_number, exc) + # Create admin todo for manual follow-up + try: + from scripts.workers.erpnext_client import ERPNextClient + ERPNextClient().create_resource("ToDo", { + "description": ( + f"[fcc-499a] {order_number}\n\n" + f"FAILED to generate engagement letter: {exc}\n" + f"Manual engagement letter needed before processing past-due 499-A." + ), + "priority": "High", + "role": "Accounting Advisor", + }) + except Exception: + pass + def _box_to_category_ids(box_num: int) -> set[str]: """Given a Line 105 box number, return the category ids that tick it. diff --git a/site/public/order/fcc-compliance/index.html b/site/public/order/fcc-compliance/index.html index 3736d92..70b8867 100644 --- a/site/public/order/fcc-compliance/index.html +++ b/site/public/order/fcc-compliance/index.html @@ -358,6 +358,7 @@ function renderServices() { '' + '' + '' + + '' + '' + '' + ''; @@ -367,6 +368,8 @@ function renderServices() { document.getElementById("pw-batch-form").addEventListener("submit", function(e){ e.preventDefault(); if(selectedSlugs.length === 0){ alert("Please select at least one service."); return; } + var engageBox=document.getElementById("pw-engage"); + if(engageBox && !engageBox.checked){ alert("Please accept the authorization terms to continue."); return; } var errEl=document.getElementById("pw-err");errEl.hidden=true; var btn=document.getElementById("pw-submit");btn.disabled=true;btn.textContent="Creating order..."; var payMethod=document.querySelector("input[name=pw_pay]:checked").value; @@ -376,7 +379,8 @@ function renderServices() { customer_email:document.getElementById("pw-email").value.trim(), customer_phone:document.getElementById("pw-phone").value.trim()||undefined, discount_code:document.getElementById("pw-promo").value.trim()||undefined, - intake_data:{frn:frn,source:"compliance-check-remediation"} + intake_data:{frn:frn,source:"compliance-check-remediation"}, + engagement_accepted:true }; fetch(API+"/api/v1/compliance-orders/batch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}) .then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||"Order failed");return d})}) diff --git a/site/public/order/neca-ocn/index.html b/site/public/order/neca-ocn/index.html index 4dbe571..0a7324f 100644 --- a/site/public/order/neca-ocn/index.html +++ b/site/public/order/neca-ocn/index.html @@ -154,6 +154,11 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px + +
@@ -229,6 +234,11 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px var contactEmail = document.getElementById('contact-email').value.trim(); var contactPhone = document.getElementById('contact-phone').value.trim(); + if (!document.getElementById('engage-check').checked) { + errEl.textContent = 'Please accept the authorization terms to continue.'; + errEl.style.display = 'block'; + return; + } if (!entityName || !contactName || !contactEmail || !contactPhone) { errEl.textContent = 'Please fill in all required fields (entity name, contact name, email, phone).'; errEl.style.display = 'block'; diff --git a/site/public/order/state-puc/index.html b/site/public/order/state-puc/index.html index 6bd9b78..1102ffd 100644 --- a/site/public/order/state-puc/index.html +++ b/site/public/order/state-puc/index.html @@ -170,6 +170,11 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px + +
@@ -410,6 +415,12 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px const errEl = document.getElementById('checkout-error'); errEl.style.display = 'none'; + if (!document.getElementById('engage-check').checked) { + errEl.textContent = 'Please accept the authorization terms to continue.'; + errEl.style.display = 'block'; + return; + } + const customerName = document.getElementById('customer-name').value.trim(); const customerEmail = document.getElementById('customer-email').value.trim(); if (!customerName || !customerEmail) { diff --git a/site/src/pages/order/bdc-broadband.astro b/site/src/pages/order/bdc-broadband.astro index 88d7389..d9e7ccd 100644 --- a/site/src/pages/order/bdc-broadband.astro +++ b/site/src/pages/order/bdc-broadband.astro @@ -15,8 +15,6 @@ const description = "Availability data only (no voice subscription). For broadba

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/bdc-filing.astro b/site/src/pages/order/bdc-filing.astro index 3ad6f31..d549faf 100644 --- a/site/src/pages/order/bdc-filing.astro +++ b/site/src/pages/order/bdc-filing.astro @@ -15,8 +15,6 @@ const description = "Both BDC blocks in one order — broadband deployment + voi

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/bdc-voice.astro b/site/src/pages/order/bdc-voice.astro index 23cf4e8..6fbe22f 100644 --- a/site/src/pages/order/bdc-voice.astro +++ b/site/src/pages/order/bdc-voice.astro @@ -15,8 +15,6 @@ const description = "Voice subscriber counts only — the part of the legacy For

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/calea-ssi.astro b/site/src/pages/order/calea-ssi.astro index 2eb58b0..4d4a7ee 100644 --- a/site/src/pages/order/calea-ssi.astro +++ b/site/src/pages/order/calea-ssi.astro @@ -15,8 +15,6 @@ const description = "System Security and Integrity plan required of every common

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/cdr-analysis.astro b/site/src/pages/order/cdr-analysis.astro index 66facdd..13ba290 100644 --- a/site/src/pages/order/cdr-analysis.astro +++ b/site/src/pages/order/cdr-analysis.astro @@ -15,8 +15,6 @@ const description = "Classified traffic study from your CDRs — feeds the 499-A

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/cores-frn-registration.astro b/site/src/pages/order/cores-frn-registration.astro index 9865cec..bf682c2 100644 --- a/site/src/pages/order/cores-frn-registration.astro +++ b/site/src/pages/order/cores-frn-registration.astro @@ -15,8 +15,6 @@ const description = "Register your carrier in FCC CORES and obtain your FRN. Req

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/cpni-certification.astro b/site/src/pages/order/cpni-certification.astro index 8539c8d..3d1806d 100644 --- a/site/src/pages/order/cpni-certification.astro +++ b/site/src/pages/order/cpni-certification.astro @@ -15,8 +15,6 @@ const description = "47 CFR § 64.2009 annual CPNI certification filed at FCC EC

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/dc-agent.astro b/site/src/pages/order/dc-agent.astro index 62e36c0..abfc450 100644 --- a/site/src/pages/order/dc-agent.astro +++ b/site/src/pages/order/dc-agent.astro @@ -15,8 +15,6 @@ const description = "Your required D.C. registered agent for service of process

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/fcc-499-initial.astro b/site/src/pages/order/fcc-499-initial.astro index 00e16e3..5fd7578 100644 --- a/site/src/pages/order/fcc-499-initial.astro +++ b/site/src/pages/order/fcc-499-initial.astro @@ -15,8 +15,6 @@ const description = "New carrier registration with USAC — obtain your Filer ID

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/fcc-499a-499q.astro b/site/src/pages/order/fcc-499a-499q.astro index a3e7cb9..52476bf 100644 --- a/site/src/pages/order/fcc-499a-499q.astro +++ b/site/src/pages/order/fcc-499a-499q.astro @@ -15,8 +15,6 @@ const description = "Annual 499-A plus the four quarterly 499-Q filings — one

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/fcc-499a.astro b/site/src/pages/order/fcc-499a.astro index 0f5d97c..8426269 100644 --- a/site/src/pages/order/fcc-499a.astro +++ b/site/src/pages/order/fcc-499a.astro @@ -15,8 +15,6 @@ const description = "Annual Telecommunications Reporting Worksheet. Due April 1

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/fcc-63-11-notification.astro b/site/src/pages/order/fcc-63-11-notification.astro index 2f4ed62..8914d16 100644 --- a/site/src/pages/order/fcc-63-11-notification.astro +++ b/site/src/pages/order/fcc-63-11-notification.astro @@ -15,8 +15,6 @@ const description = "47 CFR § 63.11 notification filed with the FCC Internation

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/fcc-compliance-checkup.astro b/site/src/pages/order/fcc-compliance-checkup.astro index 368523b..82efd7a 100644 --- a/site/src/pages/order/fcc-compliance-checkup.astro +++ b/site/src/pages/order/fcc-compliance-checkup.astro @@ -15,8 +15,6 @@ const description = "Diagnostic check — CORES, RMD, STIR/SHAKEN, CPNI, 499-A s

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/fcc-full-compliance.astro b/site/src/pages/order/fcc-full-compliance.astro index 5b2d194..1b50357 100644 --- a/site/src/pages/order/fcc-full-compliance.astro +++ b/site/src/pages/order/fcc-full-compliance.astro @@ -15,8 +15,6 @@ const description = "RMD + CPNI + STIR/SHAKEN + 499-A + 499-Q in one order. Ever

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/foreign-qualification.astro b/site/src/pages/order/foreign-qualification.astro index 4364cfd..329e2e6 100644 --- a/site/src/pages/order/foreign-qualification.astro +++ b/site/src/pages/order/foreign-qualification.astro @@ -20,12 +20,6 @@ const description =

{title}

-

- Service fee: {formatUSD(meta?.price_cents ?? 0)} - {isMulti ? " per state" : ""} - + state filing fees + registered agent -

-

{description}

{!isMulti && ( diff --git a/site/src/pages/order/new-carrier-bundle.astro b/site/src/pages/order/new-carrier-bundle.astro index 956bf02..43182b3 100644 --- a/site/src/pages/order/new-carrier-bundle.astro +++ b/site/src/pages/order/new-carrier-bundle.astro @@ -15,8 +15,6 @@ const description = "Start-to-finish for a brand-new VoIP carrier: FRN + 499 Ini

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/ocn-registration.astro b/site/src/pages/order/ocn-registration.astro index 825bf03..1fca65a 100644 --- a/site/src/pages/order/ocn-registration.astro +++ b/site/src/pages/order/ocn-registration.astro @@ -15,8 +15,6 @@ const description = "Obtain an Operating Company Number from NECA. Required for

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}

diff --git a/site/src/pages/order/rmd-filing.astro b/site/src/pages/order/rmd-filing.astro index cd56dfc..759ad77 100644 --- a/site/src/pages/order/rmd-filing.astro +++ b/site/src/pages/order/rmd-filing.astro @@ -15,12 +15,7 @@ const description = "Robocall Mitigation Database filing. Annual recertification

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)} + $100 FCC filing fee

-

{description}

-

- The FCC charges a $100 filing fee for RMD registrations and recertifications (effective 2025). This fee is passed through at cost and paid directly to the FCC during submission. -

diff --git a/site/src/pages/order/stir-shaken.astro b/site/src/pages/order/stir-shaken.astro index 0401e27..e279108 100644 --- a/site/src/pages/order/stir-shaken.astro +++ b/site/src/pages/order/stir-shaken.astro @@ -15,8 +15,6 @@ const description = "Posture update + RMD refresh + STI-CA vendor coordination.

{meta?.name}

-

{formatUSD(meta?.price_cents ?? 0)}

-

{description}