Validation: Add multi-tenancy property scoping behind multi_tenancy flag (#295)

validation-295-2026-07-26 Doc

validation pass

Verdict: PASS

Ticket

ldraney/landscaping-assistant#295 (board item #1755) -- Adds current_business helper and property_scope to ApplicationController, scoping all Property queries in controllers behind the multi_tenancy feature flag. When flag OFF, queries remain unscoped (current behavior). When flag ON, queries scope to the user's business via UserBusiness join table.

Environment

Production: https://landscaping-assistant.app
Merged PR: #298
Pipeline: Woodpecker #760 (push to main, all 6 steps green: clone, database, bundle-install, lint, test, build-and-push)

Checks

# Criterion How Verified Result Evidence
1 <code>current_business</code> helper exists in ApplicationController, returns user's business via UserBusiness or nil Code review: <code>app/controllers/application_controller.rb</code> lines 80-90 PASS Method queries <code>UserBusiness.where(user_id: current_user[:sub], status: "approved")</code>, returns <code>.business</code> or nil. Memoized with <code>@current_business</code>.
2 <code>current_business</code> available in views via <code>helper_method</code> Code review: line 8 PASS <code>helper_method :current_user, :current_crew_member, :current_business, ...</code>
3 <code>find_property_or_reject</code> scoped to current_business when flag ON Code review: lines 121-123 PASS Delegates to <code>property_scope.find_by(id:)</code>. <code>property_scope</code> (lines 96-101) returns <code>Property.where(business: current_business)</code> when flag ON, <code>Property.all</code> when OFF.
4 <code>properties_controller.rb</code>: manage, show, edit, update, create, resolve, toggle_active, new scoped when flag ON Code review: all actions use <code>property_scope</code> PASS manage (line 13), new (line 18), create (line 23), resolve (lines 44, 61), set_property for show/edit/update (line 121), toggle_active (line 109) all use <code>property_scope</code>.
5 <code>work_queue_items_controller.rb</code>: property picker, quick-add create, nearby, still-undone all scoped when flag ON Code review: all property queries use <code>property_scope</code> or <code>find_property_or_reject</code> PASS Property picker (line 17), quick-add create (line 70), find_property_or_reject (line 74), mark_other (line 197), nearby (line 299), compute_unqueued_this_week (line 335), recent_properties (line 358).
6 <code>weeks_controller.rb</code>: weekly dashboard shows only current_business properties when flag ON Code review: index and toggle_assign PASS Index (line 10): <code>Property.by_proximity(property_scope.active)</code>. toggle_assign (line 51): <code>find_property_or_reject(property_id)</code>.
7 <code>days_controller.rb</code>: day planning view, previously accordion, property list scoped when flag ON Code review: show, add_to_queue, exclude, load_previously PASS show (line 16): <code>property_scope.active</code>. add_to_queue (line 23), exclude (line 78): <code>find_property_or_reject</code>. load_previously (lines 106, 115): <code>property_scope.active</code> in joins and where clauses.
8 <code>property_comments_controller.rb</code>: set_property rejects cross-business property access when flag ON Code review: set_property method PASS Line 41: <code>@property = property_scope.find(params[:property_id])</code>. Uses <code>.find()</code> which raises <code>ActiveRecord::RecordNotFound</code> (404) for cross-business IDs. CI spec confirms: <code>property_comments_spec.rb:430</code> tests cross-business rejection.
9 Flag OFF: all controllers return unscoped results (identical to current behavior) Code review + feature_flags.rake PASS <code>property_scope</code> returns <code>Property.all</code> when <code>multi_tenancy</code> flag is OFF (line 100). Flag defaults to <code>enabled: false</code> in rake task (line 21). CI specs test flag-off behavior across all controllers.

Tier 1: CI Pipeline

Woodpecker pipeline #760 (push to main): SUCCESS. All steps green: clone, database, bundle-install, lint, test, build-and-push. CI test suite includes multi-tenancy specs across all five controllers: properties_spec.rb, work_queue_items_spec.rb, weeks_spec.rb, days_spec.rb, property_comments_spec.rb. Tests cover both flag-on (scoped) and flag-off (unscoped) behavior.

Tier 3: Production Route Smoke Tests

Route Expected Actual Result
<code>/up</code> 200 200 PASS
<code>/</code> (root) 302 (login redirect) 302 PASS
<code>/login</code> 200 200 PASS
<code>/today</code> 302 (auth required) 302 PASS
<code>/properties</code> 302 (auth required) 302 PASS
<code>/properties/manage</code> 302 (auth required) 302 PASS
<code>/weeks</code> 302 (auth required) 302 PASS
<code>/profile</code> 302 (auth required) 302 PASS
<code>/privacy</code> 200 (public) 200 PASS
<code>/crew</code> 302 (auth required) 302 PASS
All routes responding correctly. No 500s, no unexpected 404s. The multi_tenancy flag is OFF in production, so existing behavior is identical to pre-merge.

Regression Check

No regressions detected. The multi_tenancy flag defaults to enabled: false, so all Property queries continue to use Property.all (unscoped) in production -- identical to pre-merge behavior. The property_scope and find_property_or_reject helpers are additive, not modifying any existing query paths. All production routes healthy, no 500s.

Discovered Issues

None. Local test suite had a database lock conflict (PG::ObjectInUse) preventing spec execution, but this is an environment issue unrelated to the merged code. CI pipeline tests are the authoritative source and passed cleanly.