Phase 0 Inventory: Final-State Mapping

Companion document to project-plan.md. This is the authoritative mapping used to drive Phase 1 (schema rewrite). It classifies every model, controller, policy, view, job, notifier, route, migration, fixture, and calculator as one of: survives with user_id, survives with simplified behavior, or deleted entirely.

The classification taxonomy is taken from the project plan:

  • A — Survives with user_id (finance, life tools, gamification, todo resources)
  • B — Survives, simplified (calculators, helpers, application controller, some policies)
  • C — Deleted entirely (organization, membership, family, invitations, switching)

This repository is not in production. Destructive schema edits and migration rewrites are allowed. Backward compatibility is not a goal. (See project plan §"Execution Constraints".)


1. Blast Radius

Raw grep counts (across app/, config/, test/):

Pattern Hits
organization_id 343
membership_id 282
Current.membership 128
family_organization + personal_organization 37
session[:organization_id] 5
session[:view_mode] 4
current_view_mode 2
Current.organization 2

Hotspot sizes:

File LOC
db/seeds.rb 1,813
db/schema.rb 1,169
config/routes.rb 251
app/controllers/application_controller.rb 68
app/controllers/organizations/base_controller.rb 60
app/models/current.rb 9

Touched file count (rough): ~400–500 across models/controllers/views/tests/fixtures.


2. Schema Inventory

2a. Tables — final-state classification

Tables appear in db/schema.rb. Ownership columns shown are the ones that matter for this refactor (user/membership/organization).

A — Survives, becomes user_id-owned

Drop organization_id and membership_id. Add user_id (NOT NULL). Resolve formerly-shared rows (membership_id IS NULL on a family org) to organization.owner_id per locked rule §2.

Table Currently has Nullable membership? Notes
accounts org + membership yes (shared) finance core
expenses org + membership yes (shared) finance core
investments org + membership yes (shared) finance core
goals org + membership yes (shared) finance core
debts org + membership yes (shared) finance core
balance_registries org + membership yes (shared) finance core
activity_logs org + membership yes (shared) life tool
birthdays org + membership yes (shared) life tool
market_lists org + membership yes (shared) life tool
sports org + membership yes (shared) life tool
debt_payments org + membership no inherit user via debt
notes org + membership no life tool
countdowns org + membership no life tool
habits org + membership no life tool
construction_projects org + membership no life tool
construction_expenses org + membership no inherit via project
todo_boards org (unique) n/a 1:1 per user after rewrite
todo_cards org + membership no see §6.3
challenges org + membership yes drop scope='family' rows
challenge_participations membership only no  
gamification_profiles membership only (unique) no 1:1 per user; keep personal history (rule §5)
xp_transactions membership only no keep personal history only
user_achievements membership only no keep personal history only
market_list_items membership only (+market_list_id) yes collapse — see §6.4

B — Survives, no ownership change needed

Table Why
habit_completions FK to habits only
construction_phases FK to construction_project
contractors FK to construction_project
exercises optional organization_id → drop column, keep global exercises
achievements catalog, user-agnostic
affiliates, subscription_affiliates already user-scoped
announcements, noticed_*, notification_preferences already user-scoped
real_time_chat_* user-scoped
active_storage_*, solid_queue_*, action_text_* framework
pay_* see billing §7 — owner_type changes from Organization to User

C — Deleted entirely

Table Reason
organizations tenancy removed
memberships tenancy removed
access_requests invitations/requests removed
repository_accesses (if org-scoped) verify; drop organization_id and membership_id
github_repositories verify scope; likely drop organization_id
projects verify scope; appears unused in current routes

Phase 1 action: for each surviving table, write migrations that (a) add user_id, (b) backfill from memberships.user_id joined via membership_id or via organizations.owner_id for shared rows, (c) drop organization_id and membership_id. Because we are allowed to reset the database, Phase 1 should prefer rewriting the create migrations themselves over writing backfill code, then rebuild seeds.

2b. Settings migration (rule §4)

  • organizations.finance_settings (jsonb) → users.finance_settings (jsonb).
  • organizations.exchange_rate, exchange_rate_change, exchange_rate_updated_at → nested inside users.finance_settings jsonb (keys exchange_rate, exchange_rate_change, exchange_rate_updated_at). No top-level columns on users.
  • organizations.slack_webhook_urlDELETED. Slack integration removed.
  • organizations.kind → delete (only personal/family values existed).

3. Models

41 top-level model files in app/models/*.rb plus namespaced directories.

3a. Deleted entirely

File Reason
app/models/organization.rb tenancy
app/models/organization/ (all contents) see §3d — split: calculators move, org-specific logic deleted
app/models/membership.rb tenancy
app/models/access_request.rb invitations
app/models/access_request/ (subclasses) invitations
app/models/membership_invitation.rb form object
app/models/membership_request.rb form object
app/models/dashboard/family_aggregator.rb family removed
app/models/user/multitenancy.rb (concern) tenancy — delete after inlining needed helpers

3b. Rewritten: ownership changes to user_id

All finance, life-tools, gamification, and todo models listed in §2a. For each:

  • change belongs_to :membership / :organizationbelongs_to :user
  • drop visible_to(membership) scopes
  • drop shared? / owner_name helpers that distinguish shared vs personal
  • remove scope enum values referring to family (e.g. Challenge.scope)

3c. Rewritten: simplified calculators and aggregators

Class File Change
Account::BalanceCalculator app/models/account/balance_calculator.rb drop view_mode, take user
Expense::SummaryCalculator app/models/expense/summary_calculator.rb drop view_mode, take user
Investment::PerformanceCalculator app/models/investment/performance_calculator.rb drop view_mode, take user
Goal::ProgressCalculator app/models/goal/progress_calculator.rb drop view_mode, take user
Goal::SourceCalculator app/models/goal/ drop visible_to
Debt::SummaryCalculator app/models/debt/summary_calculator.rb take user
Liquidity::Analyzer app/models/liquidity/analyzer.rb drop view_mode, take user
Dashboard::DataAggregator app/models/dashboard/data_aggregator.rb drop view_mode / membership args
Organization::AnalyticsCalculator app/models/organization/analytics_calculator.rb move to User::AnalyticsCalculator or inline
Organization::ExchangeRateFetcher app/models/organization/ move to User::ExchangeRateFetcher
Organization::SlackNotifier app/models/organization/ delete unless Slack setting survives (see §10)
Challenge::ProgressCalculator app/models/challenge/ drop family scope
Habit::DashboardAggregator, AnalyticsCalculator, StreakCalculator app/models/habit/ drop membership arg
Gamification::LevelCalculator app/models/gamification/ no change (pure function)

3d. Current model — must be rewritten before Phase 2

app/models/current.rb currently delegates everything through Current.membership:

attribute :membership, :organizations
delegate :user, to: :membership
delegate :organization, to: :membership

Final state: attribute :user only, with no delegations. This change is the keystone of Phase 2 — touching it fails every authenticated request until controllers/policies are updated together.


4. Controllers

4a. Deleted entirely

File Reason
app/controllers/organization_switch_controller.rb no switching
app/controllers/organizations/memberships_controller.rb member mgmt
app/controllers/organizations/invitations_controller.rb invitations
app/controllers/organizations/membership_requests_controller.rb requests
app/controllers/organizations/base_controller.rb session-org resolver
app/controllers/admin/organizations/* (if present) admin for deleted model

4b. Rewritten: unchanged routes, new base class

All remaining app/controllers/organizations/* (≈33 files) need:

  • inherit from ApplicationController (not Organizations::BaseController)
  • drop @organization / Current.membership access
  • scope queries with current_user.<assoc>

Open design question: keep the Organizations:: namespace as a URL-only alias, or rename to Dashboard::/flat? The plan (§"Phase 2") says "rename or delete organization-scoped routes and controller namespaces where appropriate." Open question, §10.

Controllers in scope: accounts, activity_logs, analytics, balance_registries, birthdays, challenges, construction_expenses, construction_phases, construction_projects, contractors, countdowns, currency_converter, dashboard, debt_payments, debts, expenses, finance_dashboard, finance_settings, gamification, goals, habits, investments, liquidity, market_list_items, market_lists, notes, planning, simulator, sports, subscriptions, todo_boards, todo_cards.

Plus the controller-level changes:

  • finance_dashboard_controller.rb: drop toggle_view_mode action and the family branch.
  • finance_settings_controller.rb: drop create_family action.
  • subscriptions_controller.rb: rewrite for user-owned Pay (§7).
  • gamification_controller.rb: drop family leaderboard.

4c. ApplicationController (simplified)

Current methods to delete: set_current_organizations, set_sidebar_context, current_organization helper, pundit_user override, organization-based locale.

Final: before_action :authenticate_user!, Current.user = current_user, Pundit default, locale from current_user.

4d. Admin controllers

Admin inspection of Organization is deleted. Admin currently inspects subscriptions; change those views to list users instead of organizations.


5. Policies

All 35 policies in app/policies/*. Pundit currently authorises against Current.membership, so every policy must change its initializer signature.

5a. Deleted

File Reason
app/policies/organization/base_policy.rb base class
app/policies/organization_policy.rb resource deleted
app/policies/membership_policy.rb resource deleted

5b. Rewritten

All other ~32 policies: change initialize(membership, record)initialize(user, record); change Scope#initialize(membership, scope)(user, scope); change authorisation from membership-role checks to record.user_id == user.id (or user role for admin-only pages).


6. Domain-specific notes

6.1 Billing (highest risk — see §7)

6.2 Gamification

Keep personal-org history only (rule §5). For each user that has both a personal and family membership, drop the family-membership GamificationProfile and its XpTransaction / UserAchievement rows before reassigning the surviving personal rows to user_id. No leaderboard survives — it was org-scoped.

6.3 TodoBoard / TodoCard

  • todo_boards.organization_iduser_id (unique). One board per user.
  • todo_cards.membership_iduser_id (creator).
  • todo_cards.assignee_ids (JSON array of membership IDs): delete the column. Rule §6 says todo is fully user-owned and non-collaborative. Any card is implicitly assigned to its owner.
  • Delete toggle_assignee action and associated view partials.

6.4 MarketListItem

Currently has a nullable membership_id distinct from its parent list. In the target model, an item inherits ownership from market_list.user_id; drop membership_id. Keep only the FK to market_lists.

6.5 Challenges

Remove Challenge.scope == 'family'. Data migration: delete family-scoped challenges entirely. ChallengeParticipation.membership_iduser_id.

6.6 Notes

Note currently has a shared flag in addition to org/membership. Drop the shared flag — all notes are private to their owner.


7. Billing (Pay + Stripe)

Current:

  • organizations.pay_customer (Pay gem, Stripe)
  • pay_customers.owner_type = 'Organization', owner_id = organizations.id
  • Organizations::SubscriptionsController handles checkout / billing portal
  • SubscriptionHelper takes organization
  • stripe_attributes on Organization puts organization_id in Stripe metadata

Final:

  • User gets pay_customer default_payment_processor: :stripe
  • pay_customers.owner_type = 'User', owner_id = users.id
  • Rule §3: when a user has both a personal- and a family-org subscription, keep the personal one. Drop the family-org pay_customer and its subscriptions before re-pointing ownership.
  • Controller rewritten to use current_user.payment_processor.
  • Stripe metadata: include user_id (and keep the prior organization_id out of new metadata — Stripe history is not rewritten).
  • affiliates / subscription_affiliates already user-scoped; reverify the join still works after billing moves.

Files touched: app/models/organization.rb, app/models/user.rb, app/helpers/subscription_helper.rb, app/controllers/organizations/subscriptions_controller.rb, any admin billing views.


8. Views, jobs, notifiers, helpers, routes

8a. Views — deleted

  • app/views/organizations/memberships/**
  • app/views/organizations/invitations/**
  • app/views/organizations/membership_requests/**
  • organization switcher blocks in app/views/shared/_navbar.html.erb and _sidebar.html.erb
  • view-mode toggle and family branch in app/views/organizations/finance_dashboard/index.html.erb
  • create_family button in app/views/organizations/finance_settings/show.html.erb
  • family leaderboard section in app/views/organizations/gamification/show.html.erb
  • assignee UI in app/views/organizations/todo_cards/_details.html.erb and siblings

8b. Views — rewritten

All remaining app/views/organizations/** should stop rendering @organization/Current.membership and render current_user data.

8c. Jobs

File Action
fetch_exchange_rate_job.rb takes user instead of organization
refresh_total_money_goals_job.rb scope by user
generate_challenges_job.rb scope by user; drop family generation
update_challenge_progress_job.rb scope by user
gamification/achievement_check_job.rb scope by user
publish_announcement_job.rb unchanged

8d. Notifiers — deleted

  • invitation_approved_notifier.rb
  • invitation_received_notifier.rb
  • invitation_rejected_notifier.rb
  • member_removed_notifier.rb
  • member_role_changed_notifier.rb
  • membership_request_approved_notifier.rb
  • membership_request_received_notifier.rb
  • membership_request_rejected_notifier.rb
  • concerns/slack_deliverable.rb — keep only if Slack webhook survives (§10)

8e. Helpers

File Action
memberships_helper.rb delete
organizations_helper.rb delete
subscription_helper.rb rewrite to take user
others unchanged

8f. Routes — config/routes.rb

  • delete patch "switch_organization", to: "organization_switch#update"
  • delete resources :members, …, resources :invitations, …, resources :membership_requests, … within the organizations scope
  • delete post :create_family, on: :collection inside the settings resource
  • keep all other organizations-namespaced routes (decision on renaming the namespace deferred — see §10)

9. Seeds, fixtures, tests, docs

9a. db/seeds.rb — complete rewrite

1,813 lines. Current behavior:

  • creates users and their personal organizations
  • creates two family organizations (Família Test, Família Froes Sales)
  • creates memberships joining users to each org
  • sets Current.membership throughout for seed-time context
  • seeds finance/life/gamification data per membership

Final rewrite:

  • create ~4 users directly
  • seed finance/life/gamification rows with user_id directly
  • no Current.membership context
  • no family orgs, no personal orgs, no memberships

9b. Fixtures

All files under test/fixtures/*.yml referencing organization_id / membership_id. Replace with user_id. Delete organizations.yml, memberships.yml, access_requests.yml.

9c. Tests

Approximate impact:

  • test/models/*: ~47 files; ~25 reference org/membership
  • test/controllers/*: ~42 files; ~15 reference org/membership
  • delete test/models/organization_test.rb, test/models/membership_test.rb, any access_request/invitation/member tests
  • rewrite policy tests to pass user to pundit

9d. Docs and locales

  • config/locales/en.yml and pt-BR.yml: remove "Família", "Personal/Family", member management, invitations, view-mode copy
  • docs/*.md: update any that describe the multi-tenant model; this Phase 0 document and the project plan are the new reference

10. Decisions (answered 2026-04-22)

  1. SlackDELETE entirely. Drop organizations.slack_webhook_url, delete Organization::SlackNotifier, delete app/notifiers/concerns/slack_deliverable.rb, remove any Slack UI.
  2. Exchange rate storagenest inside users.finance_settings jsonb. Keys: exchange_rate, exchange_rate_change, exchange_rate_updated_at. No separate top-level columns on users.
  3. Namespaceremove Organizations:: entirely. Rename every controller, view directory, route block, policy, helper and test from organizations/ to flat (or domain-appropriate) locations. No Organizations:: namespace survives.
  4. session[:organization_id]leave alone. Keys expire with the session; no explicit purge.
  5. repository_accesses and github_repositoriesDELETE entirely. Drop both tables, their models, controllers, routes, views, tests, and fixtures.
  6. Admin toolingsimple users + subscription list. Replace admin/organizations with a users index that shows subscription status.

Migration strategy: rewrite create migrations in place (option (a) from §11). Non-production repo, so cleanest-reset wins over preserving migration history.


11. Phase 1 task list (next)

Derived directly from §2 and §3. Ordering is dependency-respecting:

  1. Lock §10 answers with human partner.
  2. Decide migration strategy. The plan authorises rewriting the create migrations. Two options:
    • (a) rewrite each create migration in place to its final shape, drop organizations / memberships / access_requests migrations, db:reset.
    • (b) keep existing migrations, write a new destructive migration that adds user_id and drops org/membership columns. Option (a) is cleaner for a fresh-reset test; option (b) is cleaner for the git diff. Recommendation: (a).
  3. User model additions. Add finance_settings jsonb, exchange-rate columns, Slack webhook, pay_customer declaration.
  4. Rewrite create migrations for the §2a "A" tables — add user_id, drop organization_id/membership_id, update indices.
  5. Delete the organizations, memberships, access_requests create migrations.
  6. Adjust schema.rb (either by running db:migrate from empty or by hand- editing). Verify db:drop && db:create && db:migrate succeeds.
  7. Rewrite db/seeds.rb so db:seed succeeds against the new schema. This is required to exit Phase 1 per project-plan §"Phase 1 Exit Criteria".
  8. Leave model code, controller code, views, tests, billing broken — those are Phase 2/3/4 work. The goal of Phase 1 is only "schema builds cleanly and seeds run."

Phase 1 exit: bin/rails db:drop db:create db:migrate db:seed succeeds; test suite is expected to be broken.