Files
sure/app/models/provider/mercury.rb
soky srm 179552657c Mercury integration (#723)
* Initial mercury impl

* FIX both mercury and generator class

* Finish mercury integration and provider generator

* Fix schema

* Fix linter and tags

* Update routes.rb

* Avoid schema drift

---------

Signed-off-by: soky srm <sokysrm@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-01-22 20:37:07 +01:00

158 lines
5.5 KiB
Ruby

class Provider::Mercury
include HTTParty
headers "User-Agent" => "Sure Finance Mercury Client"
default_options.merge!(verify: true, ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER, timeout: 120)
attr_reader :token, :base_url
def initialize(token, base_url: "https://api.mercury.com/api/v1")
@token = token
@base_url = base_url
end
# Get all accounts
# Returns: { accounts: [...] }
# Account structure: { id, name, currentBalance, availableBalance, status, type, kind, legalBusinessName, nickname }
def get_accounts
response = self.class.get(
"#{@base_url}/accounts",
headers: auth_headers
)
handle_response(response)
rescue MercuryError
raise
rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error "Mercury API: GET /accounts failed: #{e.class}: #{e.message}"
raise MercuryError.new("Exception during GET request: #{e.message}", :request_failed)
rescue => e
Rails.logger.error "Mercury API: Unexpected error during GET /accounts: #{e.class}: #{e.message}"
raise MercuryError.new("Exception during GET request: #{e.message}", :request_failed)
end
# Get a single account by ID
# Returns: { id, name, currentBalance, availableBalance, status, type, kind, ... }
def get_account(account_id)
path = "/account/#{ERB::Util.url_encode(account_id.to_s)}"
response = self.class.get(
"#{@base_url}#{path}",
headers: auth_headers
)
handle_response(response)
rescue MercuryError
raise
rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error "Mercury API: GET #{path} failed: #{e.class}: #{e.message}"
raise MercuryError.new("Exception during GET request: #{e.message}", :request_failed)
rescue => e
Rails.logger.error "Mercury API: Unexpected error during GET #{path}: #{e.class}: #{e.message}"
raise MercuryError.new("Exception during GET request: #{e.message}", :request_failed)
end
# Get transactions for a specific account
# Returns: { transactions: [...], total: N }
# Transaction structure: { id, amount, bankDescription, counterpartyId, counterpartyName,
# counterpartyNickname, createdAt, dashboardLink, details,
# estimatedDeliveryDate, failedAt, kind, note, postedAt,
# reasonForFailure, status }
def get_account_transactions(account_id, start_date: nil, end_date: nil, offset: nil, limit: nil)
query_params = {}
if start_date
query_params[:start] = start_date.to_date.to_s
end
if end_date
query_params[:end] = end_date.to_date.to_s
end
if offset
query_params[:offset] = offset.to_i
end
if limit
query_params[:limit] = limit.to_i
end
path = "/account/#{ERB::Util.url_encode(account_id.to_s)}/transactions"
path += "?#{URI.encode_www_form(query_params)}" unless query_params.empty?
response = self.class.get(
"#{@base_url}#{path}",
headers: auth_headers
)
handle_response(response)
rescue MercuryError
raise
rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error "Mercury API: GET #{path} failed: #{e.class}: #{e.message}"
raise MercuryError.new("Exception during GET request: #{e.message}", :request_failed)
rescue => e
Rails.logger.error "Mercury API: Unexpected error during GET #{path}: #{e.class}: #{e.message}"
raise MercuryError.new("Exception during GET request: #{e.message}", :request_failed)
end
private
def auth_headers
{
"Authorization" => "Bearer #{token}",
"Content-Type" => "application/json",
"Accept" => "application/json"
}
end
def handle_response(response)
case response.code
when 200
JSON.parse(response.body, symbolize_names: true)
when 400
Rails.logger.error "Mercury API: Bad request - #{response.body}"
raise MercuryError.new("Bad request to Mercury API: #{response.body}", :bad_request)
when 401
# Parse the error response for more specific messages
error_message = parse_error_message(response.body)
raise MercuryError.new(error_message, :unauthorized)
when 403
raise MercuryError.new("Access forbidden - check your API token permissions", :access_forbidden)
when 404
raise MercuryError.new("Resource not found", :not_found)
when 429
raise MercuryError.new("Rate limit exceeded. Please try again later.", :rate_limited)
else
Rails.logger.error "Mercury API: Unexpected response - Code: #{response.code}, Body: #{response.body}"
raise MercuryError.new("Failed to fetch data: #{response.code} #{response.message} - #{response.body}", :fetch_failed)
end
end
def parse_error_message(body)
parsed = JSON.parse(body, symbolize_names: true)
errors = parsed[:errors] || {}
case errors[:errorCode]
when "ipNotWhitelisted"
ip = errors[:ip] || "unknown"
"IP address not whitelisted (#{ip}). Add your IP to the API token's whitelist in Mercury dashboard."
when "noTokenInDBButMaybeMalformed"
"Invalid token format. Make sure to include the 'secret-token:' prefix."
else
errors[:message] || "Invalid API token"
end
rescue JSON::ParserError
"Invalid API token"
end
class MercuryError < StandardError
attr_reader :error_type
def initialize(message, error_type = :unknown)
super(message)
@error_type = error_type
end
end
end