Forms & Mutations

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:

Terminal window
git fetch origin
git diff HEAD..origin/milestone-19
git merge --ff-only origin/milestone-19

If 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:

Terminal window
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.mjs

Use 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:

Terminal window
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

  1. Add the supplied price helper, draft schema, and create operation. Test the request fields, identity header, accepted record, and expected errors.
  2. 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.
  3. 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

  1. Extend the API-module checks for PATCH field/version selection, accepted records, 409 errors, and empty deletion.
  2. 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.
  3. 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:

  1. Preserve the proposed title and show clear conflict feedback. Do not automatically read the latest record or send another PATCH.
  2. Keep Save edit TITLE disabled, where TITLE is the currently displayed accepted title. Offer Discard edits and reload TITLE.
  3. 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.
  4. 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:

Terminal window
git fetch origin
git diff HEAD..origin/milestone-23
git merge --ff-only origin/milestone-23

If 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

  1. Read the supplied form and schema, then implement and check the publication operation’s payload, accepted record and error behavior.
  2. 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.
  3. 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:

Terminal window
git fetch origin
git diff --stat HEAD..origin/milestone-24
git diff HEAD..origin/milestone-24
git merge --ff-only origin/milestone-24

If 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

  1. Receive the configuration, chain and export your route, and run the client checker.
  2. 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.
  3. 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:

Terminal window
git fetch origin
git diff HEAD..origin/milestone-26
git merge --ff-only origin/milestone-26

If 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:

Terminal window
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml up -d database
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --build --rm database-migrations

Client checks: run the tests and production build. The explicit type checker is a separate command; its assessment role is shown below.

Terminal window
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --build --rm --no-deps client deno task test
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --rm --no-deps client deno task check
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --rm --no-deps client deno task build
MilestoneType checking
19-23 and 25Optional diagnostic; no compiler points.
24Required for the inferred-client checks; runtime behavior is assessed separately.
26Assessed separately for five points.

API checks: retain the existing suite as client behavior changes.

Terminal window
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --build --rm api deno task test

Milestone 26 also runs the two submitted API test files explicitly:

Terminal window
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.ts

Browser checks: build and start the isolated API and client, then run the relevant Playwright file. For milestone 19, use the existing browsing regression:

Terminal window
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml up -d --build api client
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml run --build --rm e2e-tests npm test -- tests/student_browse.spec.ts

For 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:

Terminal window
docker compose -p wsd-marketplace-test -f compose.yaml -f compose.test.yaml down

Existing 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

  1. A draft save returns STALE_VERSION. What should remain visible and blocked until an explicit discard and successful reload?
  2. After a bid response is lost, what should the interface display while a fresh read establishes the latest server state?