test/shared/booking-lines.test.ts
Rama main · réplica de lectura del repositorio original.
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import type { ChildAllocation } from "#db/attendee-types.ts";
import {
bookingsForOrder,
checkoutBookingLines,
} from "#shared/booking-lines.ts";
import type { CheckoutItem } from "#shared/payments.ts";
import { testListingWithCount } from "#test-utils/factories.ts";
const item = (
listingId: number,
overrides: Partial<CheckoutItem> = {},
): CheckoutItem => ({
listingId,
name: `Listing ${listingId}`,
quantity: 1,
slug: `listing-${listingId}`,
unitPrice: 100,
...overrides,
});
const listing = (id: number, overrides = {}) =>
testListingWithCount({ id, listing_type: "standard", ...overrides });
const listingMap = (...listings: ReturnType<typeof listing>[]) =>
new Map(listings.map((entry) => [entry.id, entry]));
const allocation = (
childId: number,
parentId: number,
qty: number,
): ChildAllocation => ({ childId, parentId, qty });
describe("checkoutBookingLines", () => {
test("keeps item order, quantities, package paths, and per-path prices", () => {
const source = listing(7);
const packageItem = item(7, { packageGroupId: 5, quantity: 2 });
const standaloneItem = item(7, { quantity: 3, unitPrice: 200 });
const lines = checkoutBookingLines(
[packageItem, standaloneItem],
listingMap(source),
new Map([
[packageItem, 700],
[standaloneItem, 1100],
]),
);
expect(lines).toEqual([
{
listing: source,
listingId: 7,
packageGroupId: 5,
pricePaid: 700,
quantity: 2,
},
{
listing: source,
listingId: 7,
packageGroupId: 0,
pricePaid: 1100,
quantity: 3,
},
]);
});
test("keeps a real zero paid amount", () => {
const source = listing(7);
const checkoutItem = item(7);
expect(
checkoutBookingLines(
[checkoutItem],
listingMap(source),
new Map([[checkoutItem, 0]]),
),
).toEqual([
{
listing: source,
listingId: 7,
packageGroupId: 0,
pricePaid: 0,
quantity: 1,
},
]);
});
test("leaves paid amounts off when the order has no paid map", () => {
const lines = checkoutBookingLines([item(7)], listingMap(listing(7)));
expect("pricePaid" in lines[0]!).toBe(false);
});
test("allows the existing zero-unit item to have no priced line", () => {
const checkoutItem = item(7, { quantity: 0 });
const lines = checkoutBookingLines(
[checkoutItem],
listingMap(listing(7)),
new Map(),
);
expect(lines[0]!.quantity).toBe(0);
expect("pricePaid" in lines[0]!).toBe(false);
});
test("names a listing that was not loaded", () => {
expect(() => checkoutBookingLines([item(42)], new Map())).toThrow(
"Listing 42 was not loaded for checkout",
);
});
test("names a paid amount that was not loaded", () => {
const checkoutItem = item(42);
expect(() =>
checkoutBookingLines([checkoutItem], listingMap(listing(42)), new Map()),
).toThrow("Paid amount for listing 42 was not loaded for checkout");
});
});
describe("bookingsForOrder", () => {
test("builds one dateless standalone row without parent details", () => {
const result = bookingsForOrder(
{ date: null },
checkoutBookingLines([item(7, { quantity: 2 })], listingMap(listing(7))),
);
expect(result).toEqual([
{
date: null,
durationDays: 1,
listingId: 7,
packageGroupId: 0,
quantity: 2,
},
]);
expect(result[0]!.orderToken).toBeUndefined();
expect(result[0]!.parentListingId).toBeUndefined();
});
test("uses the order day count for a customisable daily listing", () => {
const source = listing(7, {
customisable_days: true,
duration_days: 8,
listing_type: "daily",
});
expect(
bookingsForOrder(
{ date: "2026-08-01", dayCount: 3 },
checkoutBookingLines([item(7)], listingMap(source)),
),
).toEqual([
{
date: "2026-08-01",
durationDays: 3,
listingId: 7,
packageGroupId: 0,
quantity: 1,
},
]);
});
test("empty allocations leave every row without parent details", () => {
const result = bookingsForOrder(
{ allocations: [], date: null },
checkoutBookingLines(
[item(7), item(8)],
listingMap(listing(7), listing(8)),
),
);
expect(result.map((row) => row.listingId)).toEqual([7, 8]);
for (const row of result) {
expect(row.orderToken).toBeUndefined();
expect(row.parentListingId).toBeUndefined();
}
});
test("preserves allocation order, exact prices, and each parent package", () => {
const parentA = item(10, { packageGroupId: 7 });
const child = item(20, { quantity: 3 });
const parentB = item(30, { packageGroupId: 9 });
const plain = item(40, { quantity: 2 });
const items = [parentA, child, parentB, plain];
const result = bookingsForOrder(
{
allocations: [allocation(20, 30, 1), allocation(20, 10, 1)],
date: null,
},
checkoutBookingLines(
items,
listingMap(listing(10), listing(20), listing(30), listing(40)),
new Map([
[parentA, 700],
[child, 100],
[parentB, 900],
[plain, 400],
]),
),
);
const orderToken = result[0]!.orderToken;
expect(orderToken).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
expect(result.map(({ orderToken: _, ...row }) => row)).toEqual([
{
date: null,
durationDays: 1,
listingId: 10,
packageGroupId: 7,
pricePaid: 700,
quantity: 1,
},
{
date: null,
durationDays: 1,
listingId: 20,
packageGroupId: 9,
parentListingId: 30,
pricePaid: 33,
quantity: 1,
},
{
date: null,
durationDays: 1,
listingId: 20,
packageGroupId: 7,
parentListingId: 10,
pricePaid: 33,
quantity: 1,
},
{
date: null,
durationDays: 1,
listingId: 20,
packageGroupId: 0,
pricePaid: 34,
quantity: 1,
},
{
date: null,
durationDays: 1,
listingId: 30,
packageGroupId: 9,
pricePaid: 900,
quantity: 1,
},
{
date: null,
durationDays: 1,
listingId: 40,
packageGroupId: 0,
pricePaid: 400,
quantity: 2,
},
]);
for (const row of result) expect(row.orderToken).toBe(orderToken);
});
test("does not stamp a child when its parent has mixed paths", () => {
const packageParent = item(10, { packageGroupId: 7 });
const standaloneParent = item(10);
const child = item(20);
const result = bookingsForOrder(
{
allocations: [allocation(20, 10, 1)],
date: null,
},
checkoutBookingLines(
[packageParent, standaloneParent, child],
listingMap(listing(10), listing(20)),
),
);
expect(result[2]!.listingId).toBe(20);
expect(result[2]!.packageGroupId).toBe(0);
expect(result[2]!.parentListingId).toBe(10);
});
});