From a74516a2557b32e40dea747f75b04342659a9fe0 Mon Sep 17 00:00:00 2001 From: justin Date: Tue, 16 Jun 2026 05:18:23 -0500 Subject: [PATCH] irp: attach signed POA + census-enrich address; fix date JSON crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - send_irp_submission now REQUIRES and ATTACHES the signed Power of Attorney PDF (downloaded from MinIO) — the state won't act on a third-party filing without it, and 'on file, available on request' stalls the request. If the POA isn't available we don't email and fall back to a manual todo. - Backfill missing legal_name + registered address from the FMCSA census so the submission isn't sent with a blank address (root cause of the empty 'Legal/registered address: , ,' line). Customer-supplied values win. - state_trucking passes signed_auth_key through to the IRP submitter. - Fix 'Object of type date is not JSON serializable' when creating the admin todo (json.dumps(..., default=str)) — broke the intrastate (bash-fee) path. --- scripts/workers/services/irp_filing.py | 92 +++++++++++++++++++--- scripts/workers/services/state_trucking.py | 11 ++- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/scripts/workers/services/irp_filing.py b/scripts/workers/services/irp_filing.py index d704f4f..5f05be7 100644 --- a/scripts/workers/services/irp_filing.py +++ b/scripts/workers/services/irp_filing.py @@ -83,15 +83,73 @@ def state_irp_contact(base_state: str) -> dict | None: return IRP_STATE_CONTACTS.get((base_state or "").upper()) +def _enrich_address_from_census(dot_number: str, intake: dict) -> dict: + """Fill missing legal_name / address fields from the FMCSA census so the IRP + submission isn't sent with a blank address. Customer-supplied values win.""" + out = dict(intake or {}) + need = any(not out.get(k) for k in ("legal_name", "address_street", "address_city", + "address_state", "address_zip")) + if not need or not dot_number: + return out + try: + from scripts.workers.services.mcs150_update import MCS150UpdateHandler + census = MCS150UpdateHandler.__new__(MCS150UpdateHandler)._fetch_carrier_record(dot_number) + for k in ("legal_name", "address_street", "address_city", "address_state", + "address_zip", "phone"): + if not out.get(k) and census.get(k): + out[k] = census[k] + except Exception as exc: # noqa: BLE001 + LOG.warning("[irp] census enrich failed for DOT %s: %s", dot_number, exc) + return out + + +def _download_minio(key: str) -> bytes | None: + """Fetch an object's bytes from MinIO (the signed POA PDF). None on failure.""" + if not key: + return None + try: + from minio import Minio + mc = Minio( + f"{os.getenv('MINIO_ENDPOINT', 'minio')}:{os.getenv('MINIO_PORT', '9000')}", + access_key=os.getenv("MINIO_ACCESS_KEY", ""), + secret_key=os.getenv("MINIO_SECRET_KEY", ""), + secure=os.getenv("MINIO_SECURE", "false").lower() == "true", + ) + bucket = os.getenv("MINIO_BUCKET", "performancewest") + resp = mc.get_object(bucket, key) + data = resp.read() + resp.close() + resp.release_conn() + return data + except Exception as exc: # noqa: BLE001 + LOG.warning("[irp] could not download POA %s: %s", key, exc) + return None + + def send_irp_submission(order_number: str, entity_name: str, dot_number: str, - base_state: str, intake: dict, signed_auth_note: str = "") -> bool: + base_state: str, intake: dict, signed_auth_key: str = "") -> bool: """Email the base-state IRP unit a Schedule A/B submission for this carrier, - tagged for reply matching. Returns True if sent.""" + with the signed POA attached, tagged for reply matching. Returns True if sent. + + The state will not act on a third-party filing without the signed Power of + Attorney, so we REQUIRE the POA PDF and attach it; if it's missing we do not + send (the caller falls back to a manual todo).""" contact = state_irp_contact(base_state) if not contact or not contact.get("email"): LOG.warning("[%s] No IRP submission contact for base state %s", order_number, base_state) return False + # Attach the signed POA — required by the state to act on our behalf. + poa_bytes = _download_minio(signed_auth_key) + if not poa_bytes: + LOG.warning("[%s] No signed POA available (key=%s) — not emailing IRP office", + order_number, signed_auth_key or "(none)") + return False + + # Backfill legal name + address from the FMCSA census if intake lacks them. + intake = _enrich_address_from_census(dot_number, intake) + entity_name = entity_name or intake.get("legal_name", "") + op_states = intake.get("operating_states") or [] if isinstance(op_states, str): try: @@ -99,12 +157,19 @@ def send_irp_submission(order_number: str, entity_name: str, dot_number: str, except Exception: op_states = [s.strip() for s in op_states.split(",") if s.strip()] + addr = ", ".join(p for p in [ + intake.get("address_street", ""), + intake.get("address_city", ""), + f"{intake.get('address_state','')} {intake.get('address_zip','')}".strip(), + ] if p.strip()) + subject = f"IRP Apportioned Registration Request — {entity_name} (USDOT {dot_number}) [{SUBJECT_TAG} {order_number}]" body = ( f"To {contact['agency']},\n\n" - f"On behalf of our client (signed Power of Attorney on file), we request " - f"IRP apportioned registration for the following carrier and ask that you " - f"reply with the computed apportioned fee invoice so we can remit payment.\n\n" + f"On behalf of our client, and under the signed Power of Attorney attached " + f"to this email, we request IRP apportioned registration for the following " + f"carrier. Please reply with the computed apportioned fee invoice so we can " + f"remit payment.\n\n" f"Carrier: {entity_name}\n" f"USDOT: {dot_number}\n" f"MC/MX/FF: {intake.get('mc_number','')}\n" @@ -112,10 +177,9 @@ def send_irp_submission(order_number: str, entity_name: str, dot_number: str, f"Power units: {intake.get('power_units','')}\n" f"Registered weight bracket: {intake.get('gross_weight_bracket','')}\n" f"Operating jurisdictions: {', '.join(op_states) if op_states else base_state}\n" - f"Legal/registered address: {intake.get('address_street','')}, " - f"{intake.get('address_city','')}, {intake.get('address_state','')} " - f"{intake.get('address_zip','')}\n\n" - f"{signed_auth_note or 'A signed Power of Attorney authorizing Performance West Inc. to file on the carrier behalf is available on request.'}\n\n" + f"Legal/registered address: {addr or '(see attached)'}\n\n" + f"Attached: signed Power of Attorney authorizing Performance West Inc. to " + f"file and remit fees on the carrier's behalf.\n\n" f"Please reply to {FILINGS_FROM} with the apportioned fee total (including any " f"processing fees) and any required Schedule A/B forms. Keep the subject " f"reference [{SUBJECT_TAG} {order_number}] so we can match your reply.\n\n" @@ -124,20 +188,26 @@ def send_irp_submission(order_number: str, entity_name: str, dot_number: str, f"(888) 411-0383 · {FILINGS_FROM}\n" ) try: + from email.mime.application import MIMEApplication msg = MIMEMultipart() msg["From"] = SMTP_USER msg["To"] = contact["email"] msg["Reply-To"] = FILINGS_FROM msg["Subject"] = subject msg.attach(MIMEText(body, "plain")) + poa = MIMEApplication(poa_bytes, _subtype="pdf") + poa.add_header("Content-Disposition", "attachment", + filename=f"POA_{entity_name.replace(' ','_')}_{dot_number}.pdf") + msg.attach(poa) with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as s: s.starttls() if SMTP_USER and SMTP_PASS: s.login(SMTP_USER, SMTP_PASS) s.sendmail(SMTP_USER, [contact["email"], FILINGS_FROM], msg.as_string()) - LOG.info("[%s] IRP submission emailed to %s (%s)", order_number, contact["email"], base_state) + LOG.info("[%s] IRP submission emailed to %s (%s) with POA attached", + order_number, contact["email"], base_state) send_telegram( - f"📤 IRP submission sent\n{entity_name} (DOT {dot_number})\n" + f"📤 IRP submission sent (POA attached)\n{entity_name} (DOT {dot_number})\n" f"Base state: {base_state} → {contact['email']}\n" f"Order: {order_number}\nAwaiting the state's apportioned-fee invoice." ) diff --git a/scripts/workers/services/state_trucking.py b/scripts/workers/services/state_trucking.py index 9021106..4903f3d 100644 --- a/scripts/workers/services/state_trucking.py +++ b/scripts/workers/services/state_trucking.py @@ -311,7 +311,8 @@ class StateTruckingHandler: and not order_data.get("gov_fee_paid") and not self._gov_fee_settled(order_number)): if self._request_gov_fee_payment(order_number, service_slug, service_name, - entity_name, customer_email, customer_phone, intake): + entity_name, customer_email, customer_phone, + intake, signed_auth_key): self._set_fulfillment_status(order_number, FULFILLMENT_AWAITING_FEE_APPROVAL) LOG.info("[%s] Gov fee quoted — held pending customer payment", order_number) return [] @@ -390,7 +391,7 @@ class StateTruckingHandler: order_number, service_slug, todo_description, - json.dumps(todo_data), + json.dumps(todo_data, default=str), )) conn.commit() notify_fulfillment_todo( @@ -713,7 +714,8 @@ class StateTruckingHandler: return False def _request_gov_fee_payment(self, order_number, service_slug, service_name, - entity_name, customer_email, customer_phone, intake) -> bool: + entity_name, customer_email, customer_phone, intake, + signed_auth_key=None) -> bool: """Set up government-fee collection after authorization. IRP: the fee is unknown until the base state computes it, so we EMAIL the @@ -731,7 +733,8 @@ class StateTruckingHandler: return False base_state = (intake.get("base_state") or intake.get("address_state") or "").upper() sent = send_irp_submission(order_number, entity_name, - intake.get("dot_number", ""), base_state, intake) + intake.get("dot_number", ""), base_state, intake, + signed_auth_key=signed_auth_key) if sent: try: notify_fulfillment_todo(