Story-1117: Client Email Unique Per Firm
This walkthrough documents the backend rule that a real client email can belong to only one client inside a firm, while missing email remains allowed for any number of clients.
Why This Exists
Without this rule, two clients in the same firm can share the same email address. Downstream systems then cannot reliably tell which client is meant by that email.
Allowed: Client A no email Client B no email Client C client@example.com Blocked: Client C client@example.com Client D CLIENT@example.com
Request Path
POST/PATCH /app/clients
|
v
router_clients.py
|
v
ClientService.create_client/update_client
|
v
repository.find_by_email
|
v
PramaanValidationError: pramaan.client.email_in_use
The router stays thin. The service owns the business rule because the rule needs current firm data. The shared error handler turns the service exception into the API response.
Database Guard
Migration 0092_client_email_unique_per_firm adds a partial
unique index on (firm_id, lower(email)) only when
email IS NOT NULL AND email <> ''.
firm_idkeeps the rule tenant-scoped.lower(email)makes the rule case-insensitive.- The partial predicate keeps blank email allowed.
API Contract
Create and update document the typed validation error:
pramaan.client.email_in_use. The response includes
details: {"field": "email"} so the app can attach the
message to the email field.
Test Coverage
- Two clients with blank email are accepted.
- A second client with the same non-blank email is rejected.
- A client can re-save its own existing email.
- A case-insensitive collision is rejected.