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 membership or 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-BR translation 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

  1. A debt belongs to an organization.
  2. A debt may belong to a membership or be shared with the family.
  3. A payment belongs to a debt and reduces the debt balance.
  4. A debt with zero balance becomes paid_off.
  5. minimum_payment is advisory for forecasting and KPIs.
  6. Overdue state is determined from due date behavior plus active status.
  7. 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, other
  • status: active, paid_off, overdue, renegotiated, paused
  • interest_rate_period: monthly, yearly

High-value fields:

  • organization_id
  • membership_id, nullable for shared debt
  • account_id, optional payment source account
  • name
  • lender_name
  • debt_type
  • status
  • currency
  • original_amount
  • current_balance
  • interest_rate
  • interest_rate_period
  • minimum_payment
  • due_day
  • started_on
  • ends_on
  • installment_total
  • installments_paid
  • notes

DebtPayment

Represents a payment event against a debt.

Fields:

  • debt_id
  • organization_id
  • membership_id
  • account_id, optional
  • paid_on
  • amount
  • principal_amount, optional in MVP
  • interest_amount, optional in MVP
  • fee_amount, optional in MVP
  • notes

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: nil means shared family debt
  • membership_id: Current.membership.id means 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:

  • organization required
  • membership nullable
  • account optional
  • 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, and membership required
  • account optional
  • 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:

  1. constants
  2. ransack configuration if needed
  3. associations
  4. attributes
  5. normalization
  6. enums
  7. validations
  8. scopes
  9. callbacks
  10. 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_only
  • overdue_only
  • paid_off_only
  • shared
  • owned_by(membership)
  • visible_to(membership)
  • due_within(days)

Recommended helpers:

  • shared?
  • paid_off?
  • overdue?
  • next_due_date(reference_date = Date.current)
  • remaining_installments
  • estimated_monthly_interest
  • estimated_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_debts
  • active_debts
  • total_debt
  • minimum_monthly_payment
  • due_soon_total(days: 7)
  • due_soon_count(days: 7)
  • estimated_monthly_interest
  • by_type
  • upcoming_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 present
  • show?: membership present
  • create?: membership present
  • update?: membership present
  • destroy?: membership admin only
  • mark_as_paid_off?: membership present

For nested payment creation, either:

  • authorize the parent debt through DebtPolicy, or
  • add DebtPaymentPolicy later if permissions diverge

Step 8: Add controller for debts

Create Organizations::DebtsController.

Recommended actions:

  • index
  • show
  • new
  • create
  • edit
  • update
  • destroy
  • mark_as_paid_off

Controller responsibilities:

  • load organization-scoped records only
  • authorize collection and member actions
  • set membership on 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 organization and membership
  • 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.erb
  • app/views/organizations/debts/show.html.erb
  • app/views/organizations/debts/_form.html.erb
  • app/views/organizations/debts/new.html.erb
  • app/views/organizations/debts/edit.html.erb
  • app/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: Pessoal and Famí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:

  1. Painel Financeiro
  2. Contas
  3. Despesas
  4. Dívidas
  5. Investimentos
  6. Análises
  7. Simulador
  8. Planejamento

This order reflects cash, obligations, assets, then planning and analysis.

Step 12: Add helpers and translations

Likely additions:

  • FinanceHelper methods for debt label formatting
  • icon and color mapping for debt types
  • I18n entries for debt types, statuses, labels, and buttons
  • pt-BR parity with en for 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ívidas
  • Pagamento Mínimo Mensal
  • Vencendo em 7 Dias
  • Juros Estimados do Mês

Optional integration KPI:

  • Patrimônio Líquido

Main Sections

  1. Header with count and Nova Dívida
  2. KPI row
  3. Upcoming payments panel
  4. Active debts table
  5. Debts by type card
  6. 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 Pagamento
  • Editar
  • Marcar 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:

  1. migrations
  2. models
  3. policy
  4. routes
  5. controllers
  6. helpers and translations
  7. views
  8. tests
  9. 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