1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-18 20:59:39 +02:00
Maybe/lib/tasks/data_migration.rake
Zach Gollwitzer c1d98fe73b
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions
Start and end balance anchors for historical account balances (#2455)
* Add kind field to valuation

* Fix schema conflict

* Add kind to valuation

* Scaffold opening balance manager

* Opening balance manager implementation

* Update account import to use opening balance manager + tests

* Update account to use opening balance manager

* Fix test assertions, usage of current balance manager

* Lint fixes

* Add Opening Balance manager, add tests to forward calculator

* Add credit card to "all cash" designation

* Simplify valuation model

* Add current balance manager with tests

* Add current balance logic to reverse calculator and plaid sync

* Tweaks to initial calc logic

* Ledger testing helper, tweak assertions for reverse calculator

* Update test assertions

* Extract balance transformer, simplify calculators

* Algo simplifications

* Final tweaks to calculators

* Cleanup

* Fix error, propagate sync errors up to parent

* Update migration script, valuation naming
2025-07-15 11:42:41 -04:00

157 lines
6.1 KiB
Ruby

namespace :data_migration do
desc "Migrate EU Plaid webhooks"
# 2025-02-07: EU Plaid items need to be moved over to a new webhook URL so that we can
# instantiate the correct Plaid client for verification based on which Plaid instance it comes from
task eu_plaid_webhooks: :environment do
provider = Provider::Plaid.new(Rails.application.config.plaid_eu, region: :eu)
eu_items = PlaidItem.where(plaid_region: "eu")
eu_items.find_each do |item|
request = Plaid::ItemWebhookUpdateRequest.new(
access_token: item.access_token,
webhook: "https://app.maybefinance.com/webhooks/plaid_eu"
)
provider.client.item_webhook_update(request)
puts "Updated webhook for Plaid item #{item.plaid_id}"
rescue => error
puts "Error updating webhook for Plaid item #{item.plaid_id}: #{error.message}"
end
end
desc "Migrate duplicate securities"
# 2025-05-22: older data allowed multiple rows with the same
# ticker / exchange_operating_mic (case-insensitive, NULLs collapsed).
# This task:
# 1. Finds each duplicate group
# 2. Chooses the earliest-created row as the keeper
# 3. Re-points holdings and trades to the keeper
# 4. Destroys the duplicate (which also removes its prices)
task migrate_duplicate_securities: :environment do
puts "==> Scanning for duplicate securities…"
duplicate_sets = Security
.select("UPPER(ticker) AS up_ticker,
COALESCE(UPPER(exchange_operating_mic), '') AS up_mic,
COUNT(*) AS dup_count")
.group("up_ticker, up_mic")
.having("COUNT(*) > 1")
.to_a
puts "Found #{duplicate_sets.size} duplicate groups."
duplicate_sets.each_with_index do |set, idx|
# Fetch duplicates ordered by creation; the first row becomes keeper
duplicates_scope = Security
.where("UPPER(ticker) = ? AND COALESCE(UPPER(exchange_operating_mic), '') = ?",
set.up_ticker, set.up_mic)
.order(:created_at)
keeper = duplicates_scope.first
next unless keeper
duplicates = duplicates_scope.offset(1)
dup_ids = duplicates.ids
# Skip if nothing to merge (defensive; shouldn't occur)
next if dup_ids.empty?
begin
ActiveRecord::Base.transaction do
# --------------------------------------------------------------
# HOLDINGS
# --------------------------------------------------------------
holdings_moved = 0
holdings_deleted = 0
dup_ids.each do |dup_id|
Holding.where(security_id: dup_id).find_each(batch_size: 1_000) do |holding|
# Will this holding collide with an existing keeper row?
conflict_exists = Holding.where(
account_id: holding.account_id,
security_id: keeper.id,
date: holding.date,
currency: holding.currency
).exists?
if conflict_exists
holding.destroy!
holdings_deleted += 1
else
holding.update!(security_id: keeper.id)
holdings_moved += 1
end
end
end
# --------------------------------------------------------------
# TRADES — no uniqueness constraints -> bulk update is fine
# --------------------------------------------------------------
trades_moved = Trade.where(security_id: dup_ids).update_all(security_id: keeper.id)
# Ensure no rows remain pointing at duplicates before deletion
raise "Leftover holdings detected" if Holding.where(security_id: dup_ids).exists?
raise "Leftover trades detected" if Trade.where(security_id: dup_ids).exists?
duplicates.each(&:destroy!) # destroys its security_prices via dependent: :destroy
# Log inside the transaction so counters are in-scope
total_holdings = holdings_moved + holdings_deleted
puts "[#{idx + 1}/#{duplicate_sets.size}] Merged #{dup_ids.join(', ')}#{keeper.id} " \
"(#{total_holdings} holdings → #{holdings_moved} moved, #{holdings_deleted} removed, " \
"#{trades_moved} trades)"
end
rescue => e
puts "ERROR migrating #{dup_ids.join(', ')}: #{e.message}"
end
end
puts "✅ Duplicate security migration complete."
end
desc "Migrate account valuation anchors"
# 2025-07-10: Set opening_anchor kinds for valuations to support event-sourced ledger model.
# Manual accounts get their oldest valuation marked as opening_anchor, which acts as the
# starting balance for the account. Current anchors are only used for Plaid accounts.
task migrate_account_valuation_anchors: :environment do
puts "==> Migrating account valuation anchors..."
manual_accounts = Account.manual.includes(valuations: :entry)
total_accounts = manual_accounts.count
accounts_processed = 0
opening_anchors_set = 0
manual_accounts.find_each do |account|
accounts_processed += 1
# Find oldest account entry
oldest_entry = account.entries
.order("date ASC, created_at ASC")
.first
# Check if it's a valuation that isn't already an anchor
if oldest_entry && oldest_entry.valuation?
derived_valuation_name = Valuation.build_opening_anchor_name(account.accountable_type)
Account.transaction do
oldest_entry.valuation.update!(kind: "opening_anchor")
oldest_entry.update!(name: derived_valuation_name)
end
opening_anchors_set += 1
end
if accounts_processed % 100 == 0
puts "[#{accounts_processed}/#{total_accounts}] Processed #{accounts_processed} accounts..."
end
rescue => e
puts "ERROR processing account #{account.id}: #{e.message}"
end
puts "✅ Account valuation anchor migration complete."
puts " Processed: #{accounts_processed} accounts"
puts " Opening anchors set: #{opening_anchors_set}"
end
end