Every lesson since the schema has been rehearsing for this one. Lesson 3 promised the autoincrement would delete a class of code. Lesson 8 introduced the transaction. Lesson 9 left a Place Order button unwired. Today the button works: one click, and the order row, its line items, a status row, and the inventory decrements all land together — or none of them do. Checkpoint: lesson-10.

All or nothing, spelled out

db::order::place is the longest function in the port, and it’s still just a straight line: begin, insert, loop, insert, commit.

let mut tx = pool.begin().await?;

let order_id: i64 = sqlx::query_scalar!(
    r#"INSERT INTO orders (userid, orderdate, /* …20 columns… */)
       VALUES (?1, date('now'), /* … */)
       RETURNING orderid as "orderid!""#,
    /* bindings from the draft and cart */
)
.fetch_one(&mut *tx)
.await?;

RETURNING orderid is the lesson-3 promise kept. The original gets its order number by reading a counter from the sequence table, incrementing it, and writing it back — application code doing id generation with a race condition for a bonus. Our id comes back from the insert itself, assigned by the database, unique by construction. The port has no getNextId() because there is nothing to port it to.

Then the loop, where each cart line writes two statements:

sqlx::query!(
    "INSERT INTO lineitem (orderid, linenum, itemid, quantity, unitprice)
     VALUES (?1, ?2, ?3, ?4, ?5)", /* … */
).execute(&mut *tx).await?;

let updated = sqlx::query!(
    "UPDATE inventory SET qty = qty - ?2 WHERE itemid = ?1 AND qty >= ?2",
    line.item_id,
    quantity
).execute(&mut *tx).await?.rows_affected();

if updated == 0 {
    return Err(PlaceOrderError::OutOfStock(line.item_id.clone()));
}

That UPDATE is the sharpest line in the lesson. The qty >= ?2 in the WHERE clause makes the decrement and the stock check one atomic statement — there is no read-then-write gap for a concurrent purchase to slip through. If the stock isn’t there, no row matches, rows_affected is zero, and we return an error.

And here is Rust being quietly excellent: that early return is the rollback. tx goes out of scope without commit, and a dropped transaction rolls back — the order row, the earlier line items, the earlier decrements, all of it. No try/catch/rollback choreography, no @Transactional proxy magic deciding which exceptions count. The test suite proves it: drain EST-20 to zero, try to buy it along with two angelfish, and afterward there are no orders and the angelfish count is untouched.

The moment of truth re-checks everything

The handler doesn’t trust the confirmation page:

let Some(draft) = session.get::<OrderDraft>(checkout::DRAFT_KEY).await? else {
    return Ok(Redirect::to("/checkout").into_response());
};

The confirm page’s checks were a different request. Between then and this click, the cart may have emptied, the draft may have gone. Re-checking costs four lines and buys a pleasant surprise: after a successful order clears the draft, a refresh of the POST finds no draft and bounces to /checkout — the double-submit problem solved by state hygiene rather than by clever tokens.

On success: draft removed, cart reset, redirect to the placed page. On OutOfStock: a page that says so and promises, truthfully, that nothing was written.

Guessable numbers, guarded doors

Order ids are now sequential: yours is #1, the next buyer’s is #2. Which means anyone can type /orders/placed/1. The placed page’s handler is where that stops mattering:

match db::order::owner(&pool, order_id).await? {
    Some(owner) if owner == username => { /* render */ }
    _ => Err(AppError::NotFound),
}

This class of bug — authenticated user, unauthorized object — is IDOR, insecure direct object reference, and it has headlined real breaches at companies with security teams. Two details to copy: the check happens in the handler that serves the data, not in some upstream filter that might not cover a future route; and “not yours” returns the same 404 as “not real,” because a 403 would confirm the order exists. Lesson 11’s history pages inherit this pattern wholesale.

Build it

File Action What goes in it
src/db/order.rs write PlaceOrderError, place (the shape above — the 22-column INSERT is tedious; copying it is honest), owner; three tests: everything-writes, out-of-stock-rolls-back, ids-ascend
src/db/mod.rs modify pub mod order;
src/web/orders.rs write place (re-check, call, clear, redirect; OutOfStock renders the failed page) and placed with the ownership match
src/web/cart.rs modify save becomes pub(crate)
src/web/mod.rs modify mod orders;, the POST /orders and GET /orders/placed/{id} routes
templates/confirm.html modify The placeholder becomes the real Place Order form
templates/order-placed.html, order-failed.html write Thank-you with the order number; the honest failure page
.sqlx/ generated cargo sqlx prepare

Done when: a full flow (sign in → add → checkout → confirm → Place Order) lands on “Order #1”; the cart is empty afterward; re-POSTing /orders bounces to /checkout instead of double-ordering; sqlite3 jpetstore.db 'SELECT qty FROM inventory WHERE itemid="EST-1"' shows the decrement; signing in as ACID and visiting your order’s URL gets a 404; and cargo test shows 25 passing.

Answer key: lesson-09...lesson-10.

Challenge: the price you were promised

The cart stores unit prices captured at add time (lesson 7’s decision), and place writes them into lineitem unchecked. Suppose an admin repriced EST-1 between add and checkout. Decide what should happen — honor the cart price, or re-fetch and make the buyer re-confirm — then implement it inside the transaction. There’s no universally right answer; there is a right place to enforce whichever you choose.

Next lesson: order history and the account page — reading back what this lesson wrote, and feature parity with the original.