Parent: Aligner Tracker Api
User Onboarding Flow
sequenceDiagram participant Jarvis as Jarvis/Admin participant API as FastAPI participant Cognito as Cognito participant DB as PostgreSQL Jarvis->>API: POST /internal/users API->>DB: Check email uniqueness API->>Cognito: admin_create_user() Cognito-->>API: Return cognito_sub API->>DB: Insert User record API-->>Jarvis: Return user + temp_password Note over Jarvis: Admin sets permanent password via AWS CLI
Key Points:
-
User creation is atomic: if Cognito provisioning fails, no DB record is created
-
If DB insert fails after Cognito creation, the Cognito user is cleaned up
-
Temporary password is returned but email notification is suppressed (
MessageAction=SUPPRESS) -
If user already exists in Cognito, returns existing
cognito_sub(idempotent) -
An initial Journey is automatically created with the specified
product(defaults toaligners)
Journey Lifecycle
Journeys track a user’s treatment cycle for a specific product. A user can only have one active journey at a time.
flowchart TD subgraph UserRegistration [User Registration] A[POST /internal/users] --> B[Create User] B --> C[Auto-create Journey] C --> D["Journey(status=ACTIVE, product=payload.product)"] end subgraph JourneyLifecycle [Journey Lifecycle] D --> E[Progress Events recorded] E --> F[HubSpot: aftercare_stage = Journey Complete] F --> G["Journey(status=COMPLETED)"] G --> H[Wait for new journey creation] end subgraph NewJourney [New Journey] H --> I["POST /auth/users/{sub}/journeys"] I --> J["New Journey(status=ACTIVE)"] J --> E end
Journey States:
active: Currently in progress. Only one active journey per user allowed.completed: Treatment finished. Triggered automatically when HubSpotaftercare_stageis set to “Journey Complete”.
Products:
aligners: Clear aligner treatment (default)clear_braces: Clear braces treatment
Progress Change Cycle
The core treatment flow is based on a 10-day progress cycle:
PROGRESS_CHANGE_INTERVAL_DAYS = 10
flowchart TD A[User logs progress change] --> B[Create ProgressEvent on active Journey] B --> C{First progress event?} C -->|Yes| D[Set journey.date_of_starting_treatment] C -->|No| E[Continue] D --> E E --> F[Update journey.current_progress_stage] F --> F2[Cancel all pending progress notifications] F2 --> G[Calculate next_change = now + 10 days] G --> H[Create scheduled Notification] H --> I[Link notification to event]
Progress Stage Validation
When creating a progress event:
-
Cannot go backwards:
new_stage > current_progress_stage -
Cannot exceed total:
new_stage <= total_progress_stages(if set) -
Cannot be negative:
progress_stage >= 0 -
Requires active journey: Returns 404 if no active journey exists
Expected End Date Calculation
A computed field on the journey response:
expected_end_date = date_of_starting_treatment + (total_progress_stages * 10 days)
Returns null if either field is not set.
Notification Lifecycle
stateDiagram-v2 [*] --> Pending: Created Pending --> Sent: OneSignal accepts Pending --> Cancelled: Cancelled before OneSignal Sent --> Cancelled: Cancelled before scheduled_for Sent --> Delivered: scheduled_for time reached Delivered --> [*] Cancelled --> [*]
Notification Sources:
-
progress: Auto-created when user logs aligner change -
other: Future use
OneSignal Integration:
OneSignal API calls are processed asynchronously via FastAPI background tasks, allowing the API to respond immediately while push notifications are scheduled in the background.
When a progress notification is created:
- Notification record is saved to database with status
PENDING - API response is returned to client
- (Background) Push notification is scheduled via OneSignal with
send_afterset toscheduled_for - (Background) On success: status set to
SENT,onesignal_idstored
Cancellation logic:
A notification can only be cancelled in OneSignal if:
- Status is
SENT(OneSignal accepted it) - Current time is before
scheduled_for(not yet delivered)
If notification is still PENDING (OneSignal hasn’t processed it yet), it’s marked as CANCELLED in DB only.
If notification has status SENT but scheduled_for has passed, no cancellation is attempted (already delivered).
Users are targeted via their cognito_sub_identifier as the OneSignal external_id.
When user reschedules:
-
Find existing notification for the progress event
-
If
SENTandscheduled_foris in the future: cancel in OneSignal and set status toCANCELLED -
If
PENDING: set status toCANCELLED(no OneSignal call needed) -
Create new notification with updated
scheduled_for -
Update event’s
next_notification_id
Progress Event Types
Progress events track different stages of treatment. Each event type is created via the POST endpoint with a type field. Events are always created on the user’s active journey.
| Event Type | Purpose | end_date | reason | Notification Content |
|---|---|---|---|---|
progress | Record progress stage change | Auto-computed (now + 10 days) | Not allowed | Standard: “Time to change to stage X” |
pause | Pause treatment | User-provided | Required | ”Your treatment is paused. Resume with stage X.” |
delay | Delay next change | User-provided | Required | Standard: “Time to change to stage X” |
continue | Resume treatment | Auto-computed | Not allowed | Standard: “Time to change to stage X” |
Event Fields:
user_id: FK to user (for direct queries without joins)journey_id: FK to journey (for journey-specific queries)start_date: When the event was created (automatically set to current time)end_date: When the next action is scheduled (computed or user-provided depending on type)type: One ofprogress,pause,delay,continuereason: Explanation for pause/delay events (stored in unifiedreasonfield)
Validation Rules:
pauseanddelayrequire bothend_dateandreasonto be providedprogressandcontinuemust not haveend_dateorreasonin the requestend_datecannot be in the past
Continue Event Calculation:
When a user creates a continue event to resume treatment after a pause/delay:
last_progress = most recent "progress" type event
pause_event = most recent "pause" or "delay" event
if pause_event.start_date >= last_progress.end_date:
# Pause started after progress end_date, use standard interval
new_end_date = now + 10 days
else:
# Add remaining time from when pause started
remaining_time = last_progress.end_date - pause_event.start_date
new_end_date = now + remaining_time
Notification Behavior: When a user pauses treatment, the scheduled notification content changes to inform them that their treatment is paused and which aligner to resume with. This helps users remember their current aligner when they’re ready to continue.
Image Upload Flow
flowchart LR A[User uploads photo] --> B[Generate S3 key] B --> C[Upload to S3] C --> D[Create UserImage record] D --> E[Link to current_aligner] E --> F[Return image ID]
Images are uploaded to the user’s active journey. Each image is linked to both the user and the journey for flexible querying.
S3 Key Format:
aligner-tracker-api/users/{cognito_sub}/{uuid}.{ext}
Pre-signed URLs:
-
Download URLs expire in 60 seconds by default
-
Generated on-demand when listing or fetching images
External System IDs
| Field | System | Purpose |
|---|---|---|
jarvis_id | Jarvis | Legacy patient management system |
hubspot_id | HubSpot | CRM for sales/marketing |
assigned_dentist_id | Internal | Links to dentist (external table) |
| These are optional identifiers that allow the Aligner Tracker to integrate with other Smile White systems. |
Dentist Change Flow
flowchart TD A[User requests dentist change] --> B{Already pending?} B -->|Yes| C[Return 409 Conflict] B -->|No| D[Submit to Jarvis API] D --> E{Jarvis response} E -->|Success| F[Set has_pending_dentist_change = true] F --> G[Return ticket ID to user] E -->|409| H[Return 409 Conflict] E -->|Error| I[Return 502 Bad Gateway]
Completion via HubSpot Webhook:
When the dentist change is approved, HubSpot sends a contact-property-change webhook with propertyName: dentist_assigned. This:
- Updates
assigned_dentist_idto the new dentist - Clears
has_pending_dentist_changeflag - Creates a notification informing the user
HubSpot Property Sync
The /public/webhooks/hubspot/contact-property-change endpoint handles property updates:
| Property | Action |
|---|---|
dentist_assigned | Updates dentist, clears pending flag, notifies user |
waiver_signed_version_jarvis | Sets terms_accepted = true |
aftercare_stage__contact_ | Updates active journey’s aftercare_stage field |
Aftercare Stage Values:
- “Aligner Journey Underway”: Sets
journey.aftercare_stage = aligner_journey_underway - “Journey Complete”: Sets
journey.aftercare_stage = journey_completeandjourney.status = completed
Key Constants
| Constant | Value | Location |
|---|---|---|
PROGRESS_CHANGE_INTERVAL_DAYS | 10 | app/constants.py |
| Presigned URL expiry | 60s | app/services/images.py |
Enums
Product (app/models/journey.py):
aligners- Clear aligner treatmentclear_braces- Clear braces treatment
JourneyStatus (app/models/journey.py):
active- Journey in progresscompleted- Journey finished
AfterCareStage (app/models/journey.py):
scan_appointment_booked- Initial scan appointment scheduledaligner_journey_underway- Treatment actively in progressjourney_complete- Treatment finished