Skip to main content

Move-In & Move-Out Operations

This document covers the tenant transition process, including data clearing, archival, and automated scheduling.

Key Files

  • crates/bf_types/src/user/move_in.rs - Move-in request/response types
  • crates/bf_types/src/user/move_out.rs - Move-out request/response types
  • crates/bf_user/src/scheduling.rs - Automated scheduling with DO Alarm API
  • crates/bf_user/src/durable_object_sqlx.rs - Move-in/out execution in DO
  • crates/bf_resident/src/handlers/properties.rs - Status transition handlers

Overview

Tenant transitions involve complex data management to ensure:

  1. Privacy: Previous tenant data is completely removed
  2. Continuity: Property configuration and device setup preserved
  3. Automation: Scheduled transitions execute automatically
  4. Auditability: Operations return detailed summaries
┌─────────────────────────────────────────────────────────────────────┐
│ Tenant Transition Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Tenancy ──► ScheduledMovedOut ──► Commissioned ──► PreTenancy │
│ │ │ │ │
│ ▼ │ ▼ │
│ [11 PM: auto_complete] │ ScheduledMovedIn │
│ • Archive data │ │ │
│ • Clear tenant records │ ▼ │
│ • Reset device states │ [6 AM: auto_activate]
│ • Send goodbye emails │ • Clear residual │
│ │ • Welcome emails │
│ │ • Start tenancy │
│ │ │ │
│ └───────────────┴──────────┘
│ │ │
│ ▼ │
│ Tenancy │
└─────────────────────────────────────────────────────────────────────┘

Move-Out Operation

Request Structure

// crates/bf_types/src/user/move_out.rs

pub struct MoveOutRequest {
/// Whether to archive user data before deletion (default: true)
pub archive_data: bool,
/// Whether to preserve energy data for analytics (default: false)
pub preserve_energy_data: bool,
}

Response Structure

pub struct MoveOutResponse {
/// Whether the operation was successful
pub success: bool,
/// URL to download archived data (if archive_data was true)
pub archive_url: Option<String>,
/// Summary of deleted/archived records
pub summary: MoveOutSummary,
/// Timestamp of the operation
pub timestamp: DateTime<Utc>,
}

pub struct MoveOutSummary {
pub guests_removed: usize,
pub pin_codes_removed: usize,
pub access_logs_archived: usize,
pub energy_readings_archived: usize,
pub notifications_removed: usize,
}

Data Handling

Data TypeActionNotes
GuestsDeletedAll guest records removed
PIN CodesDeletedAll PIN codes cleared from all locks (iterated with best-effort)
Access LogsArchived → DeletedUploaded to R2 if archive_data is true
Energy ReadingsArchived/PreservedBased on preserve_energy_data flag
NotificationsDeletedAll notification history cleared
Device StatesResetStates cleared, devices remain configured
Device TokensDeletedPush notification tokens removed
TenantsDeletedTenant records cleared

Automated Move-Out (ScheduledMovedOut)

When a property transitions to ScheduledMovedOut status:

  1. Scheduler Setup: DO Alarm scheduled for 11:00 PM UTC on move-out date
  2. Auto-Completion: At trigger time, auto_complete_move_out is called
  3. Status Transition: Property moves to Commissioned status
// crates/bf_user/src/scheduling.rs

/// Hour of day (in UTC) when move-out should be triggered: 11:00 PM (23:00)
pub const MOVE_OUT_TRIGGER_HOUR: u32 = 23;

/// Calculate the deadline for a move-out task
pub fn calculate_move_out_deadline(move_out_date_secs: i64) -> i64 {
let deadline_secs = move_out_date_secs + (MOVE_OUT_TRIGGER_HOUR as i64 * 60 * 60);
deadline_secs * 1000 // Convert to milliseconds
}

Move-In Operation

Request Structure

// crates/bf_types/src/user/move_in.rs

pub struct MoveInRequest {
/// Optional notes about the move-in
pub notes: Option<String>,
}

Response Structure

pub struct MoveInResponse {
/// Whether the move-in preparation was successful
pub success: bool,
/// Summary of cleared records
pub summary: MoveInSummary,
/// Timestamp of the operation
pub timestamp: DateTime<Utc>,
}

/// Summary of data cleared during move-in preparation
/// Values should be 0 if move-out was done properly
pub struct MoveInSummary {
pub guests_cleared: usize,
pub pin_codes_cleared: usize,
pub access_logs_cleared: usize,
pub notifications_cleared: usize,
pub device_states_reset: usize,
}

Clean Slate Guarantee

Move-in ensures a clean slate for new tenants. The summary shows what was cleared - ideally all zeros if the previous move-out completed successfully:

// If move-out was done properly, all counts should be 0
MoveInSummary {
guests_cleared: 0, // No leftover guests
pin_codes_cleared: 0, // No lingering PIN codes
access_logs_cleared: 0, // No access history
notifications_cleared: 0, // No old notifications
device_states_reset: 0, // States already clean
}

Automated Move-In (ScheduledMovedIn)

When a property transitions to ScheduledMovedIn status:

  1. Scheduler Setup: DO Alarm scheduled for 6:00 AM UTC on move-in date
  2. Auto-Activation: At trigger time, auto_activate_tenancy is called
  3. Status Transition: Property moves to Tenancy status
// crates/bf_user/src/scheduling.rs

/// Hour of day (in UTC) when move-in should be triggered: 6:00 AM
pub const MOVE_IN_TRIGGER_HOUR: u32 = 6;

/// Calculate the deadline for a move-in task
pub fn calculate_move_in_deadline(move_in_date_secs: i64) -> i64 {
let deadline_secs = move_in_date_secs + (MOVE_IN_TRIGGER_HOUR as i64 * 60 * 60);
deadline_secs * 1000 // Convert to milliseconds
}

Scheduling System

Task Types

The DO scheduling system manages both interval-based and deadline-based tasks:

pub enum ScheduledTaskType {
// Interval-based (recurring)
Energy, // Every 10 minutes
Backup, // Every hour
DeviceRefresh, // Every hour

// Deadline-based (one-time)
MoveInCheck, // Triggered at move_in_date + 6 hours
MoveOutCheck, // Triggered at move_out_date + 23 hours
}

Scheduling Move-In/Out Checks

// crates/bf_user/src/scheduling.rs

/// Schedule a move-in check task
pub async fn schedule_move_in_check(
conn: &DOConnection,
move_in_date_secs: i64,
) -> UserResult<i64> {
let deadline = calculate_move_in_deadline(move_in_date_secs);
let now = now_timestamp_millis();

// If deadline is in the past, schedule for 2 minutes from now
let next_run = if deadline <= now {
now + LIFECYCLE_RETRY_INTERVAL_MS
} else {
deadline
};

upsert_task_state(conn, ScheduledTaskType::MoveInCheck, next_run).await?;
Ok(next_run)
}

/// Schedule a move-out check task
pub async fn schedule_move_out_check(
conn: &DOConnection,
move_out_date_secs: i64,
) -> UserResult<i64> {
let deadline = calculate_move_out_deadline(move_out_date_secs);
// ... similar logic
}

Retry Logic

Failed lifecycle checks retry every 2 minutes:

/// Retry interval for failed move-in/move-out checks: 2 minutes
pub const LIFECYCLE_RETRY_INTERVAL_MS: i64 = 2 * 60 * 1000;

/// Maximum consecutive errors before disabling a task
pub const MAX_CONSECUTIVE_ERRORS: i64 = 5;

Error Handling

Tasks that fail repeatedly are disabled to prevent infinite loops:

pub fn get_due_tasks(task_states: &[TaskState], now: i64) -> Vec<ScheduledTaskType> {
task_states
.iter()
.filter(|state| {
let is_due = state.next_run_at <= now + DUE_TOLERANCE_MS;
let is_healthy = state.consecutive_errors < MAX_CONSECUTIVE_ERRORS;
is_due && is_healthy
})
.map(|state| state.task_type)
.collect()
}

Email Notifications

Move-Out Emails

  • Goodbye Email: Sent to all tenants when move-out completes
  • Contains: Final usage summary, important reminders

Move-In Emails

  • Welcome Email: Sent to new tenants when move-in activates
  • Contains: Property access instructions, support contacts

Data Seeding (Development)

For development and testing, a seed operation can generate sample data:

// crates/bf_types/src/user/move_out.rs

pub struct SeedRequest {
pub property_id: String,
pub guest_count: usize, // Default: 5
pub pin_code_count: usize, // Default: 5
pub access_log_count: usize, // Default: 100
pub clear_existing: bool, // Whether to clear first
pub seed: Option<u64>, // For reproducible generation
}

pub struct SeedResponse {
pub success: bool,
pub summary: SeedSummary,
pub timestamp: DateTime<Utc>,
}

Integration with Property Status

Status Transitions Triggering Scheduling

From StatusTo StatusScheduled Task
PreTenancyScheduledMovedInMoveInCheck at 6 AM on move_in_date
TenancyScheduledMovedOutMoveOutCheck at 11 PM on move_out_date

Sync Request for Metadata Updates

When status changes, the DO receives a RefreshMetadataRequest:

pub struct RefreshMetadataRequest {
pub property: SyncPropertyRequest, // Includes status, move_in_date, move_out_date
pub devices: Vec<DeviceInfo>,
pub tenants: Vec<TenantInfo>,
pub installers: Vec<InstallerInfo>,
pub documents: Vec<DocumentInfo>,
}

pub struct SyncPropertyRequest {
pub property_id: String,
pub status: PropertyStatus,
pub move_in_date: Option<i64>, // Unix timestamp in seconds
pub move_out_date: Option<i64>, // Unix timestamp in seconds
// ... other fields
}

Multi-Lock Lifecycle Operations

Properties may have multiple smart locks (e.g., Front Door Lock + Parcel Lock). The move-in, move-out, and delete operations iterate all locks with best-effort error handling:

// In move_in_internal, move_out_internal, delete_all_storage_internal:
let lock_devices = storage::list_devices_by_type(&self.conn, DeviceType::SmartLock).await?;

for lock_device in &lock_devices {
match self.clear_pin_codes_on_lock(&lock_device).await {
Ok(_) => console_log!("Cleared PINs on {}", lock_device.device_name),
Err(e) => console_log!("Failed to clear PINs on {}: {}", lock_device.device_name, e),
// Continue to next lock - don't fail the entire operation
}
}

This ensures that one lock failure (e.g., offline Parcel Lock) doesn't prevent clearing the Front Door Lock during a tenant transition.

Security Considerations

  1. Data Isolation: Each property's DO handles its own data - no cross-property access
  2. Archival Security: Archives stored in R2 with time-limited signed URLs
  3. Audit Trail: Operations return detailed summaries for compliance
  4. Role-Based: Only Admin and Property Manager can trigger manual move operations

See Also