Skip to content
This repository was archived by the owner on Jul 27, 2025. It is now read-only.

Commit 307a368

Browse files
authored
Transfer and Payment auto-matching, model and UI improvements (#1585)
* Transfer data model migration * Transfers and payment modeling and UI improvements * Fix CI * Transfer matching flow * Better UI for transfers * Auto transfer matching, approve, reject flow * Mark transfers created from form as confirmed * Account filtering * Excluded rejected transfers from calculations * Calculation tweaks with transfer exclusions * Clean up migration
1 parent 46e1293 commit 307a368

File tree

78 files changed

+1156
-677
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+1156
-677
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ GEM
121121
bindex (0.8.1)
122122
bootsnap (1.18.4)
123123
msgpack (~> 1.2)
124-
brakeman (6.2.2)
124+
brakeman (7.0.0)
125125
racc
126126
builder (3.3.0)
127127
capybara (3.40.0)

app/assets/stylesheets/application.tailwind.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
@apply focus:opacity-100 focus:outline-none focus:ring-0;
3030
@apply placeholder-shown:opacity-50;
3131
@apply disabled:text-gray-400;
32+
@apply text-ellipsis overflow-hidden whitespace-nowrap;
33+
}
34+
35+
select.form-field__input {
36+
@apply pr-8;
3237
}
3338

3439
.form-field__radio {
@@ -51,10 +56,18 @@
5156
@apply border-alpha-black-200 checked:bg-gray-900 checked:ring-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900 checked:hover:bg-gray-500;
5257
}
5358

59+
[type='checkbox'].maybe-checkbox--light:disabled {
60+
@apply cursor-not-allowed opacity-80 bg-gray-50 border-gray-200 checked:bg-gray-400 checked:ring-gray-400;
61+
}
62+
5463
[type='checkbox'].maybe-checkbox--dark {
5564
@apply ring-gray-900 checked:text-white;
5665
}
5766

67+
[type='checkbox'].maybe-checkbox--dark:disabled {
68+
@apply cursor-not-allowed opacity-80 ring-gray-600;
69+
}
70+
5871
[type='checkbox'].maybe-checkbox--dark:checked {
5972
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='111827' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
6073
}

app/controllers/account/transactions_controller.rb

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,6 @@ def bulk_update
2121
redirect_back_or_to transactions_url, notice: t(".success", count: updated)
2222
end
2323

24-
def mark_transfers
25-
Current.family
26-
.entries
27-
.where(id: bulk_update_params[:entry_ids])
28-
.mark_transfers!
29-
30-
redirect_back_or_to transactions_url, notice: t(".success")
31-
end
32-
33-
def unmark_transfers
34-
Current.family
35-
.entries
36-
.where(id: bulk_update_params[:entry_ids])
37-
.update_all marked_as_transfer: false
38-
39-
redirect_back_or_to transactions_url, notice: t(".success")
40-
end
41-
4224
private
4325
def bulk_delete_params
4426
params.require(:bulk_delete).permit(entry_ids: [])
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
class Account::TransferMatchesController < ApplicationController
2+
before_action :set_entry
3+
4+
def new
5+
@accounts = Current.family.accounts.alphabetically.where.not(id: @entry.account_id)
6+
@transfer_match_candidates = @entry.transfer_match_candidates
7+
end
8+
9+
def create
10+
@transfer = build_transfer
11+
@transfer.save!
12+
@transfer.sync_account_later
13+
14+
redirect_back_or_to transactions_path, notice: t(".success")
15+
end
16+
17+
private
18+
def set_entry
19+
@entry = Current.family.entries.find(params[:transaction_id])
20+
end
21+
22+
def transfer_match_params
23+
params.require(:transfer_match).permit(:method, :matched_entry_id, :target_account_id)
24+
end
25+
26+
def build_transfer
27+
if transfer_match_params[:method] == "new"
28+
target_account = Current.family.accounts.find(transfer_match_params[:target_account_id])
29+
30+
missing_transaction = Account::Transaction.new(
31+
entry: target_account.entries.build(
32+
amount: @entry.amount * -1,
33+
currency: @entry.currency,
34+
date: @entry.date,
35+
name: "Transfer to #{@entry.amount.negative? ? @entry.account.name : target_account.name}",
36+
)
37+
)
38+
39+
transfer = Transfer.find_or_initialize_by(
40+
inflow_transaction: @entry.amount.positive? ? missing_transaction : @entry.account_transaction,
41+
outflow_transaction: @entry.amount.positive? ? @entry.account_transaction : missing_transaction
42+
)
43+
transfer.status = "confirmed"
44+
transfer
45+
else
46+
target_transaction = Current.family.entries.find(transfer_match_params[:matched_entry_id])
47+
48+
transfer = Transfer.find_or_initialize_by(
49+
inflow_transaction: @entry.amount.negative? ? @entry.account_transaction : target_transaction.account_transaction,
50+
outflow_transaction: @entry.amount.negative? ? target_transaction.account_transaction : @entry.account_transaction
51+
)
52+
transfer.status = "confirmed"
53+
transfer
54+
end
55+
end
56+
end

app/controllers/account/transfers_controller.rb

Lines changed: 0 additions & 61 deletions
This file was deleted.

app/controllers/concerns/entryable_resource.rb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,14 @@ def update
5252
respond_to do |format|
5353
format.html { redirect_back_or_to account_path(@entry.account), notice: t("account.entries.update.success") }
5454
format.turbo_stream do
55-
render turbo_stream: turbo_stream.replace(
56-
"header_account_entry_#{@entry.id}",
57-
partial: "#{entryable_type.name.underscore.pluralize}/header",
58-
locals: { entry: @entry }
59-
)
55+
render turbo_stream: [
56+
turbo_stream.replace(
57+
"header_account_entry_#{@entry.id}",
58+
partial: "#{entryable_type.name.underscore.pluralize}/header",
59+
locals: { entry: @entry }
60+
),
61+
turbo_stream.replace("account_entry_#{@entry.id}", partial: "account/entries/entry", locals: { entry: @entry })
62+
]
6063
end
6164
end
6265
else

app/controllers/transactions_controller.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ def index
66
search_query = Current.family.transactions.search(@q).includes(:entryable).reverse_chronological
77
@pagy, @transaction_entries = pagy(search_query, limit: params[:per_page] || "50")
88

9+
totals_query = search_query.incomes_and_expenses
10+
family_currency = Current.family.currency
11+
count_with_transfers = search_query.count
12+
count_without_transfers = totals_query.count
13+
914
@totals = {
10-
count: search_query.select { |t| t.currency == Current.family.currency }.count,
11-
income: search_query.income_total(Current.family.currency).abs,
12-
expense: search_query.expense_total(Current.family.currency)
15+
count: ((count_with_transfers - count_without_transfers) / 2) + count_without_transfers,
16+
income: totals_query.income_total(family_currency).abs,
17+
expense: totals_query.expense_total(family_currency)
1318
}
1419
end
1520

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
class TransfersController < ApplicationController
2+
layout :with_sidebar
3+
4+
before_action :set_transfer, only: %i[destroy show update]
5+
6+
def new
7+
@transfer = Transfer.new
8+
end
9+
10+
def show
11+
end
12+
13+
def create
14+
from_account = Current.family.accounts.find(transfer_params[:from_account_id])
15+
to_account = Current.family.accounts.find(transfer_params[:to_account_id])
16+
17+
@transfer = Transfer.from_accounts(
18+
from_account: from_account,
19+
to_account: to_account,
20+
date: transfer_params[:date],
21+
amount: transfer_params[:amount].to_d
22+
)
23+
24+
if @transfer.save
25+
@transfer.sync_account_later
26+
27+
flash[:notice] = t(".success")
28+
29+
respond_to do |format|
30+
format.html { redirect_back_or_to transactions_path }
31+
redirect_target_url = request.referer || transactions_path
32+
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, redirect_target_url) }
33+
end
34+
else
35+
render :new, status: :unprocessable_entity
36+
end
37+
end
38+
39+
def update
40+
@transfer.update!(transfer_update_params)
41+
respond_to do |format|
42+
format.html { redirect_back_or_to transactions_url, notice: t(".success") }
43+
format.turbo_stream
44+
end
45+
end
46+
47+
def destroy
48+
@transfer.destroy!
49+
redirect_back_or_to transactions_url, notice: t(".success")
50+
end
51+
52+
private
53+
def set_transfer
54+
@transfer = Transfer.find(params[:id])
55+
56+
raise ActiveRecord::RecordNotFound unless @transfer.belongs_to_family?(Current.family)
57+
end
58+
59+
def transfer_params
60+
params.require(:transfer).permit(:from_account_id, :to_account_id, :amount, :date, :name, :excluded)
61+
end
62+
63+
def transfer_update_params
64+
params.require(:transfer).permit(:notes, :status)
65+
end
66+
end

app/helpers/account/entries_helper.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ def permitted_entryable_partial_path(entry, relative_partial_path)
33
"account/entries/entryables/#{permitted_entryable_key(entry)}/#{relative_partial_path}"
44
end
55

6-
def unconfirmed_transfer?(entry)
7-
entry.marked_as_transfer? && entry.transfer.nil?
8-
end
9-
106
def transfer_entries(entries)
117
transfers = entries.select { |e| e.transfer_id.present? }
128
transfers.map(&:transfer).uniq
@@ -18,8 +14,19 @@ def entries_by_date(entries, selectable: true, totals: false)
1814
yield grouped_entries
1915
end
2016

17+
next if content.blank?
18+
2119
render partial: "account/entries/entry_group", locals: { date:, entries: grouped_entries, content:, selectable:, totals: }
22-
end.join.html_safe
20+
end.compact.join.html_safe
21+
end
22+
23+
def entry_name_detailed(entry)
24+
[
25+
entry.date,
26+
format_money(entry.amount_money),
27+
entry.account.name,
28+
entry.display_name
29+
].join(" • ")
2330
end
2431

2532
private

app/helpers/account/transfers_helper.rb

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)