mirror of
https://github.com/we-promise/sure.git
synced 2026-06-02 01:09:01 +00:00
feat: Add Merchants and Tags API v1 Endpoints (#620)
* Add files via upload Signed-off-by: Jose <39016041+jospaquim@users.noreply.github.com> * Add merchants and tags resources to routes Signed-off-by: Jose <39016041+jospaquim@users.noreply.github.com> * update * update spaces * fix: Apply CodeRabbit suggestions and add YARD documentation * docs: Add API documentation for merchants and tags endpoints * fix: Address CodeRabbit feedback on documentation --------- Signed-off-by: Jose <39016041+jospaquim@users.noreply.github.com>
This commit is contained in:
85
app/controllers/api/v1/merchants_controller.rb
Normal file
85
app/controllers/api/v1/merchants_controller.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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 :ensure_read_scope
|
||||
|
||||
# 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
|
||||
|
||||
# Single query with OR conditions - more efficient than Ruby deduplication
|
||||
family_merchant_ids = family.merchants.select(:id)
|
||||
provider_merchant_ids = family.transactions.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
|
||||
|
||||
# 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
|
||||
|
||||
@merchant = family.merchants.find_by(id: params[:id]) ||
|
||||
Merchant.joins(:transactions)
|
||||
.where(transactions: { account_id: family.accounts.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
|
||||
|
||||
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,
|
||||
name: merchant.name,
|
||||
type: merchant.type,
|
||||
created_at: merchant.created_at,
|
||||
updated_at: merchant.updated_at
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
130
app/controllers/api/v1/tags_controller.rb
Normal file
130
app/controllers/api/v1/tags_controller.rb
Normal file
@@ -0,0 +1,130 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
# API v1 endpoint for tags
|
||||
# Provides full CRUD operations for family tags
|
||||
#
|
||||
# @example List all tags
|
||||
# GET /api/v1/tags
|
||||
#
|
||||
# @example Create a new tag
|
||||
# POST /api/v1/tags
|
||||
# { "tag": { "name": "WhiteHouse", "color": "#3b82f6" } }
|
||||
#
|
||||
class TagsController < BaseController
|
||||
before_action :ensure_read_scope, only: %i[index show]
|
||||
before_action :ensure_write_scope, only: %i[create update destroy]
|
||||
before_action :set_tag, only: %i[show update destroy]
|
||||
|
||||
# List all tags belonging to the family
|
||||
#
|
||||
# @return [Array<Hash>] JSON array of tag objects sorted alphabetically
|
||||
def index
|
||||
family = current_resource_owner.family
|
||||
@tags = family.tags.alphabetically
|
||||
|
||||
render json: @tags.map { |t| tag_json(t) }
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("API Tags Error: #{e.message}")
|
||||
render json: { error: "Failed to fetch tags" }, status: :internal_server_error
|
||||
end
|
||||
|
||||
# Get a specific tag by ID
|
||||
#
|
||||
# @param id [String] The tag ID
|
||||
# @return [Hash] JSON tag object
|
||||
def show
|
||||
render json: tag_json(@tag)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("API Tag Show Error: #{e.message}")
|
||||
render json: { error: "Failed to fetch tag" }, status: :internal_server_error
|
||||
end
|
||||
|
||||
# Create a new tag for the family
|
||||
#
|
||||
# @param name [String] Tag name (required)
|
||||
# @param color [String] Hex color code (optional, auto-assigned if not provided)
|
||||
# @return [Hash] JSON tag object with status 201
|
||||
def create
|
||||
family = current_resource_owner.family
|
||||
@tag = family.tags.new(tag_params)
|
||||
|
||||
# Assign random color if not provided
|
||||
@tag.color ||= Tag::COLORS.sample
|
||||
|
||||
if @tag.save
|
||||
render json: tag_json(@tag), status: :created
|
||||
else
|
||||
render json: { error: @tag.errors.full_messages.join(", ") }, status: :unprocessable_entity
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("API Tag Create Error: #{e.message}")
|
||||
render json: { error: "Failed to create tag" }, status: :internal_server_error
|
||||
end
|
||||
|
||||
# Update an existing tag
|
||||
#
|
||||
# @param id [String] The tag ID
|
||||
# @param name [String] New tag name (optional)
|
||||
# @param color [String] New hex color code (optional)
|
||||
# @return [Hash] JSON tag object
|
||||
def update
|
||||
if @tag.update(tag_params)
|
||||
render json: tag_json(@tag)
|
||||
else
|
||||
render json: { error: @tag.errors.full_messages.join(", ") }, status: :unprocessable_entity
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("API Tag Update Error: #{e.message}")
|
||||
render json: { error: "Failed to update tag" }, status: :internal_server_error
|
||||
end
|
||||
|
||||
# Delete a tag
|
||||
#
|
||||
# @param id [String] The tag ID
|
||||
# @return [nil] Empty response with status 204
|
||||
def destroy
|
||||
@tag.destroy!
|
||||
head :no_content
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("API Tag Destroy Error: #{e.message}")
|
||||
render json: { error: "Failed to delete tag" }, status: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Find and set the tag from params
|
||||
#
|
||||
# @raise [ActiveRecord::RecordNotFound] if tag not found
|
||||
# @return [Tag] The found tag
|
||||
def set_tag
|
||||
family = current_resource_owner.family
|
||||
@tag = family.tags.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { error: "Tag not found" }, status: :not_found
|
||||
end
|
||||
|
||||
# Strong parameters for tag creation/update
|
||||
#
|
||||
# @return [ActionController::Parameters] Permitted parameters
|
||||
def tag_params
|
||||
params.require(:tag).permit(:name, :color)
|
||||
end
|
||||
|
||||
# Serialize a tag to JSON format
|
||||
#
|
||||
# @param tag [Tag] The tag to serialize
|
||||
# @return [Hash] JSON-serializable hash
|
||||
def tag_json(tag)
|
||||
{
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
color: tag.color,
|
||||
created_at: tag.created_at,
|
||||
updated_at: tag.updated_at
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user