Debts MVP Database Schema And Rails File List

Database Schema

This schema follows the conventions already used by the finance module:

  • multi-tenant by organization_id
  • ownership by membership_id
  • decimal precision for financial amounts
  • enums stored as strings
  • optional account_id for linking payment source

Migration 1: Create Debts

Suggested file: db/migrate/TIMESTAMP_create_debts.rb

class CreateDebts < ActiveRecord::Migration[8.1]
  def change
    create_table :debts do |t|
      t.references :organization, null: false, foreign_key: true
      t.references :membership, null: true, foreign_key: true
      t.references :account, null: true, foreign_key: true

      t.string :name, null: false
      t.string :lender_name
      t.string :debt_type, null: false, default: "personal_loan"
      t.string :status, null: false, default: "active"
      t.string :currency, null: false, default: "BRL"
      t.decimal :original_amount, precision: 15, scale: 2, null: false, default: 0
      t.decimal :current_balance, precision: 15, scale: 2, null: false, default: 0
      t.decimal :interest_rate, precision: 8, scale: 4, null: false, default: 0
      t.string :interest_rate_period, null: false, default: "monthly"
      t.decimal :minimum_payment, precision: 15, scale: 2
      t.integer :due_day
      t.date :started_on
      t.date :ends_on
      t.integer :installment_total
      t.integer :installments_paid, null: false, default: 0
      t.text :notes

      t.timestamps
    end

    add_index :debts, [ :organization_id, :name ]
    add_index :debts, [ :organization_id, :status ]
    add_index :debts, [ :organization_id, :debt_type ]
    add_index :debts, [ :organization_id, :membership_id ]
    add_index :debts, [ :organization_id, :due_day ]
  end
end

Column Notes

  • membership_id nullable allows shared family debt.
  • account_id is optional because not every debt needs a source account.
  • original_amount and current_balance both remain in the record because users may start tracking a debt after it has already been partially repaid.
  • interest_rate uses scale 4 to support values like 1.9900.
  • due_day is better than storing a recurring due date string for loans and cards in the MVP.

Migration 2: Create Debt Payments

Suggested file: db/migrate/TIMESTAMP_create_debt_payments.rb

class CreateDebtPayments < ActiveRecord::Migration[8.1]
  def change
    create_table :debt_payments do |t|
      t.references :debt, null: false, foreign_key: true
      t.references :organization, null: false, foreign_key: true
      t.references :membership, null: false, foreign_key: true
      t.references :account, null: true, foreign_key: true

      t.date :paid_on, null: false
      t.decimal :amount, precision: 15, scale: 2, null: false, default: 0
      t.decimal :principal_amount, precision: 15, scale: 2
      t.decimal :interest_amount, precision: 15, scale: 2
      t.decimal :fee_amount, precision: 15, scale: 2
      t.text :notes

      t.timestamps
    end

    add_index :debt_payments, [ :debt_id, :paid_on ]
    add_index :debt_payments, [ :organization_id, :paid_on ]
  end
end

Schema Rules

Validation rules expected at model level:

  • original_amount > 0
  • current_balance >= 0
  • interest_rate >= 0
  • minimum_payment >= 0 when present
  • amount > 0
  • due_day must be in 1..31 when present
  • installments_paid <= installment_total when total exists

Rails File List For MVP

This file list is the exact recommended MVP footprint.

New Models

  • app/models/debt.rb
  • app/models/debt_payment.rb
  • app/models/debt/summary_calculator.rb

New Controllers

  • app/controllers/organizations/debts_controller.rb
  • app/controllers/organizations/debt_payments_controller.rb

New Policies

  • app/policies/debt_policy.rb

New 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

Existing Files To Update

  • config/routes.rb
  • app/views/shared/_sidebar_links.html.erb
  • app/helpers/finance_helper.rb
  • config/locales/en.yml
  • config/locales/pt-BR.yml
  • db/seeds.rb

Potentially, if the repo already centralizes nav translations elsewhere:

  • locale file containing nav labels

New Tests

  • test/models/debt_test.rb
  • test/models/debt_payment_test.rb
  • test/models/debt/summary_calculator_test.rb
  • test/controllers/organizations/debts_controller_test.rb
  • test/controllers/organizations/debt_payments_controller_test.rb
  • test/system/debts_flow_test.rb
  • fixture updates in test/fixtures/debts.yml
  • fixture updates in test/fixtures/debt_payments.yml

New Fixtures

  • test/fixtures/debts.yml
  • test/fixtures/debt_payments.yml

New Migrations

  • db/migrate/TIMESTAMP_create_debts.rb
  • db/migrate/TIMESTAMP_create_debt_payments.rb

Optional But Reasonable Additions

These are still compatible with the MVP if implementation needs them.

  • app/helpers/debts_helper.rb
  • app/views/organizations/debts/_debt_row.html.erb
  • app/views/organizations/debts/_kpi_cards.html.erb
  • app/views/organizations/debts/_upcoming_payments.html.erb

These partials are useful if the main index page becomes too large.

Missing MVP Coverage To Add

  • full debt copy in pt-BR so modal labels, placeholders, nav items, and empty states never render missing-translation output
  • realistic seeded debt examples for [email protected] so the module is immediately reviewable after db:seed
  • a simplified debt index and form flow that prioritizes balance, next payment, and payoff progress before secondary metadata

Minimal Parameter Contract

Debt params

params.expect(
  debt: [
    :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,
    :account_id,
    :notes,
    :shared
  ]
)

Implementation note:

  • shared can be a virtual form field that determines whether membership_id is set to nil or Current.membership.id.

DebtPayment params

params.expect(
  debt_payment: [
    :paid_on,
    :amount,
    :principal_amount,
    :interest_amount,
    :fee_amount,
    :account_id,
    :notes
  ]
)

Suggested Associations Summary

Debt

belongs_to :organization
belongs_to :membership, optional: true
belongs_to :account, optional: true
has_many :debt_payments, dependent: :destroy

DebtPayment

belongs_to :debt
belongs_to :organization
belongs_to :membership
belongs_to :account, optional: true

Suggested Enums Summary

Debt

enum :debt_type, {
  credit_card: "credit_card",
  personal_loan: "personal_loan",
  mortgage: "mortgage",
  vehicle_financing: "vehicle_financing",
  installment_plan: "installment_plan",
  tax_debt: "tax_debt",
  family_loan: "family_loan",
  other: "other"
}, default: :personal_loan

enum :status, {
  active: "active",
  paid_off: "paid_off",
  overdue: "overdue",
  renegotiated: "renegotiated",
  paused: "paused"
}, default: :active

enum :interest_rate_period, {
  monthly: "monthly",
  yearly: "yearly"
}, default: :monthly

Fixture Strategy

Recommended fixture coverage:

  • one shared family debt
  • one personal debt for the main signed-in member
  • one overdue debt
  • one paid-off debt
  • two payment records for one active debt

That is enough to exercise the full MVP surface in tests.