Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion src/features/public/qr-book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { qrBookErrorPage } from "#templates/public/errors.tsx";
import type {
BookingPrefill,
TicketPrefill,
} from "#templates/public/reservations/inputs.ts";
} from "#templates/public/reservations/types.ts";
import { getTicketContext, runCheckoutFlow } from "./ticket-payment.ts";
import { handleTicket } from "./ticket-submit.ts";

Expand Down
2 changes: 1 addition & 1 deletion src/features/public/ticket-submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
import type {
BookingPrefill,
TicketPrefill,
} from "#templates/public/reservations/inputs.ts";
} from "#templates/public/reservations/types.ts";
import {
applyBookingPageParentSoldOut,
childCapacityInfo,
Expand Down
2 changes: 1 addition & 1 deletion src/features/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
ItemImageProjection,
ListingWithCount,
} from "#shared/types.ts";
import type { BookingPrefill } from "#templates/public/reservations/inputs.ts";
import type { BookingPrefill } from "#templates/public/reservations/types.ts";

/** Parent listing id → its bookable-child candidates, each hydrated to a
* {@link TicketListing} so availability (isSoldOut/isClosed/maxPurchasable) is
Expand Down
16 changes: 16 additions & 0 deletions src/shared/booking/package-cap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ export const packageLimitInfo = (
...groupCapacityInfo(groupRemainingByGroupId, groupIdsByListingId),
});

/** Each page package's whole-bundle limit, keyed by group id. Builds the
* page-level {@link PackageLimitInfo} once and maps each package over
* {@link pagePackageBundleLimit}, so callers stop re-stating the four
* limit-info arguments per call site. */
export const pageBundleLimits = (
tree: BookingTree,
packages: readonly TreePackage[],
page: PackageLimitInfo,
): Map<number, number> =>
new Map(
packages.map((pkg) => [
pkg.groupId,
pagePackageBundleLimit(tree, pkg, page),
]),
);

export const childCanBeBooked: (child: TicketListing) => boolean =
childPassesAllChecks([childActive, childOpen, childInStock]);

Expand Down
102 changes: 102 additions & 0 deletions src/ui/templates/public/reservations/availability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/** Page-level availability and tree-shaping: the unavailability message, the
* booking tree plus which listings get their own standalone row, the overall
* sold-out check, and the lone header listing whose details head the page. */

/* jscpd:ignore-start */
import { t } from "#i18n";
import {
type BuildTreeInput,
buildBookingTree,
} from "#shared/booking/build-tree.ts";
import type { TicketListing } from "#shared/booking/model.ts";
import {
type PackageLimitInfo,
pageBundleLimits,
} from "#shared/booking/package-cap.ts";
import type { PagePackage } from "#shared/booking/page-packages.ts";
import type { BookingNode, BookingTree } from "#shared/booking/tree.ts";
import { standaloneListingIds } from "#shared/booking/tree.ts";
import { isReadOnly } from "#shared/env.ts";
import type { ListingWithCount } from "#shared/types.ts";
/* jscpd:ignore-end */

/** Unavailability message shown when all listings are sold out or closed */
export const unavailableMessage = (
allClosed: boolean,
isSingleListing: boolean,
): string => {
if (isReadOnly() || allClosed) return t("public.ticket.registration_closed");
return isSingleListing
? t("public.ticket.listing_full")
: t("public.multi.all_sold_out");
};

/** Each page package's bundle limit, plus whether the whole page should show as
* sold out (nothing standalone left AND no package bookable). */
export const packagePageAvailability = (
packages: PagePackage[],
tree: BookingTree,
listings: TicketListing[],
standaloneRowIds: ReadonlySet<number>,
page: PackageLimitInfo,
): { packageLimits: Map<number, number>; soldOut: boolean } => {
const packageLimits = pageBundleLimits(tree, packages, page);
const standaloneUnavailable = listings
.filter((info) => standaloneRowIds.has(info.listing.id))
.every((e) => e.isSoldOut || e.isClosed);
const packagesUnavailable = [...packageLimits.values()].every(
(limit) => limit === 0,
);
return {
packageLimits,
// Sold out only when every standalone row AND every bundle is dead (a
// bundle with an unavailable member already reads limit 0; a package-less
// page has no bundles, leaving just its listings' own availability).
soldOut: standaloneUnavailable && packagesUnavailable,
};
};

/** The lone listing whose rich details (image/date/location) head the page and
* feed its OpenGraph tags, or null for a multi-listing page OR a hidden package
* — a hidden package with one active member must not expose that member here. */
export const headerListing = (
listings: TicketListing[],
packages: PagePackage[],
): ListingWithCount | null =>
listings.length === 1 && !packages.some((pkg) => pkg.hideListings)
? listings[0]!.listing
: null;

/** Build the page's booking tree and the row-shaping facts read off it: which
* listings get their own quantity row (those with a standalone BUYER_CHOICE
* node), which node each row reads its field names from (a dual-path listing
* resolves to its standalone node, not its member node), and whether the page
* IS one package (every listing a member, nothing sold beside it — the classic
* package-page layout). */
export const buildPageTree = (
input: BuildTreeInput,
packageCount: number,
): {
tree: BookingTree;
standaloneRowIds: Set<number>;
nodeByListingId: Map<number, BookingNode>;
singlePackagePage: boolean;
} => {
const tree = buildBookingTree(input);
const standaloneRowIds = standaloneListingIds(tree);
const nodeByListingId = new Map<number, BookingNode>();
for (const node of tree.nodes) {
if (
!nodeByListingId.has(node.listingId) ||
node.quantityRule.kind === "BUYER_CHOICE"
) {
nodeByListingId.set(node.listingId, node);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return {
nodeByListingId,
singlePackagePage: packageCount === 1 && standaloneRowIds.size === 0,
standaloneRowIds,
tree,
};
};
Loading