This is the parity lesson. When it’s done, everything the Java original does, the port does: browse, search, cart, accounts, checkout, orders, history — and the two dusty personalization toggles nobody remembers fondly but the port plan promised to keep. Checkpoint: lesson-11.

Reading back what you wrote

Order history is two queries and a rule about prices. The summaries:

pub async fn history(pool: &SqlitePool, username: &str) -> Result<Vec<OrderSummary>, sqlx::Error> {
    sqlx::query_as!(
        OrderSummary,
        r#"SELECT orderid as "id!: i64", orderdate as "date!: String",
                  totalprice as "total!: Cents"
           FROM orders WHERE userid = ?1 ORDER BY orderid DESC"#,
        username
    )
    .fetch_all(pool)
    .await
}

Two small decisions worth noticing. The date maps to a plain String — SQLite stores YYYY-MM-DD, the page displays YYYY-MM-DD, and nothing in between computes with it, so pulling in a chrono dependency would be ceremony for its own sake. The dependency budget survives to lesson 12’s scorecard intact.

And the detail query joins the catalog for display names but takes its prices from lineitem:

SELECT li.itemid …, p.name …, li.unitprice as "unit_price!: Cents"
FROM lineitem li
JOIN item i ON i.itemid = li.itemid
JOIN product p ON p.productid = i.productid

History shows what you paid, frozen at purchase by lesson 10’s transaction — not what the item costs today. If you did lesson 10’s challenge, you’ve already thought hard about exactly this line.

Ownership, folded into the WHERE clause

Lesson 10 checked order ownership with a fetch-then-compare. The detail page does it better:

pub async fn summary_for(pool, order_id, username) -> Result<Option<OrderSummary>, sqlx::Error> {
    sqlx::query_as!(/* … */
        r#"SELECT … FROM orders WHERE orderid = ?1 AND userid = ?2"#,
        order_id, username
    ).fetch_optional(pool).await
}

Existence and authorization answered by one query: an order that isn’t yours and an order that isn’t real are both None, and the handler turns either into the same 404. There’s no code path where the app has loaded someone else’s order and is relying on a later if to not show it — the data never arrives. When the check is the query, it can’t be forgotten in a refactor.

The optional-auth home page

The account page grows real content (name, address, email, a link to your orders — one flat query_as! onto an AccountInfo struct). But the interesting auth problem is the home page, because of the two toggles in the seeded profile: favcategory=DOGS, mylistopt=1, banneropt=1. The original’s home page shows your favorite category’s banner and a “MyList” of its products — if you’re signed in, if you opted in.

The home page can’t take an AuthUser: that extractor redirects anonymous visitors, and the home page belongs to everyone. So web/account.rs gains the extractor’s gentle sibling:

pub(crate) async fn current_user(session: &Session) -> Option<String> {
    session.get::<String>(USER_KEY).await.ok().flatten()
}

AuthUser for pages that require a user, current_user for pages that adapt to one. The handler reads it, loads prefs if present, and the template renders zero, one, or two extra sections. Anonymous visitors get exactly the page they got yesterday.

One more old friend: bannerdata stores its banner as legacy markup — <image src="../images/banner_dogs.gif"> — because of course it does. It goes through parse_legacy_description like every other 2002 string, and the template writes its own <img> tag. The boundary parser from lesson 5 didn’t need a single change to cover a table it had never seen.

Parity

Sign in as j2ee, and the home page grows a dog banner and a dogs list. Place an order, follow it into history, open the detail. Then run down the checklist against the original in Docker: catalog, search, cart, sign-in, registration, checkout, order placement, history, personalization. The port does what the original does — minus the three things we refused on purpose (plaintext passwords, stored card numbers, the sequence table), plus the things the original never had (compile-checked SQL and templates, typed ids, a 404 that knows what it’s for).

Two features remain honestly unported, and the Build-it table calls them out: account editing (the original has it; today’s challenge is to add it) and the language preference (langpref is seeded and ignored, faithful to how most people experienced it).

Build it

File Action What goes in it
src/domain/order.rs write OrderSummary and OrderLine + subtotal()
src/domain/account.rs write AccountInfo (flat — it’s one row) and Prefs
src/db/order.rs write history shown above; summary_for with the ownership WHERE; the three-table lines join; a test that reads back a placed order and proves ACID sees nothing
src/db/account.rs write info, prefs (ints to bools in the column overrides), banner_image reusing the legacy parser
src/web/orders.rs write history and detail handlers — the shapes you know
src/web/account.rs modify current_user shown above; the account handler fetches info
src/web/catalog.rs modify home gains the session and the prefs logic
src/web/mod.rs modify GET on /orders, and /orders/{id} (the static placed segment wins over the param — no conflict)
templates/orders.html, order.html write History table; detail with frozen prices
templates/account.html, home.html, order-placed.html modify Real info; the two conditional sections; links into history
static/images/banner_*.gif copy Five more gifs from the original, five more match arms in assets.rs
.sqlx/ generated cargo sqlx prepare

Done when: signed in as j2ee, the home page shows the dogs banner and “Your DOGS list” while an anonymous window shows neither; /account shows the seeded address; a placed order appears in /orders and its detail page; ACID sees an empty history and a 404 on your order’s URL; and cargo test shows 26 passing.

Answer key: lesson-10...lesson-11.

Challenge: edit the account

The original lets you change your address and contact info; the port should too. A GET that prefills a form from AccountInfo, a POST that UPDATEs the account row — you’ve built both halves before, in lessons 8 and 9. The design question worth a minute of thought: should this form update signon too (a password change), and if so, what does lesson 8 tell you about how that flow has to differ?

Next lesson is the last: the test suite meets property-based testing, the repository trait finally earns its existence, and we put the whole port on a scale next to the original.