class ProviderMerchant::Enhancer BATCH_SIZE = 25 def initialize(family) @family = family end def enhance return { enhanced: 0, deduplicated: 0 } unless llm_provider return { enhanced: 0, deduplicated: 0 } if unenhanced_merchants.none? Rails.logger.info("Enhancing #{unenhanced_merchants.count} provider merchants for family #{@family.id}") enhanced_count = 0 deduplicated_count = 0 unenhanced_merchants.each_slice(BATCH_SIZE) do |batch| result = llm_provider.enhance_provider_merchants( merchants: batch.map { |m| { id: m.id, name: m.name } }, family: @family ) next unless result.success? result.data.each do |enhancement| next unless enhancement.business_url.present? merchant = batch.find { |m| m.id == enhancement.merchant_id } next unless merchant next if merchant.website_url.present? # Skip if already enhanced (race condition guard) # Step 1: Update the provider merchant with website + logo updates = { website_url: enhancement.business_url } updates[:logo_url] = build_logo_url(enhancement.business_url) if Setting.brand_fetch_client_id.present? merchant.update!(updates) enhanced_count += 1 # Step 2: Deduplicate — find other merchants with the same website_url # and merge them INTO this provider merchant (prefer provider over AI) deduplicated_count += deduplicate_by_website(merchant, enhancement.business_url) rescue ActiveRecord::RecordInvalid => e Rails.logger.error("Failed to enhance merchant #{merchant.id}: #{e.message}") end end Rails.logger.info("Enhanced #{enhanced_count} merchants, deduplicated #{deduplicated_count} for family #{@family.id}") { enhanced: enhanced_count, deduplicated: deduplicated_count } end private def deduplicate_by_website(target_merchant, website_url) # Find duplicate provider merchants assigned to this family with the same website_url. # Excludes FamilyMerchants — user-curated merchants should never be touched by dedup. duplicates = @family.assigned_merchants .where(type: "ProviderMerchant") .where(website_url: website_url) .where.not(id: target_merchant.id) return 0 if duplicates.none? count = 0 duplicates.each do |duplicate| # Reassign family's transactions from duplicate to target @family.transactions.where(merchant_id: duplicate.id) .update_all(merchant_id: target_merchant.id) count += 1 end count end def llm_provider @llm_provider ||= Provider::Registry.get_provider(:openai) end def unenhanced_merchants @unenhanced_merchants ||= @family.assigned_merchants .where(type: "ProviderMerchant") .where(website_url: [ nil, "" ]) .to_a end def build_logo_url(business_url) return nil unless Setting.brand_fetch_client_id.present? && business_url.present? domain = extract_domain(business_url) return nil unless domain.present? size = Setting.brand_fetch_logo_size "https://cdn.brandfetch.io/#{domain}/icon/fallback/lettermark/w/#{size}/h/#{size}?c=#{Setting.brand_fetch_client_id}" end def extract_domain(url) normalized_url = url.start_with?("http://", "https://") ? url : "https://#{url}" URI.parse(normalized_url).host&.sub(/\Awww\./, "") rescue URI::InvalidURIError url.sub(/\Awww\./, "") end end