irp: attach signed POA + census-enrich address; fix date JSON crash
- 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.
This commit is contained in:
parent
1d6693adb9
commit
a74516a255
2 changed files with 88 additions and 15 deletions
|
|
@ -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."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue