1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

Merge branch 'main' of github.com:maybe-finance/maybe into zachgoll/rules-engine-v1

This commit is contained in:
Zach Gollwitzer 2025-04-14 09:16:19 -04:00
commit ba9464960b
14 changed files with 84 additions and 19 deletions

View file

@ -139,7 +139,7 @@ GEM
bigdecimal
rexml
crass (1.0.6)
csv (3.3.3)
csv (3.3.4)
date (3.4.1)
debug (1.10.0)
irb (~> 1.10)
@ -161,7 +161,7 @@ GEM
event_stream_parser (1.0.0)
faker (3.5.1)
i18n (>= 1.8.11, < 2)
faraday (2.12.2)
faraday (2.13.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
@ -477,7 +477,7 @@ GEM
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.5)
stripe (14.0.0)
stripe (15.0.0)
tailwindcss-rails (4.2.1)
railties (>= 7.0.0)
tailwindcss-ruby (~> 4.0)

View file

@ -24,7 +24,7 @@ and eventually offer a hosted version of the app for a small monthly fee.
## Maybe Hosting
There are 3 primary ways to use the Maybe app:
There are 2 primary ways to use the Maybe app:
1. Managed (easiest) - we're in alpha and release invites in our Discord
2. [Self-host with Docker](docs/hosting/docker.md)

View file

@ -2,6 +2,6 @@ class LoansController < ApplicationController
include AccountableResource
permitted_accountable_attributes(
:id, :rate_type, :interest_rate, :term_months
:id, :rate_type, :interest_rate, :term_months, :initial_balance
)
end

View file

@ -38,7 +38,7 @@ class TransfersController < ApplicationController
def update
Transfer.transaction do
update_transfer_status
update_transfer_details
update_transfer_details unless transfer_update_params[:status] == "rejected"
end
respond_to do |format|

View file

@ -34,6 +34,7 @@ class Account < ApplicationRecord
def create_and_sync(attributes)
attributes[:accountable_attributes] ||= {} # Ensure accountable is created, even if empty
account = new(attributes.merge(cash_balance: attributes[:balance]))
initial_balance = attributes.dig(:accountable_attributes, :initial_balance)&.to_d || 0
transaction do
# Create 2 valuations for new accounts to establish a value history for users to see
@ -47,7 +48,7 @@ class Account < ApplicationRecord
account.entries.build(
name: "Initial Balance",
date: 1.day.ago.to_date,
amount: 0,
amount: initial_balance,
currency: account.currency,
entryable: Account::Valuation.new
)
@ -87,11 +88,6 @@ class Account < ApplicationRecord
end
end
def original_balance
balance_amount = balances.chronological.first&.balance || balance
Money.new(balance_amount, currency)
end
def current_holdings
holdings.where(currency: currency, date: holdings.maximum(:date)).order(amount: :desc)
end
@ -99,9 +95,13 @@ class Account < ApplicationRecord
def update_with_sync!(attributes)
should_update_balance = attributes[:balance] && attributes[:balance].to_d != balance
initial_balance = attributes.dig(:accountable_attributes, :initial_balance)
should_update_initial_balance = initial_balance && initial_balance.to_d != accountable.initial_balance
transaction do
update!(attributes)
update_balance!(attributes[:balance]) if should_update_balance
update_inital_balance!(attributes[:accountable_attributes][:initial_balance]) if should_update_initial_balance
end
sync_later
@ -122,6 +122,21 @@ class Account < ApplicationRecord
end
end
def update_inital_balance!(initial_balance)
valuation = first_valuation
if valuation
valuation.update! amount: initial_balance
else
entries.create! \
date: Date.current,
name: "Initial Balance",
amount: initial_balance,
currency: currency,
entryable: Account::Valuation.new
end
end
def start_date
first_entry_date = entries.minimum(:date) || Date.current
first_entry_date - 1.day
@ -132,6 +147,14 @@ class Account < ApplicationRecord
accountable.lock_saved_attributes!
end
def first_valuation
entries.account_valuations.order(:date).first
end
def first_valuation_amount
first_valuation&.amount_money || balance_money
end
private
def sync_balances
strategy = linked? ? :reverse : :forward

View file

@ -3,20 +3,24 @@ class Loan < ApplicationRecord
def monthly_payment
return nil if term_months.nil? || interest_rate.nil? || rate_type.nil? || rate_type != "fixed"
return Money.new(0, account.currency) if account.original_balance.amount.zero? || term_months.zero?
return Money.new(0, account.currency) if account.loan.original_balance.amount.zero? || term_months.zero?
annual_rate = interest_rate / 100.0
monthly_rate = annual_rate / 12.0
if monthly_rate.zero?
payment = account.original_balance.amount / term_months
payment = account.loan.original_balance.amount / term_months
else
payment = (account.original_balance.amount * monthly_rate * (1 + monthly_rate)**term_months) / ((1 + monthly_rate)**term_months - 1)
payment = (account.loan.original_balance.amount * monthly_rate * (1 + monthly_rate)**term_months) / ((1 + monthly_rate)**term_months - 1)
end
Money.new(payment.round, account.currency)
end
def original_balance
Money.new(account.first_valuation_amount, account.currency)
end
class << self
def color
"#D444F1"

View file

@ -5,6 +5,13 @@
<div class="space-y-2">
<%= form.fields_for :accountable do |loan_form| %>
<div class="flex items-center gap-2">
<%= loan_form.money_field :initial_balance,
label: t("loans.form.initial_balance"),
default_currency: Current.family.currency,
required: true %>
</div>
<div class="flex items-center gap-2">
<%= loan_form.number_field :interest_rate,
label: t("loans.form.interest_rate"),

View file

@ -2,7 +2,7 @@
<div class="grid grid-cols-3 gap-2">
<%= summary_card title: t(".original_principal") do %>
<%= format_money account.original_balance %>
<%= format_money account.loan.original_balance %>
<% end %>
<%= summary_card title: t(".remaining_principal") do %>

View file

@ -6,6 +6,7 @@ en:
form:
interest_rate: Interest rate
interest_rate_placeholder: '5.25'
initial_balance: Original loan balance
rate_type: Rate type
term_months: Term (months)
term_months_placeholder: '360'

View file

@ -0,0 +1,5 @@
class AddInitialBalanceField < ActiveRecord::Migration[7.2]
def change
add_column :loans, :initial_balance, :decimal, precision: 19, scale: 4
end
end

2
db/schema.rb generated
View file

@ -399,7 +399,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_11_191422) do
t.string "rate_type"
t.decimal "interest_rate", precision: 10, scale: 3
t.integer "term_months"
t.jsonb "locked_attributes", default: {}
t.decimal "initial_balance", precision: 19, scale: 4
end
create_table "merchants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|

View file

@ -22,7 +22,8 @@ class LoansControllerTest < ActionDispatch::IntegrationTest
accountable_attributes: {
interest_rate: 5.5,
term_months: 60,
rate_type: "fixed"
rate_type: "fixed",
initial_balance: 50000
}
}
}
@ -36,6 +37,7 @@ class LoansControllerTest < ActionDispatch::IntegrationTest
assert_equal 5.5, created_account.accountable.interest_rate
assert_equal 60, created_account.accountable.term_months
assert_equal "fixed", created_account.accountable.rate_type
assert_equal 50000, created_account.accountable.initial_balance
assert_redirected_to created_account
assert_equal "Loan account created", flash[:notice]
@ -54,7 +56,8 @@ class LoansControllerTest < ActionDispatch::IntegrationTest
id: @account.accountable_id,
interest_rate: 4.5,
term_months: 48,
rate_type: "fixed"
rate_type: "fixed",
initial_balance: 48000
}
}
}
@ -67,6 +70,7 @@ class LoansControllerTest < ActionDispatch::IntegrationTest
assert_equal 4.5, @account.accountable.interest_rate
assert_equal 48, @account.accountable.term_months
assert_equal "fixed", @account.accountable.rate_type
assert_equal 48000, @account.accountable.initial_balance
assert_redirected_to @account
assert_equal "Loan account updated", flash[:notice]

View file

@ -41,4 +41,24 @@ class TransfersControllerTest < ActionDispatch::IntegrationTest
assert_equal "Transfer updated", flash[:notice]
assert_equal "Test notes", transfer.reload.notes
end
test "handles rejection without FrozenError" do
transfer = transfers(:one)
assert_difference "Transfer.count", -1 do
patch transfer_url(transfer), params: {
transfer: {
status: "rejected"
}
}
end
assert_redirected_to transactions_url
assert_equal "Transfer updated", flash[:notice]
# Verify the transfer was actually destroyed
assert_raises(ActiveRecord::RecordNotFound) do
transfer.reload
end
end
end

View file

@ -59,6 +59,7 @@ class AccountsTest < ApplicationSystemTestCase
test "can create loan account" do
assert_account_created "Loan" do
fill_in "account[accountable_attributes][initial_balance]", with: 1000
fill_in "Interest rate", with: 5.25
select "Fixed", from: "Rate type"
fill_in "Term (months)", with: 360