mirror of
https://github.com/we-promise/sure.git
synced 2026-06-08 04:09:04 +00:00
* feat(merchants): add csv import endpoint for merchants * docs: update endpoint docs * fix(merchant): recommended ai fixes
154 lines
5.2 KiB
Ruby
154 lines
5.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Api
|
|
module V1
|
|
class MerchantsController < BaseController
|
|
before_action -> { authorize_scope!(:read) }, only: [ :index, :show ]
|
|
before_action -> { authorize_scope!(:write) }, only: [ :create ]
|
|
|
|
def index
|
|
family = current_resource_owner.family
|
|
user = current_resource_owner
|
|
|
|
family_merchant_ids = family.merchants.select(:id)
|
|
accessible_account_ids = family.accounts.accessible_by(user).select(:id)
|
|
provider_merchant_ids = Transaction.joins(:entry)
|
|
.where(entries: { account_id: accessible_account_ids })
|
|
.where.not(merchant_id: nil)
|
|
.select(:merchant_id)
|
|
|
|
@merchants = Merchant
|
|
.where(id: family_merchant_ids)
|
|
.or(Merchant.where(id: provider_merchant_ids, type: "ProviderMerchant"))
|
|
.distinct
|
|
.alphabetically
|
|
|
|
render json: @merchants.map { |m| merchant_json(m) }
|
|
rescue StandardError => e
|
|
Rails.logger.error("API Merchants Error: #{e.message}")
|
|
render json: { error: "Failed to fetch merchants" }, status: :internal_server_error
|
|
end
|
|
|
|
def show
|
|
family = current_resource_owner.family
|
|
user = current_resource_owner
|
|
|
|
@merchant = family.merchants.find_by(id: params[:id]) ||
|
|
Merchant.joins(transactions: :entry)
|
|
.where(entries: { account_id: family.accounts.accessible_by(user).select(:id) })
|
|
.distinct
|
|
.find_by(id: params[:id])
|
|
|
|
if @merchant
|
|
render json: merchant_json(@merchant)
|
|
else
|
|
render json: { error: "Merchant not found" }, status: :not_found
|
|
end
|
|
rescue StandardError => e
|
|
Rails.logger.error("API Merchant Show Error: #{e.message}")
|
|
render json: { error: "Failed to fetch merchant" }, status: :internal_server_error
|
|
end
|
|
|
|
def create
|
|
family = current_resource_owner.family
|
|
|
|
unless params[:file].present?
|
|
return render json: { error: "missing_file", message: "Please provide a CSV file." },
|
|
status: :unprocessable_entity
|
|
end
|
|
|
|
file = params[:file]
|
|
|
|
if file.size > Import::MAX_CSV_SIZE
|
|
return render json: {
|
|
error: "file_too_large",
|
|
message: "File is too large. Maximum size is #{Import::MAX_CSV_SIZE / 1.megabyte}MB."
|
|
}, status: :unprocessable_entity
|
|
end
|
|
|
|
unless Import::ALLOWED_CSV_MIME_TYPES.include?(file.content_type)
|
|
return render json: {
|
|
error: "invalid_file_type",
|
|
message: "Invalid file type. Please upload a CSV file."
|
|
}, status: :unprocessable_entity
|
|
end
|
|
|
|
csv = Import.parse_csv_str(file.read)
|
|
|
|
name_header = normalized_header(csv.headers, "name")
|
|
unless name_header
|
|
return render json: {
|
|
error: "missing_column",
|
|
message: "CSV must include a 'name' column."
|
|
}, status: :unprocessable_entity
|
|
end
|
|
|
|
color_header = normalized_header(csv.headers, "color")
|
|
website_url_header = normalized_header(csv.headers, "website_url", "website url", "website")
|
|
|
|
imported = []
|
|
skipped = []
|
|
|
|
csv.each do |row|
|
|
name = row[name_header].to_s.strip
|
|
next if name.blank?
|
|
|
|
merchant = family.merchants.find_or_initialize_by(name: name)
|
|
|
|
if merchant.persisted?
|
|
skipped << { name: name, reason: "already_exists" }
|
|
next
|
|
end
|
|
|
|
merchant.color = row[color_header].to_s.strip.presence if color_header
|
|
merchant.website_url = row[website_url_header].to_s.strip.presence if website_url_header
|
|
|
|
if merchant.save
|
|
imported << merchant
|
|
else
|
|
skipped << { name: name, errors: merchant.errors.full_messages }
|
|
end
|
|
end
|
|
|
|
render json: {
|
|
imported: imported.count,
|
|
skipped: skipped.count,
|
|
merchants: imported.map { |m| merchant_json(m) }
|
|
}, status: :created
|
|
rescue CSV::MalformedCSVError => e
|
|
render json: { error: "invalid_csv", message: "CSV could not be parsed: #{e.message}" },
|
|
status: :unprocessable_entity
|
|
rescue StandardError => e
|
|
Rails.logger.error("API Merchants Import Error: #{e.message}")
|
|
render json: { error: "internal_server_error", message: "An unexpected error occurred" },
|
|
status: :internal_server_error
|
|
end
|
|
|
|
private
|
|
|
|
def merchant_json(merchant)
|
|
{
|
|
id: merchant.id,
|
|
name: merchant.name,
|
|
type: merchant.type,
|
|
created_at: merchant.created_at,
|
|
updated_at: merchant.updated_at
|
|
}
|
|
end
|
|
|
|
def normalized_header(headers, *candidates)
|
|
normalized_map = headers.to_h { |h| [ normalize(h), h ] }
|
|
candidates.each do |candidate|
|
|
header = normalized_map[normalize(candidate)]
|
|
return header if header.present?
|
|
end
|
|
nil
|
|
end
|
|
|
|
def normalize(str)
|
|
str.to_s.strip.downcase.gsub(/\*/, "").gsub(/[\s_-]+/, "_")
|
|
end
|
|
end
|
|
end
|
|
end
|