Property status changes have implications beyond simply updating a database field. Marking a property "Let" should stop advertising automatically. Changing to "Available to Let" should offer to start advertising. Attempting to advertise a property with an F-rated EPC should warn about legal restrictions. Week 38 implemented intelligent status workflows that understand these relationships, preventing common mistakes whilst guiding users through correct processes.
This article explores how we built context-aware status management that makes correct workflows easy and incorrect ones difficult—reducing errors whilst improving operational efficiency.
What Your Team Will Notice
The status change interface adapts based on current property state and target status. Change a property from "Available to Let" to "Let" and the system automatically stops advertising across all portals, updates webhook subscribers, and records the status transition with timestamps. No need to remember separate "stop advertising" steps—the platform handles it intelligently.
Switch a property from "Let" back to "Available to Let" and the interface displays an advertising checklist: EPC present and valid, photos uploaded, rent price set, property description complete. Items marked complete show green checkmarks; missing requirements highlight in amber with quick-fix links. When all requirements are met, "Start Advertising" becomes available with one click.
The status dropdown respects business logic automatically. Properties can't transition from "Available to Let" to "Withdrawn" without passing through intermediate states. Status changes that have advertising implications show contextual notices: "This will stop advertising on Rightmove and Zoopla" or "This property is ready to advertise—would you like to start now?"
For properties with compliance issues (expired EPCs, F/G ratings, missing required fields), status changes include warnings before allowing transitions that would create legal risks. The system prevents advertising properties that shouldn't be advertised whilst allowing status updates for legitimate reasons (withdrawals, lettings, maintenance periods).
Under the Bonnet: State Machine Logic
Property marketing status follows a defined state machine with valid transitions and business rules:
class Property
# Marketing status options
MARKETING_STATUSES = {
'not_advertised' => 'Not For Advertising',
'available_to_let' => 'Available to Let',
'under_negotiation' => 'Under Negotiation',
'let_stc' => 'Let STC',
'let' => 'Let',
'withdrawn' => 'Withdrawn'
}.freeze
# Statuses that should actively advertise
ADVERTISING_STATUSES = %w[available_to_let under_negotiation].freeze
# Statuses that should not advertise
NON_ADVERTISING_STATUSES = %w[let_stc let withdrawn not_advertised].freeze
def can_advertise?
ADVERTISING_STATUSES.include?(marketing_status) &&
advertising_requirements_met?
end
def advertising_requirements_met?
# All required fields present
price.present? &&
beds.present? &&
bathrooms.present? &&
property_type.present? &&
description.present? &&
photos.any? &&
epc_valid_for_advertising?
end
def epc_valid_for_advertising?
return false unless current_epc.present?
return false unless current_epc.valid?
# Can't advertise F or G rated properties
%w[A B C D E].include?(current_epc.current_energy_rating)
end
end
These methods power conditional logic throughout status change workflows.
Context-Aware Status Change Modal
The status change modal displays different content based on current status and target status:
class ChangeStatusModal {
constructor(propertyId, currentStatus) {
this.propertyId = propertyId;
this.currentStatus = currentStatus;
this.targetStatus = null;
}
selectStatus(status) {
this.targetStatus = status;
this.updateModalContent();
}
updateModalContent() {
const transition = {
from: this.currentStatus,
to: this.targetStatus
};
// Check if this transition stops advertising
if (this.stopsAdvertising(transition)) {
this.showAdvertisingStopNotice();
}
// Check if this transition enables advertising
if (this.enablesAdvertising(transition)) {
this.showAdvertisingStartOption();
}
// Check for compliance warnings
if (this.hasComplianceIssues(transition)) {
this.showComplianceWarnings();
}
}
stopsAdvertising(transition) {
const advertisingStatuses = ['available_to_let', 'under_negotiation'];
const nonAdvertisingStatuses = ['let_stc', 'let', 'withdrawn', 'not_advertised'];
return advertisingStatuses.includes(transition.from) &&
nonAdvertisingStatuses.includes(transition.to);
}
enablesAdvertising(transition) {
const nonAdvertisingStatuses = ['let_stc', 'let', 'withdrawn', 'not_advertised'];
const advertisingStatuses = ['available_to_let', 'under_negotiation'];
return nonAdvertisingStatuses.includes(transition.from) &&
advertisingStatuses.includes(transition.to);
}
}
This JavaScript layer provides immediate visual feedback before form submission.
Advertising Readiness Checklist
When status changes enable advertising, the modal displays a real-time requirements checklist:
<div x-data="advertisingChecklist" x-show="showAdvertisingOption">
<h3>Advertising Readiness</h3>
<ul class="checklist">
<!-- Required fields -->
<li :class="checkmark(hasPrice)">
<span x-text="icon(hasPrice)"></span>
Rent price set
<a x-show="!hasPrice" href="#" @click="quickFix('price')">Set now</a>
</li>
<li :class="checkmark(hasPhotos)">
<span x-text="icon(hasPhotos)"></span>
Photos uploaded (<span x-text="photoCount"></span>)
<a x-show="!hasPhotos" href="#" @click="quickFix('photos')">Add photos</a>
</li>
<li :class="checkmark(hasDescription)">
<span x-text="icon(hasDescription)"></span>
Property description
<a x-show="!hasDescription" href="#" @click="quickFix('description')">Add description</a>
</li>
<li :class="checkmark(hasValidEpc)">
<span x-text="icon(hasValidEpc)"></span>
Valid EPC (Rating: <span x-text="epcRating"></span>)
<a x-show="!hasValidEpc" href="#" @click="quickFix('epc')">Add EPC</a>
</li>
</ul>
<button type="button"
@click="startAdvertising"
:disabled="!allRequirementsMet"
:class="{ 'btn-primary': allRequirementsMet, 'btn-disabled': !allRequirementsMet }">
<span x-show="allRequirementsMet">Start Advertising</span>
<span x-show="!allRequirementsMet">Complete Requirements First</span>
</button>
</div>
<script>
Alpine.data('advertisingChecklist', () => ({
hasPrice: <%= @property.price.present? %>,
hasPhotos: <%= @property.photos.any? %>,
photoCount: <%= @property.photos.count %>,
hasDescription: <%= @property.description.present? %>,
hasValidEpc: <%= @property.epc_valid_for_advertising? %>,
epcRating: '<%= @property.current_epc&.current_energy_rating %>',
get allRequirementsMet() {
return this.hasPrice && this.hasPhotos &&
this.hasDescription && this.hasValidEpc;
},
checkmark(condition) {
return condition ? 'checklist-complete' : 'checklist-incomplete';
},
icon(condition) {
return condition ? '✓' : '○';
},
quickFix(field) {
// Navigate to appropriate modal/section to fix missing requirement
}
}));
</script>
This checklist updates in real-time as requirements are met, providing clear path to advertising eligibility.
Automatic Advertising Management
When status transitions warrant advertising changes, the system handles them automatically:
class PropertiesController
def update_status
@property = Property.find(params[:id])
old_status = @property.marketing_status
new_status = params[:marketing_status]
@property.update!(marketing_status: new_status)
# Handle advertising implications
handle_advertising_for_status_change(old_status, new_status)
# Trigger webhooks for external systems
@property.trigger_webhooks('property.status.updated')
redirect_to @property, notice: status_change_message(old_status, new_status)
end
private
def handle_advertising_for_status_change(old_status, new_status)
stopping_advertising = advertising_status?(old_status) &&
!advertising_status?(new_status)
starting_advertising = !advertising_status?(old_status) &&
advertising_status?(new_status) &&
params[:start_advertising] == 'true'
if stopping_advertising
stop_advertising(@property)
elsif starting_advertising
start_advertising(@property)
end
end
def stop_advertising(property)
property.update!(advertising: false)
# Trigger portal removals via webhooks
property.trigger_webhooks('property.advertising.stopped')
# Log activity
AuditLog.log('stopped_advertising', property, current_user)
end
def start_advertising(property)
return unless property.advertising_requirements_met?
property.update!(advertising: true)
# Trigger portal additions via webhooks
property.trigger_webhooks('property.advertising.started')
# Log activity
AuditLog.log('started_advertising', property, current_user)
end
def advertising_status?(status)
Property::ADVERTISING_STATUSES.include?(status)
end
end
This automation ensures advertising state stays synchronized with marketing status without requiring separate actions.
EPC Compliance Warnings
The status change interface checks EPC compliance before allowing advertising:
<% if @property.status_enables_advertising?(target_status) %>
<% unless @property.epc_valid_for_advertising? %>
<div class="alert alert-warning">
<strong>EPC Warning:</strong>
<% if @property.current_epc.blank? %>
This property has no EPC certificate. EPCs are legally required for all rental properties.
<a href="<%= property_epc_path(@property) %>">Add EPC now</a>
<% elsif @property.current_epc.expired? %>
This property's EPC expired on <%= @property.current_epc.expiry_date.to_formatted_s(:long) %>.
<a href="<%= property_epc_path(@property) %>">Update EPC</a>
<% elsif ['F', 'G'].include?(@property.current_epc.current_energy_rating) %>
This property has an EPC rating of <%= @property.current_epc.current_energy_rating %>.
Properties rated F or G cannot be legally let and should not be advertised.
<a href="<%= property_epc_path(@property) %>">Check details</a>
<% end %>
</div>
<% end %>
<% end %>
These warnings appear before form submission, preventing non-compliant advertising attempts.
Streamlined Wizard Steps
For properties already advertising, the edit advertising wizard automatically skips unnecessary steps:
function calculateWizardSteps(property) {
const steps = [];
// Step 1: Marketing status and advertising decision
// Only show if property isn't currently advertising
if (!property.advertising) {
steps.push('marketing_status_and_advertising');
}
// Step 2: Property details (always shown)
steps.push('property_details');
// Step 3: Photos and EPC (always shown)
steps.push('photos_and_epc');
// Step 4: Review and publish (always shown)
steps.push('review');
return steps;
}
Properties not advertising see four steps; those already advertising see three. This reduces friction for common updates.
Testing Workflow Logic
Comprehensive tests verify status transition logic and advertising implications:
RSpec.describe Property, type: :model do
describe "advertising status logic" do
it "identifies statuses that should advertise" do
property = build(:property, marketing_status: 'available_to_let')
expect(property.can_advertise?).to be true
end
it "identifies statuses that should not advertise" do
property = build(:property, marketing_status: 'let')
expect(property.can_advertise?).to be false
end
it "prevents advertising with invalid EPC" do
property = create(:property, marketing_status: 'available_to_let')
create(:epc, property: property, current_energy_rating: 'F')
expect(property.can_advertise?).to be false
end
end
describe "status transitions" do
it "stops advertising when changing to let" do
property = create(:property,
marketing_status: 'available_to_let',
advertising: true)
property.update!(marketing_status: 'let')
expect(property.advertising).to be false
end
it "enables advertising option when changing to available" do
property = create(:property, marketing_status: 'let')
property.update!(marketing_status: 'available_to_let')
expect(property.status_enables_advertising?).to be true
end
end
end
These tests ensure business logic behaves correctly across various scenarios.
What's Next
The intelligent workflow foundation enables several enhancements: status transition history showing complete property lifecycle, automated reminders when properties remain in transient statuses too long (properties "Under Negotiation" for weeks likely need attention), and predictive status suggestions based on property activity patterns.
Future improvements might include role-based status transition restrictions (only managers can mark properties "Let"), approval workflows for significant status changes (require manager approval before withdrawing advertised properties), and integration with tenant management (automatically transition to "Let" when tenancy agreements are signed).
By embedding business logic into user interfaces, LetAdmin reduces errors, improves compliance, and guides users through correct processes—transforming complex property management workflows into straightforward, mistake-resistant operations.
