new-site/scripts/workers/services/ein_application.py
justin 28b1af341d Wire fulfillment alerts to Telegram + surface order progress in portal + even out ERPNext sync
Telegram notifications:
- Add shared scripts/workers/telegram_notify.py (send_telegram, notify_fulfillment_todo,
  create_admin_todo) so every worker alerts the operator the same way; fire-and-forget.
- Fire notify_fulfillment_todo after each admin_todos insert across all 8 service
  handlers (9 sites) so no fulfillment task waits unseen.
  (Orders + quotes + tickets already notified via checkout/quotes/tickets routes.)

Client portal order progress:
- order-timeline: derive real per-step status from live signals (payment paid,
  e-signature signed, fulfillment_status) instead of a static template; add
  current_step to the response.
- Extract pure applyLiveStatus into order-timeline-status.ts (DB-free) + unit test
  (api/test/test_timeline_status.ts, 8 cases).
- portal /me now returns compliance_orders.fulfillment_status.
- Dashboard renders a client-safe Progress badge (In progress / Action needed /
  Filed-awaiting-confirmation / Completed); batches show the most actionable status.
  No back-office mechanics exposed.

ERPNext sync parity:
- Create a Sales Order for formation and fcc_carrier_registration orders (previously
  only canada_crtc + compliance synced); write erpnext_sales_order back to each table.
  Non-blocking, matches existing pattern.

Verified: API tsc clean, timeline unit tests 8/8, Astro build 58 pages,
cms10114/ink/paper_batch Python tests still green, no mechanics leaks.
2026-06-07 03:17:46 -05:00

190 lines
7.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""EIN Application — IRS SS-4 Online Automation.
Automates the IRS online EIN application at:
https://sa.www4.irs.gov/modiein/individual/index.jsp
IMPORTANT: IRS online EIN is only available:
Monday Friday, 7:00 AM 10:00 PM Eastern Time
The handler checks availability before attempting. If outside hours,
it queues for the next available window.
Flow:
1. Check if within IRS business hours
2. Navigate to IRS EIN online application
3. Fill entity type, state, responsible party info
4. Submit and capture the EIN assignment
5. Store EIN in order intake_data + create admin todo
Intake data needed:
- entity_type: LLC, Corporation, Partnership, Sole Proprietor
- entity_name: legal name of the entity
- state: state of formation
- responsible_party_name: full name of responsible party
- responsible_party_ssn: SSN or ITIN (for identity)
- address: street, city, state, zip
- phone: contact phone
"""
from __future__ import annotations
import asyncio
import json
import logging
import os
from datetime import datetime, timezone, timedelta
from pathlib import Path
from scripts.workers.telegram_notify import notify_fulfillment_todo
LOG = logging.getLogger("workers.services.ein_application")
SCREENSHOTS_DIR = Path(os.getenv("SCREENSHOTS_DIR", "/tmp/ein-screenshots"))
SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)
IRS_EIN_URL = "https://sa.www4.irs.gov/modiein/individual/index.jsp"
# IRS business hours: Mon-Fri 7am-10pm Eastern
EASTERN_OFFSET = timedelta(hours=-5) # EST (EDT would be -4)
def is_irs_available() -> bool:
"""Check if IRS online EIN is currently available (Mon-Fri 7am-10pm ET)."""
now_utc = datetime.now(timezone.utc)
# Use ET (approximate — doesn't handle DST precisely, but close enough)
# EDT = UTC-4, EST = UTC-5. Use -4 (EDT) during summer months.
month = now_utc.month
offset = timedelta(hours=-4) if 3 <= month <= 11 else timedelta(hours=-5)
now_et = now_utc + offset
# Check day of week (0=Monday, 6=Sunday)
if now_et.weekday() >= 5: # Saturday or Sunday
return False
# Check time (7am - 10pm)
hour = now_et.hour
return 7 <= hour < 22
def next_available_window() -> datetime:
"""Calculate the next available IRS window."""
now_utc = datetime.now(timezone.utc)
month = now_utc.month
offset = timedelta(hours=-4) if 3 <= month <= 11 else timedelta(hours=-5)
now_et = now_utc + offset
# If it's a weekday before 10pm, next window is 7am today or tomorrow
if now_et.weekday() < 5 and now_et.hour < 22:
if now_et.hour < 7:
# Today at 7am ET
target = now_et.replace(hour=7, minute=0, second=0, microsecond=0)
else:
# Already in window
return now_utc
else:
# Next Monday at 7am ET if weekend, or tomorrow 7am if weekday after 10pm
days_ahead = 1
next_day = now_et + timedelta(days=1)
while next_day.weekday() >= 5:
next_day += timedelta(days=1)
days_ahead += 1
target = next_day.replace(hour=7, minute=0, second=0, microsecond=0)
return target - offset # Convert back to UTC
class EINApplicationHandler:
"""Handle EIN application orders."""
SERVICE_SLUG = "ein-application"
SERVICE_NAME = "EIN Application (IRS SS-4)"
async def process(self, order_data: dict) -> list[str]:
"""Entry point called by job_server."""
order_number = order_data.get("order_number", order_data.get("name", ""))
return await self.handle(order_data, order_number)
async def handle(self, order_data: dict, order_number: str) -> list[str]:
"""Process an EIN application order."""
LOG.info("[%s] Processing EIN application", order_number)
intake = order_data.get("intake_data") or {}
if isinstance(intake, str):
intake = json.loads(intake)
# Check IRS availability
if not is_irs_available():
next_window = next_available_window()
LOG.info("[%s] IRS offline — next window at %s UTC", order_number, next_window.isoformat())
# Create admin todo to process during business hours
self._create_todo(
order_number, intake,
title=f"EIN Application QUEUED — {intake.get('entity_name', 'Unknown')}",
description=(
f"IRS online EIN not available (Mon-Fri 7am-10pm ET only).\n"
f"Next available: {next_window.strftime('%A %I:%M %p ET')}.\n"
f"Will auto-retry or process manually."
),
priority="normal",
)
return []
# Attempt automated filing
is_prod = os.environ.get("NODE_ENV") == "production" or os.environ.get("ENV") == "production"
if not is_prod:
LOG.info("[%s] DEV MODE — skipping IRS EIN submission", order_number)
self._create_todo(
order_number, intake,
title=f"EIN Application (DEV) — {intake.get('entity_name', 'Unknown')}",
description="DEV MODE — IRS submission skipped.",
priority="low",
)
return []
# TODO: Playwright automation of IRS EIN form
# For now, create admin todo for manual processing
self._create_todo(
order_number, intake,
title=f"EIN Application — {intake.get('entity_name', 'Unknown')}",
description=(
f"Apply for EIN via IRS online (Mon-Fri 7am-10pm ET).\n"
f"URL: {IRS_EIN_URL}\n"
f"Entity: {intake.get('entity_name', 'N/A')}\n"
f"Type: {intake.get('entity_type', 'LLC')}\n"
f"State: {intake.get('formation_state', intake.get('state', 'N/A'))}\n"
f"Responsible party: {intake.get('signer_name', 'N/A')}\n\n"
f"EIN is issued immediately upon completion.\n"
f"Update the order intake_data with the EIN once received."
),
priority="high",
)
return []
def _create_todo(self, order_number, intake, title, description, priority="normal"):
"""Create admin todo."""
try:
import psycopg2
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
with conn.cursor() as cur:
cur.execute("""
INSERT INTO admin_todos (
title, category, priority, order_number, service_slug,
description, data, status
) VALUES (%s, %s, %s, %s, %s, %s, %s, 'pending')
""", (
title, "filing", priority, order_number,
self.SERVICE_SLUG, description, json.dumps(intake),
))
conn.commit()
notify_fulfillment_todo(
title=title,
order_number=order_number,
service_slug=self.SERVICE_SLUG,
priority=priority,
description=description,
)
conn.close()
except Exception as exc:
LOG.error("[%s] Failed to create EIN todo: %s", order_number, exc)