Marketplace Project: Make the Marketplace Interactive
Learning Objectives
- You can connect forms to versioned API mutations and recover from rejected or uncertain outcomes without losing user input.
- You can introduce a typed client without changing the component-facing behavior.
- You can verify existing reads and new mutations with client, HTTP, database, and browser tests.
We have followed the notes form through real HTTP requests, stored records,
validation, a typed-client refactor, and tests. We have also practiced recovery
from failed or uncertain operations. Now connect those ideas to the marketplace API
from Part 3. Continue in your local marketplace repository (marketplace/) after
milestone 18; this part adds milestones 19-26, from a verified read client to seller and buyer interactions.
Each card describes the next change and its checks. The shared project reference below contains the API contract, client interfaces, test commands, and submission steps used throughout these milestones. Preserve the accepted browsing interface and server behavior as you extend the client.
Chapter 4.3’s parent-owned collection and acceptance callback are enough for
these workflows. Keep seller records in their view and the confirmed listing in
its detail view; children report accepted changes or ask the parent to refresh.
The state folder does not require an application-wide reactive store.
Prepare the client for interaction
The supplied contribution includes the schema dependency and a preparation script that moves your existing browser modules into role folders and updates imports. Then carry the auction fields through the read decoder and test fixtures. The script preserves your implementation; extending the read boundary is your task.
Run the client tests and production build, then verify browsing, parent-category filtering, and direct or reloaded detail pages against the actual API/database. This checkpoint establishes a working client before seller forms add new state.
Marketplace milestone 19: prepare the client for interaction
0 / 20 points
Use the shared project reference for the API and client contracts, test commands, and submission steps.
Receive the supplied preparation
Continue on master after accepted milestone 18. The contribution supplies the
pinned schema dependency and a script that moves your existing client modules
into components/, api/, and state/ and updates their imports. It leaves
read behavior unchanged. With a clean working tree, inspect and receive it:
git fetch origingit diff HEAD..origin/milestone-19git merge --ff-only origin/milestone-19If local commits prevent fast-forwarding, inspect the contribution and use
git cherry-pick origin/milestone-19; do not reset your work. Preserve any
additional dependencies while resolving configuration conflicts.
From marketplace/, run the supplied preparation once:
docker compose run --rm --no-deps --user "$(id -u):$(id -g)" --volume "$PWD:/workspace" --workdir /workspace client deno run --allow-read=client --allow-write=client tools/prepare-part4-client.mjsUse Bash/WSL; on native PowerShell replace the user option with --user 0:0.
With host Deno, run the same deno run command from marketplace/.
Review the moved files and imports. Rebuild the client to install the supplied
pinned dependencies. No application-wide store or session module is needed.
Keep the Part 3 read representation
The supplied client/src/lib/api/marketplace-types.ts exposes the existing
Listing and Category types. Extend the Listing definition alongside its
decoder; you may move the definitions into the type module if useful. Preserve
getListings(signal?),
getCategories(signal?), getListing(id, signal?), and ApiError with its
HTTP status. Collection reads return bare arrays; a detail read returns a
bare object. Keep the /api/listings, /api/categories, and
/api/listings/:id paths and the configured VITE_API_BASE_URL.
Preserve the earlier read decoders. Extend the listing decoder and fixtures to
retain the auction fields introduced by Part 3: currentPrice (integer cents
0-2147483647), status (draft, active, or closed), version (a positive
INTEGER), endsAt (an ISO timestamp or null), and winningBidId (a positive
INTEGER or null). Keep the existing listing fields and related seller/category
names. Reject malformed successful responses, including invalid identifiers,
prices, statuses, versions, or collection envelopes; select the public fields
explicitly so unrelated response fields are not copied into client state.
Categories keep their existing id, nullable parentId, name, and slug.
Add or preserve focused checks in api/marketplace-api.test.ts for a valid
auction read and a malformed response. Update component fixtures to the same
representation while preserving empty/error/retry and stale-request tests.
Verify the prepared client
Run the shared client checks and the existing browsing regression
(tests/student_browse.spec.ts) against the isolated API/database.
The existing browser regression checks listing data, category filtering, and
direct/reloaded detail pages. Keep it passing alongside the original counter,
todos, and missing/invalid-detail feedback; no second written or manual
browsing checklist is required.
Grading awards 10 points for the extended auction read boundary and 10 points
for the built application’s actual
API-backed browsing, category filtering, and direct/reloaded detail pages.
A failed build prevents the browser check. Local deno task check is an optional
diagnostic here; this milestone awards no compiler points.
Submit the moved source, updated tests, and dependency files. Once this checkpoint passes, continue with the seller draft workflow in milestone 20.
Create and list drafts
Start with one complete browser mutation. Add the explicit create operation, connect a validated form, and display the server’s accepted draft. Begin the component suite with the returned record and a combined pending-to-rejection case that checks feedback and retained input. Then reload in the browser to verify that the API retrieves the stored row. The handout supplies price conversion.
Marketplace milestone 20: create and list drafts in the browser
0 / 65 points
Use the shared project reference for the API and client contracts, test commands, and submission steps.
Continue after milestone 19
Continue in the prepared marketplace client. This checkpoint implements a complete create-and-list interaction before adding editing, deletion, or publication.
Objective
Add createDraft(input, options?) using explicit fetch. Send only title,
description, categoryId, and startingPrice. Require 201 and return the server’s
accepted listing. Read the actor’s listings through GET /api/my/listings.
Add /my/listings and SellerListings.svelte. Label the actor selector Local development actor, and the form controls Title, Description, Category, and
Starting price in euros. Label the button Save draft. Explain that the actor
selector is temporary development infrastructure. Select the first configured
actor initially; use existing user IDs as described below.
Once categories load, preselect an available category. Use DraftFormSchema and
the supplied price helper below. Display parsed values only after server acceptance.
Show Saving draft... and disable submission while pending; preserve entered
values after rejection. Show Draft saved after acceptance and display the returned
row, including title, status, and version, in the Your listings list. Reload
must retrieve the stored row from the API.
Configure the development actors
Continue with your existing Part 3 users. Inspect the current IDs in your
development database; generated IDs need not be 1 and 2:
docker compose exec database psql -U walking_skeleton -d walking_skeleton -c "SELECT id, display_name FROM users WHERE deleted_at IS NULL ORDER BY id;"Use two of those users in the supplied configuration below, replacing the
example IDs and names with your query results. Put it in
client/src/lib/state/development-actors.ts and use its entries for the
selector. Milestone 25 reuses it for the bidder selector. This is local
configuration, not a new API endpoint or authentication feature.
export const developmentActors = [ { id: 1, name: "Jamie" }, { id: 2, name: "Noor" },];Use the same configuration with your isolated test database, or adjust it to that database’s existing users before running the browser journey. The browser checks choose an existing actor from the selector rather than assuming an ID.
Local checkpoints
- Add the supplied price helper, draft schema, and create operation. Test the request fields, identity header, accepted record, and expected errors.
- Connect the form and actor-specific list. Begin the component checks with an accepted result whose title differs from the submitted text. In another scenario, hold creation pending, then reject it with an API error; assert the pending feedback, displayed error and retained raw inputs in that case.
- Configure API CORS for the development actor header and credentialed requests from the client origin, as described in the shared reference. Create a draft in the browser, inspect its 201 response, and reload to retrieve it.
Save focused client tests in client/src/lib/state/mutation-state.test.ts.
Extend this same seller suite in later milestones. Several observations can
share one scenario; the final checkpoint assesses their assertions, not a fixed
number of tests.
Export parseEuroCents(text: string): number | null from the supplied money schema.
Editing, deletion, publication, and bidding belong to later checkpoints.
Supplied price conversion
Add the following helper to client/src/lib/schemas/money-schema.ts. It is
supplied so this milestone can focus on the form and mutation behavior. Use
its parsed value rather than implementing a second conversion in the component.
import { z } from "zod";
export const EuroCentsSchema = z.string().trim() .regex(/^(0|[1-9]\d*)(?:\.\d{1,2})?$/) .transform((text) => { const [whole, fraction = ""] = text.split("."); return Number(whole) * 100 + Number(fraction.padEnd(2, "0")); }) .pipe(z.number().int().min(0).max(2147483647));
export const parseEuroCents = (text: string): number | null => { const parsed = EuroCentsSchema.safeParse(text); return parsed.success ? parsed.data : null;};Test and submit
Run the shared client and API checks, then complete the browser checkpoints above. Keep earlier tests passing and submit the cumulative project. Type checking is optional here; browser checks require a successful production build.
Points
- 25 points: Construct and interpret the explicit draft-creation request.
- 25 points: Convert prices and retain input during pending and rejected creation.
- 15 points: Create a draft through the browser and retrieve it after reload.
Edit and delete drafts
Extend the API-module checks for versioned PATCH and empty DELETE responses, then connect editing and deletion controls on the working seller page. Update the view from accepted results, preserve the proposed edit after rejection, and verify accepted edits and deletion through the browser and a subsequent reload. Reuse the creation suite; the new component case in the next milestone will cover stale-edit recovery. The server operations already exist from Part 3.
Marketplace milestone 21: edit and delete drafts
0 / 55 points
Use the shared project reference for the API and client contracts, test commands, and submission steps.
Continue after milestone 20
Keep the accepted creation form, actor-specific listing read, price conversion, and tests. Extend the seller page with draft editing and deletion.
Objective
Add updateDraft(id, input, options?) and deleteDraft(id, options?). PATCH sends
only edited fields and the version the user loaded, requires 200, and returns the
accepted listing. DELETE requires 204 and must not parse its empty response.
For each draft, provide a title control labelled Proposed title for TITLE, a
Save edit TITLE button, and a Delete TITLE button. Update the row only from an
accepted result. Disable the relevant action while pending. Hide draft edit and
delete controls for active or closed listings; the API remains authoritative.
If an edit is rejected, retain the proposed title and the last accepted row and
show feedback. Rejected deletion must leave the row visible. This includes a
STALE_VERSION rejection; explicit discard-and-reload recovery comes in milestone 22.
Local checkpoints
- Extend the API-module checks for PATCH field/version selection, accepted records, 409 errors, and empty deletion.
- Connect editing and deletion using the same pending and retained-input behavior as creation. Keep the existing component suite; milestone 22 adds the distinct stale-edit recovery case.
- Edit a real draft, reload to verify its accepted title, delete it, and reload to verify its absence. Retain the existing creation tests.
Keep the request checks and creation cases in
client/src/lib/state/mutation-state.test.ts. The grader also checks pending
and rejected edits and deletion; a second component-test matrix is not required.
Test and submit
Run the shared client and API checks, then complete the browser checkpoints above. Keep earlier tests passing and submit the cumulative project. Type checking is optional here; browser checks require a successful production build.
Points
- 20 points: Construct versioned edits and handle empty deletion.
- 20 points: Preserve proposals and rows during pending and rejected changes.
- 15 points: Edit and delete a stored draft through the browser.
Recover conflicting draft edits
Build on the working edit controls. After a stale-version conflict, keep the proposal visible and block further edits. Offer an explicit discard-and-reload action; do not read or retry automatically. A successful read replaces the proposal with the current record, while a failed read keeps the proposal and save block. Add this recovery sequence to the same component suite. Chapter 4.6 demonstrates the state transition.
Marketplace milestone 22: recover conflicting draft edits
0 / 35 points
Use the shared project reference for the API and client contracts, test commands, and submission steps.
Continue after milestone 21
Keep the accepted creation, editing, and deletion behavior. Extend the existing
seller page with recovery from a STALE_VERSION edit rejection.
Objective
After an edit returns STALE_VERSION:
- Preserve the proposed title and show clear conflict feedback. Do not automatically read the latest record or send another PATCH.
- Keep
Save edit TITLEdisabled, where TITLE is the currently displayed accepted title. OfferDiscard edits and reload TITLE. - That explicit action reads the current actor’s listings. Keep the proposal and save block while reading. Only after success replace the displayed records, discard this draft’s proposal, and allow a new edit using the newly read version. Do not submit the discarded proposal.
- If the read fails, keep the proposal, save block, and recovery action. The user can choose discard and reload again. Keep recovery unavailable while its read is pending to prevent duplicate requests.
The API endpoints already exist. This core editor does not compare old and new proposals or reapply a retained edit. Part 9 introduces that richer recovery. Chapter 4.6’s discard-and-reload example demonstrates the state transition.
Local checkpoints
Extend client/src/lib/state/mutation-state.test.ts with one controlled
recovery sequence. A stale edit preserves the proposal and blocks saving
without an automatic read. Start an explicit discard-and-reload, let that read
fail, and check that the input and save block remain. A later explicit reload
succeeds, replaces the input and allows a new edit with the current version,
without replaying the old proposal. While a recovery read is pending, another
save or recovery request remains blocked.
Keep the creation and API-module cases from earlier milestones. This sequence adds recovery coverage to the same suite rather than repeating ordinary saves.
Test and submit
Run the shared client checks and submit the cumulative project. Type checking is optional here. The grader controls responses to isolate the recovery behavior.
Points
- 20 points: Block a stale edit until explicit discard and successful reload.
- 15 points: Preserve input and the edit block after a failed reload, then recover explicitly.
Publish a versioned draft
Receive the supplied publication form and deadline schema, then implement the publication operation through the existing API module. Connect its callback to the parent-owned row, sending the loaded version and displaying active state and the deadline only from the accepted response. The supplied form handles local-time conversion, pending feedback and retained input; the API decides acceptance. Reuse the final browser journey to check publication and its parent update.
Marketplace milestone 23: publish a versioned draft
0 / 25 points
Use the shared project reference for the API and client contracts, test commands, and submission steps.
Receive the supplied publication form after milestone 22
Keep your accepted creation, editing, deletion and recovery behavior. The
contribution supplies PublishDraft.svelte and publication-schema.ts, including
the labelled deadline control, local-time conversion, pending guard and feedback.
It supplies no publication API operation or parent-list integration. With a clean
working tree, inspect and receive these two files:
git fetch origingit diff HEAD..origin/milestone-23git merge --ff-only origin/milestone-23If local commits prevent fast-forwarding, inspect the contribution and use
git cherry-pick origin/milestone-23; preserve your work when resolving conflicts.
The contribution uses the existing client dependencies and API configuration.
Objective
Implement publishDraft(id, version, endsAt, options?) in the existing mutation
module. Send only the loaded version and supplied ISO timestamp to POST
/api/listings/:id/publish. Require 200 and return the accepted listing; retain
the module’s documented error and unconfirmed-outcome handling.
Mount the supplied PublishDraft.svelte for each editable draft and connect
onpublished(acceptedListing) to the parent-owned row. Use the returned record,
including any server-normalized values. Display active state, current price, deadline, and the new version only
after acceptance. Rejection preserves the proposal and draft state. Hide draft
editing and deletion after successful publication.
Chapter 4.4’s deadline preview demonstrates local-to-UTC conversion. The API decides whether the instant is an acceptable future deadline.
Component interface
PublishDraft.svelte receives listing, options, and onpublished(acceptedListing).
Its input is labelled Auction deadline (your local time) and its button Publish TITLE.
It shows Publishing... and disables the button while pending. On acceptance, it
shows Published until followed by the returned deadline and invokes the callback
with the accepted listing. It never invokes the callback on rejection and preserves
the proposal for correction. Your parent component replaces its row with the
accepted record.
Local checkpoints
- Read the supplied form and schema, then implement and check the publication operation’s payload, accepted record and error behavior.
- Connect the form’s callback to the parent. Keep a rejected draft unchanged and replace it only after acceptance. The supplied component handles local conversion and pending feedback, which remain covered by the grader.
- Reuse the final browser journey in milestone 26 to check publication and the accepted parent update. Keep the existing seller component suite; publication needs no additional authored component suite.
Typed clients are introduced in milestone 24 after this behavior works.
Test and submit
Run the relevant existing client and API checks. Keep earlier tests passing and submit the cumulative project. Type checking is optional here; browser checks require a successful production build.
Points
- 25 points: Implement versioned publication and connect accepted state to the parent view. Supplied form behavior remains protected by the same checks.
Refactor creation to a typed client
Milestone 24 supplies the source-type alias and Docker configuration for your accepted project. Follow the handout to receive this contribution; no additional download is needed.
Chain the existing Hono routes, export their inferred type, and run the supplied
type-checking command. Refactor the working create operation to hc and rerun its HTTP
tests. Observe a request-type diagnostic and one response-field change in the
actual API, then restore the working contract and verify draft creation. Use
import type { AppType } from "#api" to consume the actual route type.
The supplied source mount and check:watch task keep diagnostics current after API edits.
Marketplace milestone 24: refactor creation to an inferred HTTP client
0 / 45 points
Use the shared project reference for the API and client contracts, test commands, and submission steps.
Receive the configured tools after milestone 23
Continue in your accepted marketplace repository. The contribution supplies the source-type alias, pinned dependencies and lockfiles, compiler settings, watch-mode check, and Docker/Compose configuration. It adds no route-chain or client-operation answer. With a clean working tree, inspect and receive it:
git fetch origingit diff --stat HEAD..origin/milestone-24git diff HEAD..origin/milestone-24git merge --ff-only origin/milestone-24If local commits prevent fast-forwarding, inspect the contribution and use
git cherry-pick origin/milestone-24; do not reset your work. Resolve configuration
conflicts by retaining your existing application settings alongside the supplied
contract settings. There is no additional download.
Objective
Refactor the registrations in api/src/routes/public-api.ts into a chain and export
AppType = typeof publicApi. Preserve full /api paths and existing route behavior.
Public service input types should not depend on persistence types.
The supplied #api alias resolves to api/src/routes/public-api.ts. The client
Docker build reads the API through an additional build context, and a read-only
source mount exposes API edits to the checker during development. API and client
keep the same sibling layout on the host and inside the client container.
In client/src/lib/api/marketplace-mutations.ts, use
import type { AppType } from "#api" and construct hc<AppType> from the API origin.
Refactor only createDraft to client.api.listings.$post({json: input}). Preserve its
signature, injected transport, development-identity header, accepted record, and
expected/unknown error behavior. Keep existing read decoders and explicit update,
delete, and publication operations.
Create client/contract-checks/valid.ts with a typed draft-creation call and a
successful response use. Narrow the response status to 201 and read
body.listing.id as a number. This file is for the compiler; do not import it
into the running application. Types do not validate arbitrary response bytes
or prevent transport failures.
Observe one API contract change
Temporarily add receiptVersion: 1 beside listing in the actual POST
/api/listings route’s 201 JSON response. In valid.ts, consume
body.receiptVersion after narrowing the response to 201. Do not add a response
interface or cast: the direct #api import supplies this field.
Run the focused checker below. Assigning the inferred receipt field to a
string must produce a type diagnostic; restoring a numeric use must pass.
Also change the typed request’s title to a number, observe its diagnostic, and
restore it. Then remove the temporary receipt field and its example use,
keeping the original listing response and numeric body.listing.id example.
These are local compiler observations, with no separate written report.
Local checkpoints
- Receive the configuration, chain and export your route, and run the client checker.
- Refactor createDraft to hc. Rerun its accepted/rejected HTTP tests to verify that the refactor preserves observable behavior. Reuse the existing component suite without adding another creation case.
- Observe the request and response diagnostics above, restore the contract, and verify creation still works in the browser.
Test and submit
Run the shared client and API checks. Rebuild the client once to receive the
supplied dependencies and configuration; later API source edits need no
generation or client rebuild. Submit the route, client, and configuration
changes. The normal client check is already configured. For the focused compiler check, run deno task prepare, then
deno run -A node_modules/typescript/lib/tsc.js -p tsconfig.contract.json in the client
container. Unrelated component diagnostics do not gate contract points.
For continuous diagnostics, run docker compose run --rm --no-deps client deno task check:watch
in a second terminal. It watches client files and imported API source. Stop it
with Ctrl+C. Browser hot reload and the API watcher remain separate from this check.
Points
- 5 points: The API source and request example type-check.
- 25 points: The client imports the actual server type and preserves route, request, and response inference.
- 15 points: The create operation uses hc while preserving its HTTP behavior.
The inference check requires the compiler check. Runtime behavior is assessed independently. The supplied tools’ internals are not a new implementation task.
Bidding and uncertain outcomes
Connect a bid form to the independently addressable listing detail page. Keep the proposed amount separate from the last confirmed price and deadline, and update accepted state from the server’s response. A lost response leaves the outcome uncertain, so recovery needs a fresh read and a deliberate next action. Focus the new component case on that uncertain-outcome recovery; observe ordinary acceptance and a low-bid rejection in the browser.
Marketplace milestone 25: authoritative bidding feedback
0 / 55 points
Use the shared project reference for the API and client contracts, test commands, and submission steps.
Objective
Add the buyer bid form to the independently addressable listing detail page.
Use a clearly labelled synthetic bidder selector with the existing
developmentActors configuration. Convert the displayed euro proposal to integer cents and send
only { amount }; bidder, price, deadline, status and version remain server-owned.
Keep the user’s proposal, the pending request and the last confirmed listing snapshot as separate state. Do not raise the displayed current price or announce success before status 201. Reconcile the accepted view with the returned current price, fixed deadline and version. Bidding does not extend the published deadline.
On BID_TOO_LOW or AUCTION_CLOSED, display the server’s decision and refresh
the listing. A rejected network promise does not establish rollback: describe the
outcome as unknown, refresh, and require a deliberate next action rather than
automatically replaying the create request. A browser countdown is advisory and
must not decide whether the bid arrived in time.
Add one required local component check for an unknown transport outcome and its recovery: retain the proposed amount and confirmed snapshot, let the first refresh fail, then retry the read successfully without sending another bid. Check that bidding stays blocked until the successful read. Run and submit this case with your project; its test code is not separately scored. The grader uses instructor-owned checks for recovery and ordinary pending, acceptance and rejection behavior. Keep the raw Deno validation and locking tests from Part 3; its independent checks continue to cover transaction rollback.
Use Chapter 4.6’s unknown-result distinction:
update the confirmed listing through a fresh read without remounting the bid
form or replacing its proposed amount. A read must not automatically place a bid.
If the read fails, retain the confirmed snapshot and proposal, show a refresh
error in a role="alert" region, and offer a Refresh listing button. Disable
bidding until a fresh read succeeds, then clear the refresh alert. Retrying this
read sends no bid. Keep the proposal visible when the refreshed auction is closed,
with bidding disabled.
The parent can pass refreshBlocked?: boolean to BidForm for this purpose;
default it to false for isolated component use.
Files to create or modify
Create client/src/lib/components/BidForm.svelte and extend the existing client/src/lib/components/ListingDetail.svelte. Modify client/src/lib/api/marketplace-mutations.ts and the listing-detail route to connect them. Add the focused bid-recovery case beside the marketplace components, reusing existing test setup where useful. Retain the seller suite and raw API tests.
Test and submit
Run the shared client checks and the raw API tests against the isolated database. Then exercise one accepted bid and one low-bid rejection in the browser before submitting. The controlled case covers interrupted-outcome recovery. Type checking is optional here; this milestone’s points assess application behavior through the instructor-owned checks.
Points
- 30 points: Send only the proposed amount and reconcile an accepted server result.
- 25 points: Distinguish business rejection from an unknown transport outcome.
Component contract
BidForm.svelte receives listing, onaccepted(listingSnapshot), and
onrefresh(). It can use the global fetch for controlled tests. Render bid feedback
in a role="status" region. Label its input Bid amount in euros, its temporary
selector Local bidder, and its
button Place bid. Pending status is Submitting bid...; disable the button.
For acceptance, call onaccepted with the result’s listing and show
Bid accepted at €AMOUNT. with two decimals. A low bid message includes too low;
an interrupted request includes outcome is unknown. Preserve the entered text
and call onrefresh once for a rejected/unknown outcome. The detail view displays
Current price: €AMOUNT from confirmed state and is reachable at /listings/:id.
Test the complete marketplace workflow
Reuse the focused tests written during milestones 20-25. Complete any missing observations in the existing seller suite: pending creation feedback, a displayed API error, the server-normalized result, raw input after rejection, and stale-edit recovery. One scenario can establish several observations; there is no fixed test count or new component suite. Complete the meaningful assertions in the supplied browser journey from seller publication to buyer bidding. Preserve the raw request and database tests that protect the server rules. Run these checks against your application, focusing on visible input, feedback, and accepted server state. No separate written report or deliberately broken application is required.
Marketplace milestone 26: Part 4 interaction regression and acceptance
0 / 35 points
Use the shared project reference for the API and client contracts, test commands, and submission steps.
The returned client/src/lib/state/mutation-state.test.ts imports the operations
from ../api/marketplace-mutations, the money parser from
../schemas/money-schema, and components such as
../components/SellerListings.svelte. Keep these paths aligned with the role
folders. Use the supplied Vitest and Testing Library imports.
Receive the browser journey after milestone 25
The contribution supplies only e2e-tests/tests/student_mutations.spec.ts.
Its unique records, local actor choices, navigation, deadline conversion and
response waits are complete; two assertion sections are left for you. Receive
it into your accepted project without replacing your application:
git fetch origingit diff HEAD..origin/milestone-26git merge --ff-only origin/milestone-26If local commits prevent fast-forwarding, inspect the contribution and use
git cherry-pick origin/milestone-26, preserving your work. If that test file
already exists, merge the useful supplied setup into your own journey.
Objective
Reuse the seller Vitest suite from the earlier milestones. Add only missing observations: pending feedback during creation, a displayed API error, a server-normalized creation result, a stale-version edit conflict that blocks saving until explicit discard and successful reload, and preservation of raw creation inputs after rejection. A single scenario can establish several of these observations; there is no fixed test count. Keep the accepted-result, pending-to-rejection and conflict-recovery cases you already wrote, rather than creating another component suite.
Complete the supplied browser journey’s two assertion sections. Assert that publication returned 200 and the UI displays its accepted publication. Assert that the other actor’s bid returned 201 and the UI displays the accepted bid and confirmed current price. Use the supplied response objects and Playwright’s retrying visible-state assertions; merely clicking buttons or receiving any response is not evidence of acceptance. Keep one journey and its supplied waits.
Keep direct Deno request tests for malformed JSON, wrong types, unexpected keys, draft state/version rules and bidding invariants. Keep the real database locking tests. This checkpoint runs both submitted API test files and an independent transaction-rollback check before the browser suite. Typed-client tests establish request construction; they do not replace adversarial callers or PostgreSQL evidence.
Keep the development actor header visibly identified as temporary and unsafe for public mutation access.
Files to create or modify
Keep or extend api/tests/student_draft_test.ts, api/tests/student_bid_http_test.ts,
client/src/lib/state/mutation-state.test.ts, and
e2e-tests/tests/student_mutations.spec.ts. Modify implementation files only to repair a behavior demonstrated by those
tests; do not replace the cumulative Part 4 application with a test fixture.
Test and submit
For the final checkpoint, run the two submitted API tests with deno test —no-check,
the existing complete client suite with deno task test, the explicit client check
and build, and
e2e-tests/tests/student_mutations.spec.ts against the isolated Compose stack.
Run the API and browser suites serially while they share that database.
The submitted suites must contain executable assertions and pass against your
cumulative application. Test through public controls and request/response
behavior rather than private component state. Keep any test helpers in
client/src/. No separate evidence report or deliberately faulty version is
required. Type checking, runtime tests, and building are assessed separately.
The grader also runs the seller suite against a compatible course implementation
to check the listed observations. Keep those tests portable: render
SellerListings.svelte, use the documented labels and controls, and control
HTTP responses through fetch. Avoid importing private stores, mocking a
particular internal module, or depending on the number or placement of feedback
regions. Submitted helpers remain available, but application internals may differ.
Read actor IDs from the configured selector or development-actors.ts; do not
assume that the local users have IDs 1 and 2.
Points
- 5 points: The cumulative client and browser contract pass the type checker.
- 20 points: The submitted API and client runtime tests, the independent transaction-rollback check, and the client production build pass.
- 10 points: Meaningful assertions establish accepted seller publication and buyer bidding while the API and PostgreSQL remain authoritative.
Type-checking diagnostics appear in the 5-point test result. Type errors do not block the runtime, build or browser points. A failed production build prevents browser checks from running; their feedback identifies the build prerequisite.
The accepted marketplace now supports interaction through the browser. Keep the repository for Part 5, which replaces the temporary development actor controls with real accounts, sessions, and permission checks.
Shared project reference
Use this reference as each milestone needs it. The API operations already exist from Part 3; each card identifies the changes to implement. Milestone 24 supplies the additional configuration for typed clients.
Working repository and submission
Continue on master in marketplace/ from the preceding accepted milestone.
Keep the existing features, migrations, and tests. Paths such as services/...
are relative to api/src/, and @src/ resolves to that directory. The API uses
Deno, Hono, Zod, sValidator, and postgres.js through api/deno.json.
Before submitting, inspect git diff, commit the requested source, tests, and any
changed lockfile, then run git push origin master. Exclude secrets, dependencies,
build output, and test results.
Local checks
Run commands from the marketplace repository root. Docker Compose supplies
database, database-migrations, api, client, and e2e-tests. The test override
removes exposed API/client ports and uses temporary database storage. Compose sets
DATABASE_URL before the API imports its modules.
Use both Compose files and the separate wsd-marketplace-test project throughout.
Start its database and apply migrations:
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml up -d databasedocker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --build --rm database-migrationsClient checks: run the tests and production build. The explicit type checker is a separate command; its assessment role is shown below.
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --build --rm --no-deps client deno task testdocker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --rm --no-deps client deno task checkdocker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --rm --no-deps client deno task build| Milestone | Type checking |
|---|---|
| 19-23 and 25 | Optional diagnostic; no compiler points. |
| 24 | Required for the inferred-client checks; runtime behavior is assessed separately. |
| 26 | Assessed separately for five points. |
API checks: retain the existing suite as client behavior changes.
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --build --rm api deno task testMilestone 26 also runs the two submitted API test files explicitly:
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --build --rm api deno test --no-check --allow-env --allow-net tests/student_draft_test.ts tests/student_bid_http_test.tsBrowser checks: build and start the isolated API and client, then run the relevant Playwright file. For milestone 19, use the existing browsing regression:
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml up -d --build api clientdocker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --build --rm e2e-tests npm test -- tests/student_browse.spec.tsFor milestone 26, use tests/student_mutations.spec.ts instead. A successful
production build is required before browser checks can run. Run suites
sequentially because they share the isolated test database.
For the manual browser observations in each card, start the normal development
stack with docker compose up -d --build and visit http://localhost:5173.
It uses development data; the isolated test project exposes no host ports.
After the test runs, stop only the test project:
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml downExisting API contract
The baseline API uses full /api paths. POST /api/listings takes strict JSON
{title,description,categoryId,startingPrice} and returns 201 {listing}.
Title is trimmed to 1-120 characters, description has at most 4000 characters,
categoryId is an existing positive INTEGER, and price is integer cents
0-2147483647. A new draft starts at version 1 with null deadline/winner.
PATCH /api/listings/:id takes {version, ...editableFields}, requires at least
one editable field, and returns 200 {listing} with version incremented once.
POST /api/listings/:id/publish takes {version,endsAt}; endsAt is an ISO string
with an explicit timezone, strictly in the future. The supplied API schema
accepts seconds with optional fractional seconds and normalizes to a UTC ISO
string at millisecond precision. It returns
200 {listing} with active state and a new version. DELETE /api/listings/:id
allows drafts only and returns empty 204. GET /api/my/listings returns the
current actor’s bare listing array; GET /api/listings/:id returns a bare listing.
POST /api/listings/:id/bids takes only {amount} (integer cents 1-2147483647),
and returns 201 {bid:{id,listingId,amount,createdAt}, listing:{id,currentPrice,endsAt,status,version}}. A bid must exceed currentPrice
and arrive before the post-lock server time reaches the deadline. Accepted bids
preserve the deadline set when the listing was published.
Errors are {error:{code,message,fields?}}, with optional {path,message} field
entries. Missing resources give 404 NOT_FOUND; invalid bodies 400 INVALID_INPUT;
stale drafts 409 STALE_VERSION; non-drafts 409 NOT_DRAFT; invalid publication
deadlines 400 INVALID_DEADLINE; low bids 409 BID_TOO_LOW; closed/expired auctions
409 AUCTION_CLOSED. Rejections preserve stored state. New mutation operations may assume
the documented bodies; additional mutation-response validators are not required.
Preserve the existing read decoders and their regression tests.
Listing representation
A listing JSON object contains numeric id, sellerId, categoryId,
startingPrice, currentPrice, and version; strings title, description,
sellerName, and categoryName; status (draft, active, or closed);
endsAt (ISO timestamp with milliseconds, or null); and winningBidId (integer,
or null). Names come from the related user/category records. Prices are integer
cents. Collection reads return a bare array, ordered by listing ID ascending and
limited to 100; a detail read returns a bare object. Draft creation, editing, and
publication responses use {listing}. Path IDs match [1-9][0-9]* and are at most
2147483647.
Client mutation interfaces
In client/src/lib/api/marketplace-mutations.ts, export async functions
createDraft(input, options?), updateDraft(id, input, options?),
deleteDraft(id, options?), publishDraft(id, version, endsAt, options?), and
placeBid(id, amount, options?). Options accept baseUrl?: string,
actorId?: number, and transport?: typeof fetch. The default transport is
fetch; the default base is VITE_API_BASE_URL ?? "http://localhost:8000".
Preserve an explicitly empty base. Send actorId as X-Development-User-ID.
Create/update/publish return the accepted listing; delete returns undefined;
bid returns the full {bid,listing} result. HTTP errors expose status, code,
message, and optional field details. Keep a generic error path for unreadable
or unexpected responses; propagate network rejections without replaying a request.
Export parseEuroCents(text: string): number | null from
client/src/lib/schemas/money-schema.ts. Trim surrounding whitespace; accept
nonnegative dot-decimal text with at most two fractional digits and no exponent,
separator, or leading zeros except zero itself. Convert exactly to integer cents
0-2147483647; invalid input returns null. Examples: 12.3 -> 1230, 0 -> 0,
12.345 and 21474836.48 -> null. Bid amounts additionally require at least 1.
When connecting the first seller form in milestone 20, configure the API’s CORS
middleware to allow X-Development-User-ID and credentialed requests from the
configured client origin. Keep that configuration for subsequent milestones.
Use the existing nondeleted users from your Part 3 database for the local actor
selectors. Milestone 20 supplies a small configuration module and the query for
looking up their actual IDs; milestone 25 reuses it for bidders. Do not assume
generated IDs are 1 and 2. The actor header is temporary development
infrastructure; Part 5 replaces it with authentication and authorization.
Check Your Understanding
- A draft save returns
STALE_VERSION. What should remain visible and blocked until an explicit discard and successful reload? - After a bid response is lost, what should the interface display while a fresh read establishes the latest server state?