Properties tab: proximity-based default sort order #207
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "205-proximity-sort"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Changes
app/models/property.rb: AddedProperty.by_proximityclass method with Haversine distance and nearest-neighbor chain algorithm. Supports optional scope parameter and ENV overrides (PROXIMITY_BASE_LAT,PROXIMITY_BASE_LNG).app/controllers/properties_controller.rb: Changedmanageaction fromLOWER(client_name)sort toProperty.by_proximity.app/controllers/weeks_controller.rb: Changedindexaction toProperty.by_proximity. Updated@assigned/@unassignedfiltering to Arrayselect/rejectsinceby_proximityreturns an Array.spec/models/property_spec.rb: 9 new tests covering nearest-neighbor chain correctness, coords-before-no-coords ordering, alphabetical fallback, scope support, edge cases, ENV override.spec/requests/properties_spec.rb: 2 new tests verifying geolocated properties render before non-geolocated and proximity chain renders in manage view.Test Plan
Review Checklist
Related Notes
landscaping-assistant-- the project this work belongs toPR #207 Review
DOMAIN REVIEW
Stack: Ruby on Rails 8.1, RSpec, PostgreSQL (float columns for lat/lng)
Algorithm correctness (Property.by_proximity)
The Haversine implementation is textbook-correct. The nearest-neighbor greedy chain is a sound approach for this use case (route-like ordering for a landscaping crew). The O(n^2) complexity is explicitly documented and appropriate -- the comment says "fine for n <= 200" which aligns with a landscaping business's property count.
Good decisions:
private_class_methodon all helper methods -- clean public API surfacePROXIMITY_BASE_LAT/PROXIMITY_BASE_LNG) -- allows depot-based routing without code changesto_aearly and returning Array -- honest about the fact that ordering can't be expressed as SQL, avoids leaking a fake relationclient_name.to_s.downcasehandles nil client_name gracefully in the fallback sortReturn type change (ActiveRecord::Relation -> Array)
This is the most architecturally significant change.
by_proximityreturns an Array, which means:@properties.where(...)in WeeksController correctly changed toselect/rejecton Array@properties.countcorrectly changed to.size.eachwhich works on both Array and Relation -- no view changes neededOne place to watch:
@properties.any?inmanage.html.erb(line 24 of that template) works on Arrays, so no issue there.Scope parameter design
Property.by_proximity(Property.active)andProperty.by_proximity(Property.includes(:services))-- the scope parameter is clean. It calls.to_aon whatever relation is passed, so eager loading viaincludeswill execute before the in-memory sort. This is correct.WeeksController
.to_setoptimizationConverting
scheduled_idsto a Set before theselect/rejectloop is a good performance choice -- O(1) lookups instead of O(n) on each property.BLOCKERS
None.
NITS
Duplicated Haversine in spec (
spec/models/property_spec.rblines ~218-224): The test file defines its ownhaversinehelper method that duplicates the model's implementation. This is intentional (the spec independently verifies the greedy property), so it's not a DRY violation in the security sense. However, consider extracting the Haversine formula to a shared concern or utility module if it's ever needed elsewhere (geocoding, distance display). Low priority.ENV coupling:
proximity_base_pointreads ENV directly rather than through Rails configuration. For a Rails 8.1 app,Rails.application.configorRails.application.credentialswould be more conventional. However, ENV is fine for this use case (container-deployed, no sensitive data). Not blocking.No guard on latitude/longitude type: The
latitudeandlongitudecolumns arefloatin the schema. The code checksp.latitude.present?which handles nil, but a property withlatitude: 0.0would pass the check (0.0 is present in Ruby). Latitude 0, longitude 0 is a valid point (Gulf of Guinea), but for a Utah landscaping business, a property at 0,0 is almost certainly bad data. This is an edge case not worth blocking on, but worth noting for future data validation.Request spec assertion weakness (
spec/requests/properties_spec.rb, "renders proximity-sorted properties by nearest-neighbor chain"): The test creates 3 properties on the same longitude and asserts all 3 appear in the response body, but does not assert the actual order. The comment says "the three should appear contiguously" but theexpectonly checks that all 3 positions are non-nil. Consider assertingmiddle_pos < south_pos(or whichever order the centroid-based chain produces) to make this test actually verify ordering.Model method count:
Propertyis accumulating class methods (by_proximity,proximity_base_point,haversine,to_rad,nearest_neighbor_chain). If more geo functionality is added later, consider extracting to aGeocodableconcern. Not needed now with 5 methods, allprivate_class_method.SOP COMPLIANCE
PROCESS OBSERVATIONS
.eachwhich is polymorphic.VERDICT: APPROVED
PR #207 Review
DOMAIN REVIEW
Tech stack: Ruby on Rails 8.1, RSpec, PostgreSQL (float columns for lat/lng).
Algorithm correctness (Property.by_proximity):
The Haversine implementation and nearest-neighbor greedy traversal are correct. The
to_radhelper, the Haversine formula, and the chain logic all follow standard implementations. The O(n^2) complexity is explicitly documented as acceptable for n <= 200, which is reasonable for a landscaping business.Partition logic:
records.partition { |p| p.latitude.present? && p.longitude.present? }correctly handles nil vs present coordinates. Theclient_name.to_s.downcasefallback handles nil client names safely.ENV override:
proximity_base_pointcorrectly parses ENV vars withto_fand falls back to centroid. Both paths are tested.Array vs Relation change in WeeksController: The switch from
@properties.where(...)to@properties.select/rejectis necessary becauseby_proximityreturns an Array. The view template only calls.any?,.each, and attribute accessors on@assigned/@unassigned, so the Array interface is fully compatible. The.countto.sizechange is also correct (Array hassize, notcount-- well, it does havecounttoo, butsizeis idiomatic for Arrays and avoids confusion with ARcountwhich issues a SQL query).Encapsulation:
private_class_methodis used correctly to hideproximity_base_point,haversine,to_rad, andnearest_neighbor_chain. Good practice.BLOCKERS
None found.
to_f(safe; invalid strings become 0.0, which is a valid coordinate).NITS
ENV var documentation (nit):
PROXIMITY_BASE_LATandPROXIMITY_BASE_LNGare not documented in.env.example. Consider adding them with a comment explaining they are optional overrides for the proximity sort base point.Haversine duplication in test (nit):
spec/models/property_spec.rbduplicates the Haversine formula as a private test helper. This is actually fine for test independence -- the test should not call the production method it is verifying -- but worth noting that if the formula ever changes, the test helper would need updating too. The duplication is intentional and correct here.scheduled_idsto Set (nit): The change fromscheduled_ids = @completions.keys | @queued.keysto.to_setis a nice performance improvement for theinclude?lookups that follow. Good change.No migration needed (confirmed): latitude and longitude columns already exist as
floatin the schema. No migration required. Confirmed.SOP COMPLIANCE
205-proximity-sortreferences issue #205Closes #205present in Related Notes sectionPROCESS OBSERVATIONS
by_proximitywill get a NoMethodError, but this is by design and the current callers handle it correctly.VERDICT: APPROVED