mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-21 14:19:39 +02:00
Add partial account sync support (#653)
* Add partial account sync support * Fix indentation * Use historical balance as base when doing partial sync * Simplify controller crud tests * Fix linter errors
This commit is contained in:
parent
b3f8ab78d9
commit
ad4de99f1a
9 changed files with 260 additions and 111 deletions
|
@ -75,7 +75,7 @@ class TransactionsController < ApplicationController
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @transaction.save
|
if @transaction.save
|
||||||
@transaction.account.sync_later
|
@transaction.account.sync_later(@transaction.date)
|
||||||
format.html { redirect_to transactions_url, notice: t(".success") }
|
format.html { redirect_to transactions_url, notice: t(".success") }
|
||||||
else
|
else
|
||||||
format.html { render :new, status: :unprocessable_entity }
|
format.html { render :new, status: :unprocessable_entity }
|
||||||
|
@ -85,8 +85,9 @@ class TransactionsController < ApplicationController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
sync_start_date = [ @transaction.date, Date.parse(transaction_params[:date]) ].compact.min
|
||||||
if @transaction.update(transaction_params)
|
if @transaction.update(transaction_params)
|
||||||
@transaction.account.sync_later
|
@transaction.account.sync_later(sync_start_date)
|
||||||
|
|
||||||
format.html { redirect_to transaction_url(@transaction), notice: t(".success") }
|
format.html { redirect_to transaction_url(@transaction), notice: t(".success") }
|
||||||
format.turbo_stream do
|
format.turbo_stream do
|
||||||
|
@ -102,8 +103,10 @@ class TransactionsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@account = @transaction.account
|
||||||
|
sync_start_date = @account.transactions.where("date < ?", @transaction.date).order(date: :desc).first&.date
|
||||||
@transaction.destroy!
|
@transaction.destroy!
|
||||||
@transaction.account.sync_later
|
@account.sync_later(sync_start_date)
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to transactions_url, notice: t(".success") }
|
format.html { redirect_to transactions_url, notice: t(".success") }
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
class ValuationsController < ApplicationController
|
class ValuationsController < ApplicationController
|
||||||
|
before_action :set_valuation, only: %i[ edit update destroy ]
|
||||||
def create
|
def create
|
||||||
@account = Current.family.accounts.find(params[:account_id])
|
@account = Current.family.accounts.find(params[:account_id])
|
||||||
|
|
||||||
# TODO: placeholder logic until we have a better abstraction for trends
|
# TODO: placeholder logic until we have a better abstraction for trends
|
||||||
@valuation = @account.valuations.new(valuation_params.merge(currency: Current.family.currency))
|
@valuation = @account.valuations.new(valuation_params.merge(currency: Current.family.currency))
|
||||||
if @valuation.save
|
if @valuation.save
|
||||||
@valuation.account.sync_later
|
@valuation.account.sync_later(@valuation.date)
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to account_path(@account), notice: "Valuation created" }
|
format.html { redirect_to account_path(@account), notice: "Valuation created" }
|
||||||
|
@ -24,13 +25,12 @@ class ValuationsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
@valuation = Valuation.find(params[:id])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@valuation = Valuation.find(params[:id])
|
sync_start_date = [ @valuation.date, Date.parse(valuation_params[:date]) ].compact.min
|
||||||
if @valuation.update(valuation_params)
|
if @valuation.update(valuation_params)
|
||||||
@valuation.account.sync_later
|
@valuation.account.sync_later(sync_start_date)
|
||||||
|
|
||||||
redirect_to account_path(@valuation.account), notice: "Valuation updated"
|
redirect_to account_path(@valuation.account), notice: "Valuation updated"
|
||||||
else
|
else
|
||||||
|
@ -42,10 +42,10 @@ class ValuationsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@valuation = Valuation.find(params[:id])
|
|
||||||
@account = @valuation.account
|
@account = @valuation.account
|
||||||
|
sync_start_date = @account.valuations.where("date < ?", @valuation.date).order(date: :desc).first&.date
|
||||||
@valuation.destroy!
|
@valuation.destroy!
|
||||||
@account.sync_later
|
@account.sync_later(sync_start_date)
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to account_path(@account), notice: "Valuation deleted" }
|
format.html { redirect_to account_path(@account), notice: "Valuation deleted" }
|
||||||
|
@ -59,6 +59,11 @@ class ValuationsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
# Use callbacks to share common setup or constraints between actions.
|
||||||
|
def set_valuation
|
||||||
|
@valuation = Valuation.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
def valuation_params
|
def valuation_params
|
||||||
params.require(:valuation).permit(:date, :value)
|
params.require(:valuation).permit(:date, :value)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class AccountSyncJob < ApplicationJob
|
class AccountSyncJob < ApplicationJob
|
||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
def perform(account)
|
def perform(account, start_date = nil)
|
||||||
account.sync
|
account.sync(start_date)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,7 +25,7 @@ class Account::Balance::Calculator
|
||||||
|
|
||||||
prior_balance = current_balance
|
prior_balance = current_balance
|
||||||
|
|
||||||
{ date: date, balance: current_balance, currency: @account.currency, updated_at: Time.current }
|
{ date:, balance: current_balance, currency: @account.currency, updated_at: Time.current }
|
||||||
end
|
end
|
||||||
|
|
||||||
@daily_balances = [
|
@daily_balances = [
|
||||||
|
@ -92,11 +92,15 @@ class Account::Balance::Calculator
|
||||||
end
|
end
|
||||||
|
|
||||||
def implied_start_balance
|
def implied_start_balance
|
||||||
|
if @calc_start_date > @account.effective_start_date
|
||||||
|
return @account.balance_on(@calc_start_date)
|
||||||
|
end
|
||||||
|
|
||||||
oldest_valuation_date = normalized_valuations.first&.dig("date")
|
oldest_valuation_date = normalized_valuations.first&.dig("date")
|
||||||
oldest_transaction_date = normalized_transactions.first&.dig("date")
|
oldest_transaction_date = normalized_transactions.first&.dig("date")
|
||||||
oldest_entry_date = [ oldest_valuation_date, oldest_transaction_date ].compact.min
|
oldest_entry_date = [ oldest_valuation_date, oldest_transaction_date ].compact.min
|
||||||
|
|
||||||
if oldest_entry_date == oldest_valuation_date
|
if oldest_entry_date.present? && oldest_entry_date == oldest_valuation_date
|
||||||
oldest_valuation = normalized_valuations.find { |v| v["date"] == oldest_valuation_date }
|
oldest_valuation = normalized_valuations.find { |v| v["date"] == oldest_valuation_date }
|
||||||
oldest_valuation["value"].to_d
|
oldest_valuation["value"].to_d
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
module Account::Syncable
|
module Account::Syncable
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
def sync_later
|
def sync_later(start_date = nil)
|
||||||
AccountSyncJob.perform_later self
|
AccountSyncJob.perform_later(self, start_date)
|
||||||
end
|
end
|
||||||
|
|
||||||
def sync
|
def sync(start_date = nil)
|
||||||
update!(status: "syncing")
|
update!(status: "syncing")
|
||||||
|
|
||||||
sync_exchange_rates
|
sync_exchange_rates
|
||||||
|
|
||||||
calculator = Account::Balance::Calculator.new(self)
|
calc_start_date = start_date - 1.day if start_date.present? && self.balance_on(start_date - 1.day).present?
|
||||||
|
|
||||||
|
calculator = Account::Balance::Calculator.new(self, { calc_start_date: })
|
||||||
calculator.calculate
|
calculator.calculate
|
||||||
self.balances.upsert_all(calculator.daily_balances, unique_by: :index_account_balances_on_account_id_date_currency_unique)
|
self.balances.upsert_all(calculator.daily_balances, unique_by: :index_account_balances_on_account_id_date_currency_unique)
|
||||||
self.balances.where("date < ?", effective_start_date).delete_all
|
self.balances.where("date < ?", effective_start_date).delete_all
|
||||||
|
|
|
@ -4,6 +4,7 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
setup do
|
setup do
|
||||||
sign_in @user = users(:family_admin)
|
sign_in @user = users(:family_admin)
|
||||||
@transaction = transactions(:checking_one)
|
@transaction = transactions(:checking_one)
|
||||||
|
@account = @transaction.account
|
||||||
end
|
end
|
||||||
|
|
||||||
test "should get index" do
|
test "should get index" do
|
||||||
|
@ -31,6 +32,12 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_redirected_to transactions_url
|
assert_redirected_to transactions_url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "create should sync account with correct start date" do
|
||||||
|
assert_enqueued_with(job: AccountSyncJob, args: [ @account, @transaction.date ]) do
|
||||||
|
post transactions_url, params: { transaction: { account_id: @transaction.account_id, amount: @transaction.amount, currency: @transaction.currency, date: @transaction.date, name: @transaction.name } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "creation preserves decimals" do
|
test "creation preserves decimals" do
|
||||||
assert_difference("Transaction.count") do
|
assert_difference("Transaction.count") do
|
||||||
post transactions_url, params: { transaction: {
|
post transactions_url, params: { transaction: {
|
||||||
|
@ -91,6 +98,18 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_redirected_to transaction_url(@transaction)
|
assert_redirected_to transaction_url(@transaction)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "update should sync account with correct start date" do
|
||||||
|
new_date = @transaction.date - 1.day
|
||||||
|
assert_enqueued_with(job: AccountSyncJob, args: [ @account, new_date ]) do
|
||||||
|
patch transaction_url(@transaction), params: { transaction: { account_id: @transaction.account_id, amount: @transaction.amount, currency: @transaction.currency, date: new_date, name: @transaction.name } }
|
||||||
|
end
|
||||||
|
|
||||||
|
new_date = @transaction.reload.date + 1.day
|
||||||
|
assert_enqueued_with(job: AccountSyncJob, args: [ @account, @transaction.date ]) do
|
||||||
|
patch transaction_url(@transaction), params: { transaction: { account_id: @transaction.account_id, amount: @transaction.amount, currency: @transaction.currency, date: new_date, name: @transaction.name } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "should destroy transaction" do
|
test "should destroy transaction" do
|
||||||
assert_difference("Transaction.count", -1) do
|
assert_difference("Transaction.count", -1) do
|
||||||
delete transaction_url(@transaction)
|
delete transaction_url(@transaction)
|
||||||
|
@ -98,4 +117,16 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
|
||||||
assert_redirected_to transactions_url
|
assert_redirected_to transactions_url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "destroy should sync account with correct start date" do
|
||||||
|
first, second = @transaction.account.transactions.order(:date).all
|
||||||
|
|
||||||
|
assert_enqueued_with(job: AccountSyncJob, args: [ @account, first.date ]) do
|
||||||
|
delete transaction_url(second)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_enqueued_with(job: AccountSyncJob, args: [ @account, nil ]) do
|
||||||
|
delete transaction_url(first)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,8 @@ require "test_helper"
|
||||||
class ValuationsControllerTest < ActionDispatch::IntegrationTest
|
class ValuationsControllerTest < ActionDispatch::IntegrationTest
|
||||||
setup do
|
setup do
|
||||||
sign_in @user = users(:family_admin)
|
sign_in @user = users(:family_admin)
|
||||||
@account = accounts(:checking)
|
@valuation = valuations(:savings_one)
|
||||||
|
@account = @valuation.account
|
||||||
end
|
end
|
||||||
|
|
||||||
test "new" do
|
test "new" do
|
||||||
|
@ -11,9 +12,55 @@ class ValuationsControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_response :success
|
assert_response :success
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create" do
|
test "should create valuation" do
|
||||||
assert_difference("Valuation.count") do
|
assert_difference("Valuation.count") do
|
||||||
post account_valuations_url(@account), params: { valuation: { value: 1, date: Date.current, type: "Appraisal" } }
|
post account_valuations_url(@account), params: { valuation: { value: 1, date: Date.current, type: "Appraisal" } }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "create should sync account with correct start date" do
|
||||||
|
date = Date.current - 1.day
|
||||||
|
|
||||||
|
assert_enqueued_with(job: AccountSyncJob, args: [ @account, date ]) do
|
||||||
|
post account_valuations_url(@account), params: { valuation: { value: 2, date:, type: "Appraisal" } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should update valuation" do
|
||||||
|
date = @valuation.date
|
||||||
|
patch valuation_url(@valuation), params: { valuation: { account_id: @valuation.account_id, value: 1, date:, type: "Appraisal" } }
|
||||||
|
assert_redirected_to account_path(@valuation.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "update should sync account with correct start date" do
|
||||||
|
new_date = @valuation.date - 1.day
|
||||||
|
assert_enqueued_with(job: AccountSyncJob, args: [ @account, new_date ]) do
|
||||||
|
patch valuation_url(@valuation), params: { valuation: { account_id: @valuation.account_id, value: @valuation.value, date: new_date, type: "Appraisal" } }
|
||||||
|
end
|
||||||
|
|
||||||
|
new_date = @valuation.reload.date + 1.day
|
||||||
|
assert_enqueued_with(job: AccountSyncJob, args: [ @account, @valuation.date ]) do
|
||||||
|
patch valuation_url(@valuation), params: { valuation: { account_id: @valuation.account_id, value: @valuation.value, date: new_date, type: "Appraisal" } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should destroy valuation" do
|
||||||
|
assert_difference("Valuation.count", -1) do
|
||||||
|
delete valuation_url(@valuation)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to account_path(@account)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "destroy should sync account with correct start date" do
|
||||||
|
first, second = @account.valuations.order(:date).all
|
||||||
|
|
||||||
|
assert_enqueued_with(job: AccountSyncJob, args: [ @account, first.date ]) do
|
||||||
|
delete valuation_url(second)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_enqueued_with(job: AccountSyncJob, args: [ @account, nil ]) do
|
||||||
|
delete valuation_url(first)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -95,4 +95,18 @@ class Account::Balance::CalculatorTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
assert_equal expected_balances, actual_balances
|
assert_equal expected_balances, actual_balances
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "syncs with overridden start date" do
|
||||||
|
account = accounts(:multi_currency)
|
||||||
|
account.sync
|
||||||
|
calc_start_date = 10.days.ago.to_date
|
||||||
|
calculator = Account::Balance::Calculator.new(account, { calc_start_date: })
|
||||||
|
calculator.calculate
|
||||||
|
|
||||||
|
expected_balances = @expected_balances.filter { |row| row["date"] >= calc_start_date }.map { |row| row["multi_currency"].to_d }
|
||||||
|
|
||||||
|
actual_balances = calculator.daily_balances.map { |b| b[:balance] }
|
||||||
|
|
||||||
|
assert_equal expected_balances, actual_balances
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,49 @@ class Account::SyncableTest < ActiveSupport::TestCase
|
||||||
assert_equal 31, account.balances.count
|
assert_equal 31, account.balances.count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "partial sync with missing historical balances performs a full sync" do
|
||||||
|
account = accounts(:savings_with_valuation_overrides)
|
||||||
|
account.sync 10.days.ago.to_date
|
||||||
|
|
||||||
|
assert_equal 31, account.balances.count
|
||||||
|
end
|
||||||
|
|
||||||
|
test "balances are updated after syncing" do
|
||||||
|
account = accounts(:savings_with_valuation_overrides)
|
||||||
|
balance_date = 10.days.ago
|
||||||
|
account.balances.create!(date: balance_date, balance: 1000)
|
||||||
|
account.sync
|
||||||
|
|
||||||
|
assert_equal 19500, account.balances.find_by(date: balance_date)[:balance]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "balances before sync start date are not updated after syncing" do
|
||||||
|
account = accounts(:savings_with_valuation_overrides)
|
||||||
|
balance_date = 10.days.ago
|
||||||
|
account.balances.create!(date: balance_date, balance: 1000)
|
||||||
|
account.sync 5.days.ago.to_date
|
||||||
|
|
||||||
|
assert_equal 1000, account.balances.find_by(date: balance_date)[:balance]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "balances after sync start date are updated after syncing" do
|
||||||
|
account = accounts(:savings_with_valuation_overrides)
|
||||||
|
balance_date = 10.days.ago
|
||||||
|
account.balances.create!(date: balance_date, balance: 1000)
|
||||||
|
account.sync 20.days.ago.to_date
|
||||||
|
|
||||||
|
assert_equal 19500, account.balances.find_by(date: balance_date)[:balance]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "balance on the sync date is updated after syncing" do
|
||||||
|
account = accounts(:savings_with_valuation_overrides)
|
||||||
|
balance_date = 5.days.ago
|
||||||
|
account.balances.create!(date: balance_date, balance: 1000)
|
||||||
|
account.sync balance_date.to_date
|
||||||
|
|
||||||
|
assert_equal 19700, account.balances.find_by(date: balance_date)[:balance]
|
||||||
|
end
|
||||||
|
|
||||||
test "foreign currency account has balances in each currency after syncing" do
|
test "foreign currency account has balances in each currency after syncing" do
|
||||||
account = accounts(:eur_checking)
|
account = accounts(:eur_checking)
|
||||||
account.sync
|
account.sync
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue