Case Management
What this module does
A Case is the central object that everything in Orchid orbits around. It represents one complete fertility journey — from the moment a match is accepted through medical treatment, legal contracts, and delivery. Every document, note, task, contract, email, and medical event belongs to a Case.
This module covers the Case model and everything directly attached to it: parties (who is involved), documents, notes, logs, medical cycles (transfers, retrievals, pregnancies, deliveries), and milestones.
Business value
Case management is the operational core of a fertility agency. Staff need to know what is happening in each case at a glance, what is overdue, who is involved, and where things stand medically and legally. Orchid provides this visibility and serves as the single source of truth for every active journey.
Key files
Directoryorchid/
Directorymodels/
- case.py 136 KB — Case, CaseDocument, CaseEmail, CaseLog, CaseNote, CaseParty, Family, GCCase, CaseClosingDetails, JourneyPhoto
- cycle.py 59 KB — Transfer, Retrieval, SDRetrieval, Lab, Pregnancy, Delivery, RetrievalBatch
- milestone.py CaseMilestone, MileStone, MileStoneBase
- activity.py ActivityFeed, ActivityNote, ChangeLog
Directoryviews/
- admin_case_management.py 48 KB — case management UI
- admin.py 62 KB — main admin dashboard and case views
- admin_journey.py case journey view
Directoryapi/
- admin.py 379 KB — includes the vast majority of case API operations
Data model
erDiagram Case { int id string case_type string status string stage int agency_id } CaseParty { int case_id int user_id string party_type } User { int id string user_type } CaseDocument { int case_id string s3_key string doc_type } CaseNote { int case_id int admin_id text body bool is_pinned } CaseLog { int case_id string action datetime created_at } GCCase { int case_id } CaseClosingDetails { int case_id string outcome date closed_date }
Case ||--o{ CaseParty : "has" CaseParty }o--|| User : "is" Case ||--o{ CaseDocument : "has" Case ||--o{ CaseNote : "has" Case ||--o{ CaseLog : "has" Case ||--o| GCCase : "may have" Case ||--o| CaseClosingDetails : "may have"Medical cycle models
erDiagram Case ||--o{ Retrieval : "has" Case ||--o{ SDRetrieval : "has" Case ||--o{ Transfer : "has" Case ||--o{ Lab : "has" Case ||--o{ Pregnancy : "has" Case ||--o{ Delivery : "has" Retrieval ||--o{ RetrievalBatch : "has" Transfer { int case_id date transfer_date string outcome int embryos_transferred } Pregnancy { int case_id date confirmed_date string status } Delivery { int case_id date delivery_date string outcome }Case types
The case_type column determines what kind of journey the case represents:
case_type value | Display name | What it means |
|---|---|---|
'surrogate' | GC Case | Gestational carrier journey |
'donor' | ED Case | Egg donation journey |
'sperm_donor' | SD Case | Sperm donation journey |
'IP' | IP Case | Intended parent primary record |
The Jinja filter {{ case.case_type | case_type_filter }} converts these code values to human-readable labels.
CaseParty — linking people to cases
A CaseParty record is the join table that links a User to a Case with a specific role. Never query users directly from a case — always go through CaseParty:
# Get all parties on a caseparties = models.CaseParty.query.filter_by(case_id=case.id).all()
# Get the IP on a GC caseip_party = models.CaseParty.query.filter_by( case_id=case.id, party_type='IP').first()Common party_type values: 'IP', 'IP rep', 'surrogate', 'donor', 'sperm_donor'
GCCase — the GC supplement
For gestational carrier cases, there is a supplemental GCCase model that stores GC-specific data. It has a one-to-one relationship with Case:
gc_case = models.GCCase.query.filter_by(case_id=case.id).first()Not every Case has a GCCase — only cases with case_type = 'surrogate'.
Case status and stage
Case status is stored as a string field. The valid values and their display labels are defined in large_dicts._CASE_STAGE_DICT. There is no database-level enum — the only enforcement is in application code.
Gotcha: Changing a status key in large_dicts.py without updating all the places that filter or display that status will silently break things. Always grep for the old key before renaming.
Medical cycles
Medical events are tracked as separate model records attached to the case:
Retrieval— An egg retrieval procedure. May have multipleRetrievalBatchrecords (for batched retrievals).SDRetrieval— Sperm retrieval. Has its own model (SDRetrieval,SDRetrievalDate,SDRetrievalBatch).Transfer— An embryo transfer. Key fields:transfer_date,embryos_transferred,outcome.Lab— A lab test result (hormone levels, genetic screening, etc.).Pregnancy— Confirmed pregnancy. Tracks confirmation date and progression.Delivery— The birth event. Records date, babies born, and outcome.
A single case can have multiple of each of these — for example, a case might have three transfer attempts before a successful pregnancy.
Common operations
Get a case by ID:
case = models.Case.query.filter_by(id=case_id).first_or_404()Get all active cases for an admin’s dashboard:
cases = models.Case.query.filter( models.Case.status.notin_(['closed', 'archived'])).all()Add a note to a case:
note = models.CaseNote( case_id=case.id, admin_id=g.admin.id, body=note_text, is_pinned=False)Crudler.add(note)Crudler.commit()Log a case activity:
log = models.CaseLog( case_id=case.id, action='status_changed', detail=f'Status changed to {new_status}')Crudler.add(log)