Skip to content

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 valueDisplay nameWhat it means
'surrogate'GC CaseGestational carrier journey
'donor'ED CaseEgg donation journey
'sperm_donor'SD CaseSperm donation journey
'IP'IP CaseIntended 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 case
parties = models.CaseParty.query.filter_by(case_id=case.id).all()
# Get the IP on a GC case
ip_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 multiple RetrievalBatch records (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)

Gotchas