Class: Amortizy::Disclosure

Inherits:
Object
  • Object
show all
Defined in:
lib/amortizy/disclosure.rb

Constant Summary collapse

APR_MAX_ITERATIONS =
100
APR_CONVERGENCE_TOLERANCE =
1e-8
APR_REG_Z_TOLERANCE =

1/8 of 1 percentage point

0.00125

Instance Method Summary collapse

Constructor Details

#initialize(schedule, third_party_payments: 0, prepayment_penalty_max: 0, additional_prepayment_fees: []) ⇒ Disclosure

Returns a new instance of Disclosure.

Raises:

  • (ArgumentError)


31
32
33
34
35
36
37
38
39
# File 'lib/amortizy/disclosure.rb', line 31

def initialize(schedule, third_party_payments: 0, prepayment_penalty_max: 0,
               additional_prepayment_fees: [])
  raise ArgumentError, 'First argument must be an Amortizy::AmortizationSchedule' unless schedule.is_a?(Amortizy::AmortizationSchedule)

  @schedule = schedule
  @third_party_payments = third_party_payments.to_f
  @prepayment_penalty_max = prepayment_penalty_max.to_f
  @additional_prepayment_fees = additional_prepayment_fees
end

Instance Method Details

#amount_financedObject

— Amount Financed (§900(a)(1)(B)) — Principal + financed amounts - prepaid finance charges (origination fee).



53
54
55
56
57
58
59
# File 'lib/amortizy/disclosure.rb', line 53

def amount_financed
  @amount_financed ||= begin
    base = principal - origination_fee
    base += additional_fee if fee_treatment == :add_to_principal
    base
  end
end

#aprObject

— APR (§940) — Actuarial method per Appendix J, Regulation Z (12 CFR Part 1026). Rounded to nearest 10 basis points per §901(a)(5).



119
120
121
122
123
124
# File 'lib/amortizy/disclosure.rb', line 119

def apr
  @apr ||= begin
    raw_apr = calculate_apr
    round_to_ten_basis_points(raw_apr)
  end
end

#average_monthly_costObject

— Average Monthly Cost (§910(a)(12)) — Only for non-monthly payment frequencies.



108
109
110
111
112
113
# File 'lib/amortizy/disclosure.rb', line 108

def average_monthly_cost
  return nil if @schedule.frequency == :monthly
  return nil if term_days <= 0

  total_payment_amount / (term_days / 30.4)
end

#finance_chargeObject

— Finance Charge (§943) — Total dollar cost of the financing. Derived from the Reg Z identity:

Finance Charge = Total Payment Amount - Amount Financed

This ensures the three core disclosure values are always consistent.



46
47
48
# File 'lib/amortizy/disclosure.rb', line 46

def finance_charge
  @finance_charge ||= total_payment_amount - amount_financed
end

#payment_amountObject

— Payment Amount —



69
70
71
# File 'lib/amortizy/disclosure.rb', line 69

def payment_amount
  @schedule.payment_amount
end

#payment_frequencyObject

— Payment Frequency —



75
76
77
# File 'lib/amortizy/disclosure.rb', line 75

def payment_frequency
  @schedule.frequency
end

#prepaymentObject

— Prepayment (§910(a)(8-10)) —



128
129
130
131
132
133
134
135
# File 'lib/amortizy/disclosure.rb', line 128

def prepayment
  @prepayment ||= {
    has_non_interest_charges: @prepayment_penalty_max.positive?,
    max_non_interest_finance_charge: @prepayment_penalty_max.positive? ? @prepayment_penalty_max : nil,
    has_additional_fees: @additional_prepayment_fees.any?,
    additional_fees: @additional_prepayment_fees
  }
end

#recipient_fundsObject

— Recipient Funds (§900(a)(26)) — Net amount given directly to the borrower.



82
83
84
# File 'lib/amortizy/disclosure.rb', line 82

def recipient_funds
  amount_financed - @third_party_payments
end

#term_daysObject

— Term (§901(a)(4)) —



88
89
90
# File 'lib/amortizy/disclosure.rb', line 88

def term_days
  @term_days ||= (@schedule.end_date - disbursement_date).to_i
end

#term_displayObject



92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/amortizy/disclosure.rb', line 92

def term_display
  if term_days <= 365
    "#{term_days} days"
  else
    years = term_days / 365
    remaining_days = term_days % 365
    months = (remaining_days / 30.4).round(2)
    year_label = years == 1 ? 'year' : 'years'
    month_label = (months - 1.0).abs < 0.005 ? 'month' : 'months'
    format('%d %s, %.2f %s', years, year_label, months, month_label)
  end
end

#to_hObject

— Output Methods —



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/amortizy/disclosure.rb', line 139

def to_h
  {
    amount_financed: amount_financed.round(2),
    apr: apr,
    finance_charge: finance_charge.round(2),
    total_payment_amount: total_payment_amount.round(2),
    payment_amount: payment_amount.round(2),
    payment_frequency: payment_frequency,
    term_days: term_days,
    term_display: term_display,
    average_monthly_cost: average_monthly_cost&.round(2),
    recipient_funds: recipient_funds.round(2),
    prepayment: prepayment
  }
end

#to_labeled_hObject



155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/amortizy/disclosure.rb', line 155

def to_labeled_h
  {
    amount_financed: { label: 'Funding Provided', value: amount_financed.round(2) },
    apr: { label: 'Annual Percentage Rate (APR)', value: apr },
    finance_charge: { label: 'Finance Charge', value: finance_charge.round(2) },
    total_payment_amount: { label: 'Total Payment Amount', value: total_payment_amount.round(2) },
    payment_amount: { label: 'Payment', value: payment_amount.round(2) },
    payment_frequency: { label: 'Payment Frequency', value: payment_frequency },
    term_days: { label: 'Term', value: term_display },
    average_monthly_cost: { label: 'Average Monthly Cost', value: average_monthly_cost&.round(2) },
    recipient_funds: { label: 'Recipient Funds', value: recipient_funds.round(2) },
    prepayment: prepayment
  }
end

#total_payment_amountObject

— Total Payment Amount (§910(a)(5)) —



63
64
65
# File 'lib/amortizy/disclosure.rb', line 63

def total_payment_amount
  @schedule.total_paid
end