Files
project-work/worker-toolkit-stocks-in-the-future/repo/app/models/order.rb
Eric Bell 392781f7aa chore: init commit
in worker.../repo/GITFOLDER.zip is the .git folder.
2026-08-11 14:44:09 -04:00

209 lines
6.8 KiB
Ruby

# frozen_string_literal: true
class Order < ApplicationRecord
include ApplicationHelper
belongs_to :user
belongs_to :stock
belongs_to :portfolio_stock, optional: true
belongs_to :portfolio_transaction, optional: true
enum :status, { pending: 0, completed: 1, canceled: 2 }, default: :pending
enum :action, { buy: "buy", sell: "sell" }
validates :shares, presence: true, numericality: { greater_than: 0 }
validate :sufficient_funds_for_buy_when_update, on: :update, if: -> { buy? }
validate :order_is_pending, on: :update
# Now runs when creating orders or when user changes the share amount.
# Skips during status changes (pending→completed) to avoid false errors.
validate :sufficient_shares_for_sell,
if: -> { sell? && validate_share_availability? },
on: %i[create update]
validate :sufficient_funds_for_buy, if: -> { buy? }, on: :create
validate :prevent_archived_stock_purchase, if: -> { buy? }, on: %i[create update]
validate :trading_enabled_for_classroom, on: :create
after_update :update_portfolio_transaction_for_pending_order
delegate :portfolio, to: :user
scope :buy, -> { where(action: :buy) }
scope :sell, -> { where(action: :sell) }
scope :pending, -> { where(status: :pending) }
scope :completed, -> { where(status: :completed) }
scope :canceled, -> { where(status: :canceled) }
scope :for_student, ->(student) { where(user: student) }
scope :for_teacher, lambda { |teacher|
joins(user: :classroom).where(users: { classroom: teacher.classrooms })
}
scope :order_by_created_at, lambda { |direction = :asc|
reorder(created_at: safe_direction(direction))
}
scope :order_by_username, lambda { |direction = :asc|
joins(:user).reorder(users: { username: safe_direction(direction) })
}
scope :order_by_classroom, lambda { |direction = :asc|
joins(user: :classroom)
.reorder(classrooms: { name: safe_direction(direction) })
}
scope :order_by_stock, lambda { |direction = :asc|
joins(:stock).reorder(stocks: { ticker: safe_direction(direction) })
}
scope :order_by_price_per_share, lambda { |direction = :asc|
joins(:stock)
.reorder(stocks: { price_cents: safe_direction(direction) })
}
scope :order_by_shares, lambda { |direction = :asc|
reorder(shares: safe_direction(direction))
}
scope :order_by_status, lambda { |direction = :asc|
reorder(status: safe_direction(direction))
}
scope :order_by_total_cost, lambda { |direction = :asc|
dir = safe_direction(direction)
expression = Arel.sql("stocks.price_cents * orders.shares")
joins(:stock)
.reorder(dir == :desc ? expression.desc : expression.asc)
}
# Apply sorting based on sort column param
# @param collection [ActiveRecord::Relation] The collection to sort (optional)
# @param sort_column [String] The column to sort by
# @param direction [Symbol] :asc or :desc
# @return [ActiveRecord::Relation] The sorted collection
def self.apply_sorting(collection = nil, sort_column = nil, direction = :asc)
base_scope = collection || all
sorting_method = SORTING_METHODS[sort_column]
return base_scope.order_by_created_at(:desc) unless sorting_method
base_scope.send(sorting_method, direction)
end
# Map of sort column names to scope method names
SORTING_METHODS = {
"created_at" => :order_by_created_at,
"username" => :order_by_username,
"classroom" => :order_by_classroom,
"stock" => :order_by_stock,
"price_per_share" => :order_by_price_per_share,
"shares" => :order_by_shares,
"status" => :order_by_status,
"total_cost" => :order_by_total_cost
}.freeze
def cancel!
update(status: :canceled)
end
def purchase_cost
stock.price_cents * shares
end
def existing_transaction_type
order_transaction_type = portfolio_transaction&.transaction_type&.to_sym
order_transaction_type == :debit ? "buy" : "sell"
end
def self.safe_direction(direction)
direction.to_s.downcase == "desc" ? :desc : :asc
end
private
def number_of_shares_valid_for_calculations?
shares.is_a?(Numeric) && shares.positive?
end
def sufficient_shares_for_sell
return unless number_of_shares_valid_for_calculations?
current_shares = user.portfolio&.shares_owned(stock_id) || 0
return unless shares > current_shares
formatted_shares = (current_shares % 1).zero? ? current_shares.to_i : current_shares
errors.add(:base, "Cannot sell more shares than you own (#{formatted_shares} available)")
end
def sufficient_funds_for_buy
return unless number_of_shares_valid_for_calculations?
current_balance_cents = (user.portfolio&.cash_balance || 0) * 100
total_cost = purchase_cost + transaction_fee
return if (current_balance_cents - total_cost) >= 0
formatted_balance = format_money(current_balance_cents)
formatted_cost = format_money(total_cost)
errors.add(:shares, "Insufficient funds. You have #{formatted_balance} but need #{formatted_cost}")
end
def transaction_fee
user.orders.pending.where.not(id:).exists? ? 0 : PortfolioTransaction::TRANSACTION_FEE_CENTS
end
def sufficient_funds_for_buy_when_update
return unless number_of_shares_valid_for_calculations?
return unless shares_changed?
previous_shares = shares_was
refund = (stock.price_cents * previous_shares) + PortfolioTransaction::TRANSACTION_FEE_CENTS
current_balance_cents = ((user.portfolio&.cash_balance || 0) * 100) + refund
total_cost = purchase_cost + transaction_fee
return if (current_balance_cents - total_cost) >= 0
formatted_balance = format_money(current_balance_cents)
formatted_cost = format_money(total_cost)
errors.add(
:shares,
"Insufficient funds. You have #{formatted_balance} but need #{formatted_cost}"
)
end
def order_is_pending
prev_status, = status_change_to_be_saved
return if pending? || prev_status.nil? || prev_status == "pending"
errors.add(:base, "Cannot update non-pending orders")
end
def update_portfolio_transaction_for_pending_order
return unless pending? && portfolio_transaction.present?
portfolio_transaction.amount_cents = purchase_cost
portfolio_transaction.transaction_type = translated_transaction_type
portfolio_transaction.save
end
def translated_transaction_type
buy? ? :debit : :credit
end
def prevent_archived_stock_purchase
return unless stock&.archived?
errors.add(:stock, "Cannot purchase shares of archived stocks")
end
# Only validate share availability when shares quantity changes,
# not during status-only updates (pending → completed)
def validate_share_availability?
return true if new_record?
shares_changed?
end
def trading_enabled_for_classroom
return if user&.classroom&.trading_enabled?
errors.add(:base, "Trading is currently disabled for your classroom")
end
end