Debts MVP Implementation Plan
Goal
Create a first-class Dívidas module that gives users a reliable view of liabilities, upcoming obligations, and payoff progress without overbuilding the first release.
This module should fit Lifehub's existing finance architecture:
- multi-tenant by
organization - ownership by
membershipor shared family records - Pundit authorization through
Current.membership - thin controllers
- calculator logic in namespaced model classes
- flat organization-scoped routes
UX And Localization Requirements
The first release should optimize for clarity, not density.
pt-BRtranslation coverage is part of the MVP, not a follow-up- debt pages should default to the simplest reading path: current balance, next payment, status, and payoff progress
- forms should group fields into a few clear sections instead of presenting every attribute with equal visual weight
- seeded examples should exist for
[email protected]so the module can be reviewed without manual data entry
Product Scope
Included In MVP
- create, edit, list, show, and settle debts
- log debt payments manually
- show upcoming due debts
- show total liabilities and monthly burden
- support shared and individual debts
- show a simple payoff forecast for active debts
Excluded From MVP
- credit card statement-cycle modeling
- refinance comparison
- automated amortization schedules
- snowball and avalanche payoff engines
- recurring notifications and reminders
- automatic conversion of expenses into debts
- external bank integrations
Core Product Decisions
Domain Rules
- A debt belongs to an organization.
- A debt may belong to a membership or be shared with the family.
- A payment belongs to a debt and reduces the debt balance.
- A debt with zero balance becomes
paid_off. minimum_paymentis advisory for forecasting and KPIs.- Overdue state is determined from due date behavior plus active status.
- The module tracks liabilities, not spending.
Why Debts Is Separate From Expenses
Expenses represent consumed money.
Debts represent obligations that still exist.
Keeping debts separate preserves:
- current balance
- payoff progress
- due dates
- interest burden
- net worth accuracy
Data Model
Debt
Represents a live obligation.
Suggested enums:
debt_type:credit_card,personal_loan,mortgage,vehicle_financing,installment_plan,tax_debt,family_loan,otherstatus:active,paid_off,overdue,renegotiated,pausedinterest_rate_period:monthly,yearly
High-value fields:
organization_idmembership_id, nullable for shared debtaccount_id, optional payment source accountnamelender_namedebt_typestatuscurrencyoriginal_amountcurrent_balanceinterest_rateinterest_rate_periodminimum_paymentdue_daystarted_onends_oninstallment_totalinstallments_paidnotes
DebtPayment
Represents a payment event against a debt.
Fields:
debt_idorganization_idmembership_idaccount_id, optionalpaid_onamountprincipal_amount, optional in MVPinterest_amount, optional in MVPfee_amount, optional in MVPnotes
Calculator
Debt::SummaryCalculator should handle all cross-record calculations needed by the index page.
Recommended responsibilities:
- total active debt
- minimum monthly obligation
- debts due in next 7 days
- estimated monthly interest
- grouped totals by type
- debt trend by month
- visible scope handling for shared versus individual records
Visibility And Multi-Tenancy
The debts module should follow the same pattern already used by finance records.
Shared Versus Individual
membership_id: nilmeans shared family debtmembership_id: Current.membership.idmeans individual debt
Suggested Visibility Scope
For the MVP, match the current visible-record pattern used in other finance models:
- personal/shared debt visible to owner and family members as appropriate
- shared debts visible to all members in the organization
If the app later expands family view modes for debts, the calculator should accept membership and optional view_mode similarly to other finance calculators.
Implementation Steps
Step 1: Add routes
Update config/routes.rb with organization-scoped debt routes.
Recommended shape:
resources :debts do
member do
patch :mark_as_paid_off
end
resources :debt_payments, path: "payments", only: %i[create]
end
Why this route shape:
- matches existing flat finance routes
- keeps payment creation nested under the parent debt
- gives a simple member action for settlement without adding excessive surface area
Step 2: Create migration for debts
Create debts table with fields listed in the schema doc.
Key migration rules:
organizationrequiredmembershipnullableaccountoptional- decimal precision aligned with finance resources
- indexes for organization, status, type, due day, and visibility
Step 3: Create migration for debt payments
Create debt_payments table.
Key rules:
debt,organization, andmembershiprequiredaccountoptional- positive payment amount validation at model level
- order payments by
paid_on DESC, created_at DESC
Step 4: Add Debt model
Implement with the standard model order used by Lifehub:
- constants
- ransack configuration if needed
- associations
- attributes
- normalization
- enums
- validations
- scopes
- callbacks
- public methods
Important associations:
belongs_to :organization
belongs_to :membership, optional: true
belongs_to :account, optional: true
has_many :debt_payments, dependent: :destroy
Recommended validations:
- name presence
- debt_type presence
- status presence
- currency presence
- original_amount numericality greater than zero
- current_balance numericality greater than or equal to zero
- interest_rate numericality greater than or equal to zero
- minimum_payment numericality greater than or equal to zero, allow blank
- due_day inclusion
1..31, allow blank - installments paid not greater than total
Recommended scopes:
active_onlyoverdue_onlypaid_off_onlysharedowned_by(membership)visible_to(membership)due_within(days)
Recommended helpers:
shared?paid_off?overdue?next_due_date(reference_date = Date.current)remaining_installmentsestimated_monthly_interestestimated_months_to_payoff
Step 5: Add DebtPayment model
Associations:
belongs_to :debt
belongs_to :organization
belongs_to :membership
belongs_to :account, optional: true
Recommended validations:
- paid_on presence
- amount presence and positive numericality
- optional split fields non-negative if present
Recommended callback:
- after create, recalculate parent debt balance
For MVP simplicity, a payment may reduce current_balance by amount directly.
Step 6: Add Debt::SummaryCalculator
New namespaced model class under app/models/debt/summary_calculator.rb.
Constructor:
Debt::SummaryCalculator.new(@organization, membership: Current.membership)
Public methods:
visible_debtsactive_debtstotal_debtminimum_monthly_paymentdue_soon_total(days: 7)due_soon_count(days: 7)estimated_monthly_interestby_typeupcoming_payments(days: 30)monthly_trend(months: 6)
This keeps controllers thin and makes the dashboard/index logic testable.
Step 7: Add policy
Create DebtPolicy inheriting from Organization::BasePolicy.
Recommended policy rules for MVP:
index?: membership presentshow?: membership presentcreate?: membership presentupdate?: membership presentdestroy?: membership admin onlymark_as_paid_off?: membership present
For nested payment creation, either:
- authorize the parent debt through
DebtPolicy, or - add
DebtPaymentPolicylater if permissions diverge
Step 8: Add controller for debts
Create Organizations::DebtsController.
Recommended actions:
indexshownewcreateeditupdatedestroymark_as_paid_off
Controller responsibilities:
- load organization-scoped records only
- authorize collection and member actions
- set
membershipon create when debt is personal - support modal form rendering like existing finance flows
- preload calculator data for index
Keep actions thin. All summary logic belongs in Debt::SummaryCalculator or model methods.
Step 9: Add controller for payments
Create Organizations::DebtPaymentsController with only create in MVP.
Responsibilities:
- load and authorize parent debt
- create payment from nested form
- set
organizationandmembership - optionally attach account
- redirect back to debt show with success or render modal on failure
Step 10: Build views
Create the following views:
app/views/organizations/debts/index.html.erbapp/views/organizations/debts/show.html.erbapp/views/organizations/debts/_form.html.erbapp/views/organizations/debts/new.html.erbapp/views/organizations/debts/edit.html.erbapp/views/organizations/debt_payments/_form.html.erb
View expectations:
- same dark-first styling as finance pages
- summary cards on top
- compact debt list focused on balance, payment plan, next due date, and status
- turbo modal forms for create, edit, and payment logging
- visible ownership badges:
PessoalandFamília - fewer competing columns and fewer low-priority fields shown above the fold
Step 11: Add navigation
Add debts to the finance sidebar near other finance modules.
Recommended order:
- Painel Financeiro
- Contas
- Despesas
- Dívidas
- Investimentos
- Análises
- Simulador
- Planejamento
This order reflects cash, obligations, assets, then planning and analysis.
Step 12: Add helpers and translations
Likely additions:
FinanceHelpermethods for debt label formatting- icon and color mapping for debt types
- I18n entries for debt types, statuses, labels, and buttons
pt-BRparity withenfor the complete debts flow, including placeholders and navigation labels
Step 13: Add seed examples
Update db/seeds.rb with debt examples for [email protected].
Seed recommendations:
- one credit-card style debt
- one loan or financing debt
- one overdue debt
- at least two payment records so the history UI is reviewable immediately
Index Page Requirements
The debts index should deliver useful information even before the user opens a detail page.
KPI Cards
Total em DívidasPagamento Mínimo MensalVencendo em 7 DiasJuros Estimados do Mês
Optional integration KPI:
Patrimônio Líquido
Main Sections
- Header with count and
Nova Dívida - KPI row
- Upcoming payments panel
- Active debts table
- Debts by type card
- Trend card
Show Page Requirements
The debt detail page should answer whether the debt is under control.
Recommended sections:
- balance and metadata summary
- next due block
- payoff progress
- recent payments table
- simple payoff estimate
Main actions:
Registrar PagamentoEditarMarcar como Quitada
Test Plan
Model Tests
test/models/debt_test.rb
Cover:
- required fields
- enum validity
- visibility scopes
- overdue detection
- remaining installments calculation
- payoff estimate behavior
- zero balance transitions to
paid_off
test/models/debt_payment_test.rb
Cover:
- payment requires positive amount
- payment belongs to debt and organization
- payment reduces debt balance
- payment cannot reduce below zero without explicit handling rule
test/models/debt/summary_calculator_test.rb
Cover:
- total debt calculation
- minimum monthly payment calculation
- due soon filtering
- grouping by type
- visibility behavior for shared versus personal debts
Controller Tests
test/controllers/organizations/debts_controller_test.rb
Cover:
- index success
- create debt
- update debt
- show debt
- mark debt as paid off
- destroy limited to admin if that rule is used
- records are organization-scoped
test/controllers/organizations/debt_payments_controller_test.rb
Cover:
- create payment
- payment updates parent balance
- invalid payment re-renders modal with
unprocessable_entity
System Tests
test/system/debts_flow_test.rb
Cover:
- create a debt from index page
- see KPIs update
- open detail page
- log payment
- see balance update and payment in history
Delivery Sequence
Use this order to keep the implementation stable:
- migrations
- models
- policy
- routes
- controllers
- helpers and translations
- views
- tests
- sidebar navigation
Acceptance Criteria
The MVP is complete when:
- users can create and edit debts
- users can log payments against debts
- total liabilities render correctly on the index page
- due-soon debts are visible without opening details
- individual and shared debts are distinguishable
- debt detail page shows payment history and payoff progress
- active debt balance remains accurate after every payment
- tests cover the core model, controller, and end-to-end flow