mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
Recurring scoping implementation (#1300)
* Recurring scoping implementation * FIX tests and reviews
This commit is contained in:
@@ -3,6 +3,7 @@ class RecurringTransactionsController < ApplicationController
|
||||
|
||||
def index
|
||||
@recurring_transactions = Current.family.recurring_transactions
|
||||
.accessible_by(Current.user)
|
||||
.includes(:merchant)
|
||||
.order(status: :asc, next_expected_date: :asc)
|
||||
@family = Current.family
|
||||
@@ -42,7 +43,7 @@ class RecurringTransactionsController < ApplicationController
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
@recurring_transaction = Current.family.recurring_transactions.find(params[:id])
|
||||
@recurring_transaction = Current.family.recurring_transactions.accessible_by(Current.user).find(params[:id])
|
||||
|
||||
if @recurring_transaction.active?
|
||||
@recurring_transaction.mark_inactive!
|
||||
@@ -61,7 +62,7 @@ class RecurringTransactionsController < ApplicationController
|
||||
end
|
||||
|
||||
def destroy
|
||||
@recurring_transaction = Current.family.recurring_transactions.find(params[:id])
|
||||
@recurring_transaction = Current.family.recurring_transactions.accessible_by(Current.user).find(params[:id])
|
||||
@recurring_transaction.destroy!
|
||||
|
||||
flash[:notice] = t("recurring_transactions.deleted")
|
||||
|
||||
@@ -54,6 +54,7 @@ class TransactionsController < ApplicationController
|
||||
|
||||
# Load projected recurring transactions for next 10 days
|
||||
@projected_recurring = Current.family.recurring_transactions
|
||||
.accessible_by(Current.user)
|
||||
.active
|
||||
.where("next_expected_date <= ? AND next_expected_date >= ?",
|
||||
10.days.from_now.to_date,
|
||||
@@ -304,6 +305,7 @@ class TransactionsController < ApplicationController
|
||||
|
||||
# Check if a recurring transaction already exists for this pattern
|
||||
existing = Current.family.recurring_transactions.find_by(
|
||||
account_id: transaction.entry.account_id,
|
||||
merchant_id: transaction.merchant_id,
|
||||
name: transaction.merchant_id.present? ? nil : transaction.entry.name,
|
||||
currency: transaction.entry.currency,
|
||||
|
||||
@@ -19,6 +19,7 @@ class Account < ApplicationRecord
|
||||
has_many :trades, through: :entries, source: :entryable, source_type: "Trade"
|
||||
has_many :holdings, dependent: :destroy
|
||||
has_many :balances, dependent: :destroy
|
||||
has_many :recurring_transactions, dependent: :destroy
|
||||
|
||||
monetize :balance, :cash_balance
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ class RecurringTransaction < ApplicationRecord
|
||||
include Monetizable
|
||||
|
||||
belongs_to :family
|
||||
belongs_to :account, optional: true
|
||||
belongs_to :merchant, optional: true
|
||||
|
||||
monetize :amount
|
||||
@@ -35,6 +36,9 @@ class RecurringTransaction < ApplicationRecord
|
||||
|
||||
scope :for_family, ->(family) { where(family: family) }
|
||||
scope :expected_soon, -> { active.where("next_expected_date <= ?", 1.month.from_now) }
|
||||
scope :accessible_by, ->(user) {
|
||||
where(account_id: Account.accessible_by(user).select(:id)).or(where(account_id: nil))
|
||||
}
|
||||
|
||||
# Class methods for identification and cleanup
|
||||
# Schedules pattern identification with debounce to run after all syncs complete
|
||||
@@ -66,7 +70,8 @@ class RecurringTransaction < ApplicationRecord
|
||||
name: transaction.merchant_id.present? ? nil : entry.name,
|
||||
currency: entry.currency,
|
||||
expected_day: expected_day,
|
||||
lookback_months: 6
|
||||
lookback_months: 6,
|
||||
account: entry.account
|
||||
)
|
||||
|
||||
# Calculate amount variance from historical data
|
||||
@@ -89,6 +94,7 @@ class RecurringTransaction < ApplicationRecord
|
||||
|
||||
create!(
|
||||
family: family,
|
||||
account: entry.account,
|
||||
merchant_id: transaction.merchant_id,
|
||||
name: transaction.merchant_id.present? ? nil : entry.name,
|
||||
amount: entry.amount,
|
||||
@@ -106,10 +112,10 @@ class RecurringTransaction < ApplicationRecord
|
||||
end
|
||||
|
||||
# Find matching transaction entries for variance calculation
|
||||
def self.find_matching_transaction_entries(family:, merchant_id:, name:, currency:, expected_day:, lookback_months: 6)
|
||||
def self.find_matching_transaction_entries(family:, merchant_id:, name:, currency:, expected_day:, lookback_months: 6, account: nil)
|
||||
lookback_date = lookback_months.months.ago.to_date
|
||||
|
||||
entries = family.entries
|
||||
entries = (account.present? ? account.entries : family.entries)
|
||||
.where(entryable_type: "Transaction")
|
||||
.where(currency: currency)
|
||||
.where("entries.date >= ?", lookback_date)
|
||||
@@ -131,14 +137,15 @@ class RecurringTransaction < ApplicationRecord
|
||||
end
|
||||
|
||||
# Find matching transaction amounts for variance calculation
|
||||
def self.find_matching_transaction_amounts(family:, merchant_id:, name:, currency:, expected_day:, lookback_months: 6)
|
||||
def self.find_matching_transaction_amounts(family:, merchant_id:, name:, currency:, expected_day:, lookback_months: 6, account: nil)
|
||||
matching_entries = find_matching_transaction_entries(
|
||||
family: family,
|
||||
merchant_id: merchant_id,
|
||||
name: name,
|
||||
currency: currency,
|
||||
expected_day: expected_day,
|
||||
lookback_months: lookback_months
|
||||
lookback_months: lookback_months,
|
||||
account: account
|
||||
)
|
||||
|
||||
matching_entries.map(&:amount)
|
||||
@@ -173,8 +180,10 @@ class RecurringTransaction < ApplicationRecord
|
||||
def matching_transactions
|
||||
# For manual recurring with amount variance, match within range
|
||||
# For automatic recurring, match exact amount
|
||||
base = account.present? ? account.entries : family.entries
|
||||
|
||||
entries = if manual? && has_amount_variance?
|
||||
family.entries
|
||||
base
|
||||
.where(entryable_type: "Transaction")
|
||||
.where(currency: currency)
|
||||
.where("entries.amount BETWEEN ? AND ?", expected_amount_min, expected_amount_max)
|
||||
@@ -183,7 +192,7 @@ class RecurringTransaction < ApplicationRecord
|
||||
[ expected_day_of_month + 2, 31 ].min)
|
||||
.order(date: :desc)
|
||||
else
|
||||
family.entries
|
||||
base
|
||||
.where(entryable_type: "Transaction")
|
||||
.where(currency: currency)
|
||||
.where("entries.amount = ?", amount)
|
||||
|
||||
@@ -24,12 +24,12 @@ class RecurringTransaction
|
||||
transaction = entry.entryable
|
||||
# Use merchant_id if present, otherwise use entry name
|
||||
identifier = transaction.merchant_id.present? ? [ :merchant, transaction.merchant_id ] : [ :name, entry.name ]
|
||||
[ identifier, entry.amount.round(2), entry.currency ]
|
||||
[ identifier, entry.amount.round(2), entry.currency, entry.account_id ]
|
||||
end
|
||||
|
||||
recurring_patterns = []
|
||||
|
||||
grouped_transactions.each do |(identifier, amount, currency), entries|
|
||||
grouped_transactions.each do |(identifier, amount, currency, account_id), entries|
|
||||
next if entries.size < 3 # Must have at least 3 occurrences
|
||||
|
||||
# Check if the last occurrence was within the last 45 days
|
||||
@@ -49,6 +49,7 @@ class RecurringTransaction
|
||||
pattern = {
|
||||
amount: amount,
|
||||
currency: currency,
|
||||
account_id: account_id,
|
||||
expected_day_of_month: expected_day,
|
||||
last_occurrence_date: last_occurrence.date,
|
||||
occurrence_count: entries.size,
|
||||
@@ -70,7 +71,8 @@ class RecurringTransaction
|
||||
# Build find conditions based on whether it's merchant-based or name-based
|
||||
find_conditions = {
|
||||
amount: pattern[:amount],
|
||||
currency: pattern[:currency]
|
||||
currency: pattern[:currency],
|
||||
account_id: pattern[:account_id]
|
||||
}
|
||||
|
||||
if pattern[:merchant_id].present?
|
||||
@@ -148,7 +150,8 @@ class RecurringTransaction
|
||||
name: recurring.name,
|
||||
currency: recurring.currency,
|
||||
expected_day: recurring.expected_day_of_month,
|
||||
lookback_months: 6
|
||||
lookback_months: 6,
|
||||
account: recurring.account
|
||||
)
|
||||
|
||||
next if matching_entries.empty?
|
||||
@@ -180,7 +183,8 @@ class RecurringTransaction
|
||||
name: recurring_transaction.name,
|
||||
currency: recurring_transaction.currency,
|
||||
expected_day: recurring_transaction.expected_day_of_month,
|
||||
lookback_months: 6
|
||||
lookback_months: 6,
|
||||
account: recurring_transaction.account
|
||||
)
|
||||
|
||||
# Update if we have any matching transactions
|
||||
|
||||
Reference in New Issue
Block a user