Property portals like Rightmove don't just want basic information—they expect comprehensive data about every property feature tenants care about. Does it have a garden? What parking is available? What's the council tax band? How much deposit is required? Without this data, properties rank lower in search results and receive fewer enquiries.
Week 37 enhanced LetAdmin's property data model with fields that improve advertising effectiveness whilst ensuring legal compliance around deposit calculations. This article explores how we implemented these enhancements with automatic validation, real-time calculation, and portal synchronisation.
What Your Team Will Notice
The property editing interface now captures details that were previously tracked manually in spreadsheets or notes. When editing a property's advertising details, staff see dropdown menus for outside space ("Private garden", "Communal garden", "Patio/Terrace", "Balcony") and parking arrangements ("On-street", "Off-street allocated", "Off-street unallocated"). Council tax band selects from A through I with an optional "To Be Confirmed" during initial property setup.
Deposit management becomes automatic. Enter the monthly rent, and the system immediately calculates the maximum legal deposit following Tenant Fees Act 2019 regulations. Properties with annual rent under £50,000 allow five weeks' deposit; those above allow six weeks. The calculation happens in real-time as you type, with visual indicators showing whether your entered deposit complies with legal limits.
These fields populate automatically to Rightmove when properties sync, improving search visibility. Tenants filtering for "Properties with Gardens" or "Off-street Parking" now see your properties in results, driving more qualified enquiries without additional work.
Under the Bonnet: Deposit Calculation Compliance
The Tenant Fees Act 2019 caps security deposits at five or six weeks' rent depending on annual rent amount. Exceeding these limits creates legal liability for letting agencies. Our implementation ensures compliance automatically:
class Property
# Deposit calculation following Tenant Fees Act 2019
def calculate_max_legal_deposit
return nil unless price.present?
annual_rent = price * 12
weeks = annual_rent <= 50_000 ? 5 : 6
# Calculate weekly rent and multiply by allowed weeks
weekly_rent = price * 12 / 52.0
max_deposit = weekly_rent * weeks
# Always round DOWN to ensure we stay within legal limits
max_deposit.floor(2)
end
def deposit_compliant?
return true unless deposit_required.present?
deposit_required <= calculate_max_legal_deposit
end
end
This calculation runs server-side for validation and client-side for real-time feedback. The JavaScript implementation matches the Ruby logic exactly:
function calculateMaxLegalDeposit(monthlyRent) {
if (!monthlyRent || monthlyRent <= 0) return null;
const annualRent = monthlyRent * 12;
const weeks = annualRent <= 50000 ? 5 : 6;
const weeklyRent = (monthlyRent * 12) / 52.0;
const maxDeposit = weeklyRent * weeks;
// Floor to 2 decimal places to match Ruby behaviour
return Math.floor(maxDeposit * 100) / 100;
}
The Alpine.js reactive system updates deposit validation instantly as rent amounts change:
<div x-data="{
monthlyRent: <%= @property.price %>,
depositRequired: <%= @property.deposit_required %>,
maxLegalDeposit() {
return calculateMaxLegalDeposit(this.monthlyRent);
},
isCompliant() {
return !this.depositRequired ||
this.depositRequired <= this.maxLegalDeposit();
}
}">
<input type="number"
x-model="monthlyRent"
@input="depositRequired = maxLegalDeposit()" />
<input type="number"
x-model="depositRequired"
:class="{ 'border-red-500': !isCompliant() }" />
<p x-show="!isCompliant()" class="text-red-600">
Maximum legal deposit: £<span x-text="maxLegalDeposit()"></span>
</p>
</div>
This real-time validation prevents compliance issues before they occur, protecting agencies from regulatory penalties.
Outside Space Classification
Rightmove categorises outside space into specific types, each with numeric codes for API submission:
class Property
OUTSIDE_SPACE_OPTIONS = [
['None', nil],
['Private garden/s', 'private_garden'],
['Communal garden/s', 'communal_garden'],
['Patio/Terrace', 'patio_terrace'],
['Balcony', 'balcony']
].freeze
validates :outside_space,
inclusion: { in: OUTSIDE_SPACE_OPTIONS.map(&:last) },
allow_nil: true
end
The Rightmove serialiser maps these to Rightmove's expected format:
class RightmovePropertySerializer
def build_outside_space
return [] unless @property.outside_space.present?
case @property.outside_space
when 'private_garden'
[33] # Private Garden
when 'communal_garden'
[30] # Communal Garden
when 'patio_terrace'
[36, 35] # Patio and Terrace
when 'balcony'
[35] # Terrace (closest Rightmove match)
else
[]
end
end
end
This mapping ensures consistent categorisation across platforms. A property marked "Private garden" in LetAdmin displays as "Private Garden" on Rightmove, maintaining data integrity throughout the advertising workflow.
Parking Arrangements
Parking options are similarly detailed, reflecting the variety of parking situations in UK lettings:
class Property
PARKING_OPTIONS = [
['None', nil],
['On-street parking', 'on_street'],
['Off-street parking', 'off_street'],
['Off-street parking (allocated)', 'off_street_allocated'],
['Off-street parking (unallocated)', 'off_street_unallocated']
].freeze
validates :parking,
inclusion: { in: PARKING_OPTIONS.map(&:last) },
allow_nil: true
end
Rightmove parking codes require careful mapping to match their specific categorisation:
def build_parking
return [] unless @property.parking.present?
case @property.parking
when 'on_street'
[20] # On Street
when 'off_street'
[19] # Off Street
when 'off_street_allocated'
[13, 19] # Allocated Parking, Off Street
when 'off_street_unallocated'
[19, 55] # Off Street, Not Allocated
when nil
[56] # No Parking Available
else
[]
end
end
The double codes for allocated and unallocated parking provide maximum search visibility—properties appear in both general "off-street parking" searches and more specific "allocated parking" searches.
Council Tax Banding
Council tax bands (A through I) help tenants estimate monthly living costs. The implementation is straightforward but requires validation:
class Property
COUNCIL_TAX_BANDS = %w[A B C D E F G H I TBC].freeze
validates :council_tax_band,
inclusion: { in: COUNCIL_TAX_BANDS },
allow_nil: true
def council_tax_band_confirmed?
council_tax_band.present? && council_tax_band != 'TBC'
end
end
The Rightmove serialiser excludes unconfirmed bands to maintain data quality:
def build_council_tax
return nil unless @property.council_tax_band_confirmed?
@property.council_tax_band
end
This ensures only verified council tax information appears on property portals, avoiding confusion from placeholder values.
Responsive Form Layout
All new fields integrate into the property editing modal with a responsive three-column layout that adapts to screen size:
<!-- Desktop: 3 columns, Mobile: 1 column -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Deposit column -->
<div>
<label>Deposit Required</label>
<input type="number"
name="property[deposit_required]"
x-model="depositRequired"
step="0.01" />
<p class="text-sm text-gray-600">
Max legal: £<span x-text="maxLegalDeposit()"></span>
</p>
</div>
<!-- Council tax column -->
<div>
<label>Council Tax Band</label>
<select name="property[council_tax_band]">
<option value="">Select...</option>
<% Property::COUNCIL_TAX_BANDS.each do |band| %>
<option value="<%= band %>"
<%= 'selected' if @property.council_tax_band == band %>>
<%= band %>
</option>
<% end %>
</select>
</div>
<!-- Outside space column -->
<div>
<label>Outside Space</label>
<select name="property[outside_space]">
<option value="">None</option>
<% Property::OUTSIDE_SPACE_OPTIONS.each do |label, value| %>
<option value="<%= value %>"
<%= 'selected' if @property.outside_space == value %>>
<%= label %>
</option>
<% end %>
</select>
</div>
</div>
On mobile, fields stack vertically for comfortable thumb-friendly interaction. On desktop, the three-column layout makes efficient use of screen space whilst maintaining visual clarity.
Webhook Integration
All new fields trigger webhooks when updated, ensuring external systems stay synchronised:
class Property
WEBHOOK_ATTRIBUTES = %w[
reference headline description price beds bathrooms
deposit_required council_tax_band outside_space parking
].freeze
end
When an agency updates a property's parking information, subscribed webhooks fire automatically. External systems like accounting software or agency websites receive these updates within seconds, maintaining consistent property data across platforms without manual synchronisation.
Testing Enhanced Data Models
Comprehensive tests verify both business logic and Rightmove integration:
RSpec.describe Property do
describe "deposit calculation" do
it "allows 5 weeks for annual rent ≤£50,000" do
property = build(:property, price: 1000) # £12k annual
expect(property.calculate_max_legal_deposit).to eq(1153.84)
end
it "allows 6 weeks for annual rent >£50,000" do
property = build(:property, price: 5000) # £60k annual
expect(property.calculate_max_legal_deposit).to eq(6923.07)
end
it "flags non-compliant deposits" do
property = build(:property, price: 1000, deposit_required: 2000)
expect(property).not_to be_deposit_compliant
end
end
describe "outside space validation" do
it "accepts valid outside space types" do
property = build(:property, outside_space: 'private_garden')
expect(property).to be_valid
end
it "rejects invalid outside space types" do
property = build(:property, outside_space: 'swimming_pool')
expect(property).not_to be_valid
end
end
end
RSpec.describe RightmovePropertySerializer do
it "maps outside space to Rightmove codes correctly" do
property = build(:property, outside_space: 'private_garden')
serializer = described_class.new(property, 'BRANCH123')
expect(serializer.build_outside_space).to eq([33])
end
it "excludes unconfirmed council tax bands" do
property = build(:property, council_tax_band: 'TBC')
serializer = described_class.new(property, 'BRANCH123')
expect(serializer.build_council_tax).to be_nil
end
end
These tests ensure legal compliance, data validation, and correct portal synchronisation—critical for avoiding regulatory issues and maintaining listing quality.
What's Next
These data model enhancements establish a foundation for increasingly sophisticated property advertising. Future enhancements might include EPC (Energy Performance Certificate) integration with automatic rating imports, furnished status tracking with detailed inventory specifications, and tenure information (assured shorthold tenancy details, break clauses, etc.).
The pattern established here—capture data once in LetAdmin, validate automatically, synchronise to portals seamlessly—extends naturally to additional property attributes as portal requirements evolve and tenant search behaviours change.
By enriching property data whilst ensuring legal compliance and maintaining data quality, these enhancements help letting agencies advertise properties more effectively, attract more qualified enquiries, and operate with confidence that their systems prevent regulatory missteps.
