Skip to content

[BP] Add SSN prefix search to client profile lookup#1996

Open
jaeeungracelee wants to merge 9 commits intomainfrom
bp/port-over-profile-lookup-system
Open

[BP] Add SSN prefix search to client profile lookup#1996
jaeeungracelee wants to merge 9 commits intomainfrom
bp/port-over-profile-lookup-system

Conversation

@jaeeungracelee
Copy link
Copy Markdown
Contributor

@jaeeungracelee jaeeungracelee commented Mar 26, 2026

Summary

  • Adds ssn field to ClientProfile model
  • Updates ClientProfileFilter.search to detect numeric input and search by SSN/California ID prefix instead of name fields
  • Existing name prefix search and permission checks (view_clientprofile) are unchanged

Summary by Sourcery

Add SSN support to client profile search, enabling numeric queries to match SSN or California ID prefixes while preserving existing name-based search behavior.

New Features:

  • Introduce an indexed SSN field on the ClientProfile model for storing 9-digit social security numbers.
  • Enable client profile search to treat purely numeric input as an SSN or California ID prefix search instead of a name search.

Tests:

  • Add GraphQL query tests covering numeric search behavior for SSN prefixes, exact SSN matches, and non-matching or alphanumeric input cases.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 26, 2026

Reviewer's Guide

Adds an indexed SSN field to ClientProfile and updates the client profile search logic to use numeric input for SSN/California ID prefix searches while keeping existing name-based searches and permissions intact, covered by new query tests.

Class diagram for updated ClientProfile model

classDiagram
    class ClientProfile {
        CharField nickname
        ImageField profile_photo
        TextChoicesField race
        CharField ssn
        TextChoicesField veteran_status
    }
Loading

Flow diagram for updated ClientProfileFilter.search logic

flowchart TD
    A["Start search(value, queryset)"] --> B["Split value into search_terms"]
    B --> C{search_terms is empty?}
    C -->|Yes| D["Return queryset, empty Q"]
    C -->|No| E["Check all terms: term.isdigit()"]
    E --> F{is_numeric_input?}
    F -->|Yes| G["Set searchable_fields = [california_id, ssn]"]
    F -->|No| H["Set searchable_fields = [california_id, first_name, last_name, middle_name, nickname]"]
    G --> I["Build direct_queries using searchable_fields"]
    H --> I["Build direct_queries using searchable_fields"]
    I --> J["Combine queries and filter queryset"]
    J --> K["Return filtered queryset, built Q"]
Loading

File-Level Changes

Change Details Files
Add SSN field to ClientProfile with DB migration and index for lookup performance.
  • Introduce ssn CharField on ClientProfile with max_length=9, nullable/blank, and db_index enabled
  • Create corresponding Django migration to add the ssn column and index in the database
apps/betterangels-backend/clients/models.py
apps/betterangels-backend/clients/migrations/0032_clientprofile_ssn.py
Adjust client profile text search to route purely numeric search terms to SSN/California ID prefix matching instead of name fields.
  • Early-return when search string is empty, avoiding unnecessary query building
  • Detect whether all search terms are numeric via str.isdigit()
  • For numeric input, restrict searchable fields to california_id and ssn for prefix matching
  • For non-numeric input, keep existing california_id and name-based fields (first/middle/last/nickname) for search
apps/betterangels-backend/clients/types.py
Add tests validating numeric prefix search behavior for SSN while preserving existing name search behavior.
  • Add parameterized test that logs in a case manager, sets SSNs on existing fixtures, and queries clientProfiles with various numeric and mixed search values
  • Assert totalCount matches expectations for short/long SSN prefixes, exact SSN, no matches, and alphanumeric search falling back to name search
apps/betterangels-backend/clients/tests/test_queries.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The numeric search path currently relies on str.isdigit(), so inputs with common SSN formatting like 123-45-6789 or spaces will fall back to name search; consider normalizing the search string (e.g., stripping non-digits) before deciding whether to treat it as numeric and before querying ssn.
  • The new ssn field is a plain indexed CharField; if SSNs are considered sensitive in this context, consider adding at-rest protection (e.g., encryption field type or hashing with a separate lookup key) or at least server-side validation to constrain the allowed format and length.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The numeric search path currently relies on `str.isdigit()`, so inputs with common SSN formatting like `123-45-6789` or spaces will fall back to name search; consider normalizing the search string (e.g., stripping non-digits) before deciding whether to treat it as numeric and before querying `ssn`.
- The new `ssn` field is a plain indexed `CharField`; if SSNs are considered sensitive in this context, consider adding at-rest protection (e.g., encryption field type or hashing with a separate lookup key) or at least server-side validation to constrain the allowed format and length.

## Individual Comments

### Comment 1
<location path="apps/betterangels-backend/clients/models.py" line_range="151" />
<code_context>
     nickname = models.CharField(max_length=50, blank=True, null=True)
     profile_photo = models.ImageField(upload_to=get_client_profile_photo_file_path, blank=True, null=True)
     race = TextChoicesField(choices_enum=RaceEnum, blank=True, null=True)
+    ssn = models.CharField(max_length=9, blank=True, null=True, db_index=True)
     veteran_status = TextChoicesField(choices_enum=VeteranStatusEnum, blank=True, null=True)

</code_context>
<issue_to_address>
**suggestion (bug_risk):** SSN is stored as a free-form CharField with only a max_length constraint, which could allow invalid or malformed values.

`CharField(max_length=9)` alone will accept any <=9-character string (including non-digits, too-short values, or obviously invalid patterns). If other code treats this as a real SSN (for matching/searching), that can introduce data-quality bugs. Please add a validator to enforce 9 numeric digits and, if relevant, reject clearly invalid/test values so the DB constraint aligns with the intended semantics.

Suggested implementation:

```python
    race = TextChoicesField(choices_enum=RaceEnum, blank=True, null=True)
    ssn = models.CharField(
        max_length=9,
        blank=True,
        null=True,
        db_index=True,
        validators=[
            RegexValidator(
                regex=r'^\d{9}$',
                message=_('SSN must be exactly 9 numeric digits.'),
                code='invalid_ssn_format',
            ),
            reject_obviously_invalid_ssn,
        ],
    )
    veteran_status = TextChoicesField(choices_enum=VeteranStatusEnum, blank=True, null=True)

```

To complete this change, you also need to:

1. Add the necessary imports near the top of `apps/betterangels-backend/clients/models.py`:
   - `from django.core.exceptions import ValidationError`
   - `from django.core.validators import RegexValidator`
   - `from django.utils.translation import gettext_lazy as _`

2. Define the `reject_obviously_invalid_ssn` validator function in the same file (at module level), for example:

   ```python
   def reject_obviously_invalid_ssn(value: str) -> None:
       if not value:
           return

       # Normalize just in case future changes allow formatting characters
       normalized = ''.join(ch for ch in value if ch.isdigit())

       if normalized in {
           '000000000',
           '123456789',
           '111111111',
           '999999999',
       }:
           raise ValidationError(
               _('This looks like a placeholder/test SSN and is not allowed.'),
               code='invalid_ssn_value',
           )
   ```

3. Ensure any migrations are generated and applied so the updated field constraints (validators) are reflected in your schema and admin/forms.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@jaeeungracelee jaeeungracelee changed the title Add SSN prefix search to client profile lookup [BP] Add SSN prefix search to client profile lookup Mar 26, 2026
nickname = models.CharField(max_length=50, blank=True, null=True)
profile_photo = models.ImageField(upload_to=get_client_profile_photo_file_path, blank=True, null=True)
race = TextChoicesField(choices_enum=RaceEnum, blank=True, null=True)
ssn = models.CharField(
Copy link
Copy Markdown
Contributor

@frank731 frank731 Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there exists a SSN field in the HMIS instead in field '"ssn1",
"ssn2",
"ssn3",' sorry for not mentioning this in tickets but would like to test that you can search off an existing client that has one of these attached. Can look at HmisClientProfileBaseType

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants