Validation: Add multi-tenancy property scoping behind multi_tenancy flag (#295)
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:
Merged PR: #298
Pipeline: Woodpecker #760 (push to main, all 6 steps green: clone, database, bundle-install, lint, test, build-and-push)
https://landscaping-assistant.appMerged 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.