chore: init commit
in worker.../repo/GITFOLDER.zip is the .git folder.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "net/http"
|
||||
require "json"
|
||||
|
||||
class AlphaVantageApiClient
|
||||
class ApiError < StandardError; end
|
||||
class InvalidResponseError < ApiError; end
|
||||
|
||||
def initialize
|
||||
@api_key = ENV.fetch("ALPHA_VANTAGE_API_KEY", nil)
|
||||
end
|
||||
|
||||
def fetch_quote(symbol)
|
||||
return nil unless api_key_present?
|
||||
|
||||
Rails.logger.debug { "Fetching quote for #{symbol}" }
|
||||
|
||||
data = make_request(symbol)
|
||||
extract_price_data(data, symbol)
|
||||
rescue JSON::ParserError => e
|
||||
Rails.logger.error "Failed to parse API response for #{symbol}: #{e.message}"
|
||||
nil
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "API request failed for #{symbol}: #{e.message}"
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api_key_present?
|
||||
return true if @api_key
|
||||
|
||||
Rails.logger.error "ALPHA_VANTAGE_API_KEY environment variable not configured"
|
||||
false
|
||||
end
|
||||
|
||||
def make_request(symbol)
|
||||
url = build_url(symbol)
|
||||
uri = URI.parse(url)
|
||||
|
||||
response = Net::HTTP.get(uri)
|
||||
JSON.parse(response)
|
||||
end
|
||||
|
||||
def build_url(symbol)
|
||||
"https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=#{symbol}&apikey=#{@api_key}"
|
||||
end
|
||||
|
||||
def extract_price_data(data, symbol)
|
||||
price = data&.dig("Global Quote", "05. price")
|
||||
trading_day = data&.dig("Global Quote", "07. latest trading day")
|
||||
|
||||
if price && trading_day
|
||||
Rails.logger.info "API returned price #{price} for #{symbol} (trading day: #{trading_day})"
|
||||
return { price: price, trading_day: trading_day }
|
||||
end
|
||||
|
||||
Rails.logger.error "Invalid API response for #{symbol}: #{data}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class BulkStudentImportService
|
||||
ResultWithLineNumber = Struct.new(:result, :line_number) do
|
||||
delegate_missing_to :result
|
||||
end
|
||||
|
||||
def self.import_from_csv(csv_file_path)
|
||||
results = []
|
||||
|
||||
CSV.foreach(csv_file_path, headers: true).with_index(2) do |row, csv_line_number|
|
||||
classroom_id = row["classroom_id"]&.strip
|
||||
username = row["username"]&.strip
|
||||
|
||||
next if classroom_id.blank? || username.blank?
|
||||
|
||||
result = ImportStudentService.call(
|
||||
username: username,
|
||||
classroom_id: classroom_id
|
||||
)
|
||||
|
||||
results << ResultWithLineNumber.new(result: result, line_number: csv_line_number)
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
def self.generate_csv_template
|
||||
[
|
||||
"classroom_id,username",
|
||||
"1,student001",
|
||||
"1,student002",
|
||||
"2,student003"
|
||||
].join("\n")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DistributeEarnings
|
||||
def initialize(grade_book)
|
||||
@grade_book = grade_book
|
||||
@previous_entries = find_previous_entries
|
||||
end
|
||||
|
||||
def self.execute(...)
|
||||
new(...).execute
|
||||
end
|
||||
|
||||
def execute
|
||||
return unless @grade_book.verified?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
distribute_funds_to_students
|
||||
@grade_book.completed!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def distribute_funds_to_students
|
||||
@grade_book.grade_entries.each do |entry|
|
||||
previous_entry = @previous_entries[entry.user_id]&.first
|
||||
|
||||
distribute_earnings(entry.user, attendance_earnings(entry, previous_entry), :attendance_earnings)
|
||||
distribute_earnings(entry.user, math_earnings(entry, previous_entry), :math_earnings)
|
||||
distribute_earnings(entry.user, reading_earnings(entry, previous_entry), :reading_earnings)
|
||||
end
|
||||
end
|
||||
|
||||
def find_previous_entries
|
||||
previous_quarter = @grade_book.quarter.previous
|
||||
return {} unless previous_quarter
|
||||
|
||||
previous_grade_book = GradeBook.find_by(classroom: @grade_book.classroom, quarter: previous_quarter)
|
||||
return {} unless previous_grade_book
|
||||
|
||||
previous_grade_book.grade_entries.to_a.group_by(&:user_id)
|
||||
end
|
||||
|
||||
def distribute_earnings(user, amount_cents, reason_key)
|
||||
return if amount_cents.zero?
|
||||
|
||||
user.portfolio.portfolio_transactions.create!(
|
||||
amount_cents: amount_cents,
|
||||
transaction_type: :deposit,
|
||||
reason: reason_key
|
||||
)
|
||||
end
|
||||
|
||||
def attendance_earnings(entry, _previous_entry)
|
||||
[
|
||||
entry.earnings_for_attendance,
|
||||
entry.attendance_perfect_earnings
|
||||
].sum
|
||||
end
|
||||
|
||||
def math_earnings(entry, previous_entry)
|
||||
[
|
||||
entry.earnings_for_math,
|
||||
entry.math_improvement_earnings(previous_entry)
|
||||
].sum
|
||||
end
|
||||
|
||||
def reading_earnings(entry, previous_entry)
|
||||
[
|
||||
entry.earnings_for_reading,
|
||||
entry.reading_improvement_earnings(previous_entry)
|
||||
].sum
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ExecuteOrder
|
||||
delegate :portfolio, :shares, :stock, to: :order, private: true
|
||||
delegate :price_cents, to: :stock, prefix: true, private: true
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def self.execute(...)
|
||||
new(...).execute
|
||||
end
|
||||
|
||||
def execute
|
||||
return unless order.pending?
|
||||
|
||||
if order.buy? && insufficient_funds?
|
||||
order.cancel!
|
||||
return
|
||||
end
|
||||
|
||||
if order.sell? && insufficient_shares?
|
||||
order.cancel!
|
||||
return
|
||||
end
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
create_portfolio_transaction
|
||||
create_portfolio_stock
|
||||
update_order_status
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_accessor :order, :portfolio_stock, :portfolio_transaction
|
||||
|
||||
def create_portfolio_transaction
|
||||
@portfolio_transaction = if order.buy?
|
||||
portfolio
|
||||
.portfolio_transactions
|
||||
.debit
|
||||
.create!(amount_cents: purchase_cost)
|
||||
else
|
||||
portfolio
|
||||
.portfolio_transactions
|
||||
.credit
|
||||
.create!(amount_cents: purchase_cost)
|
||||
end
|
||||
end
|
||||
|
||||
def create_portfolio_stock
|
||||
share_amount = order.sell? ? -shares : shares
|
||||
@portfolio_stock = portfolio
|
||||
.portfolio_stocks
|
||||
.create!(stock:, shares: share_amount, purchase_price: stock.current_price)
|
||||
end
|
||||
|
||||
def update_order_status
|
||||
order.update!(status: :completed, portfolio_stock:, portfolio_transaction:)
|
||||
end
|
||||
|
||||
def insufficient_funds?
|
||||
portfolio.cash_balance.negative?
|
||||
end
|
||||
|
||||
def insufficient_shares?
|
||||
portfolio.shares_owned(stock.id) < shares
|
||||
end
|
||||
|
||||
def purchase_cost
|
||||
order.purchase_cost
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ImportStudentService
|
||||
Result = Struct.new(:success?, :student, :error_message, :action) do
|
||||
def created?
|
||||
action == :created
|
||||
end
|
||||
|
||||
def skipped?
|
||||
action == :skipped
|
||||
end
|
||||
|
||||
def failed?
|
||||
action == :failed
|
||||
end
|
||||
end
|
||||
|
||||
def self.call(username:, classroom_id:)
|
||||
new.call(username: username, classroom_id: classroom_id)
|
||||
end
|
||||
|
||||
def call(username:, classroom_id:)
|
||||
username = sanitize_input(username)
|
||||
classroom_id = sanitize_input(classroom_id)
|
||||
return skip_result("Username is required") if username.blank?
|
||||
return skip_result("Student with username '#{username}' already exists") if Student.exists?(username: username)
|
||||
return skip_result("Classroom ID is required") if classroom_id.blank?
|
||||
|
||||
import_student(username: username, classroom_id: classroom_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sanitize_input(input)
|
||||
input&.to_s&.strip
|
||||
end
|
||||
|
||||
def import_student(username:, classroom_id:)
|
||||
student = Student.new(
|
||||
username: username,
|
||||
classroom_id: classroom_id,
|
||||
password: MemorablePasswordGenerator.generate
|
||||
)
|
||||
|
||||
if student.save
|
||||
success_result(student)
|
||||
else
|
||||
error_result(student)
|
||||
end
|
||||
rescue ActiveRecord::InvalidForeignKey
|
||||
student.errors.add(:classroom, "can't be blank")
|
||||
error_result(student)
|
||||
end
|
||||
|
||||
def success_result(student)
|
||||
Result.new(
|
||||
success?: true,
|
||||
student: student,
|
||||
error_message: nil,
|
||||
action: :created
|
||||
)
|
||||
end
|
||||
|
||||
def skip_result(message)
|
||||
Result.new(
|
||||
success?: true,
|
||||
student: nil,
|
||||
error_message: message,
|
||||
action: :skipped
|
||||
)
|
||||
end
|
||||
|
||||
def error_result(student)
|
||||
Result.new(
|
||||
success?: false,
|
||||
student: student,
|
||||
error_message: student.errors.full_messages.join(", "),
|
||||
action: :failed
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# TODO: more robust solution later.
|
||||
class MemorablePasswordGenerator
|
||||
RANGE = (1..99)
|
||||
|
||||
class << self
|
||||
def generate
|
||||
password = Faker::Superhero.name + rand(RANGE).to_s + Faker::Superhero.name
|
||||
normalize password
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize(password)
|
||||
invalid_characters = [" ", "-", "'"]
|
||||
invalid_characters.each { password.delete! it }
|
||||
password
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "net/http"
|
||||
require "json"
|
||||
|
||||
class StockAttributeUpdate
|
||||
def initialize(stock)
|
||||
@stock = stock
|
||||
end
|
||||
|
||||
def self.execute(stock)
|
||||
new(stock).execute
|
||||
end
|
||||
|
||||
def execute
|
||||
return if stock.ticker.blank?
|
||||
|
||||
log_start_update
|
||||
data = fetch_api_data
|
||||
|
||||
return unless valid_api_response?(data)
|
||||
|
||||
update_stock_attributes(data)
|
||||
save_stock
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :stock
|
||||
|
||||
def log_start_update
|
||||
Rails.logger.info "Updating attributes for stock #{stock.ticker}"
|
||||
end
|
||||
|
||||
def fetch_api_data
|
||||
api_request(stock.ticker)
|
||||
end
|
||||
|
||||
def valid_api_response?(data)
|
||||
if data && data["Symbol"]
|
||||
Rails.logger.info "API returned data for #{stock.ticker}"
|
||||
true
|
||||
else
|
||||
Rails.logger.error "Invalid API response for #{stock.ticker}: #{data}"
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def save_stock
|
||||
if stock.save
|
||||
Rails.logger.info "Successfully updated attributes for #{stock.ticker}"
|
||||
else
|
||||
Rails.logger.error "Failed to save #{stock.ticker}: #{stock.errors.full_messages}"
|
||||
end
|
||||
end
|
||||
|
||||
def update_stock_attributes(data)
|
||||
update_basic_info(data)
|
||||
update_financial_info(data)
|
||||
end
|
||||
|
||||
def update_basic_info(data)
|
||||
stock.company_name = data["Name"] if data["Name"]
|
||||
stock.description = data["Description"] if data["Description"]
|
||||
stock.stock_exchange = data["Exchange"] if data["Exchange"]
|
||||
stock.industry = data["Industry"] if data["Industry"]
|
||||
end
|
||||
|
||||
def update_financial_info(data)
|
||||
stock.company_website = data["OfficialSite"] if data["OfficialSite"]
|
||||
stock.profit_margin = data["ProfitMargin"].to_f if data["ProfitMargin"]
|
||||
end
|
||||
|
||||
def api_request(symbol)
|
||||
url = "https://www.alphavantage.co/query?function=OVERVIEW&symbol=#{symbol}&apikey=#{API_KEY}"
|
||||
uri = URI.parse(url)
|
||||
JSON.parse Net::HTTP.get(uri)
|
||||
rescue JSON::ParserError, Net::HTTPError => e
|
||||
Rails.logger.error "API request failed for #{symbol}: #{e.message}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TransactionFeeProcessor
|
||||
def initialize(orders)
|
||||
@orders = orders
|
||||
@users = Set.new
|
||||
end
|
||||
|
||||
def self.execute(...)
|
||||
new(...).execute
|
||||
end
|
||||
|
||||
def execute
|
||||
ActiveRecord::Base.transaction do
|
||||
@orders.each do |order|
|
||||
add_transaction_fee order
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_transaction_fee(order)
|
||||
return if @users.include?(order.portfolio.user_id)
|
||||
|
||||
order.portfolio.portfolio_transactions.fees.create!(
|
||||
amount_cents: PortfolioTransaction::TRANSACTION_FEE_CENTS,
|
||||
reason: :transaction_fees
|
||||
)
|
||||
@users << order.portfolio.user_id
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user