mirror of
https://github.com/we-promise/sure.git
synced 2026-06-08 12:19:03 +00:00
feat(merchants): add raw data import (csv) for merchants (#1992)
* feat(merchants): add csv import endpoint for merchants * docs: update endpoint docs * fix(merchant): recommended ai fixes
This commit is contained in:
@@ -2,29 +2,14 @@
|
||||
|
||||
module Api
|
||||
module V1
|
||||
# API v1 endpoint for merchants
|
||||
# Provides read-only access to family and provider merchants
|
||||
#
|
||||
# @example List all merchants
|
||||
# GET /api/v1/merchants
|
||||
#
|
||||
# @example Get a specific merchant
|
||||
# GET /api/v1/merchants/:id
|
||||
#
|
||||
class MerchantsController < BaseController
|
||||
before_action -> { authorize_scope!(:read) }
|
||||
before_action -> { authorize_scope!(:read) }, only: [ :index, :show ]
|
||||
before_action -> { authorize_scope!(:write) }, only: [ :create ]
|
||||
|
||||
# List all merchants available to the family
|
||||
#
|
||||
# Returns both family-owned merchants and provider merchants
|
||||
# that are assigned to the family's transactions.
|
||||
#
|
||||
# @return [Array<Hash>] JSON array of merchant objects
|
||||
def index
|
||||
family = current_resource_owner.family
|
||||
user = current_resource_owner
|
||||
|
||||
# Single query with OR conditions - more efficient than Ruby deduplication
|
||||
family_merchant_ids = family.merchants.select(:id)
|
||||
accessible_account_ids = family.accounts.accessible_by(user).select(:id)
|
||||
provider_merchant_ids = Transaction.joins(:entry)
|
||||
@@ -44,13 +29,6 @@ module Api
|
||||
render json: { error: "Failed to fetch merchants" }, status: :internal_server_error
|
||||
end
|
||||
|
||||
# Get a specific merchant by ID
|
||||
#
|
||||
# Returns a merchant if it belongs to the family or is assigned
|
||||
# to any of the family's transactions.
|
||||
#
|
||||
# @param id [String] The merchant ID
|
||||
# @return [Hash] JSON merchant object or error
|
||||
def show
|
||||
family = current_resource_owner.family
|
||||
user = current_resource_owner
|
||||
@@ -71,12 +49,83 @@ module Api
|
||||
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
|
||||
|
||||
# Serialize a merchant to JSON format
|
||||
#
|
||||
# @param merchant [Merchant] The merchant to serialize
|
||||
# @return [Hash] JSON-serializable hash
|
||||
def merchant_json(merchant)
|
||||
{
|
||||
id: merchant.id,
|
||||
@@ -86,6 +135,19 @@ module Api
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user