4.9 KiB
type, universe, status, consumes, produces
| type | universe | status | consumes | produces | |||||
|---|---|---|---|---|---|---|---|---|---|
| process | live | verified |
|
|
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
OrdersController#createsaves the order withuser: 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).- The order sits
pending.Portfolio#cash_on_hand_in_centsalready subtracts it and one fee, so the money is reserved (app/models/portfolio.rb:93-100). - Cron fires
OrderExecutionJobevery 15 minutes (config/recurring.yml:2-6). It retries up to 3 times with exponential backoff (app/jobs/order_execution_job.rb:6). - For each pending order,
ExecuteOrder.executeruns (app/jobs/order_execution_job.rb:35-39). ExecuteOrderreturns unless still pending, then cancels on a negative balance (buy) or insufficient shares (sell) (app/services/execute_order.rb:16-26,64-70).- Otherwise, inside one DB transaction: create the ledger row —
debitfor a buy,creditfor a sell (:39-51); create the lot with negative shares for a sell andpurchase_price: stock.current_pricein dollars (:53-58); mark the ordercompletedand link both records (:60-62). - After the loop,
TransactionFeeProcessor.executecharges $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;
Ordervalidations, 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 —
TransactionFeeProcessorreceives the originalpending_ordersrelation and does not check status (app/jobs/order_execution_job.rb:29-33). - A stock with
price_cents = nilraises inOrder#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