diff --git a/scripts/workers/job_server.py b/scripts/workers/job_server.py index 52c88a0..5c5a15c 100644 --- a/scripts/workers/job_server.py +++ b/scripts/workers/job_server.py @@ -1136,6 +1136,19 @@ def handle_process_compliance_service(payload: dict) -> dict: order.get("entity", {}).get("frn"), exc) handler = handler_cls() + + # Ensure order_number is always available (some handlers use it instead of name) + if order_number: + order["order_number"] = order_number + + # Inject eSign approval flags from payload (set by handle_esign_completed) + if payload.get("client_approved"): + order["client_approved"] = True + if payload.get("esign_document_type"): + order["esign_document_type"] = payload["esign_document_type"] + if payload.get("esign_signer_email"): + order["esign_signer_email"] = payload["esign_signer_email"] + # Final entity check before dispatch ent = order.get("entity", {}) LOG.info( @@ -1562,9 +1575,9 @@ def handle_esign_completed(payload: dict) -> dict: Called by portal-esign-generic.ts after a client signs any document. Payload: { order_number, document_type, esign_record_id, signer_email } - Looks up the compliance order for this order_number and re-dispatches - the service handler with client_approved=true so it continues past - the signing checkpoint. + Re-dispatches through the standard handle_process_compliance_service + path with client_approved=true injected into the payload so the handler + skips past its signing checkpoint on the second run. """ order_number = payload.get("order_number", "") document_type = payload.get("document_type", "") @@ -1578,7 +1591,7 @@ def handle_esign_completed(payload: dict) -> dict: conn = psycopg2.connect(os.environ.get("DATABASE_URL", "")) with conn.cursor() as cur: cur.execute( - "SELECT service_slug FROM compliance_orders WHERE order_number = %s", + "SELECT service_slug, erpnext_sales_order FROM compliance_orders WHERE order_number = %s", (order_number,), ) row = cur.fetchone() @@ -1589,22 +1602,20 @@ def handle_esign_completed(payload: dict) -> dict: return {"warning": f"No compliance order for {order_number}"} service_slug = row[0] + erpnext_so = row[1] or order_number - # Re-dispatch the service handler with approval flag - from scripts.workers.services import SERVICE_HANDLERS - handler_cls = SERVICE_HANDLERS.get(service_slug) - if handler_cls: - LOG.info("[esign_completed] Re-dispatching %s for %s", service_slug, order_number) - handler = handler_cls() - handler.process(order_number, { - "client_approved": True, - "esign_document_type": document_type, - "esign_signer_email": payload.get("signer_email", ""), - }) - else: - LOG.warning("[esign_completed] No handler for slug=%s", service_slug) + LOG.info("[esign_completed] Re-dispatching %s for %s via standard pipeline", service_slug, order_number) - return {"success": True, "order_number": order_number, "document_type": document_type} + # Re-dispatch through the standard compliance service handler. + # client_approved is injected and will be merged into order_data. + return handle_process_compliance_service({ + "order_name": erpnext_so, + "order_number": order_number, + "service_slug": service_slug, + "client_approved": True, + "esign_document_type": document_type, + "esign_signer_email": payload.get("signer_email", ""), + }) except Exception as exc: LOG.error("[esign_completed] Error for %s: %s", order_number, exc) return {"error": str(exc)} diff --git a/scripts/workers/services/calea_ssi.py b/scripts/workers/services/calea_ssi.py index 05d59e9..481f913 100644 --- a/scripts/workers/services/calea_ssi.py +++ b/scripts/workers/services/calea_ssi.py @@ -167,6 +167,46 @@ class CALEASSIHandler(BaseServiceHandler): except Exception as exc: logger.warning("CALEA SSI PDF conversion failed: %s", exc) + # ── Client eSign gate ────────────────────────────────────────── + # The CALEA SSI plan requires officer signature before delivery. + client_approved = order_data.get("client_approved", False) + if not client_approved and generated: + from scripts.document_gen import MinioStorage + storage = MinioStorage() + pdf_minio_key = "" + for path in generated: + if path.endswith(".pdf"): + remote = f"compliance/{order_number}/{os.path.basename(path)}" + try: + storage.upload_file(path, remote) + pdf_minio_key = remote + except Exception as exc: + logger.warning("MinIO upload failed for %s: %s", path, exc) + break + + try: + import psycopg2 + conn = psycopg2.connect(os.environ.get("DATABASE_URL", "")) + from scripts.workers.services.telecom.esign_helper import request_esign + request_esign( + conn=conn, + order_number=order_number, + document_type="calea", + document_title="CALEA System Security & Integrity Plan", + entity_name=entity.get("legal_name", ""), + customer_email=order_data.get("customer_email") or entity.get("contact_email", ""), + customer_name=order_data.get("customer_name") or entity.get("contact_name", ""), + document_minio_key=pdf_minio_key, + requires_perjury=False, + metadata={"frn": entity.get("frn", "")}, + ) + conn.close() + except Exception as exc: + logger.warning("Could not create CALEA eSign record: %s", exc) + + logger.info("CALEASSIHandler: paused for client eSign — order %s", order_number) + return generated + # Persist + schedule annual review if entity_id: self._persist_calea_state( diff --git a/scripts/workers/services/cpni_certification.py b/scripts/workers/services/cpni_certification.py index 197a6e7..cc38e2f 100644 --- a/scripts/workers/services/cpni_certification.py +++ b/scripts/workers/services/cpni_certification.py @@ -188,7 +188,50 @@ class CPNIFilingHandler(BaseServiceHandler): ) return generated - # ── 2a. Auto-filing toggle ────────────────────────────────────── + # ── 2a. Client eSign gate ────────────────────────────────────── + # Officer must sign the CPNI certification before FCC submission + # (47 CFR § 64.2009(e) requires officer attestation). + client_approved = order_data.get("client_approved", False) + if not client_approved: + # Upload cert PDF to MinIO for the signing portal preview + from scripts.document_gen import MinioStorage + storage = MinioStorage() + pdf_minio_key = "" + for path in generated: + if path.endswith(".pdf") and "certification" in path.lower(): + remote = f"compliance/{order_number}/{os.path.basename(path)}" + try: + storage.upload_file(path, remote) + pdf_minio_key = remote + except Exception as exc: + logger.warning("MinIO upload failed for %s: %s", path, exc) + break + + # Create eSign record + send signing email + try: + import psycopg2 + conn = psycopg2.connect(os.environ.get("DATABASE_URL", "")) + from scripts.workers.services.telecom.esign_helper import request_esign + request_esign( + conn=conn, + order_number=order_number, + document_type="cpni", + document_title="CPNI Annual Certification", + entity_name=entity.get("legal_name", ""), + customer_email=order_data.get("customer_email") or entity.get("contact_email", ""), + customer_name=order_data.get("customer_name") or entity.get("contact_name", ""), + document_minio_key=pdf_minio_key, + requires_perjury=True, + metadata={"frn": entity.get("frn", ""), "docket": CPNI_DOCKET}, + ) + conn.close() + except Exception as exc: + logger.warning("Could not create CPNI eSign record: %s", exc) + + logger.info("CPNIFilingHandler: paused for client eSign — order %s", order_number) + return generated + + # ── 2b. Auto-filing toggle ────────────────────────────────────── decision = check_auto_filing(order_data) if not decision.may_submit: logger.info( diff --git a/scripts/workers/services/form_499a.py b/scripts/workers/services/form_499a.py index 20226c6..6b9be32 100644 --- a/scripts/workers/services/form_499a.py +++ b/scripts/workers/services/form_499a.py @@ -179,7 +179,7 @@ class Form499AHandler(BaseServiceHandler): or (multi_year and len(multi_year) >= 2) ) if needs_engagement: - esign_signed = order_data.get("engagement_esign_signed_at") + esign_signed = order_data.get("engagement_esign_signed_at") or order_data.get("client_approved") 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") @@ -1537,72 +1537,37 @@ class Form499AHandler(BaseServiceHandler): cur.execute( """UPDATE compliance_orders SET engagement_esign_required = TRUE, - engagement_letter_minio_key = %s, - payment_status = 'pending_esign' + engagement_letter_minio_key = %s 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) + conn = None - # Email client the engagement signing link - if customer_email: + # Create eSign record + send signing email via generic portal + if customer_email and conn: 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" + from scripts.workers.services.telecom.esign_helper import request_esign 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"
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
" + request_esign( + conn=conn, + order_number=order_number, + document_type="499a-engagement", + document_title=f"Engagement Letter — 499-A Filing ({years_str})", + entity_name=entity.get("legal_name") or intake.get("entity_legal_name", ""), + customer_email=customer_email, + customer_name=customer_name, + document_minio_key=minio_key, + requires_perjury=False, + metadata={"frn": entity.get("frn", ""), "filing_years": multi_year}, ) - - msg = MIMEMultipart("alternative") - msg["Subject"] = subject - msg["From"] = os.environ.get("SMTP_FROM", "Performance West