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:
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