Recurring fixes (#454)

* Fix record violation

and add toggle for recurring feature

* Run only once per sync cycle ( 30 sec )

* FIX params passing

* Add collapsible to recurring section

* FIX preferences error catch
This commit is contained in:
soky srm
2025-12-17 16:03:05 +01:00
committed by GitHub
parent 7d5b0c425c
commit 0300bf9c24
16 changed files with 403 additions and 110 deletions

View File

@@ -29,7 +29,7 @@ class Family::SyncCompleteEvent
Rails.logger.error("Family::SyncCompleteEvent net_worth_chart broadcast failed: #{e.message}\n#{e.backtrace&.join("\n")}")
end
# Identify recurring transaction patterns after sync
# Schedule recurring transaction pattern identification (debounced to run after all syncs complete)
begin
RecurringTransaction.identify_patterns_for(family)
rescue => e

View File

@@ -37,7 +37,14 @@ class RecurringTransaction < ApplicationRecord
scope :expected_soon, -> { active.where("next_expected_date <= ?", 1.month.from_now) }
# Class methods for identification and cleanup
# Schedules pattern identification with debounce to run after all syncs complete
def self.identify_patterns_for(family)
IdentifyRecurringTransactionsJob.schedule_for(family)
0 # Return immediately, actual count will be determined by the job
end
# Synchronous pattern identification (for manual triggers from UI)
def self.identify_patterns_for!(family)
Identifier.new(family).identify_recurring_patterns
end

View File

@@ -81,35 +81,55 @@ class RecurringTransaction
find_conditions[:merchant_id] = nil
end
recurring_transaction = family.recurring_transactions.find_or_initialize_by(find_conditions)
begin
recurring_transaction = family.recurring_transactions.find_or_initialize_by(find_conditions)
# Handle manual recurring transactions specially
if recurring_transaction.persisted? && recurring_transaction.manual?
# Update variance for manual recurring transactions
update_manual_recurring_variance(recurring_transaction, pattern)
next
end
# Set the name or merchant_id on new records
if recurring_transaction.new_record?
if pattern[:merchant_id].present?
recurring_transaction.merchant_id = pattern[:merchant_id]
else
recurring_transaction.name = pattern[:name]
# Handle manual recurring transactions specially
if recurring_transaction.persisted? && recurring_transaction.manual?
# Update variance for manual recurring transactions
update_manual_recurring_variance(recurring_transaction, pattern)
next
end
# New auto-detected recurring transactions are not manual
recurring_transaction.manual = false
# Set the name or merchant_id on new records
if recurring_transaction.new_record?
if pattern[:merchant_id].present?
recurring_transaction.merchant_id = pattern[:merchant_id]
else
recurring_transaction.name = pattern[:name]
end
# New auto-detected recurring transactions are not manual
recurring_transaction.manual = false
end
recurring_transaction.assign_attributes(
expected_day_of_month: pattern[:expected_day_of_month],
last_occurrence_date: pattern[:last_occurrence_date],
next_expected_date: calculate_next_expected_date(pattern[:last_occurrence_date], pattern[:expected_day_of_month]),
occurrence_count: pattern[:occurrence_count],
status: recurring_transaction.new_record? ? "active" : recurring_transaction.status
)
recurring_transaction.save!
rescue ActiveRecord::RecordNotUnique
# Race condition: another process created the same record between find and save.
# Retry with find to get the existing record and update it.
recurring_transaction = family.recurring_transactions.find_by(find_conditions)
next unless recurring_transaction
# Skip manual recurring transactions
if recurring_transaction.manual?
update_manual_recurring_variance(recurring_transaction, pattern)
next
end
recurring_transaction.update!(
expected_day_of_month: pattern[:expected_day_of_month],
last_occurrence_date: pattern[:last_occurrence_date],
next_expected_date: calculate_next_expected_date(pattern[:last_occurrence_date], pattern[:expected_day_of_month]),
occurrence_count: pattern[:occurrence_count]
)
end
recurring_transaction.assign_attributes(
expected_day_of_month: pattern[:expected_day_of_month],
last_occurrence_date: pattern[:last_occurrence_date],
next_expected_date: calculate_next_expected_date(pattern[:last_occurrence_date], pattern[:expected_day_of_month]),
occurrence_count: pattern[:occurrence_count],
status: recurring_transaction.new_record? ? "active" : recurring_transaction.status
)
recurring_transaction.save!
end
# Also check for manual recurring transactions that might need variance updates

View File

@@ -234,6 +234,29 @@ class User < ApplicationRecord
end
end
# Transactions preferences management
def transactions_section_collapsed?(section_key)
preferences&.dig("transactions_collapsed_sections", section_key) == true
end
def update_transactions_preferences(prefs)
transaction do
lock!
updated_prefs = (preferences || {}).deep_dup
prefs.each do |key, value|
if value.is_a?(Hash)
updated_prefs["transactions_#{key}"] ||= {}
updated_prefs["transactions_#{key}"] = updated_prefs["transactions_#{key}"].merge(value)
else
updated_prefs["transactions_#{key}"] = value
end
end
update!(preferences: updated_prefs)
end
end
private
def default_dashboard_section_order
%w[cashflow_sankey outflows_donut net_worth_chart balance_sheet]