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
1,415 changes: 953 additions & 462 deletions AGENTS.md

Large diffs are not rendered by default.

1,253 changes: 649 additions & 604 deletions TODO.md

Large diffs are not rendered by default.

19 changes: 12 additions & 7 deletions scripts/mutation/equivalent-mutants/features.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ src/ui/templates/admin/holidays.tsx::HolidayEditPanel.html~1nrsidb ?? → ||
src/ui/client/admin/manual-checkin.ts::initManualCheckin.filterOptions.text~0hoqwoy ?? → || # textContent is string|null; its only falsy string is the empty fallback itself
src/ui/client/admin/order-gallery.ts::initOrderGallery.refresh.timer~1xo126t ?? → || # states is a record object when present, so it is always truthy

# Paid snapshot collection fallbacks: every present value is an object or array,
# which remains truthy even when empty; only undefined reaches the empty fallback.
src/features/api/payment-processing/snapshot/build.ts::modifierSpecs.listingIds~1k3bo9n ?? → || # scopes.get(): SnapshotModifierScopeRow[]|undefined — arrays are always truthy

# ticket-payment values whose present form is always truthy, whose falsy value
# equals the fallback, or whose mutated constant is normalized before use.
src/features/public/ticket-payment.ts::checkAvailability~0dpsw9a ?? → || # date is nullish or a non-empty ISO date; only the nullish values use the null fallback
Expand Down Expand Up @@ -139,17 +143,19 @@ src/ui/client/dom.ts::createButton.button%2eclassName~16pvzvs = → += # crea
# stored package quantities are positive; equal zero fallbacks stay equal.
src/features/api/payment-processing/index.ts::replaySuccess.paymentReference~12k7fdb → "mutated" # replaySuccess is private and both callers always pass session.paymentReference, so the default is never evaluated
src/features/api/payment-processing/items.ts::bookingPaths.allocations~1sr8ybw ?? → || # intent.allocations is an array or undefined, and arrays (including []) are truthy
src/features/api/payment-processing/items.ts::validateAllItems.staleNonStandaloneChild~0bp0o7q ?? → || # a parent list length is non-negative; its only falsy value is 0, which equals the fallback
src/features/api/payment-processing/items.ts::validateAllItems.expectedPrice~0mb68sn ?? → || # extractIntent emits a positive dayCount or undefined, so every present value is truthy
src/features/api/payment-processing/pricing.ts::paidByItem.current~0h5qe1t ?? → || # paidByItem stores numeric charged totals; the only falsy present total is 0, and both operators keep the 0 fallback
src/features/api/payment-processing/package-pricing.ts::allocatedUnitsByChild~1sr8ybw ?? → || # intent.allocations is an array or undefined, and arrays (including []) are truthy
src/features/api/payment-processing/package-pricing.ts::allocatedUnitsByChild.prior~0fwromb ?? → || # the running allocated quantity is non-negative and the fallback is 0, so 0 ?? 0 and 0 || 0 agree
src/features/api/payment-processing/package-pricing.ts::packageBundleMismatch.count~0paa7g7 ?? → || # stored per-package quantities are at least 1, so every present value is truthy
src/features/api/payment-processing/package-pricing.ts::orderEdgeDrifted.fullyFolded~0b7s0d3 ?? → || # the allocated quantity is non-negative and the fallback is 0, so both operators agree for 0 and undefined
src/features/api/payment-processing/package-pricing.ts::orderEdgeDrifted.topLevel~1g6e18j false → true # orderEdgeDrifted uses buildTicketListing only to pass its listing row into buildBookingTree; the closed/availability fields never affect node keys
src/features/api/payment-processing/package-pricing.ts::orderEdgeDrifted.childrenByParentId~0iift5t false → true # child TicketListing availability is likewise unread by buildBookingTree's node-key construction
src/features/api/payment-processing/package-pricing.ts::orderEdgeDrifted.packages.hideListings~0xzhsy5 false → true # hideListings changes node visibility only, while edgeDrifted reads node keys and child structure, never visibility
src/features/api/payment-processing/package-pricing.ts::orderEdgeDrifted~1sr8ybw ?? → || # intent.allocations is an array or undefined, and arrays (including []) are truthy
src/features/api/payment-processing/package-pricing.ts::hasStaleStandaloneChild.allocated~0ystvom ?? → || # the allocated quantity is non-negative and the fallback is 0, so both operators agree for 0 and undefined
src/features/api/payment-processing/package-pricing.ts::orderEdgeDriftedFromFacts.fullyFolded~0b7s0d3 ?? → || # the allocated quantity is non-negative and the fallback is 0, so both operators agree for 0 and undefined
src/features/api/payment-processing/package-pricing.ts::orderEdgeDriftedFromFacts.topLevel~1g6e18j false → true # orderEdgeDriftedFromFacts uses buildTicketListing only to pass its listing row into buildBookingTree; the closed/availability fields never affect node keys
src/features/api/payment-processing/package-pricing.ts::orderEdgeDriftedFromFacts.childrenByParentId~0t0xtlh ?? → || # child id lists are arrays (always truthy), while a missing parent selects [] under either operator
src/features/api/payment-processing/package-pricing.ts::orderEdgeDriftedFromFacts.childrenByParentId~04idxs9 false → true # child TicketListing availability is likewise unread by buildBookingTree's node-key construction
src/features/api/payment-processing/package-pricing.ts::orderEdgeDriftedFromFacts.packages.hideListings~0xzhsy5 false → true # hideListings changes node visibility only, while edgeDrifted reads node keys and child structure, never visibility
src/features/api/payment-processing/package-pricing.ts::orderEdgeDriftedFromFacts~1sr8ybw ?? → || # intent.allocations is an array or undefined, and arrays (including []) are truthy
src/features/api/payment-processing/package-pricing.ts::hasStaleStandaloneChildFromFacts.allocated~0ystvom ?? → || # the allocated quantity is non-negative and the fallback is 0, so both operators agree for 0 and undefined

# Payment success replay only loads a listing fallback while thankYouUrl is the
# empty string, so assignment and append produce the same string for every path.
Expand Down Expand Up @@ -183,7 +189,6 @@ src/features/public/cart.ts::resolveCartSlugs~1nfyz8t@2 continue; → (removed)

# Listing detail group context (features/admin/listings-view.ts).
src/features/admin/listings-view.ts::loadGroupContext.remaining~08seojj Missing group remaining → "" # getGroupRemainingByGroupId returns an entry for every group id whose cap is positive, and only those ids are looked up, so requiredMapValue never raises this message
src/features/api/payment-processing/package-pricing.ts::loadPackagePricingByGroup.pricing~1bkld7z Missing package pricing → "" # loadPackageMemberPricingByGroupIds is given exactly the order's group ids, and only those are looked up, so requiredMapValue never raises this message

# Page packages (shared/booking/page-packages.ts): nullish fallbacks on values
# that are only ever a number-or-undefined with a matching fallback.
Expand Down
2 changes: 0 additions & 2 deletions scripts/mutation/equivalent-mutants/shared-a-l.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ src/shared/uptime-kuma/matching.ts::SCHEDULED_MONITOR_RULES.holds~14f0p4m 1000
src/shared/checkout-pricing.ts::priceCheckout.modifierSpecs~1b95fmx ?? → || # intent.modifiers: an array is always truthy
src/shared/booking-lines.ts::checkoutBookingLines.packageGroupId~0eqzp6w ?? → || # packageGroupId is absent or a positive database group id; an explicit 0 also has the same 0 fallback
src/features/api/payment-processing/create.ts::bookingSlot.packageGroupId~0zn56vl ?? → || # lineGroupId returns undefined or a positive package group id; an explicit 0 would also keep the same 0 fallback
src/features/api/payment-processing/create.ts::saveSessionAnswers.grouped~1qjy0pt ?? → || # listingAnswerIds is an object when present, and objects are always truthy
src/features/api/payment-processing/create.ts::saveSessionAnswers.refs~13mow4w ?? → || # a listing's text refs are an array when present, and arrays are always truthy
src/features/api/payment-processing/create.ts::saveSessionAnswers.existing~1kealbd ?? → || # a grouped answer set is an object when present, and objects are always truthy
src/features/api/payment-processing/create.ts::saveSessionAnswers.textAnswerIds~1ex96yt ?? → || # existing text answer ids are an array when present, and arrays are always truthy
Expand Down Expand Up @@ -72,7 +71,6 @@ src/shared/db/migrations/schema/admin-feature-triggers.ts::featureWriteTrigger.u
src/shared/square-provider.ts::squarePaymentProvider.isPaymentRefunded.charged~0ek3sg6 ?? → || # amount is bigint|undefined, and its only falsy bigint is 0n, which equals the fallback
src/shared/square-provider.ts::squarePaymentProvider.isPaymentRefunded.refunded~1mw8ezt ?? → || # refunded amount is bigint|undefined, and its only falsy bigint is 0n, which equals the fallback
src/shared/square-provider.ts::squarePaymentProvider.resolveWebhookSession~08j5olw ?? → || # retrieveSession returns a session object or null, so every present value is truthy
src/shared/square-provider.ts::squarePaymentProvider.retrieveSession.paymentReference~1nqjvuw ?? → || # the tender's paymentId is string|undefined, and its only falsy string is the empty fallback itself

# Invariant-based, confirmed by reading toUnits: `lineIdxs` comes from toUnits,
# which assigns lineIdx as the flatMap array index, so the sort's input is
Expand Down
9 changes: 9 additions & 0 deletions scripts/mutation/equivalent-mutants/shared-db.txt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ src/shared/db/attendees/select.ts::attendees.join~02qy6z7 ?? → || # query.j
# error message carries the label.
src/shared/db/processed-payments.ts::parseSessionFailure~1rv3mep processed_payments.failure_data → "" # read-path label: read errors are caught and replaced with CORRUPT_FAILURE before the message can be observed

# Attendee answer collection fallbacks: all present values are arrays or answer
# objects, so they are truthy; only undefined reaches each empty fallback.
src/shared/db/questions/attendee-answers/save.ts::saveAttendeeAnswers.normalized.textAnswerIds~1h5o1g4 ?? → || # textAnswerIds is TextAnswerId[]|undefined — arrays are always truthy
src/shared/db/questions/attendee-answers/save.ts::saveAttendeeAnswers.normalized.textAnswers~0ycn62j ?? → || # textAnswers is TextAnswer[]|undefined — arrays are always truthy
src/shared/db/questions/attendee-answers/save.ts::groupListingAnswerSets.answerIds~1pbmjye ?? → || # listingAnswerIds[key] is number[]|undefined — arrays are always truthy
src/shared/db/questions/attendee-answers/save.ts::groupListingAnswerSets.textAnswers~1f880d0 ?? → || # listingTextAnswers[key] is TextAnswer[]|undefined — arrays are always truthy
src/shared/db/questions/attendee-answers/save.ts::groupListingAnswerSets.existing~0ka51i3 ?? → || # Map.get() returns AttendeeAnswerSet|undefined — answer-set objects are always truthy
src/shared/db/questions/attendee-answers/save.ts::groupListingAnswerSets.existing%2etextAnswers~04d6ypa ?? → || # existing.textAnswers is TextAnswer[]|undefined — arrays are always truthy

# The news card query names a single table, so its columns need no alias to
# resolve.
src/shared/db/news-posts.ts::getNewsPostCards.rows.columns~0dqqett news_post → "" # dropping the alias leaves the column names unqualified, and the read selects from one table (news_posts AS news_post) plus scalar subqueries that carry their own qualified id, so every name still resolves to the same column
Expand Down
7 changes: 5 additions & 2 deletions scripts/mutation/equivalent-mutants/shared-m-z.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Known-equivalent mutants — see README.txt in this directory.

# Registration package facts are objects when supplied, so they are always
# truthy; undefined selects the loader under either nullish or OR fallback.
src/shared/webhook.ts::sendRegistrationWebhooks.facts~007uooi ?? → || # suppliedFacts is RegistrationPackageFacts|undefined
src/shared/webhook.ts::queueRegistrationNotifications.packageFacts~0fhpirc ?? → || # suppliedPackageFacts is RegistrationPackageFacts|undefined

# CRUD configuration values are functions or objects when present, and hydrated
# map values are records, so none can be falsy-but-non-null.
src/shared/rest/crud-api.ts::defineCrudApi.policy~11y0iup ?? → || # policy is an AuthPolicy object or undefined
Expand Down Expand Up @@ -171,8 +176,6 @@ src/ui/templates/admin/images.tsx::imageValue.alt_text~17v2zve ?? → || # al
src/ui/templates/admin/images.tsx::imageValue.name~0pg0nzi ?? → || # name ?? "" vs || "": identical for every string — "" falls through to "" either way
src/features/admin/images.ts::parseImageTargets.itemType~01dj453 → "mutated" # itemType destructure default: split(":") always yields ≥1 part, so the default is unreachable
src/features/admin/images.ts::parseImageTargets.itemId~1gk0kbs → "mutated" # itemId destructure default: Number("") is 0 and Number("mutated") is NaN — both rejected by the id guard
src/features/admin/images.ts::handleImageEditGet~0t9nij6 applyFlash(request); → (removed) # only records ?form= targeting; no form on the images pages has an id, so the flash renders at the Layout backstop either way
src/features/admin/images.ts::handleImageDeleteGet~0t9nij6 applyFlash(request); → (removed) # same as images.ts:56 — no id'd form on the page

# Attendee merge service (attendee-merge.ts) — `?? → ||` between
# `targetAnswers.get(qid)` (a `{answerId, answerText}` object or undefined) and
Expand Down
29 changes: 18 additions & 11 deletions src/features/api/payment-processing/completion.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,41 @@
import {
type CreatedEntry,
logPromoCodeModifiers,
promoCodeActivities,
saveSessionAnswers,
sessionSuccess,
} from "#routes/api/payment-processing/create.ts";
import type { PaymentResult } from "#routes/api/webhook-types.ts";
import type { BookingIntent } from "#shared/booking-intent.ts";
import type { ModifierApplication } from "#shared/checkout-pricing.ts";
import type { ModifierSpec } from "#shared/payments.ts";
import type { RegistrationPackageFacts } from "#shared/registration-package-facts.ts";
import { logAndNotifyRegistration } from "#shared/webhook.ts";

/** Finish every effect after a paid booking has definitely committed. */
export const completePaidBooking = async (
createdEntries: CreatedEntry[],
intent: BookingIntent,
codeSpecs: ModifierSpec[],
modifierApplications: ModifierApplication[],
ticketTokens: string[],
notificationPackages: RegistrationPackageFacts,
): Promise<PaymentResult> => {
await saveSessionAnswers(createdEntries, intent);
const firstEntry = createdEntries[0]!;
if (codeSpecs.length > 0) {
await logPromoCodeModifiers(
codeSpecs,
modifierApplications,
firstEntry.listing,
firstEntry.attendee.id,
);
}
await logAndNotifyRegistration(createdEntries, intent.siteTokenIndex);
const promoActivities =
codeSpecs.length > 0
? promoCodeActivities(
codeSpecs,
modifierApplications,
firstEntry.listing,
firstEntry.attendee.id,
)
: [];
await logAndNotifyRegistration(
createdEntries,
intent.siteTokenIndex,
promoActivities,
notificationPackages,
);
return sessionSuccess(
firstEntry.attendee.id,
firstEntry.listing.id,
Expand Down
35 changes: 21 additions & 14 deletions src/features/api/payment-processing/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ import type {
} from "#shared/checkout-pricing.ts";
/* jscpd:ignore-end */
import { formatCurrency } from "#shared/currency.ts";
import { logActivity } from "#shared/db/activity-log.ts";
import { requirePublicStatusId } from "#shared/db/attendee-statuses.ts";
import type { ActivityToLog } from "#shared/db/activity-log.ts";
import { attendeesApi } from "#shared/db/attendees/api.ts";
import {
decryptSessionTokens,
Expand Down Expand Up @@ -215,36 +214,41 @@ export const saveSessionAnswers = async (
await saveAttendeeAnswers(grouped);
};

export const attendeeBaseFields = async (
export const attendeeBaseFields = (
session: ValidatedPaymentSession,
intent: BookingIntent,
publicStatusId: number,
) => ({
address: intent.address,
email: intent.email,
name: intent.name,
paymentId: session.paymentReference,
phone: intent.phone,
special_instructions: intent.special_instructions,
statusId: await requirePublicStatusId(),
statusId: publicStatusId,
});

export const logPromoCodeModifiers = async (
export const promoCodeActivities = (
specs: ModifierSpec[],
applications: ModifierApplication[],
listing: ListingWithCount,
attendeeId: number,
): Promise<void> => {
): ActivityToLog[] => {
const byId = new Map(applications.map((a) => [a.modifierId, a]));
for (const spec of specs) {
const delta = byId.get(spec.id)!.delta;
return specs.map((spec) => {
const delta = requiredMapValue(
byId,
spec.id,
`Modifier application ${spec.id} was not loaded for promo code activity`,
).delta;
const effect =
delta < 0 ? `${formatCurrency(-delta)} off` : `+${formatCurrency(delta)}`;
await logActivity(
`Promo code '${spec.name}' used: ${effect}`,
listing,
return {
attendeeId,
);
}
listing,
message: `Promo code '${spec.name}' used: ${effect}`,
};
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

/**
Expand All @@ -263,6 +267,8 @@ export const createAttendeeForSession = async (
pricingIntent: CheckoutIntent,
pricedOrder: PricedOrder,
ticketToken: string,
publicStatusId: number,
parentIdsByChild: ReadonlyMap<number, readonly number[]>,
): Promise<HonourResult> => {
let prepared: {
attendeeInput: Parameters<typeof attendeesApi.createBookingAtomic>[0];
Expand Down Expand Up @@ -302,8 +308,9 @@ export const createAttendeeForSession = async (
);
prepared = {
attendeeInput: {
...(await attendeeBaseFields(session, intent)),
...attendeeBaseFields(session, intent, publicStatusId),
bookings,
parentIdsByChild,
remainingBalance,
ticketToken,
},
Expand Down
Loading