Skip to content

Sequence 10: E-Filing Status Tracking - Implementation Plan

Created: 2025-12-24 Status: Ready for Approval Depends On: Sequence 9 (complete)


Overview

Sequence 10 implements e-filing status tracking. UltraTax CS handles actual IRS/state transmission (confirmed ASM-001). Our system tracks status via SurePrep CS Connect webhooks and notifies clients/staff of acceptance or rejection.

Stories

Story Title Priority Effort
S10-001 SurePrep E-File Status Integration P0 Medium
S10-002 E-File Workflow Trigger P0 Small
S10-003 Acceptance Notification P0 Small
S10-004 Rejection Handling P0 Medium

Existing Infrastructure (Reuse)

Already implemented: - src/services/sureprep_service.py - SurePrep API (needs e-file status methods) - src/workflows/communication/notification_workflow.py - Email/SMS notifications (S8) - src/domain/workflow.py - TaxReturn entity with state machine - src/api/routes/webhooks.py - Webhook handlers - src/repositories/workflow_repository.py - Tax return state management - config.yaml - SurePrep configuration section

Key patterns to follow: - Service pattern with dry_run mode for development - Repository pattern with async methods and _to_entity() conversion - Result dataclasses with ok/fail factory methods - Webhook handlers update DB then trigger workflows - structlog logging with bound context


Parallel Execution Strategy

To maximize throughput, organize phases into parallel tracks:

TRACK A (Domain/Repository)     TRACK B (Service/Webhook)      TRACK C (Tests)
─────────────────────────────   ───────────────────────────    ─────────────────
Phase 1: Domain Model           Phase 2: SurePrep Methods      (wait)
         ↓                               ↓
Phase 3: Repository             Phase 4: Webhook Handler       Phase 6: Unit Tests
         ↓                               ↓                      (background)
Phase 5: EFilingWorkflow        (merge)                                ↓
         ↓                                                      Phase 7: Integration
Phase 8: API Routes ←───────────────────────────────────────────       Tests
Phase 9: Integration conftest
Phase 10: Code Audit

Parallel opportunities: - Phase 1 + Phase 2 can run in parallel (no dependencies) - Phase 6 (unit tests) can run in background after Phase 5 - Track C tests can start once domain is defined


Implementation Order

Phase 1: Domain Model (Track A)

New File: src/domain/efiling.py

"""
E-filing status domain entities for Tax Practice AI.

Per USER_STORIES.md S10-001 through S10-004.
Note: UltraTax handles transmission; we track status via SurePrep.
"""

from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import List, Optional
from uuid import UUID


class EFileStatus(str, Enum):
    """E-file submission status."""
    READY = "ready"              # Ready for transmission (8879 signed)
    PENDING = "pending"          # Submitted to UltraTax, awaiting acknowledgement
    TRANSMITTED = "transmitted"  # Transmitted to IRS/state
    ACCEPTED = "accepted"        # Accepted by IRS/state
    REJECTED = "rejected"        # Rejected - needs correction


class EFileJurisdiction(str, Enum):
    """Filing jurisdiction."""
    FEDERAL = "federal"
    STATE = "state"


class RejectionCategory(str, Enum):
    """E-file rejection categories."""
    DATA_MISMATCH = "data_mismatch"    # SSN/name mismatch, AGI mismatch
    MISSING_INFO = "missing_info"       # Missing schedule, incomplete data
    DUPLICATE = "duplicate"             # Already filed
    TECHNICAL = "technical"             # Transmission error, try again
    IDENTITY = "identity"               # Identity verification required


@dataclass
class EFileSubmission:
    """
    E-file submission record.

    Tracks status for each jurisdiction (federal + each state).
    """
    id: UUID
    tax_return_id: UUID
    client_id: UUID
    jurisdiction: EFileJurisdiction
    state_code: Optional[str]           # e.g., "FL", "GA" (None for federal)
    status: EFileStatus
    sureprep_tracking_id: Optional[str] # SurePrep's tracking reference
    ultratax_confirmation: Optional[str] # UltraTax confirmation number
    irs_confirmation: Optional[str]     # IRS/state confirmation number
    submitted_at: Optional[datetime]
    transmitted_at: Optional[datetime]
    accepted_at: Optional[datetime]
    rejected_at: Optional[datetime]
    created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    def is_complete(self) -> bool:
        """Check if submission has final status."""
        return self.status in (EFileStatus.ACCEPTED, EFileStatus.REJECTED)

    def is_accepted(self) -> bool:
        """Check if accepted."""
        return self.status == EFileStatus.ACCEPTED

    def is_rejected(self) -> bool:
        """Check if rejected."""
        return self.status == EFileStatus.REJECTED

    def can_resubmit(self) -> bool:
        """Check if can be resubmitted after rejection."""
        return self.status == EFileStatus.REJECTED


@dataclass
class EFileRejection:
    """
    E-file rejection details.

    Stores error codes and categorization for resolution tracking.
    """
    id: UUID
    submission_id: UUID
    tax_return_id: UUID
    error_code: str                     # IRS/state error code
    error_message: str                  # Human-readable message
    category: RejectionCategory
    auto_correctable: bool              # Can system auto-fix?
    resolution_notes: Optional[str]
    resolved_at: Optional[datetime]
    resolved_by: Optional[UUID]         # Staff user ID
    created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    def is_resolved(self) -> bool:
        """Check if rejection has been resolved."""
        return self.resolved_at is not None


# =============================================================================
# CONSTANTS
# =============================================================================

# Common IRS rejection codes and their categories
REJECTION_CODE_CATEGORIES = {
    # Data mismatch
    "IND-031": RejectionCategory.DATA_MISMATCH,  # SSN/name mismatch
    "IND-032": RejectionCategory.DATA_MISMATCH,  # Spouse SSN/name mismatch
    "IND-181": RejectionCategory.DATA_MISMATCH,  # AGI doesn't match
    "IND-182": RejectionCategory.DATA_MISMATCH,  # PIN doesn't match

    # Duplicate filing
    "IND-510": RejectionCategory.DUPLICATE,      # Already filed
    "IND-511": RejectionCategory.DUPLICATE,      # Duplicate SSN

    # Missing info
    "IND-901": RejectionCategory.MISSING_INFO,   # Missing schedule
    "IND-902": RejectionCategory.MISSING_INFO,   # Incomplete form

    # Identity
    "IND-516": RejectionCategory.IDENTITY,       # ID verification needed
    "IND-517": RejectionCategory.IDENTITY,       # IP PIN required

    # Technical (default)
    "R0000": RejectionCategory.TECHNICAL,        # Transmission error
}

# Rejection codes that can be auto-corrected
AUTO_CORRECTABLE_CODES = {
    "R0000",  # Retry transmission
}


def categorize_rejection(error_code: str) -> RejectionCategory:
    """Categorize rejection by error code."""
    return REJECTION_CODE_CATEGORIES.get(error_code, RejectionCategory.TECHNICAL)


def is_auto_correctable(error_code: str) -> bool:
    """Check if rejection can be auto-corrected."""
    return error_code in AUTO_CORRECTABLE_CODES

Update: src/domain/__init__.py - Export efiling entities


Phase 2: SurePrep E-File Status Methods (Track B - PARALLEL with Phase 1)

Update: src/services/sureprep_service.py

Add e-file status methods to existing service:

# =============================================================================
# E-FILE STATUS (S10)
# =============================================================================

class EFileStatusEvent(str, Enum):
    """E-file status events from SurePrep."""
    PENDING = "pending"
    TRANSMITTED = "transmitted"
    ACCEPTED = "accepted"
    REJECTED = "rejected"


@dataclass
class EFileStatusUpdate:
    """E-file status update from SurePrep."""
    tracking_id: str
    jurisdiction: str                   # "federal" or state code
    status: EFileStatusEvent
    confirmation_number: Optional[str]
    error_code: Optional[str]
    error_message: Optional[str]
    timestamp: datetime


class SurePrepService(BaseService):
    # ... existing methods ...

    # =========================================================================
    # E-FILE STATUS (S10)
    # =========================================================================

    async def get_efile_status(
        self,
        binder_id: str,
    ) -> List[EFileStatusUpdate]:
        """
        Get e-file status for all jurisdictions in binder.

        Args:
            binder_id: SurePrep binder ID

        Returns:
            List of status updates for each jurisdiction
        """
        log = self._log.bind(
            operation="get_efile_status",
            binder_id=binder_id,
        )
        log.info("getting_efile_status")

        if self._config.dry_run:
            log.info("efile_status_dry_run")
            return self._simulate_efile_status(binder_id)

        # TODO: Implement actual API call to SurePrep
        log.warning("efile_status_not_implemented")
        return []

    async def poll_efile_status(
        self,
        binder_ids: List[str],
    ) -> Dict[str, List[EFileStatusUpdate]]:
        """
        Poll e-file status for multiple binders.

        Used by scheduled job to batch status checks.

        Args:
            binder_ids: List of SurePrep binder IDs

        Returns:
            Dict mapping binder_id to status updates
        """
        results = {}
        for binder_id in binder_ids:
            results[binder_id] = await self.get_efile_status(binder_id)
        return results

    def _simulate_efile_status(self, binder_id: str) -> List[EFileStatusUpdate]:
        """Simulate e-file status for dry_run mode."""
        return [
            EFileStatusUpdate(
                tracking_id=f"track_{binder_id}",
                jurisdiction="federal",
                status=EFileStatusEvent.ACCEPTED,
                confirmation_number="IRS-2024-123456789",
                error_code=None,
                error_message=None,
                timestamp=datetime.now(timezone.utc),
            )
        ]

Phase 3: Repository Layer (Track A)

New File: src/repositories/efiling_repository.py

"""
E-filing repository for Tax Practice AI.

Data access for e-file submissions and rejections.
Per USER_STORIES.md S10-001 through S10-004.
"""

class EFileSubmissionRepository:
    """Repository for e-file submissions."""

    async def create(
        self,
        tax_return_id: UUID,
        client_id: UUID,
        jurisdiction: EFileJurisdiction,
        state_code: Optional[str] = None,
    ) -> EFileSubmission

    async def find_by_id(self, submission_id: UUID) -> Optional[EFileSubmission]

    async def find_by_return(self, tax_return_id: UUID) -> List[EFileSubmission]

    async def find_pending(self) -> List[EFileSubmission]

    async def update_status(
        self,
        submission_id: UUID,
        status: EFileStatus,
        confirmation: Optional[str] = None,
    ) -> EFileSubmission

    async def mark_transmitted(
        self,
        submission_id: UUID,
        sureprep_tracking_id: str,
    ) -> EFileSubmission

    async def mark_accepted(
        self,
        submission_id: UUID,
        irs_confirmation: str,
    ) -> EFileSubmission

    async def mark_rejected(
        self,
        submission_id: UUID,
    ) -> EFileSubmission


class EFileRejectionRepository:
    """Repository for e-file rejections."""

    async def create(
        self,
        submission_id: UUID,
        tax_return_id: UUID,
        error_code: str,
        error_message: str,
        category: RejectionCategory,
        auto_correctable: bool,
    ) -> EFileRejection

    async def find_by_submission(self, submission_id: UUID) -> Optional[EFileRejection]

    async def find_unresolved(self) -> List[EFileRejection]

    async def find_by_category(self, category: RejectionCategory) -> List[EFileRejection]

    async def mark_resolved(
        self,
        rejection_id: UUID,
        resolved_by: UUID,
        notes: Optional[str] = None,
    ) -> EFileRejection

Update: src/repositories/__init__.py - Export efiling repositories


Phase 4: Webhook Handler (Track B)

Update: src/api/routes/webhooks.py

Add SurePrep e-file status webhook handler:

# =============================================================================
# SUREPREP E-FILE WEBHOOKS (S10)
# =============================================================================

@router.post("/sureprep/efile-status")
async def sureprep_efile_status_webhook(
    request: Request,
    efiling_workflow: EFilingWorkflow = Depends(get_efiling_workflow),
) -> dict:
    """
    Handle SurePrep e-file status webhook.

    Events:
    - efile.pending: Submitted to UltraTax
    - efile.transmitted: Sent to IRS/state
    - efile.accepted: Accepted by IRS/state
    - efile.rejected: Rejected with error codes
    """
    body = await request.json()

    # Verify webhook signature
    signature = request.headers.get("X-SurePrep-Signature")
    if not verify_sureprep_signature(body, signature):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event_type = body.get("event_type")
    binder_id = body.get("binder_id")

    if event_type == "efile.accepted":
        result = await efiling_workflow.process_acceptance(
            binder_id=binder_id,
            jurisdiction=body.get("jurisdiction"),
            confirmation_number=body.get("confirmation_number"),
        )
    elif event_type == "efile.rejected":
        result = await efiling_workflow.process_rejection(
            binder_id=binder_id,
            jurisdiction=body.get("jurisdiction"),
            error_code=body.get("error_code"),
            error_message=body.get("error_message"),
        )
    else:
        result = await efiling_workflow.process_status_update(
            binder_id=binder_id,
            event_type=event_type,
            payload=body,
        )

    return {"received": True, "processed": result.success}

Phase 5: E-Filing Workflow (Track A - after Phase 3)

New File: src/workflows/filing/__init__.py New File: src/workflows/filing/efiling_workflow.py

"""
E-filing workflow for Tax Practice AI.

Orchestrates e-file status tracking and notifications.
Per USER_STORIES.md S10-001 through S10-004.

Note: UltraTax handles actual IRS/state transmission.
We track status via SurePrep CS Connect.
"""

from dataclasses import dataclass
from typing import List, Optional
from uuid import UUID

import structlog

from src.domain.efiling import (
    EFileSubmission,
    EFileRejection,
    EFileStatus,
    EFileJurisdiction,
    RejectionCategory,
    categorize_rejection,
    is_auto_correctable,
)
from src.repositories.efiling_repository import (
    EFileSubmissionRepository,
    EFileRejectionRepository,
)
from src.repositories.workflow_repository import TaxReturnRepository


log = structlog.get_logger()


# =============================================================================
# RESULT TYPES
# =============================================================================

@dataclass
class EFileResult:
    """Result of e-filing operations."""
    success: bool
    submission: Optional[EFileSubmission] = None
    submissions: Optional[List[EFileSubmission]] = None
    error: Optional[str] = None

    @classmethod
    def ok(cls, submission: EFileSubmission = None, submissions: List[EFileSubmission] = None):
        return cls(success=True, submission=submission, submissions=submissions)

    @classmethod
    def fail(cls, error: str):
        return cls(success=False, error=error)


@dataclass
class RejectionResult:
    """Result of rejection processing."""
    success: bool
    rejection: Optional[EFileRejection] = None
    auto_corrected: bool = False
    error: Optional[str] = None

    @classmethod
    def ok(cls, rejection: EFileRejection, auto_corrected: bool = False):
        return cls(success=True, rejection=rejection, auto_corrected=auto_corrected)

    @classmethod
    def fail(cls, error: str):
        return cls(success=False, error=error)


# =============================================================================
# E-FILING WORKFLOW (S10)
# =============================================================================

class EFilingWorkflow:
    """
    Orchestrates e-file status tracking.

    Handles:
    - Marking returns ready for e-filing (S10-002)
    - Processing status updates from SurePrep (S10-001)
    - Sending acceptance notifications (S10-003)
    - Processing rejections (S10-004)
    """

    def __init__(
        self,
        submission_repository: EFileSubmissionRepository,
        rejection_repository: EFileRejectionRepository,
        return_repository: TaxReturnRepository,
        sureprep_service=None,
        notification_workflow=None,
        audit_service=None,
    ):
        self._submission_repo = submission_repository
        self._rejection_repo = rejection_repository
        self._return_repo = return_repository
        self._sureprep = sureprep_service
        self._notification = notification_workflow
        self._audit = audit_service
        self._log = structlog.get_logger().bind(workflow="EFilingWorkflow")

    # =========================================================================
    # S10-002: E-FILE WORKFLOW TRIGGER
    # =========================================================================

    async def mark_ready_for_filing(
        self,
        tax_return_id: UUID,
        jurisdictions: List[str],  # ["federal", "FL", "GA"]
    ) -> EFileResult:
        """
        Mark return as ready for e-filing after 8879 signature.

        Creates submission records for each jurisdiction.
        """
        self._log.info(
            "marking_ready_for_filing",
            tax_return_id=str(tax_return_id),
            jurisdictions=jurisdictions,
        )

        try:
            # Get tax return
            tax_return = await self._return_repo.find_by_id(tax_return_id)
            if not tax_return:
                return EFileResult.fail("Tax return not found")

            submissions = []
            for jurisdiction in jurisdictions:
                if jurisdiction == "federal":
                    sub = await self._submission_repo.create(
                        tax_return_id=tax_return_id,
                        client_id=tax_return.client_id,
                        jurisdiction=EFileJurisdiction.FEDERAL,
                    )
                else:
                    sub = await self._submission_repo.create(
                        tax_return_id=tax_return_id,
                        client_id=tax_return.client_id,
                        jurisdiction=EFileJurisdiction.STATE,
                        state_code=jurisdiction,
                    )
                submissions.append(sub)

            # Update return status
            await self._return_repo.update_status(
                tax_return_id,
                "ready_to_file",
            )

            if self._audit:
                await self._audit.log_efile_ready(
                    tax_return_id=tax_return_id,
                    jurisdictions=jurisdictions,
                )

            return EFileResult.ok(submissions=submissions)

        except Exception as e:
            self._log.error("mark_ready_failed", error=str(e))
            return EFileResult.fail(str(e))

    # =========================================================================
    # S10-001: STATUS PROCESSING
    # =========================================================================

    async def process_status_update(
        self,
        binder_id: str,
        event_type: str,
        payload: dict,
    ) -> EFileResult:
        """Process generic status update from SurePrep."""
        self._log.info(
            "processing_status_update",
            binder_id=binder_id,
            event_type=event_type,
        )

        try:
            # Find submission by binder/tracking ID
            # Update status based on event type
            # Map: pending, transmitted

            return EFileResult.ok()

        except Exception as e:
            self._log.error("status_update_failed", error=str(e))
            return EFileResult.fail(str(e))

    # =========================================================================
    # S10-003: ACCEPTANCE NOTIFICATION
    # =========================================================================

    async def process_acceptance(
        self,
        binder_id: str,
        jurisdiction: str,
        confirmation_number: str,
    ) -> EFileResult:
        """
        Process e-file acceptance from SurePrep.

        Updates status and sends client notification.
        """
        self._log.info(
            "processing_acceptance",
            binder_id=binder_id,
            jurisdiction=jurisdiction,
        )

        try:
            # Find submission
            submission = await self._find_submission_by_binder(
                binder_id, jurisdiction
            )
            if not submission:
                return EFileResult.fail("Submission not found")

            # Update status
            submission = await self._submission_repo.mark_accepted(
                submission_id=submission.id,
                irs_confirmation=confirmation_number,
            )

            # Check if all jurisdictions accepted
            all_submissions = await self._submission_repo.find_by_return(
                submission.tax_return_id
            )
            all_accepted = all(s.is_accepted() for s in all_submissions)

            if all_accepted:
                await self._return_repo.update_status(
                    submission.tax_return_id,
                    "filed",
                )

            # Send client notification
            if self._notification:
                await self._send_acceptance_notification(
                    submission=submission,
                    confirmation_number=confirmation_number,
                )

            if self._audit:
                await self._audit.log_efile_accepted(
                    submission_id=submission.id,
                    confirmation=confirmation_number,
                )

            return EFileResult.ok(submission=submission)

        except Exception as e:
            self._log.error("acceptance_processing_failed", error=str(e))
            return EFileResult.fail(str(e))

    # =========================================================================
    # S10-004: REJECTION HANDLING
    # =========================================================================

    async def process_rejection(
        self,
        binder_id: str,
        jurisdiction: str,
        error_code: str,
        error_message: str,
    ) -> RejectionResult:
        """
        Process e-file rejection from SurePrep.

        Categorizes error, creates rejection record, notifies preparer.
        """
        self._log.info(
            "processing_rejection",
            binder_id=binder_id,
            jurisdiction=jurisdiction,
            error_code=error_code,
        )

        try:
            # Find submission
            submission = await self._find_submission_by_binder(
                binder_id, jurisdiction
            )
            if not submission:
                return RejectionResult.fail("Submission not found")

            # Update submission status
            submission = await self._submission_repo.mark_rejected(
                submission_id=submission.id,
            )

            # Categorize rejection
            category = categorize_rejection(error_code)
            auto_correctable = is_auto_correctable(error_code)

            # Create rejection record
            rejection = await self._rejection_repo.create(
                submission_id=submission.id,
                tax_return_id=submission.tax_return_id,
                error_code=error_code,
                error_message=error_message,
                category=category,
                auto_correctable=auto_correctable,
            )

            # Update return workflow
            await self._return_repo.update_status(
                submission.tax_return_id,
                "rejected",
            )

            # Handle based on category
            if auto_correctable:
                # Attempt auto-correction (retry transmission)
                await self._attempt_auto_correction(submission, rejection)
            else:
                # Notify preparer
                if self._notification:
                    await self._send_rejection_notification(
                        submission=submission,
                        rejection=rejection,
                    )

            # Notify client if info needed
            if category in (RejectionCategory.DATA_MISMATCH, RejectionCategory.IDENTITY):
                if self._notification:
                    await self._send_client_rejection_notification(
                        submission=submission,
                        rejection=rejection,
                    )

            if self._audit:
                await self._audit.log_efile_rejected(
                    submission_id=submission.id,
                    error_code=error_code,
                    category=category.value,
                )

            return RejectionResult.ok(
                rejection=rejection,
                auto_corrected=auto_correctable,
            )

        except Exception as e:
            self._log.error("rejection_processing_failed", error=str(e))
            return RejectionResult.fail(str(e))

    # =========================================================================
    # POLLING (Fallback if webhooks unavailable)
    # =========================================================================

    async def poll_pending_submissions(self) -> int:
        """
        Poll SurePrep for status of pending submissions.

        Called by scheduled job as fallback to webhooks.
        Returns count of submissions updated.
        """
        if not self._sureprep:
            return 0

        pending = await self._submission_repo.find_pending()
        updated = 0

        for submission in pending:
            # Get binder ID from tax return
            # Poll SurePrep for status
            # Process any updates
            pass

        return updated

    # =========================================================================
    # INTERNAL METHODS
    # =========================================================================

    async def _find_submission_by_binder(
        self,
        binder_id: str,
        jurisdiction: str,
    ) -> Optional[EFileSubmission]:
        """Find submission by SurePrep binder and jurisdiction."""
        # Implementation: lookup by sureprep_tracking_id or via return->binder mapping
        pass

    async def _send_acceptance_notification(
        self,
        submission: EFileSubmission,
        confirmation_number: str,
    ) -> None:
        """Send acceptance email to client."""
        if not self._notification:
            return

        # Get client info
        # Send email with confirmation number
        self._log.info(
            "sending_acceptance_notification",
            submission_id=str(submission.id),
        )

    async def _send_rejection_notification(
        self,
        submission: EFileSubmission,
        rejection: EFileRejection,
    ) -> None:
        """Send rejection notification to preparer."""
        if not self._notification:
            return

        self._log.info(
            "sending_preparer_rejection_notification",
            submission_id=str(submission.id),
        )

    async def _send_client_rejection_notification(
        self,
        submission: EFileSubmission,
        rejection: EFileRejection,
    ) -> None:
        """Send rejection notification to client when info needed."""
        if not self._notification:
            return

        self._log.info(
            "sending_client_rejection_notification",
            submission_id=str(submission.id),
        )

    async def _attempt_auto_correction(
        self,
        submission: EFileSubmission,
        rejection: EFileRejection,
    ) -> bool:
        """Attempt auto-correction for technical errors."""
        # For now, just log - would retry transmission
        self._log.info(
            "attempting_auto_correction",
            submission_id=str(submission.id),
            error_code=rejection.error_code,
        )
        return False

Phase 6: Unit Tests (Track C - BACKGROUND after Phase 5)

Can run in parallel with Phase 7-8

New Files: - tests/unit/domain/test_efiling.py (~30 tests) - tests/unit/workflows/filing/__init__.py - tests/unit/workflows/filing/test_efiling_workflow.py (~40 tests)

Test coverage: - Domain entity creation and methods - Rejection categorization - Workflow status transitions - Notification triggers - Auto-correction logic


Phase 7: API Routes and Schemas (after Phase 5)

New File: src/api/schemas/efiling_schemas.py

"""
E-filing API schemas for Tax Practice AI.

Request/response schemas for e-file status tracking.
Per USER_STORIES.md S10-001 through S10-004.
"""

class EFileSubmissionResponse(BaseModel):
    id: UUID
    tax_return_id: UUID
    jurisdiction: str
    state_code: Optional[str]
    status: str
    confirmation_number: Optional[str]
    submitted_at: Optional[datetime]
    accepted_at: Optional[datetime]
    rejected_at: Optional[datetime]


class EFileStatusResponse(BaseModel):
    tax_return_id: UUID
    submissions: List[EFileSubmissionResponse]
    all_accepted: bool
    has_rejections: bool


class EFileRejectionResponse(BaseModel):
    id: UUID
    error_code: str
    error_message: str
    category: str
    auto_correctable: bool
    is_resolved: bool


class MarkReadyRequest(BaseModel):
    jurisdictions: List[str] = Field(
        ...,
        description="Jurisdictions to file: 'federal' and/or state codes",
        example=["federal", "FL"],
    )

New File: src/api/routes/efiling.py

router = APIRouter(prefix="/v1/efiling", tags=["efiling"])

@router.post("/returns/{return_id}/ready")
async def mark_ready_for_filing(return_id: UUID, body: MarkReadyRequest)

@router.get("/returns/{return_id}/status")
async def get_efile_status(return_id: UUID)

@router.get("/returns/{return_id}/rejections")
async def get_rejections(return_id: UUID)

@router.post("/rejections/{rejection_id}/resolve")
async def resolve_rejection(rejection_id: UUID, body: ResolveRequest)

@router.get("/dashboard/pending")
async def get_pending_filings()

@router.get("/dashboard/rejections")
async def get_unresolved_rejections()

Phase 8: Update Main App and Routes

Update: src/api/main.py - Register efiling router Update: src/api/routes/__init__.py - Export efiling module


Phase 9: Integration Test Schema

Update: tests/integration/conftest.py

-- E-file status enum (S10-001)
CREATE TYPE efile_status_enum AS ENUM (
    'ready', 'pending', 'transmitted', 'accepted', 'rejected'
);

-- E-file jurisdiction enum
CREATE TYPE efile_jurisdiction_enum AS ENUM ('federal', 'state');

-- Rejection category enum (S10-004)
CREATE TYPE rejection_category_enum AS ENUM (
    'data_mismatch', 'missing_info', 'duplicate', 'technical', 'identity'
);

-- E-file submissions (S10-001)
CREATE TABLE efile_submission (
    id                      UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tax_return_id           UUID NOT NULL REFERENCES tax_return(id) ON DELETE CASCADE,
    client_id               UUID NOT NULL REFERENCES client(id) ON DELETE CASCADE,
    jurisdiction            efile_jurisdiction_enum NOT NULL,
    state_code              VARCHAR(2),
    status                  efile_status_enum NOT NULL DEFAULT 'ready',
    sureprep_tracking_id    VARCHAR(100),
    ultratax_confirmation   VARCHAR(100),
    irs_confirmation        VARCHAR(100),
    submitted_at            TIMESTAMP WITH TIME ZONE,
    transmitted_at          TIMESTAMP WITH TIME ZONE,
    accepted_at             TIMESTAMP WITH TIME ZONE,
    rejected_at             TIMESTAMP WITH TIME ZONE,
    created_at              TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    UNIQUE(tax_return_id, jurisdiction, state_code)
);
CREATE INDEX idx_efile_submission_return ON efile_submission(tax_return_id);
CREATE INDEX idx_efile_submission_status ON efile_submission(status);
CREATE INDEX idx_efile_submission_pending ON efile_submission(status)
    WHERE status IN ('ready', 'pending', 'transmitted');

-- E-file rejections (S10-004)
CREATE TABLE efile_rejection (
    id                      UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    submission_id           UUID NOT NULL REFERENCES efile_submission(id) ON DELETE CASCADE,
    tax_return_id           UUID NOT NULL REFERENCES tax_return(id) ON DELETE CASCADE,
    error_code              VARCHAR(20) NOT NULL,
    error_message           TEXT NOT NULL,
    category                rejection_category_enum NOT NULL,
    auto_correctable        BOOLEAN NOT NULL DEFAULT FALSE,
    resolution_notes        TEXT,
    resolved_at             TIMESTAMP WITH TIME ZONE,
    resolved_by             UUID REFERENCES staff_user(id),
    created_at              TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_efile_rejection_submission ON efile_rejection(submission_id);
CREATE INDEX idx_efile_rejection_unresolved ON efile_rejection(resolved_at)
    WHERE resolved_at IS NULL;

Phase 10: Code Audit Checklist

After completing S10 implementation, verify:

Pattern Compliance

  • All files have docstrings with story references (S10-00X)
  • Result dataclasses use ok/fail factory methods
  • Constructor dependency injection for all workflows
  • structlog logging with bound context
  • Section headers (=====) separating major code sections
  • Optional AuditService with conditional if self._audit:
  • Error handling: try/except with log.error()

Integration Points

  • SurePrepService e-file status methods added
  • Webhook handler for SurePrep e-file events
  • NotificationWorkflow for acceptance/rejection emails
  • TaxReturnRepository for status updates
  • Audit logging for all status changes

Domain Completeness

  • EFileStatus enum covers all states
  • RejectionCategory covers IRS error types
  • REJECTION_CODE_CATEGORIES mapping is comprehensive
  • State code validation for state filings

Test Coverage

  • Domain entity tests (enums, methods, categorization)
  • Workflow tests (status transitions, notifications)
  • Webhook handler tests (signature verification, event routing)
  • Repository tests (CRUD, status queries)
  • All tests passing before commit

Documentation Sync

  • ARCHITECTURE.md updated with S10 files
  • backlog.md S10 marked complete
  • Presentation updated (55% progress, 44 stories)
  • USER_STORIES.md unchanged (already updated)

Test Coverage Summary

Component Unit Integration E2E
EFilingDomain 30+ - -
EFilingRepository - 15+ -
EFilingWorkflow 40+ 10 3
EFilingAPI 15+ - 5
Webhooks 10+ 5 2

Estimated new tests: ~100


Ready to Implement

Approval requested to proceed with parallel execution: - Track A: Phase 1 (Domain) → Phase 3 (Repository) → Phase 5 (Workflow) - Track B: Phase 2 (SurePrep methods) → Phase 4 (Webhooks) - Track C: Phase 6 (Unit tests in background)

Estimated implementation: 4 stories, ~100 tests