Skip to content

Sequence 2: Client Identity & Onboarding

Implementation guide with patterns carried forward from Sequence 1


Overview

Attribute Value
Sequence 2 of 18
Stories S2-001 through S2-006
Dependencies Sequence 1 (Foundation) - Complete
Status In Progress

Stories in This Sequence

Story Title Requirements Status
S2-001 Client Registration INT-001, INT-003, INT-004 Done
S2-002 Returning Client Authentication (Tier 1) INT-002, INT-010, INT-011, SEC-003-005 Done
S2-003 New Client Identity Verification (Tier 2) INT-012-014, IDV-001-005 Done
S2-004 High-Risk Identity Verification (Tier 3) INT-015-017, GWS-001 Not Started
S2-005 Multi-Contact Household Support INT-005 Not Started
S2-006 Conflict of Interest Check PRE-001 to PRE-004 Not Started
S2-007 Staff Client Creation INT-040-045 Not Started
S2-008 Existing Client Portal Activation INT-030-032 Not Started
S2-009 Staff Portal Invitation INT-033, INT-045 Not Started
S2-010 Staff Document Upload DOC-030-037 Not Started

Patterns Carried Forward from Sequence 1

These patterns are established in ARCHITECTURE.md and PATTERNS.md and MUST be applied:

Error Handling

  • All database operations wrapped in try/catch
  • Custom exceptions (ValidationError, ConflictError, NotFoundError) - never raw HTTPException
  • Errors logged with context before raising (no PII in logs)
  • Database error mapping per PATTERNS.md Section 1.3

Logging

  • structlog with correlation ID in all logs
  • Repository layer logs at DEBUG level
  • Workflow layer logs at INFO level
  • Never log SSN, passwords, tokens, or PII

Repository Pattern

  • BaseRepository with error handling
  • _row_to_entity / _entity_to_row methods
  • Soft delete pattern (deleted_at timestamp)
  • All queries use parameterized statements

Testing

  • Test pyramid: 80% unit, 15% integration, 5% e2e
  • Integration tests use Docker PostgreSQL
  • Mock external services in unit tests
  • Test both success and error paths

API Layer

  • Pydantic schemas for validation
  • FastAPI dependency injection
  • Request/response logging middleware
  • Rate limiting on public endpoints

S2-001: Client Registration

Requirements Traced

Req ID Requirement How Addressed
INT-001 New client self-registration via portal Web registration form with validation
INT-003 Email validation required Verification link sent, must click before active
INT-004 Unique account number generation Format: YYYYMMDD-XXXX, atomic sequence

Acceptance Criteria

  • Registration form captures: name, email, phone, address
  • Email validation with verification link
  • Unique account number generated (format: YYYYMMDD-XXXX)
  • Duplicate detection based on SSN-4 + DOB + Name
  • Terms of service acceptance recorded
  • Registration confirmation email sent

Database Changes

New Tables/Columns:

-- Add to client table (if not present)
ALTER TABLE client ADD COLUMN IF NOT EXISTS
    email_verified_at TIMESTAMP;

ALTER TABLE client ADD COLUMN IF NOT EXISTS
    terms_accepted_at TIMESTAMP;

ALTER TABLE client ADD COLUMN IF NOT EXISTS
    terms_version VARCHAR(20);

-- Email verification tokens
CREATE TABLE email_verification (
    id                  UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    client_id           UUID NOT NULL REFERENCES client(id) ON DELETE CASCADE,
    token               VARCHAR(64) NOT NULL UNIQUE,
    expires_at          TIMESTAMP NOT NULL,
    verified_at         TIMESTAMP,
    created_at          TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_email_verification_token ON email_verification(token);
CREATE INDEX idx_email_verification_client ON email_verification(client_id);

Implementation Checklist

Domain Layer: - [ ] src/domain/client.py - Add email_verified_at, terms_accepted_at fields (if not present) - [ ] src/domain/registration.py - RegistrationRequest, EmailVerification entities

API Layer: - [ ] src/api/schemas/registration_schemas.py - Pydantic models - ClientRegistrationRequest (name, email, phone, address, terms_accepted, ssn_last4, dob) - ClientRegistrationResponse (id, account_number, email_verification_sent) - EmailVerificationRequest (token) - [ ] src/api/routes/registration.py - Endpoints - POST /v1/register - Create new client registration - POST /v1/register/verify-email - Verify email with token

Workflow Layer: - [ ] src/workflows/intake/registration.py - RegistrationWorkflow class - register() - Main registration flow - verify_email() - Email verification flow - check_duplicate() - Duplicate detection - generate_account_number() - Atomic account number generation

Repository Layer: - [ ] src/repositories/email_verification_repository.py - Token management - [ ] Update src/repositories/client_repository.py - Add email verification fields

Services Layer: - [ ] src/services/email_service.py - Send verification emails (stub for now)

Tests: - [ ] tests/unit/workflows/test_registration.py - [ ] tests/integration/repositories/test_email_verification_repository.py - [ ] tests/e2e/test_registration_api.py

Implementation Details

Account Number Generation

Format: YYYYMMDD-XXXX where XXXX is a sequential number per day.

# Atomic generation using PostgreSQL sequence per day
async def generate_account_number(self) -> str:
    """
    Generate unique account number.
    Format: YYYYMMDD-XXXX
    Uses atomic counter to prevent duplicates.
    """
    today = datetime.now().strftime("%Y%m%d")

    # Atomic increment - INSERT ON CONFLICT handles race conditions
    result = await self._aurora.execute_returning(
        """
        INSERT INTO account_number_seq (date_prefix, next_val)
        VALUES ($1, 1)
        ON CONFLICT (date_prefix) DO UPDATE
        SET next_val = account_number_seq.next_val + 1
        RETURNING next_val
        """,
        today
    )

    seq = result["next_val"]
    return f"{today}-{seq:04d}"

Duplicate Detection

Check for potential duplicates before creating:

async def check_duplicate(
    self,
    ssn_last4: str,
    date_of_birth: date,
    name: str,
) -> Optional[Dict]:
    """
    Check for potential duplicate clients.

    Returns match info if found, None otherwise.
    Uses fuzzy name matching + exact SSN-4/DOB.
    """
    # Exact match on SSN-4 + DOB
    existing = await self._aurora.execute_query(
        """
        SELECT id, name, email, account_number
        FROM client
        WHERE ssn_last4 = $1
          AND date_of_birth = $2
          AND deleted_at IS NULL
        """,
        ssn_last4, date_of_birth
    )

    if existing:
        # Check name similarity
        for row in existing:
            if self._names_similar(name, row["name"]):
                return {
                    "potential_match": True,
                    "existing_account": row["account_number"],
                    "reason": "SSN-4 + DOB + Name match"
                }

    return None

Email Verification Flow

async def send_verification_email(self, client_id: UUID, email: str) -> None:
    """
    Generate verification token and send email.
    Token expires in 24 hours.
    """
    token = secrets.token_urlsafe(32)
    expires_at = datetime.utcnow() + timedelta(hours=24)

    await self._verification_repo.create(
        client_id=client_id,
        token=token,
        expires_at=expires_at,
    )

    # TODO: Implement EmailService
    await self._email_service.send_verification(
        to=email,
        verification_url=f"{self._config.portal_url}/verify?token={token}",
    )

Transaction Boundaries

Registration is atomic - all writes succeed or all fail:

async def register(self, data: ClientRegistrationRequest) -> ClientRegistrationResponse:
    """
    Register a new client.

    Transaction scope: client + verification token + audit log.
    Email sent AFTER commit (side effect).
    """
    # Check for duplicates (outside transaction - read-only)
    duplicate = await self.check_duplicate(
        data.ssn_last4, data.date_of_birth, data.name
    )
    if duplicate:
        raise ConflictError(
            "Potential duplicate client",
            details=duplicate
        )

    # === TRANSACTION START ===
    async with self._aurora.transaction() as conn:
        # Generate account number (atomic)
        account_number = await self.generate_account_number()

        # Create client
        client = await self._client_repo.create(
            Client(
                account_number=account_number,
                name=data.name,
                email=data.email,
                phone=data.phone,
                address=data.address,
                ssn_last4=data.ssn_last4,
                date_of_birth=data.date_of_birth,
                terms_accepted_at=datetime.utcnow(),
                terms_version=data.terms_version,
                status=ClientStatus.PENDING_VERIFICATION,
            ),
            conn=conn
        )

        # Create verification token
        token = secrets.token_urlsafe(32)
        await self._verification_repo.create(
            client_id=client.id,
            token=token,
            expires_at=datetime.utcnow() + timedelta(hours=24),
            conn=conn
        )

        # Audit log
        await self._audit.log(AuditEvent(
            action=AuditAction.CREATE,
            resource_type="client",
            resource_id=client.id,
            actor_type=ActorType.SYSTEM,
            outcome=AuditOutcome.SUCCESS,
        ))
    # === TRANSACTION COMMIT ===

    # Side effect - send email AFTER commit
    await self._email_service.send_verification(
        to=data.email,
        token=token,
    )

    return ClientRegistrationResponse(
        id=client.id,
        account_number=account_number,
        email_verification_sent=True,
    )

Error Handling

Scenario Exception User Message
Duplicate detected ConflictError "An account may already exist with these details"
Invalid email format ValidationError "Please enter a valid email address"
Email already registered ConflictError "This email is already registered"
Verification token expired ValidationError "Verification link has expired"
Verification token invalid NotFoundError "Invalid verification link"

S2-002: Returning Client Authentication (Tier 1)

Requirements Traced

Req ID Requirement How Addressed
INT-002 Returning client login Match on Name + SSN-4 + DOB + Prior Year AGI
INT-010 Magic link fallback Send login link if partial match
INT-011 Security question Additional verification factor
SEC-003 Session timeout JWT with expiration per role
SEC-004 Failed login tracking Audit log + lockout tracking
SEC-005 Account lockout Lock after N failed attempts

Acceptance Criteria

  • Match on Name + SSN-4 + DOB + Prior Year AGI
  • Magic link fallback if partial match
  • Security question as additional factor
  • Session created with appropriate timeout
  • Failed attempts tracked and logged
  • Account lockout after configurable attempts

Implementation Checklist

  • src/domain/authentication.py - LoginAttempt, MagicLink entities
  • src/api/schemas/auth_schemas.py - Pydantic models for client auth
  • src/api/routes/client_auth.py - Client authentication endpoints
  • src/workflows/intake/client_authentication.py - Authentication workflow
  • src/repositories/login_attempt_repository.py - Track attempts
  • src/repositories/magic_link_repository.py - Magic link tokens
  • Tests for success and failure paths

S2-003: New Client Identity Verification (Tier 2)

Requirements Traced

Req ID Requirement How Addressed
INT-012 Email verification Verification link (from S2-001)
INT-013 Phone verification SMS or voice call code
INT-014 Government ID upload Document upload interface
IDV-001 Identity verification service Persona API integration
IDV-002 ID analysis Persona returns confidence score
IDV-004 Staff review queue Scores 70-89% need manual review
IDV-005 Auto-approve threshold Scores >= 90% auto-approved

Acceptance Criteria

  • Email verification (click link)
  • Phone verification (SMS or voice call option)
  • Government ID upload interface
  • Persona API integration for ID analysis
  • Confidence score stored with verification record
  • Staff review queue for 70-89% scores
  • Auto-approve for scores >= 90%

Implementation Checklist

  • src/services/persona_service.py - Persona API integration
  • src/services/sms_service.py - Twilio SMS/voice
  • src/domain/identity_verification.py - VerificationTier, IDDocument
  • src/workflows/intake/identity_verification.py - Verification workflow
  • src/api/routes/verification.py - Verification endpoints
  • Staff review queue UI (future)
  • Tests with mocked Persona responses

S2-004: High-Risk Identity Verification (Tier 3)

Details to be expanded when S2-003 is complete


S2-005: Multi-Contact Household Support

Details to be expanded when S2-001 is complete


S2-006: Conflict of Interest Check

Details to be expanded when S2-001 is complete


S2-007: Staff Client Creation

Requirements Traced

Req ID Requirement How Addressed
INT-040 Staff create client via admin interface Admin UI separate from self-registration
INT-041 Generate account number for staff-created clients Same atomic generation as self-registration
INT-042 Mark staff-created clients as ACTIVE Skip PENDING_VERIFICATION status
INT-043 Log staff member who created client Audit log with staff user ID
INT-044 Support partial client information Minimum: name + (email OR phone)
INT-045 Staff can send portal invitation Optional email with activation link

Acceptance Criteria

  • Admin UI for client creation (role-restricted)
  • Minimum required fields: name + (email OR phone)
  • Auto-generate account number on creation
  • Client status set to ACTIVE (no verification needed)
  • Creating staff member logged for audit
  • Option to send portal invitation email
  • Integration with existing ClientRepository

Implementation Notes

Key difference from self-registration: - Status = ACTIVE immediately (trusted source) - No email verification required - Staff user ID recorded in audit log - Can skip SSN-4, DOB if not available


S2-008: Existing Client Portal Activation

Requirements Traced

Req ID Requirement How Addressed
INT-030 Allow existing clients to activate portal without duplicate Match to existing record
INT-031 Link portal activation to existing record via verification Tier 1 or 2 auth required
INT-032 Preserve existing account number No new account number generated

Acceptance Criteria

  • Client enters identifying info (name, email, SSN-4, DOB)
  • System matches to existing client record
  • If match found, trigger identity verification
  • On successful verification, set portal_activated_at
  • Preserve existing account number
  • No duplicate client record created
  • Audit log records portal activation event

Implementation Notes

Flow: 1. Client attempts to "register" 2. System detects existing record (SSN-4 + DOB + Name match) 3. Instead of ConflictError, redirect to "Activate Your Account" flow 4. Client completes Tier 1 (returning) or Tier 2 (new to portal) verification 5. On success: set portal_activated_at, create portal credentials 6. Client can now log in

Database changes:

ALTER TABLE client ADD COLUMN IF NOT EXISTS
    portal_activated_at TIMESTAMP;


S2-009: Staff Portal Invitation

Requirements Traced

Req ID Requirement How Addressed
INT-033 Staff send portal invitation emails Admin UI action on client record
INT-045 Optional invitation at client creation Checkbox during staff creation

Acceptance Criteria

  • Select client from admin UI
  • Generate secure invitation link (24-hour expiry)
  • Email contains: firm name, client name, activation link
  • Link pre-fills known client info
  • Client completes identity verification on click
  • Track invitation status (sent, clicked, activated)
  • Resend capability for expired links

Implementation Notes

Invitation table:

CREATE TABLE portal_invitation (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    client_id       UUID NOT NULL REFERENCES client(id),
    token           VARCHAR(64) NOT NULL UNIQUE,
    sent_by         UUID NOT NULL REFERENCES users(id),
    sent_at         TIMESTAMP NOT NULL DEFAULT NOW(),
    expires_at      TIMESTAMP NOT NULL,
    clicked_at      TIMESTAMP,
    activated_at    TIMESTAMP
);


S2-010: Staff Document Upload

Requirements Traced

Req ID Requirement How Addressed
DOC-030 Staff upload on behalf of clients Admin UI with client selector
DOC-031 Upload entire folder for a client Folder upload support
DOC-032 Auto-classify by filename patterns Reuse migration classification logic
DOC-033 Assign tax year to uploads Default current year, dropdown to override
DOC-034 Log uploading staff member Audit log with staff user ID
DOC-035 Drag-and-drop folder upload HTML5 directory upload
DOC-036 Upload summary with counts Show classified counts by document type
DOC-037 Flag unclassified documents Queue for manual classification

Acceptance Criteria

  • Select client from admin UI (search by name/account number)
  • Upload single file or entire folder
  • Auto-classify by filename (W-2, 1099, etc.)
  • Assign tax year (default current, selectable dropdown)
  • Malware scan all uploads (reuse S3Service)
  • Log uploading staff member for audit
  • Generate summary: X documents, Y classified, Z need review
  • Unclassified documents queued for manual review

Implementation Notes

Reuse from Migration: - Document classification patterns (MIG-040 series) - Filename parsing logic - S3Service upload with malware scanning

API Endpoints:

POST /v1/admin/clients/{client_id}/documents
  - Multipart form upload
  - Query param: tax_year (optional, default current)
  - Returns: upload summary

GET /v1/admin/documents/unclassified
  - List documents needing manual classification
  - Filter by client, date range

PATCH /v1/admin/documents/{document_id}/classify
  - Manual classification assignment

Use Cases: 1. Walk-in client with receipts → staff scans to folder → uploads folder 2. Mailed documents → staff scans → uploads to client 3. Faxed documents → staff saves to folder → uploads 4. Legacy documents during onboarding


Files to Create/Update

New Files Required

File Purpose Story
src/domain/registration.py Registration entities S2-001
src/api/schemas/registration_schemas.py Pydantic models S2-001
src/api/routes/registration.py Registration endpoints S2-001
src/workflows/intake/registration.py Registration workflow S2-001
src/repositories/email_verification_repository.py Token management S2-001
src/services/email_service.py Send emails S2-001
src/api/routes/admin/clients.py Staff client creation S2-007
src/api/schemas/admin_schemas.py Admin UI Pydantic models S2-007
src/workflows/intake/portal_activation.py Portal activation workflow S2-008
src/repositories/portal_invitation_repository.py Invitation token management S2-009
tests/unit/workflows/test_registration.py Unit tests S2-001
tests/unit/workflows/test_portal_activation.py Unit tests S2-008
tests/integration/repositories/test_email_verification_repository.py Integration tests S2-001
tests/integration/repositories/test_portal_invitation_repository.py Integration tests S2-009
tests/e2e/test_registration_api.py E2E tests S2-001
tests/e2e/test_admin_clients_api.py E2E tests S2-007
src/api/routes/admin/documents.py Staff document upload endpoints S2-010
src/workflows/documents/classification.py Document classification logic S2-010
tests/unit/workflows/test_classification.py Classification unit tests S2-010
tests/e2e/test_admin_documents_api.py E2E tests S2-010

Existing Files to Update

File Update Required Story
src/domain/client.py Add email_verified_at, terms_accepted_at, portal_activated_at S2-001, S2-008
src/repositories/client_repository.py Add verification status methods, portal activation S2-001, S2-008
tests/integration/conftest.py Add email_verification, portal_invitation table schemas S2-001, S2-009
src/api/main.py Register new routes (registration, admin) S2-001, S2-007
DATABASE_SCHEMA.sql Add email_verification, portal_invitation tables S2-001, S2-009

Verification Checklist

Before marking Sequence 2 complete:

  • All database operations wrapped in try/catch
  • Errors logged with context (operation, parameters, error type)
  • Custom exceptions used throughout (not raw HTTPException)
  • Correlation ID flows through all logs
  • No PII in log messages (SSN, DOB, etc.)
  • Unit tests for all workflows
  • Integration tests for all repositories
  • E2E tests for registration API
  • Email verification flow works end-to-end
  • Duplicate detection prevents duplicate accounts
  • Account numbers are unique and sequential