Files
sure/app/models/provider_merchant/enhancer.rb
soky srm 12d2f4e36d Provider merchants enhancement (#1254)
* Add AI merchant enhancement and dedup

* Enhancements

Add error if job is already running
add note that we also merge merchants

* Allow updating provider website

* Review fixes

* Update provider_merchant.rb

* Linter and fixes

* FIX transaction quick menu modal
2026-03-23 12:34:43 +01:00

99 lines
3.6 KiB
Ruby

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