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 typescrates/bf_types/src/user/move_out.rs- Move-out request/response typescrates/bf_user/src/scheduling.rs- Automated scheduling with DO Alarm APIcrates/bf_user/src/durable_object_sqlx.rs- Move-in/out execution in DOcrates/bf_resident/src/handlers/properties.rs- Status transition handlers
Overview
Tenant transitions involve complex data management to ensure:
- Privacy: Previous tenant data is completely removed
- Continuity: Property configuration and device setup preserved
- Automation: Scheduled transitions execute automatically
- 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 Type | Action | Notes |
|---|---|---|
| Guests | Deleted | All guest records removed |
| PIN Codes | Deleted | All PIN codes cleared from all locks (iterated with best-effort) |
| Access Logs | Archived → Deleted | Uploaded to R2 if archive_data is true |
| Energy Readings | Archived/Preserved | Based on preserve_energy_data flag |
| Notifications | Deleted | All notification history cleared |
| Device States | Reset | States cleared, devices remain configured |
| Device Tokens | Deleted | Push notification tokens removed |
| Tenants | Deleted | Tenant records cleared |
Automated Move-Out (ScheduledMovedOut)
When a property transitions to ScheduledMovedOut status:
- Scheduler Setup: DO Alarm scheduled for 11:00 PM UTC on move-out date
- Auto-Completion: At trigger time,
auto_complete_move_outis called - Status Transition: Property moves to
Commissionedstatus
// 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:
- Scheduler Setup: DO Alarm scheduled for 6:00 AM UTC on move-in date
- Auto-Activation: At trigger time,
auto_activate_tenancyis called - Status Transition: Property moves to
Tenancystatus
// 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 Status | To Status | Scheduled Task |
|---|---|---|
| PreTenancy | ScheduledMovedIn | MoveInCheck at 6 AM on move_in_date |
| Tenancy | ScheduledMovedOut | MoveOutCheck 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
- Data Isolation: Each property's DO handles its own data - no cross-property access
- Archival Security: Archives stored in R2 with time-limited signed URLs
- Audit Trail: Operations return detailed summaries for compliance
- Role-Based: Only Admin and Property Manager can trigger manual move operations
See Also
- Property Lifecycle - Status state machine
- Admin & PM Roles - Who can trigger operations
- User Durable Object - DO architecture
- Notification System - Email delivery