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.