Files
project-work/worker-toolkit-stocks-in-the-future/repo/docs/map/processes/place-and-execute-order.md

4.9 KiB

type, universe, status, consumes, produces
type universe status consumes produces
process live verified
../objects/trading/order.md
../objects/trading/stock.md
../objects/money/portfolio.md
../objects/trading/portfolio-stock.md
../objects/money/portfolio-transaction.md

place-and-execute-order

A student's buy or sell becomes shares and cash — up to 15 minutes later.

Verified 2026-08-16 against commit 63732df.

Input → Movement → Output

A student submits a buy or sell, which is saved as a pending order and nothing else. Every 15 minutes OrderExecutionJob sweeps all pending orders, re-validates each against the current balance and holdings, and either completes or cancels it. Completion writes one portfolio-transaction and one portfolio-stock lot, then a single $1.00 fee per user for the whole batch.

Why this shape

Deferral is the design, not a queue optimisation. Students trade at the price prevailing when the job runs, not when they click — the sweep re-reads stock.price_cents at execution time. This deliberately blunts day-trading in a classroom tool, and it is why ExecuteOrder re-checks funds and shares even though the model already validated them on create: the balance may have moved in between (app/services/execute_order.rb:18-26).

The fee is charged per user per sweep, after all executions, by a separate service that tracks which users it has already billed (app/services/transaction_fee_processor.rb:23-31). portfolio mirrors this by anticipating exactly one pending fee (app/models/portfolio.rb:98-100) — if the fee ever became per-order, that balance formula must change too.

Cancellation is silent. An order that fails re-validation is cancelled, not errored (app/services/execute_order.rb:18-26). The student sees canceled with no reason attached — there is no failure-reason column.

Steps

  1. OrdersController#create saves the order with user: current_user (app/controllers/orders_controller.rb:20-35). Model validations check funds, shares, archived stock, and that the classroom has trading enabled (app/models/order.rb:16-26).
  2. The order sits pending. Portfolio#cash_on_hand_in_cents already subtracts it and one fee, so the money is reserved (app/models/portfolio.rb:93-100).
  3. Cron fires OrderExecutionJob every 15 minutes (config/recurring.yml:2-6). It retries up to 3 times with exponential backoff (app/jobs/order_execution_job.rb:6).
  4. For each pending order, ExecuteOrder.execute runs (app/jobs/order_execution_job.rb:35-39).
  5. ExecuteOrder returns unless still pending, then cancels on a negative balance (buy) or insufficient shares (sell) (app/services/execute_order.rb:16-26,64-70).
  6. Otherwise, inside one DB transaction: create the ledger row — debit for a buy, credit for a sell (:39-51); create the lot with negative shares for a sell and purchase_price: stock.current_price in dollars (:53-58); mark the order completed and link both records (:60-62).
  7. After the loop, TransactionFeeProcessor.execute charges $1.00 once per user across the whole batch (app/jobs/order_execution_job.rb:41-43, app/services/transaction_fee_processor.rb:13-31).

If you change this

  • Hits: portfolio balance — every step here is an input to it; portfolio-stock and portfolio-position; portfolio-transaction; Order validations, which duplicate the service's checks and must stay consistent with them.
  • Does not hit: portfolio-snapshot. Trades change what the next month-end snapshot will record but never write or amend one. Nor does it touch the gradebook — trading and earning are fully independent.

Failure modes seen in the code

  • The fee is charged for every pending order's user even if every order in the batch was cancelled — TransactionFeeProcessor receives the original pending_orders relation and does not check status (app/jobs/order_execution_job.rb:29-33).
  • A stock with price_cents = nil raises in Order#purchase_cost (app/models/order.rb:105-107).

Surfaces

Surface Role
OrdersController, order_form Stimulus controller student writes
OrderExecutionJob (Solid Queue, every 15 min) executes
teacher/admin order lists read

See

  • Objects: order, portfolio, portfolio-stock
  • Source: app/services/execute_order.rb, app/jobs/order_execution_job.rb
  • As-built: docs/orders-and-transactions.md