Files
sure/app/models/insight/generators/subscription_audit_generator.rb
Guillem Arias Fauste 59852ca0f3 fix(insights): respect recurring_transactions_disabled in subscription_audit (#2831)
* fix(insights): respect recurring_transactions_disabled in subscription_audit

SubscriptionAuditGenerator queried family.recurring_transactions
directly, so disabling recurring-transaction detection (Settings ->
Recurring Transactions) never stopped already-identified rows from
surfacing "recurring charge overdue" insights on the Insights feed —
the family-wide flag was already checked at both call sites in
IdentifyRecurringTransactionsJob, just not here.

Returning [] early is enough for existing insights to self-clean up:
produced_types is a class-level declaration, so GenerateInsightsJob
still counts subscription_audit as a succeeded type and expires any
insight whose dedup_key wasn't regenerated on the next nightly run.

* docs(insights): document why cash_flow_warning skips the recurring-disabled guard

Answers jjmata's open review question. Unlike SubscriptionAuditGenerator,
recurring transactions here are one input into a broader cash-flow
projection, not the insight's entire subject — so it intentionally
keeps using the last-known identified set rather than gating on
family.recurring_transactions_disabled?. No behavior change.
2026-08-01 08:47:50 +02:00

53 lines
1.8 KiB
Ruby

# Surfaces recurring expenses that are well past their expected date — the
# provider may have raised the price, the charge may have moved, or the user
# may be paying for something that quietly stopped (or should stop).
class Insight::Generators::SubscriptionAuditGenerator < Insight::Generator
produces "subscription_audit"
OVERDUE_DAYS = 45
MAX_INSIGHTS = 3
def generate
return [] if family.recurring_transactions_disabled?
overdue_recurring.map do |recurring|
name = recurring.merchant&.name.presence || recurring.name
amount = Money.new(recurring.amount, recurring.currency).format
days_overdue = (Date.current - recurring.next_expected_date).to_i
build_insight(
insight_type: "subscription_audit",
priority: "medium",
title: I18n.t("insights.titles.subscription_audit", name: name),
template_key: "subscription_audit",
facts: {
name: name,
amount: amount,
days_overdue: days_overdue,
expected_on: I18n.l(recurring.next_expected_date)
},
# days_overdue is deliberately left out: it changes every night, and
# metadata drift would resurrect insights the user already dismissed.
metadata: {
recurring_transaction_id: recurring.id,
amount: round(recurring.amount, 2),
expected_on: recurring.next_expected_date.iso8601
},
dedup_key: "subscription_audit:#{recurring.id}"
)
end
end
private
def overdue_recurring
family.recurring_transactions
.includes(:merchant)
.active
.where(destination_account_id: nil)
.where("amount > 0")
.where("next_expected_date < ?", OVERDUE_DAYS.days.ago.to_date)
.order(:next_expected_date)
.limit(MAX_INSIGHTS)
end
end