mirror of
https://github.com/we-promise/sure.git
synced 2026-05-07 12:54:04 +00:00
- Add `Setting.insights_enabled` feature gate at top of GenerateInsightsJob#perform - Fix race condition in upsert_insight with recursive retry on RecordNotUnique - Use Provider::Openai.effective_model instead of hardcoded DEFAULT_MODEL - Rescue Money::Currency::UnknownCurrency in currency_symbol fallback - Fix budget dedup key to stable "budget_pacing:<date>" (prevents flip-flop between at_risk/on_track) - Fix cash flow projection off-by-one: iterate (1..PROJECTION_DAYS) not (0..PROJECTION_DAYS) - Eliminate N+1 in IdleCashGenerator by precomputing active_account_ids in one SQL query - Rename all_time_high -> thirty_day_high (series is 30-day, not truly all-time) - Update net_worth_milestone i18n: title_ath -> title_30d_high, "30-day high" language - Anchor SpendingAnomalyGenerator baseline to current_period.start_date (not calendar months) - Fix spending_anomaly dedup key to use current_period.start_date instead of Date.current - Add .order(last_occurrence_date: :asc) to SubscriptionAuditGenerator for deterministic results - Stub Setting.insights_enabled in job tests; add generated_at assertion to upsert test https://claude.ai/code/session_014vY9xohpm3abSAxVxRF27a
50 lines
1.9 KiB
Ruby
50 lines
1.9 KiB
Ruby
# Identifies recurring transactions that are overdue — meaning they haven't appeared
|
|
# in the family's entries for longer than expected. This signals a subscription may
|
|
# have been cancelled, changed, or is otherwise worth reviewing.
|
|
class Insight::Generators::SubscriptionAuditGenerator < Insight::Generator
|
|
OVERDUE_DAYS = 45 # days past last_occurrence_date before we flag it
|
|
|
|
def generate
|
|
stale = family.recurring_transactions
|
|
.active
|
|
.where("last_occurrence_date < ?", OVERDUE_DAYS.days.ago.to_date)
|
|
.where("next_expected_date < ?", Date.current)
|
|
.includes(:merchant)
|
|
.order(last_occurrence_date: :asc)
|
|
.limit(5)
|
|
|
|
return [] if stale.empty?
|
|
|
|
stale.map do |rt|
|
|
display_name = rt.merchant&.name || rt.name
|
|
monthly_cost = (rt.expected_amount_avg || rt.amount).to_f.abs
|
|
|
|
metadata = {
|
|
"recurring_transaction_id" => rt.id,
|
|
"merchant_name" => display_name,
|
|
"monthly_cost" => monthly_cost.round(2),
|
|
"last_seen_date" => rt.last_occurrence_date.iso8601,
|
|
"days_overdue" => (Date.current - rt.last_occurrence_date).to_i
|
|
}
|
|
|
|
body = generate_body(
|
|
"#{display_name} (#{currency_symbol}#{monthly_cost.round(2)}/month) " \
|
|
"hasn't appeared in your transactions since #{rt.last_occurrence_date.strftime("%B %-d")}. " \
|
|
"It may have been cancelled or changed."
|
|
)
|
|
|
|
GeneratedInsight.new(
|
|
insight_type: "subscription_audit",
|
|
priority: "medium",
|
|
title: I18n.t("insights.subscription_audit.title", name: display_name),
|
|
body: body,
|
|
metadata: metadata,
|
|
currency: family.currency,
|
|
period_start: rt.last_occurrence_date,
|
|
period_end: Date.current,
|
|
dedup_key: "subscription_audit:#{rt.id}:#{Date.current.strftime("%Y-%m")}"
|
|
)
|
|
end
|
|
end
|
|
end
|