import { describe, expect, it } from "vitest"; import { bookingsToCsv, bucketByLocalDate, revenueByMethod, toCsvRow, upcomingFulfillments, utilizationByDateLocation, type BookingSummary, type CapacitySummary, } from "../../app/services/dashboard.server"; const ZONE = "America/Toronto"; function booking(overrides: Partial = {}): BookingSummary { return { id: "b1", orderName: "#1001", locationId: "loc_1", locationName: "Main Bakery", method: "PICKUP", slotStart: new Date("2026-08-25T13:00:00.000Z"), // 9 AM EDT, a Tuesday slotEnd: new Date("2026-08-25T14:00:00.000Z"), timezone: ZONE, status: "confirmed", totalPriceCents: 2500, customerEmail: "shopper@example.com", ...overrides, }; } describe("bucketByLocalDate", () => { it("groups bookings under their location-local calendar date", () => { const buckets = bucketByLocalDate([booking()]); expect(Object.keys(buckets)).toEqual(["2026-08-25"]); }); it("sorts bookings within a day by slot start time", () => { const late = booking({ id: "late", slotStart: new Date("2026-08-25T20:00:00.000Z") }); const early = booking({ id: "early", slotStart: new Date("2026-08-25T13:00:00.000Z") }); const buckets = bucketByLocalDate([late, early]); expect(buckets["2026-08-25"].map((b) => b.id)).toEqual(["early", "late"]); }); it("puts a booking near midnight into the correct location-local day, not the UTC day", () => { // 11 PM EDT on Aug 25 is already Aug 26 in UTC. const b = booking({ slotStart: new Date("2026-08-26T03:00:00.000Z") }); const buckets = bucketByLocalDate([b]); expect(Object.keys(buckets)).toEqual(["2026-08-25"]); }); }); describe("revenueByMethod", () => { it("sums confirmed and fulfilled bookings per method", () => { const bookings = [ booking({ method: "PICKUP", totalPriceCents: 1000, status: "confirmed" }), booking({ method: "PICKUP", totalPriceCents: 500, status: "fulfilled" }), booking({ method: "LOCAL_DELIVERY", totalPriceCents: 2000, status: "confirmed" }), ]; expect(revenueByMethod(bookings)).toEqual({ SHIPPING: 0, LOCAL_DELIVERY: 2000, PICKUP: 1500 }); }); it("excludes cancelled and no_show bookings", () => { const bookings = [ booking({ totalPriceCents: 1000, status: "cancelled" }), booking({ totalPriceCents: 1000, status: "no_show" }), ]; expect(revenueByMethod(bookings).PICKUP).toBe(0); }); it("treats a missing totalPriceCents as 0, not an error", () => { expect(revenueByMethod([booking({ totalPriceCents: null })]).PICKUP).toBe(0); }); }); describe("utilizationByDateLocation", () => { it("computes booked/capacity percentage for a date+location+method with bookings", () => { // 2026-08-25 is a Tuesday -> weekday 2. const capacities: CapacitySummary[] = [{ locationId: "loc_1", method: "PICKUP", weekday: 2, capacity: 4 }]; const bookings = [booking(), booking({ id: "b2" })]; const result = utilizationByDateLocation(bookings, capacities); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ date: "2026-08-25", booked: 2, capacity: 4, utilizationPct: 50 }); }); it("sums capacity across multiple slot templates for the same weekday", () => { const capacities: CapacitySummary[] = [ { locationId: "loc_1", method: "PICKUP", weekday: 2, capacity: 4 }, { locationId: "loc_1", method: "PICKUP", weekday: 2, capacity: 6 }, ]; const result = utilizationByDateLocation([booking()], capacities); expect(result[0].capacity).toBe(10); }); it("caps utilization at 100% even if overbooked", () => { const capacities: CapacitySummary[] = [{ locationId: "loc_1", method: "PICKUP", weekday: 2, capacity: 1 }]; const bookings = [booking(), booking({ id: "b2" }), booking({ id: "b3" })]; expect(utilizationByDateLocation(bookings, capacities)[0].utilizationPct).toBe(100); }); it("excludes cancelled bookings from the booked count", () => { const capacities: CapacitySummary[] = [{ locationId: "loc_1", method: "PICKUP", weekday: 2, capacity: 4 }]; const result = utilizationByDateLocation([booking({ status: "cancelled" })], capacities); expect(result).toHaveLength(0); }); it("omits a date/location/method with no bookings at all", () => { expect(utilizationByDateLocation([], [{ locationId: "loc_1", method: "PICKUP", weekday: 2, capacity: 4 }])).toEqual([]); }); }); describe("upcomingFulfillments", () => { it("returns only confirmed bookings, nearest-first", () => { const bookings = [ booking({ id: "later", slotStart: new Date("2026-08-26T13:00:00.000Z") }), booking({ id: "sooner", slotStart: new Date("2026-08-25T13:00:00.000Z") }), booking({ id: "fulfilled-already", status: "fulfilled" }), ]; const result = upcomingFulfillments(bookings); expect(result.map((b) => b.id)).toEqual(["sooner", "later"]); }); it("respects the limit", () => { const bookings = Array.from({ length: 5 }, (_, i) => booking({ id: `b${i}` })); expect(upcomingFulfillments(bookings, 2)).toHaveLength(2); }); }); describe("toCsvRow / bookingsToCsv", () => { it("quotes a field containing a comma", () => { expect(toCsvRow(["a,b", "c"])).toBe('"a,b",c'); }); it("escapes an embedded quote by doubling it", () => { expect(toCsvRow(['say "hi"'])).toBe('"say ""hi"""'); }); it("produces a header row plus one row per booking", () => { const csv = bookingsToCsv([booking()]); const lines = csv.split("\n"); expect(lines).toHaveLength(2); expect(lines[0]).toContain("Order"); expect(lines[1]).toContain("#1001"); expect(lines[1]).toContain("25.00"); }); });