Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,32 +277,6 @@ keeps the bundles honest by failing when a route reads a key it didn't declare.
computing bundle caps for several packages in one pass over shared capacity
maps — worthwhile only if a page with many package links shows up hot.

## Standalone child selector suppressed for sold-out / hidden package members

`src/ui/templates/public/reservations/packages.ts` — `buildPageListingRows`
derives `memberIds` from `packageMemberIds(opts.packages)` (every package's
members), then suppresses the child selector on any standalone row whose listing
is in that set (a rendered package member carries the one selector). But a
**sold-out** package (`limit < 1`) renders a sold-out card with no member rows,
and a `hideListings` package renders no member rows either — so their members'
child selectors are never rendered in the package section. If such a member is
ALSO a standalone parent whose own row is still bookable, its child selector is
suppressed on both paths, so a buyer can't satisfy its multi-choice child
requirements.

CodeRabbit flagged this on PR #1693; it is pre-existing behaviour (verbatim from
the original monolith), not introduced by the template split, so it's out of
scope for that mechanical refactor. Fix: build the suppression set from only the
packages that actually render member rows (skip `pkg.hideListings` and
`packageLimits.get(pkg.groupId)! < 1`), so standalone parents whose package rows
were omitted keep `opts.childCtx`. Ships with a regression test that books a
standalone parent whose sibling-capacity sold-out package hid its member row and
asserts the child selector still renders. (Note the `hideListings` half may be
unreachable — the code assumes only visible packages contain parents — so verify
that invariant before widening the fix.)

---

## Test-suite speed — remaining opportunities

*Origin: the test-suite performance pass (lazy Sentry, fast `toContain`,
Expand Down
21 changes: 15 additions & 6 deletions src/ui/templates/public/reservations/packages.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { t } from "#i18n";
import type { TicketListing } from "#shared/booking/model.ts";
import {
type PagePackage,
packageMemberIds,
} from "#shared/booking/page-packages.ts";
import type { PagePackage } from "#shared/booking/page-packages.ts";
import {
type BookingNode,
packageQuantityFieldName,
Expand Down Expand Up @@ -164,7 +161,18 @@ export const buildPageListingRows = (opts: {
const packageSections = opts.packages
.map((pkg) => renderPackageSection(renderFor(pkg)))
.join("");
const memberIds = packageMemberIds(opts.packages);
// Suppress a standalone row's child selector only when a package section
// actually renders that member's row (which carries the one selector). A
// SOLD-OUT package (`limit < 1`) renders a bare sold-out card with no member
// rows, so a standalone parent that is also one of its members would
// otherwise lose its child selector on both paths — leaving a multi-choice
// parent unbookable. (A `hideListings` package also renders no member rows,
// but its members are never standalone rows, so they need no handling here.)
const renderedMemberIds = new Set<number>(
opts.packages
.filter((pkg) => opts.packageLimits.get(pkg.groupId)! >= 1)
.flatMap((pkg) => [...pkg.memberListingIds]),
);
const standalone = opts.listings.filter((info) =>
opts.standaloneRowIds.has(info.listing.id),
);
Expand All @@ -183,7 +191,8 @@ export const buildPageListingRows = (opts: {
opts.isSingleListing && opts.packages.length === 0,
opts.hideQuantity,
opts.prefill,
(info) => (memberIds.has(info.listing.id) ? undefined : opts.childCtx),
(info) =>
renderedMemberIds.has(info.listing.id) ? undefined : opts.childCtx,
opts.attributesByListing,
)
);
Expand Down
44 changes: 44 additions & 0 deletions test/templates/public/ticket-page-packages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,50 @@ describe("ticketPage — packages", () => {
expect(html).toContain("Sorry, all listings are sold out.");
});

test("keeps a standalone parent's child selector when its package is sold out", () => {
// Listing 1 is a bookable standalone row AND a member of a package whose
// OTHER member (listing 2) is full, so the whole bundle is sold out and its
// section renders no member rows. Its child selector must still render on
// the standalone row — otherwise a buyer can't satisfy the parent's child
// requirement. Regression: the suppression set used to include members of
// packages that render no member rows (sold-out or hide-listings).
const parent = ticketListing({
attendee_count: 0,
id: 1,
max_attendees: 100,
max_quantity: 5,
name: "Tent",
slug: "tent1",
});
const soldOutSibling = ticketListing({
attendee_count: 100,
id: 2,
max_attendees: 100,
name: "Chair",
slug: "chr12",
});
const child = ticketListing({
attendee_count: 0,
id: 3,
max_attendees: 100,
max_quantity: 5,
name: "Add-on",
slug: "add01",
});
const html = ticketPage({
childDatesById: new Map(),
childrenByParentId: new Map([[1, [child]]]),
listings: [parent, soldOutSibling, child],
packages: [pagePackage(5, [1, 2], { name: "Camp Kit" })],
slugs: [PKG_SLUG, "tent1"],
});
// The bundle is sold out (its sibling member is full)…
expect(html).toContain('class="ticket-package sold-out"');
// …but the standalone parent keeps its own row and its child selector.
expect(html).toContain('name="quantity_1"');
expect(html).toContain('data-parent-id="1"');
});

test("caps the package by a SECOND capped group the members share", () => {
const listings = bigAndSmallListings();
// The package group (7) is roomy — floor(10 / 2) = 5 packages fit — but both
Expand Down