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 frommemberships.user_idjoined viamembership_idor viaorganizations.owner_idfor shared rows, (c) droporganization_idandmembership_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 insideusers.finance_settingsjsonb (keysexchange_rate,exchange_rate_change,exchange_rate_updated_at). No top-level columns onusers.organizations.slack_webhook_url→ DELETED. Slack integration removed.organizations.kind→ delete (onlypersonal/familyvalues 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/:organization→belongs_to :user - drop
visible_to(membership)scopes - drop
shared?/owner_namehelpers that distinguish shared vs personal - remove
scopeenum values referring tofamily(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(notOrganizations::BaseController) - drop
@organization/Current.membershipaccess - 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: droptoggle_view_modeaction and the family branch.finance_settings_controller.rb: dropcreate_familyaction.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_id→user_id(unique). One board per user.todo_cards.membership_id→user_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_assigneeaction 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_id → user_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.idOrganizations::SubscriptionsControllerhandles checkout / billing portalSubscriptionHelpertakesorganizationstripe_attributeson Organization putsorganization_idin Stripe metadata
Final:
Usergetspay_customer default_payment_processor: :stripepay_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_customerand its subscriptions before re-pointing ownership. - Controller rewritten to use
current_user.payment_processor. - Stripe metadata: include
user_id(and keep the priororganization_idout of new metadata — Stripe history is not rewritten). affiliates/subscription_affiliatesalready 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.erband_sidebar.html.erb - view-mode toggle and family branch in
app/views/organizations/finance_dashboard/index.html.erb create_familybutton inapp/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.erband 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.rbinvitation_received_notifier.rbinvitation_rejected_notifier.rbmember_removed_notifier.rbmember_role_changed_notifier.rbmembership_request_approved_notifier.rbmembership_request_received_notifier.rbmembership_request_rejected_notifier.rbconcerns/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: :collectioninside 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.membershipthroughout for seed-time context - seeds finance/life/gamification data per membership
Final rewrite:
- create ~4 users directly
- seed finance/life/gamification rows with
user_iddirectly - no
Current.membershipcontext - 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/membershiptest/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
userto pundit
9d. Docs and locales
config/locales/en.ymlandpt-BR.yml: remove "Família", "Personal/Family", member management, invitations, view-mode copydocs/*.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)
- Slack — DELETE entirely. Drop
organizations.slack_webhook_url, deleteOrganization::SlackNotifier, deleteapp/notifiers/concerns/slack_deliverable.rb, remove any Slack UI. - Exchange rate storage — nest inside
users.finance_settingsjsonb. Keys:exchange_rate,exchange_rate_change,exchange_rate_updated_at. No separate top-level columns onusers. - Namespace — remove
Organizations::entirely. Rename every controller, view directory, route block, policy, helper and test fromorganizations/to flat (or domain-appropriate) locations. NoOrganizations::namespace survives. session[:organization_id]— leave alone. Keys expire with the session; no explicit purge.repository_accessesandgithub_repositories— DELETE entirely. Drop both tables, their models, controllers, routes, views, tests, and fixtures.- Admin tooling — simple users + subscription list. Replace
admin/organizationswith 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:
- Lock §10 answers with human partner.
- 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_requestsmigrations,db:reset. - (b) keep existing migrations, write a new destructive migration that adds
user_idand drops org/membership columns. Option (a) is cleaner for a fresh-reset test; option (b) is cleaner for the git diff. Recommendation: (a).
- (a) rewrite each create migration in place to its final shape, drop
- User model additions. Add
finance_settingsjsonb, exchange-rate columns, Slack webhook,pay_customerdeclaration. - Rewrite create migrations for the §2a "A" tables — add
user_id, droporganization_id/membership_id, update indices. - Delete the
organizations,memberships,access_requestscreate migrations. - Adjust schema.rb (either by running
db:migratefrom empty or by hand- editing). Verifydb:drop && db:create && db:migratesucceeds. - Rewrite
db/seeds.rbsodb:seedsucceeds against the new schema. This is required to exit Phase 1 per project-plan §"Phase 1 Exit Criteria". - 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.