Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5503431
Temporarily removed 'README.md' so that it can not be used as context…
nathjaco1016 Mar 20, 2026
9d5d348
Added specs for pii masking functionality, which is necessary to prop…
nathjaco1016 Mar 20, 2026
898509f
Implemented the transactions feature. Wrote tests, ensured that all t…
nathjaco1016 Mar 20, 2026
fc9841d
Added alert specs, updated todo to reflect what needs to be done.
nathjaco1016 Mar 20, 2026
441b5d5
Implemented alerts functionality. Created and ensured that tests passed.
nathjaco1016 Mar 20, 2026
2fcc062
Wrote the state machine specs, updated the todo.
nathjaco1016 Mar 20, 2026
35bae25
Implemented state machine functionality based on the spec. Wrote test…
nathjaco1016 Mar 20, 2026
540f63d
Updated the todo list to reflect what will be done regarding pii mask…
nathjaco1016 Mar 20, 2026
235a905
Implemented PII masking, wrote tests, and ensured that the test passe…
nathjaco1016 Mar 20, 2026
9cbb01e
Wrote specs for filtering fraud alerts, wrote to TODO.
nathjaco1016 Mar 20, 2026
9681170
Implemented filtering functionality to allow for filtering and sortin…
nathjaco1016 Mar 20, 2026
9ab5d08
Wrote specs for summarization endpoint functionality. Updated the todo.
nathjaco1016 Mar 20, 2026
1fb007b
Implemented the last spec, summary stats, and wrote and executed test…
nathjaco1016 Mar 20, 2026
971af8b
Updated the README.md to include more details that may be helpful.
nathjaco1016 Mar 20, 2026
67c9a96
Remove cached files and claude settings from tracking
nathjaco1016 Mar 20, 2026
56a56d3
Remove committed database file
nathjaco1016 Mar 20, 2026
0c781a0
Modified git ignore
nathjaco1016 Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fraud-alert-service/fraud_alerts.db
__pycache__/
*.pyc
.pytest_cache/
__pycache__/
.claude/
116 changes: 73 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,73 @@
# Candidate Assessment: Spec-Driven Development With Codegen Tools

This assessment evaluates how you use modern code generation tools (for example `5.2-Codex`, `Claude`, `Copilot`, and similar) to design, build, and test a software application using a spec-driven development pattern. You may build a frontend, a backend, or both.

## Goals
- Build a working application with at least one meaningful feature.
- Create a testing framework to validate the application.
- Demonstrate effective use of code generation tools to accelerate delivery.
- Show clear, maintainable engineering practices.

## Deliverables
- Application source code in this repository.
- A test suite and test harness that can be run locally.
- Documentation that explains how to run the app and the tests.

## Scope Options
Pick one:
- Frontend-only application.
- Backend-only application.
- Full-stack application.

Your solution should include at least one real workflow, for example:
- Create and view a resource.
- Search or filter data.
- Persist data in memory or storage.

## Rules
- You must use a code generation tool (for example `5.2-Codex`, `Claude`, or similar). You can use multiple tools.
- You must build the application and a testing framework for it.
- The application and tests must run locally.
- Do not include secrets or credentials in this repository.

## Evaluation Criteria
- Working product: Does the app do what it claims?
- Test coverage: Do tests cover key workflows and edge cases?
- Engineering quality: Clarity, structure, and maintainability.
- Use of codegen: How effectively you used tools to accelerate work.
- Documentation: Clear setup and run instructions.

## What to Submit
- When you are complete, put up a Pull Request against this repository with your changes.
- A short summary of your approach and tools used in your PR submission
- Any additional information or approach that helped you.
# Fraud Alert Validation Service

This project is essentially a REST API for storing fraud alerts, enforcing lifecycle transitions, and properly handling customer data to comply with Ally's PII sensitivity principles.

I put everything in fraud-alert-service to keep the project separated from given spec-driven-dev environment.

To test this, just follow the steps below and test out the interactive docs at `http://localhost:8000/docs`.

## Tech Stack

- **FastAPI** — REST framework with automatic OpenAPI/Swagger docs
- **Pydantic v2** — request validation and response serialization
- **SQLite** — lightweight persistent storage via Python's built-in `sqlite3`
- **pytest** + **httpx** — integration tests using `TestClient` with per-test isolated databases

## Setup

```bash
cd fraud-alert-service
pip install -r requirements.txt
```

## Run

```bash
uvicorn src.main:app --reload
```

The API will be available at `http://localhost:8000`. Interactive docs at `http://localhost:8000/docs`.

## Test

```bash
python -m pytest tests/ -v
```

## API Overview

- POST | `/transactions` | Create a transaction
- GET | `/transactions/{id}` | Get a transaction by ID
- POST | `/alerts` | Create an alert for a transaction
- GET | `/alerts` | List and filter alerts
- GET | `/alerts/summary` | Aggregated stats by status, risk level, and resolution time
- GET | `/alerts/{id}` | Get a single alert
- PATCH | `/alerts/{id}/assign` | Assign an analyst to an alert
- PATCH | `/alerts/{id}/status` | Transition alert status

## PII Masking

`card_id` and `account_id` are masked by default in all responses (e.g. `****1234`). Append `?show_pii=true` to any endpoint to reveal full values.

## Test Coverage

142 tests across 6 test files, each corresponding to a spec:

- `test_transactions.py` | 23 | Transaction creation, validation, field storage
- `test_alerts.py` | 30 | Alert creation, risk level derivation, boundary values
- `test_state_machine.py` | 33 | Status transitions, analyst assignment, audit trail
- `test_pii_masking.py` | 22 | Masking behavior, `show_pii` parameter, edge cases
- `test_filtering.py` | 21 | Filter parameters, combined filters, sort order
- `test_summary_stats.py` | 13 | Counts by status/risk level, resolution time calculation

Each test uses an isolated SQLite database via `tmp_path`, so tests are fully independent and leave no state behind.

## Spec Driven Dev

I have experience with spec driven development, so I essentially just used my standard workflow where I broke down my project into specs and wrote these out myself, used Claude to review these and enhance them if necessary (it's good at finding edge cases and such that I may have missed), then I added actionable TODO tasks before implementation.

## Code Generation Tools Used

I used Claude as my primary code generation tool.


48 changes: 48 additions & 0 deletions SPECS/alerts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Feature Spec: Alerts

## Goal
- Provide endpoints for creating and retrieving fraud alerts that wrap a flagged transaction with risk assessment data and lifecycle tracking.

## Scope
- In: Creating alerts linked to transactions, retrieving alerts by ID, risk score validation, automatic risk level derivation, timestamp generation
- Out: Alert status transitions (see state-machine spec), alert filtering/listing (see filtering spec), summary statistics (see summary-stats spec)

## Requirements
- Each alert must have a unique `id` (UUID, server-generated)
- Required fields on creation: `transaction_id`, `risk_score`
- `transaction_id` must reference an existing transaction
- A transaction can only have one alert (no duplicate alerts for the same transaction)
- `risk_score` must be a float between 0.0 and 1.0 inclusive
- `risk_level` is automatically derived from `risk_score` — never provided by the client:
- `low`: 0.0 <= score < 0.3
- `medium`: 0.3 <= score < 0.6
- `high`: 0.6 <= score < 0.8
- `critical`: 0.8 <= score <= 1.0
- `status` is initialized to `pending` on creation
- `analyst_id` is null on creation
- `contains_pii` is set to `true` by default (since the linked transaction contains card_id and account_id)
- `created_at` is server-generated at creation time
- `updated_at` is server-generated and updated on any modification
- `status_history` is initialized with one entry: `{status: "pending", timestamp: <created_at>, changed_by: "system"}`
- GET response returns the alert with its linked transaction's PII fields masked by default

## Acceptance Criteria
- [x] POST /alerts creates an alert and returns it with generated UUID, pending status, and derived risk_level
- [x] POST /alerts returns 422 for missing transaction_id or risk_score
- [x] POST /alerts returns 404 if transaction_id does not reference an existing transaction
- [x] POST /alerts returns 409 if an alert already exists for the given transaction_id
- [x] POST /alerts returns 422 for risk_score < 0.0
- [x] POST /alerts returns 422 for risk_score > 1.0
- [x] POST /alerts returns 422 for non-numeric risk_score
- [x] Risk level is correctly derived at boundary: score 0.0 → low
- [x] Risk level is correctly derived at boundary: score 0.29 → low
- [x] Risk level is correctly derived at boundary: score 0.3 → medium
- [x] Risk level is correctly derived at boundary: score 0.59 → medium
- [x] Risk level is correctly derived at boundary: score 0.6 → high
- [x] Risk level is correctly derived at boundary: score 0.79 → high
- [x] Risk level is correctly derived at boundary: score 0.8 → critical
- [x] Risk level is correctly derived at boundary: score 1.0 → critical
- [x] Alert is created with status "pending" and a single status_history entry
- [x] GET /alerts/{id} returns the alert with all fields populated
- [x] GET /alerts/{id} returns 404 for nonexistent alert ID
- [x] Client cannot override risk_level, status, or created_at on creation
14 changes: 0 additions & 14 deletions SPECS/feature-template.md

This file was deleted.

48 changes: 48 additions & 0 deletions SPECS/filtering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Feature Spec: Alert Filtering and Listing

## Goal
- Provide a flexible query interface for listing and filtering fraud alerts, enabling analysts to efficiently triage their workload by status, risk level, assignment, and time range.

## Scope
- In: GET /alerts with query parameters for filtering and sorting
- Out: Individual alert retrieval (see alerts spec), summary aggregation (see summary-stats spec)

## Requirements

### Filter Parameters (all optional, combinable)
- `status` — filter by alert status (e.g., `?status=pending`). Accepts a single value.
- `risk_level` — filter by risk level (e.g., `?risk_level=critical`). Accepts a single value.
- `analyst_id` — filter by assigned analyst (e.g., `?analyst_id=analyst_42`). Use `unassigned` as a special value to find alerts with no analyst.
- `created_after` — ISO 8601 datetime, return alerts created on or after this time
- `created_before` — ISO 8601 datetime, return alerts created on or before this time
- When multiple filters are provided, they are combined with AND logic

### Response Format
- Returns an object with `alerts` (array) and `total` (integer)
- Results are sorted by `created_at` descending (newest first)
- Each alert in the array includes all fields (with PII masked by default)
- Empty results return `{"alerts": [], "total": 0}` — not 404

## Acceptance Criteria

### Single Filters
- [x] GET /alerts with no filters returns all alerts
- [x] GET /alerts?status=pending returns only pending alerts
- [x] GET /alerts?risk_level=critical returns only critical alerts
- [x] GET /alerts?analyst_id=analyst_1 returns only alerts assigned to analyst_1
- [x] GET /alerts?analyst_id=unassigned returns only unassigned alerts
- [x] GET /alerts?created_after=<datetime> returns alerts created on or after that time
- [x] GET /alerts?created_before=<datetime> returns alerts created on or before that time

### Combined Filters
- [x] GET /alerts?status=pending&risk_level=high returns alerts matching both conditions
- [x] GET /alerts?status=under_review&analyst_id=analyst_1 returns correct intersection
- [x] GET /alerts?created_after=<t1>&created_before=<t2> returns alerts within the date range

### Edge Cases
- [x] Invalid status value returns 422
- [x] Invalid risk_level value returns 422
- [x] Invalid datetime format for created_after or created_before returns 422
- [x] Filters that match zero alerts return {"alerts": [], "total": 0} with 200 status
- [x] Date range where created_after > created_before returns empty results (not an error)
- [x] Results are sorted by created_at descending by default
61 changes: 61 additions & 0 deletions SPECS/pii-masking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Feature Spec: PII Masking

## Goal
- Ensure PII-sensitive fields are masked by default in all API responses, reflecting Ally Financial's commitment to responsible data handling. Ally contributed a PII Masking module to LangChain's open-source ecosystem specifically because customer interactions always involve PII — this feature demonstrates awareness of that principle.

## Scope
- In: Masking card_id and account_id in API responses, an authorized access flag to reveal full values, consistent masking across all endpoints that return transaction data
- Out: Encryption at rest, role-based access control, authentication/authorization (this is a demonstration of the concept, not a production auth system)

## Requirements

### Masked Fields
- `card_id` and `account_id` are the PII-sensitive fields
- When masked, only the last 4 characters are visible, prefixed with asterisks: `****1234`
- If the value is 4 characters or fewer, mask the entire value: `****`
- Masking is applied at the API response layer — storage retains the full value

### Default Behavior
- All API responses that include transaction data mask PII fields by default
- This applies to:
- GET /transactions/{id}
- POST /transactions (response body)
- GET /alerts/{id} (embedded transaction data)
- GET /alerts (embedded transaction data in each alert)

### Authorized Access
- A query parameter `show_pii=true` reveals the unmasked values
- This simulates an authorized analyst session — in production this would be gated by role-based auth
- When `show_pii=true`, card_id and account_id are returned in full
- When `show_pii` is absent or `false`, fields are masked

### contains_pii Flag
- Each alert has a `contains_pii` boolean
- Set to `true` by default since linked transactions always contain card_id and account_id
- This flag is informational — it does not control masking behavior (masking always applies regardless)

## Acceptance Criteria

### Default Masking
- [x] GET /transactions/{id} returns card_id and account_id masked (e.g., "****5678")
- [x] POST /transactions response body returns masked PII fields
- [x] GET /alerts/{id} returns embedded transaction data with masked PII
- [x] GET /alerts list returns all embedded transaction data with masked PII
- [x] Masking shows last 4 characters: "1234567890" → "****7890"
- [x] Values with 4 or fewer characters are fully masked: "1234" → "****"

### Authorized Access
- [x] GET /transactions/{id}?show_pii=true returns full card_id and account_id
- [x] GET /alerts/{id}?show_pii=true returns full PII in embedded transaction
- [x] GET /alerts?show_pii=true returns full PII across all results
- [x] show_pii=false behaves the same as omitting the parameter (masked)

### Consistency
- [x] PII masking is applied consistently across all endpoints — no endpoint leaks unmasked data by default
- [x] Masking does not affect stored data — full values are preserved in the database
- [x] The contains_pii flag on alerts is set to true by default

### Edge Cases
- [x] Empty string card_id or account_id is masked as "****"
- [x] Very long PII values are correctly masked (only last 4 shown)
- [x] show_pii parameter with non-boolean values (e.g., "yes", "1") is handled gracefully (treat as false or return 422)
73 changes: 73 additions & 0 deletions SPECS/state-machine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Feature Spec: Alert Lifecycle State Machine

## Goal
- Enforce a strict state machine governing how fraud alerts move through the analyst review pipeline, with full audit trail for every transition. This is the core business logic of the service.

## Scope
- In: Status transitions via PATCH endpoint, analyst assignment, transition validation, status_history tracking, business rules around assignment and resolution
- Out: Alert creation (see alerts spec), filtering by status (see filtering spec)

## Requirements

### Status Transitions
- Valid transitions:
- `pending` → `under_review` (requires analyst_id to be assigned first)
- `under_review` → `confirmed_fraud`
- `under_review` → `false_positive`
- `under_review` → `escalated`
- All other transitions are invalid and must be rejected with 409 Conflict
- Terminal states: `confirmed_fraud`, `false_positive` (no further transitions allowed)
- `escalated` is also terminal for the scope of this service

### Analyst Assignment
- PATCH /alerts/{id}/assign accepts `analyst_id` (string)
- Assignment is only allowed when status is `pending` or `under_review`
- Cannot assign to an alert in a terminal state (confirmed_fraud, false_positive, escalated)
- Assigning updates `updated_at`
- Re-assignment is allowed (analyst_id can be changed while alert is pending or under_review)

### Transition Rules
- PATCH /alerts/{id}/status accepts `status` (the target status) and `changed_by` (string identifier)
- Transitioning to `under_review` requires that `analyst_id` is not null (someone must own it)
- `changed_by` is recorded in the status_history entry for the transition
- Each transition appends to `status_history`: `{status: <new_status>, timestamp: <now>, changed_by: <value>}`
- `updated_at` is refreshed on every transition

### Audit Trail
- `status_history` is append-only — entries are never modified or deleted
- The full history is returned on GET /alerts/{id}
- History entries are ordered chronologically

## Acceptance Criteria

### Valid Transitions
- [x] pending → under_review succeeds when analyst_id is assigned
- [x] under_review → confirmed_fraud succeeds
- [x] under_review → false_positive succeeds
- [x] under_review → escalated succeeds
- [x] Each successful transition appends to status_history with correct status, timestamp, and changed_by

### Invalid Transitions
- [x] pending → confirmed_fraud returns 409
- [x] pending → false_positive returns 409
- [x] pending → escalated returns 409
- [x] under_review → pending returns 409
- [x] confirmed_fraud → any status returns 409
- [x] false_positive → any status returns 409
- [x] escalated → any status returns 409
- [x] pending → under_review without analyst_id assigned returns 409 (or 422)

### Analyst Assignment
- [x] Assigning analyst to a pending alert succeeds
- [x] Assigning analyst to an under_review alert succeeds (re-assignment)
- [x] Assigning analyst to a confirmed_fraud alert returns 409
- [x] Assigning analyst to a false_positive alert returns 409
- [x] Assigning analyst to an escalated alert returns 409
- [x] Assignment updates the updated_at timestamp

### Audit Trail
- [x] A newly created alert has exactly one status_history entry (pending)
- [x] After transitioning pending → under_review → confirmed_fraud, status_history has 3 entries
- [x] Status history entries are in chronological order
- [x] Each entry contains the correct changed_by value from the request
- [x] Status history is immutable — previous entries are unchanged after new transitions
Loading