@final-commerce/command-frame 0.8.0-staging.1 → 0.8.0-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,24 +5,41 @@ export const mockAddBookingToCart = async (params) => {
5
5
  // One call, two effects — the window is claimed AND the service is in the cart. The claim goes
6
6
  // first and its refusal propagates: a cart line for a window somebody else took is worse than
7
7
  // no line at all.
8
- const { booking } = await mockHoldBooking(params);
8
+ const held = await mockHoldBooking(params);
9
+ if (!held.success || !held.booking) {
10
+ return { success: false, reason: held.reason, timestamp: new Date().toISOString() };
11
+ }
12
+ const booking = held.booking;
13
+ // An unknown product used to be sold as "Booking" at zero. The host refuses it, and a mock that
14
+ // quietly sells something no catalogue contains teaches a screen a case that cannot happen.
9
15
  const product = MOCK_PRODUCTS.find(({ _id }) => _id === params.productId);
16
+ if (!product) {
17
+ return { success: false, reason: `No product ${params.productId}`, timestamp: new Date().toISOString() };
18
+ }
19
+ // A named variant is honoured or refused, never substituted — the host does the same.
10
20
  const variant = params.variantId
11
- ? product?.variants?.find(({ _id }) => _id === params.variantId)
12
- : product?.variants?.[0];
13
- const price = variant?.price ?? product?.minPrice ?? 0;
21
+ ? product.variants?.find(({ _id }) => _id === params.variantId)
22
+ : product.variants?.[0];
23
+ if (params.variantId && !variant) {
24
+ return {
25
+ success: false,
26
+ reason: `${product.name} has no variant ${params.variantId}`,
27
+ timestamp: new Date().toISOString(),
28
+ };
29
+ }
30
+ const price = variant?.price ?? product.minPrice ?? 0;
14
31
  const reservation = {
15
32
  internalId: `res_${booking.id}`,
16
33
  bookingId: booking.id,
17
34
  productId: params.productId,
18
35
  variantId: variant?._id ?? null,
19
36
  resourceId: params.resourceId,
20
- name: product?.name ?? 'Booking',
37
+ name: product.name,
21
38
  resourceName: booking.resourceName,
22
39
  price,
23
40
  quantity: 1,
24
41
  total: price,
25
- taxTableId: product?.taxTable,
42
+ taxTableId: product.taxTable,
26
43
  startAt: booking.startAt,
27
44
  endAt: booking.endAt,
28
45
  bufferEndAt: booking.bufferEndAt,
@@ -37,6 +54,13 @@ export const mockAddBookingToCart = async (params) => {
37
54
  MOCK_CART.remainingBalance = MOCK_CART.total;
38
55
  // The cart topic, not the bookings one: a screen that only watches bookings still has to
39
56
  // repaint its cart, and every other cart mutation announces itself the same way.
40
- mockPublishEvent('cart', 'reservation-added', { reservation });
41
- return { booking, reservationInternalId: reservation.internalId, timestamp: new Date().toISOString() };
57
+ // `cart-created`, as the host publishes: the cart topic carries the whole cart, and a screen
58
+ // written against the host's event was not listening for a name only the mock used.
59
+ mockPublishEvent('cart', 'cart-created', { cart: MOCK_CART });
60
+ return {
61
+ success: true,
62
+ booking,
63
+ reservationInternalId: reservation.internalId,
64
+ timestamp: new Date().toISOString(),
65
+ };
42
66
  };
@@ -14,10 +14,14 @@ export interface AddBookingToCartParams {
14
14
  customerId?: string;
15
15
  }
16
16
  export interface AddBookingToCartResponse {
17
+ /** False when the command was refused — a taken window, a rule, a booking that is not yours. */
18
+ success: boolean;
19
+ /** Why it was refused, in words a cashier can act on. Absent on success. */
20
+ reason?: string;
17
21
  /** The claim the cart now stands on: `id` cancels or releases it, `expiresAt` is your clock. */
18
- booking: CFBooking;
22
+ booking?: CFBooking;
19
23
  /** Identity of the reservation inside the cart and, later, the order. */
20
- reservationInternalId: string;
24
+ reservationInternalId?: string;
21
25
  timestamp: string;
22
26
  }
23
27
  export type AddBookingToCart = (params: AddBookingToCartParams) => Promise<AddBookingToCartResponse>;
@@ -1,10 +1,31 @@
1
- import { MOCK_BOOKINGS } from '../../demo/database';
1
+ import { MOCK_BOOKINGS, MOCK_CART, mockPublishEvent } from '../../demo/database';
2
2
  import { ReservationStatus } from '@final-commerce/common';
3
3
  export const mockCancelBooking = async (params) => {
4
4
  console.log('[Mock] cancelBooking called', params);
5
5
  const booking = MOCK_BOOKINGS.find(({ id }) => id === params.bookingId);
6
- if (!booking)
7
- throw new Error(`No booking ${params.bookingId}`);
6
+ if (!booking) {
7
+ return { success: false, reason: `No booking ${params.bookingId}`, timestamp: new Date().toISOString() };
8
+ }
9
+ // A paid booking is not cancellable: the window would be freed while the money stays taken.
10
+ if (booking.status === ReservationStatus.CONFIRMED && booking.orderId) {
11
+ return {
12
+ success: false,
13
+ reason: `That booking is paid for on order ${booking.orderId} — refund the service instead`,
14
+ timestamp: new Date().toISOString(),
15
+ };
16
+ }
8
17
  booking.status = ReservationStatus.CANCELLED;
9
- return { booking, timestamp: new Date().toISOString() };
18
+ // A window given away may be the one an open cart is selling, so the cart follows — the host
19
+ // does this, and a mock that left the line behind sold time that had just been released.
20
+ const index = (MOCK_CART.reservations ?? []).findIndex((entry) => entry.bookingId === params.bookingId);
21
+ if (index >= 0) {
22
+ const [removed] = MOCK_CART.reservations.splice(index, 1);
23
+ const line = removed.total || removed.price * (removed.quantity ?? 1);
24
+ MOCK_CART.subtotal -= line;
25
+ MOCK_CART.total -= line;
26
+ MOCK_CART.amountToBeCharged = MOCK_CART.total;
27
+ MOCK_CART.remainingBalance = MOCK_CART.total;
28
+ mockPublishEvent('cart', 'cart-created', { cart: MOCK_CART });
29
+ }
30
+ return { success: true, booking, timestamp: new Date().toISOString() };
10
31
  };
@@ -4,7 +4,11 @@ export interface CancelBookingParams {
4
4
  bookingId: string;
5
5
  }
6
6
  export interface CancelBookingResponse {
7
- booking: CFBooking;
7
+ /** False when the command was refused — a taken window, a rule, a booking that is not yours. */
8
+ success: boolean;
9
+ /** Why it was refused, in words a cashier can act on. Absent on success. */
10
+ reason?: string;
11
+ booking?: CFBooking;
8
12
  timestamp: string;
9
13
  }
10
14
  export type CancelBooking = (params: CancelBookingParams) => Promise<CancelBookingResponse>;
@@ -1,6 +1,12 @@
1
1
  import { mockBookingAvailability } from '../../demo/database';
2
2
  export const mockGetBookingAvailability = async (params) => {
3
3
  console.log('[Mock] getBookingAvailability called', params);
4
- const availability = mockBookingAvailability(params.productId, new Date(params.from), new Date(params.to), params.resourceId);
5
- return { availability, timestamp: new Date().toISOString() };
4
+ // `fromDay` + `days` is the shape a calendar actually asks in; the mock resolves it on the
5
+ // machine's clock, which is the only one it has.
6
+ const start = params.fromDay ? new Date(`${params.fromDay}T00:00:00`) : params.from ? new Date(params.from) : new Date();
7
+ const end = params.to
8
+ ? new Date(params.to)
9
+ : new Date(start.getFullYear(), start.getMonth(), start.getDate() + Math.max(1, params.days ?? 1));
10
+ const availability = mockBookingAvailability(params.productId, start, end, params.resourceId);
11
+ return { success: true, availability, timestamp: new Date().toISOString() };
6
12
  };
@@ -2,17 +2,36 @@ import { CFBookingAvailability } from '../../CommonTypes';
2
2
  export interface GetBookingAvailabilityParams {
3
3
  /** The bookable product (`productType: 'booking'`) whose calendar you are drawing. */
4
4
  productId: string;
5
- /** Range start, ISO 8601. */
6
- from: string;
7
- /** Range end, ISO 8601. */
8
- to: string;
5
+ /**
6
+ * Range start, ISO 8601. Optional when `fromDay` is given.
7
+ *
8
+ * Prefer `fromDay`: a calendar asks about DAYS, and turning "this day at this shop" into two
9
+ * instants needs the shop's zone — which the host has and the caller has to go and find. A
10
+ * caller that widened the range by a day at each end and filtered the answer afterwards was
11
+ * working around the absence of `fromDay`.
12
+ */
13
+ from?: string;
14
+ /** Range end, ISO 8601. Optional when `fromDay` is given. */
15
+ to?: string;
16
+ /**
17
+ * First shop day to cover, `YYYY-MM-DD`. Resolved to local midnight in the shop's own zone,
18
+ * so a day that is 23 or 25 hours long because the clocks moved is still exactly one day.
19
+ * Omit both this and `from`/`to` to get today at the shop.
20
+ */
21
+ fromDay?: string;
22
+ /** How many shop days from `fromDay`. Default 1. */
23
+ days?: number;
9
24
  /** Narrow the answer to one resource — "only Marco", "only room 4". */
10
25
  resourceId?: string;
11
26
  /** Narrow to the resources standing at one outlet. */
12
27
  outletId?: string;
13
28
  }
14
29
  export interface GetBookingAvailabilityResponse {
15
- availability: CFBookingAvailability;
30
+ /** False when the command was refused — a taken window, a rule, a booking that is not yours. */
31
+ success: boolean;
32
+ /** Why it was refused, in words a cashier can act on. Absent on success. */
33
+ reason?: string;
34
+ availability?: CFBookingAvailability;
16
35
  timestamp: string;
17
36
  }
18
37
  export type GetBookingAvailability = (params: GetBookingAvailabilityParams) => Promise<GetBookingAvailabilityResponse>;
@@ -3,5 +3,5 @@ export const mockGetBookingResources = async (params) => {
3
3
  console.log('[Mock] getBookingResources called', params);
4
4
  // A resource knows nothing about outlets — where it works lives in its own records, which the
5
5
  // host reads. The mock has no such records, so `outletId` narrows nothing here.
6
- return { resources: MOCK_BOOKING_RESOURCES, timestamp: new Date().toISOString() };
6
+ return { success: true, resources: MOCK_BOOKING_RESOURCES, timestamp: new Date().toISOString() };
7
7
  };
@@ -6,7 +6,11 @@ export interface GetBookingResourcesParams {
6
6
  outletId?: string;
7
7
  }
8
8
  export interface GetBookingResourcesResponse {
9
- resources: CFBookingResource[];
9
+ /** False when the command was refused — a taken window, a rule, a booking that is not yours. */
10
+ success: boolean;
11
+ /** Why it was refused, in words a cashier can act on. Absent on success. */
12
+ reason?: string;
13
+ resources?: CFBookingResource[];
10
14
  timestamp: string;
11
15
  }
12
16
  export type GetBookingResources = (params?: GetBookingResourcesParams) => Promise<GetBookingResourcesResponse>;
@@ -16,5 +16,5 @@ export const mockGetBookings = async (params) => {
16
16
  const to = new Date(params.to);
17
17
  bookings = bookings.filter((entry) => new Date(entry.startAt) < to);
18
18
  }
19
- return { bookings, timestamp: new Date().toISOString() };
19
+ return { success: true, bookings, timestamp: new Date().toISOString() };
20
20
  };
@@ -12,7 +12,11 @@ export interface GetBookingsParams {
12
12
  includeExpired?: boolean;
13
13
  }
14
14
  export interface GetBookingsResponse {
15
- bookings: CFBooking[];
15
+ /** False when the command was refused — a taken window, a rule, a booking that is not yours. */
16
+ success: boolean;
17
+ /** Why it was refused, in words a cashier can act on. Absent on success. */
18
+ reason?: string;
19
+ bookings?: CFBooking[];
16
20
  timestamp: string;
17
21
  }
18
22
  export type GetBookings = (params?: GetBookingsParams) => Promise<GetBookingsResponse>;
@@ -2,6 +2,7 @@ import { MOCK_BOOKINGS, MOCK_BOOKING_RESOURCES, mockLiveBookings } from '../../d
2
2
  import { ReservationStatus } from '@final-commerce/common';
3
3
  const BUFFER_MINUTES = 5;
4
4
  const HOLD_MINUTES = 10;
5
+ let held = 0;
5
6
  export const mockHoldBooking = async (params) => {
6
7
  console.log('[Mock] holdBooking called', params);
7
8
  const startAt = new Date(params.startAt);
@@ -12,10 +13,13 @@ export const mockHoldBooking = async (params) => {
12
13
  const clash = mockLiveBookings().some((entry) => entry.resourceId === params.resourceId &&
13
14
  new Date(entry.startAt) < bufferEndAt &&
14
15
  new Date(entry.bufferEndAt) > startAt);
15
- if (clash)
16
- throw new Error('That window has just been taken — pick another slot.');
16
+ if (clash) {
17
+ return { success: false, reason: 'That window has just been taken — pick another slot.', timestamp: new Date().toISOString() };
18
+ }
17
19
  const booking = {
18
- id: `bk_mock_${Date.now()}`,
20
+ // A counter, not a clock: two holds in the same millisecond used to share an id, and cancel
21
+ // or remove then acted on the wrong row. Scripted tests hit that routinely.
22
+ id: `bk_mock_${(held += 1)}`,
19
23
  productId: params.productId,
20
24
  resourceId: params.resourceId,
21
25
  variantId: params.variantId,
@@ -29,5 +33,5 @@ export const mockHoldBooking = async (params) => {
29
33
  };
30
34
  // Held in the same list availability reads, so the slot really does disappear.
31
35
  MOCK_BOOKINGS.push(booking);
32
- return { booking, timestamp: new Date().toISOString() };
36
+ return { success: true, booking, timestamp: new Date().toISOString() };
33
37
  };
@@ -7,7 +7,7 @@ export interface HoldBookingParams {
7
7
  startAt: string;
8
8
  /**
9
9
  * End of the window — the `endAt` of the slot you picked, or of the last slot in a stay.
10
- * Required: the server refuses a window it cannot line up with its own grid, and the till
10
+ * Required: a window that cannot be lined up with the grid is refused, and the till
11
11
  * has no local copy of the rules to derive it from.
12
12
  */
13
13
  endAt: string;
@@ -17,8 +17,12 @@ export interface HoldBookingParams {
17
17
  customerId?: string;
18
18
  }
19
19
  export interface HoldBookingResponse {
20
+ /** False when the command was refused — a taken window, a rule, a booking that is not yours. */
21
+ success: boolean;
22
+ /** Why it was refused, in words a cashier can act on. Absent on success. */
23
+ reason?: string;
20
24
  /** The claim. `expiresAt` says how long it survives unpaid; `id` is what you confirm or cancel. */
21
- booking: CFBooking;
25
+ booking?: CFBooking;
22
26
  timestamp: string;
23
27
  }
24
28
  export type HoldBooking = (params: HoldBookingParams) => Promise<HoldBookingResponse>;
@@ -1,3 +1,4 @@
1
+ import { ReservationStatus } from '@final-commerce/common';
1
2
  import { MOCK_BOOKINGS, MOCK_CART, mockPublishEvent } from '../../demo/database';
2
3
  export const mockRemoveBookingFromCart = async (params) => {
3
4
  console.log('[Mock] removeBookingFromCart called', params);
@@ -10,15 +11,28 @@ export const mockRemoveBookingFromCart = async (params) => {
10
11
  MOCK_CART.total -= line;
11
12
  MOCK_CART.amountToBeCharged = MOCK_CART.total;
12
13
  MOCK_CART.remainingBalance = MOCK_CART.total;
13
- mockPublishEvent('cart', 'reservation-removed', { reservation: removed });
14
+ mockPublishEvent('cart', 'cart-created', { cart: MOCK_CART });
14
15
  }
15
16
  // The hold has to go back, or the mock is a trap: `addBookingToCart` pushes into the very list
16
17
  // availability reads, so add → remove → add the same slot used to refuse forever with "that
17
18
  // window has just been taken". The reservation id is `res_<booking.id>`, the link back to the row.
18
19
  const bookingId = removed?.bookingId ?? params.reservationInternalId.replace(/^res_/, '');
19
- const index = MOCK_BOOKINGS.findIndex(({ id }) => id === bookingId);
20
- const [released] = index >= 0 ? MOCK_BOOKINGS.splice(index, 1) : [];
20
+ // CANCELLED, not deleted — the host releases a window by moving the row, and a mock that
21
+ // erased it hid both the trail and the fact that a released booking is still a booking.
22
+ const released = MOCK_BOOKINGS.find(({ id }) => id === bookingId);
23
+ if (released)
24
+ released.status = ReservationStatus.CANCELLED;
25
+ // An id the cart does not have is a refusal, as it is on the host — resolving with an empty
26
+ // booking made a failed remove look like a successful one.
27
+ if (!removed) {
28
+ return {
29
+ success: false,
30
+ reason: `Reservation ${params.reservationInternalId} is not in the cart`,
31
+ timestamp: new Date().toISOString(),
32
+ };
33
+ }
21
34
  return {
35
+ success: true,
22
36
  reservationInternalId: params.reservationInternalId,
23
37
  booking: released,
24
38
  timestamp: new Date().toISOString(),
@@ -4,7 +4,11 @@ export interface RemoveBookingFromCartParams {
4
4
  reservationInternalId: string;
5
5
  }
6
6
  export interface RemoveBookingFromCartResponse {
7
- reservationInternalId: string;
7
+ /** False when the command was refused — a taken window, a rule, a booking that is not yours. */
8
+ success: boolean;
9
+ /** Why it was refused, in words a cashier can act on. Absent on success. */
10
+ reason?: string;
11
+ reservationInternalId?: string;
8
12
  /** The released hold, so the caller can see it is no longer held. */
9
13
  booking?: CFBooking;
10
14
  timestamp: string;
@@ -94,6 +94,12 @@ export declare const createOrderFromCart: (paymentType: string, amount: number,
94
94
  export declare const applyMockPayment: (amount: number, paymentType: string, processor?: string) => CFActiveOrder | null;
95
95
  export declare const MOCK_BOOKING_RULES_ID = "rule_salon_30";
96
96
  export declare const MOCK_BOOKING_RESOURCES: CFBookingResource[];
97
+ /**
98
+ * The clock this mock shop keeps — the same one `getContext` reports. They used to disagree: the
99
+ * slot hours were built with `setHours`, which is the MACHINE's clock, while the context claimed
100
+ * Vancouver. On a machine in Paris a correct screen showed a salon open from midnight to nine.
101
+ */
102
+ export declare const MOCK_SHOP_TIME_ZONE = "America/Vancouver";
97
103
  export declare const MOCK_BOOKINGS: CFBooking[];
98
104
  /** Live = confirmed, or held and not yet expired. An expired hold occupies nothing. */
99
105
  export declare const mockLiveBookings: () => CFBooking[];
@@ -928,6 +928,18 @@ export const createOrderFromCart = (paymentType, amount, processor = 'cash') =>
928
928
  createdAt: new Date().toISOString(),
929
929
  };
930
930
  MOCK_ORDERS.push(newOrder);
931
+ // Payment is what turns a held window into a kept appointment, so the ROW moves too — the
932
+ // comment above said CONFIRMED while nothing set it. Left held, a paid appointment dropped out
933
+ // of `getBookings` when its deadline passed and the window could be taken again, which is the
934
+ // one transition a booking screen most needs to be able to test.
935
+ for (const reservation of newOrder.reservations ?? []) {
936
+ const row = MOCK_BOOKINGS.find(({ id }) => id === reservation.bookingId);
937
+ if (row) {
938
+ row.status = ReservationStatus.CONFIRMED;
939
+ row.orderId = newOrder._id;
940
+ row.expiresAt = undefined;
941
+ }
942
+ }
931
943
  resetMockCart();
932
944
  // Publish cart-created event after cart is reset (simulates new empty cart)
933
945
  mockPublishEvent('cart', 'cart-created', {});
@@ -973,11 +985,33 @@ export const MOCK_BOOKING_RESOURCES = [
973
985
  { id: 'res_room_1', name: 'Room 1', kind: BookingResourceKind.ROOM },
974
986
  { id: 'res_room_2', name: 'Room 2', kind: BookingResourceKind.ROOM },
975
987
  ];
988
+ /**
989
+ * The clock this mock shop keeps — the same one `getContext` reports. They used to disagree: the
990
+ * slot hours were built with `setHours`, which is the MACHINE's clock, while the context claimed
991
+ * Vancouver. On a machine in Paris a correct screen showed a salon open from midnight to nine.
992
+ */
993
+ export const MOCK_SHOP_TIME_ZONE = 'America/Vancouver';
976
994
  const at = (dayOffset, hour, minute = 0) => {
977
- const date = new Date();
978
- date.setDate(date.getDate() + dayOffset);
979
- date.setHours(hour, minute, 0, 0);
980
- return date;
995
+ // The shop's wall clock, not this machine's. Two passes, because the offset is a function of
996
+ // the instant and the instant is what we are solving for.
997
+ const naive = Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate() + dayOffset, hour, minute);
998
+ const offsetAt = (instant) => {
999
+ const parts = {};
1000
+ for (const part of new Intl.DateTimeFormat('en-US', {
1001
+ timeZone: MOCK_SHOP_TIME_ZONE,
1002
+ hour12: false,
1003
+ year: 'numeric',
1004
+ month: '2-digit',
1005
+ day: '2-digit',
1006
+ hour: '2-digit',
1007
+ minute: '2-digit',
1008
+ }).formatToParts(new Date(instant))) {
1009
+ parts[part.type] = part.value;
1010
+ }
1011
+ const asUtc = Date.UTC(Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour) % 24, Number(parts.minute));
1012
+ return asUtc - instant;
1013
+ };
1014
+ return new Date(naive - offsetAt(naive - offsetAt(naive)));
981
1015
  };
982
1016
  const booking = (id, resourceId, start, status, customerName) => ({
983
1017
  id,
@@ -1000,6 +1034,19 @@ export const MOCK_BOOKINGS = [
1000
1034
  export const mockLiveBookings = () => MOCK_BOOKINGS.filter(({ status, expiresAt }) => status === ReservationStatus.CONFIRMED ||
1001
1035
  (status === ReservationStatus.HELD && (!expiresAt || new Date(expiresAt) > new Date())));
1002
1036
  const takenBy = (resourceId, startAt, bufferEndAt) => mockLiveBookings().some((entry) => entry.resourceId === resourceId && new Date(entry.startAt) < bufferEndAt && new Date(entry.bufferEndAt) > startAt);
1037
+ /** The mock's shop day for an instant, `YYYY-MM-DD`, built from parts so it never reads as D/M/Y. */
1038
+ const shopDayOf = (instant) => {
1039
+ const parts = {};
1040
+ for (const part of new Intl.DateTimeFormat('en-CA', {
1041
+ timeZone: MOCK_SHOP_TIME_ZONE,
1042
+ year: 'numeric',
1043
+ month: '2-digit',
1044
+ day: '2-digit',
1045
+ }).formatToParts(instant)) {
1046
+ parts[part.type] = part.value;
1047
+ }
1048
+ return `${parts.year}-${parts.month}-${parts.day}`;
1049
+ };
1003
1050
  export const mockBookingAvailability = (productId, from, to, resourceId) => {
1004
1051
  const serving = resourceId
1005
1052
  ? MOCK_BOOKING_RESOURCES.filter((resource) => resource.id === resourceId)
@@ -1008,9 +1055,12 @@ export const mockBookingAvailability = (productId, from, to, resourceId) => {
1008
1055
  const step = (SLOT_MINUTES + BUFFER_MINUTES) * 60000;
1009
1056
  for (let day = 0; day < 14; day += 1) {
1010
1057
  const open = at(day, OPEN_HOUR);
1011
- if (open < from || open > to)
1012
- continue;
1013
1058
  const close = at(day, CLOSE_HOUR);
1059
+ // Overlap, not containment. Asking from noon used to return nothing for today, because the
1060
+ // day's opening hour lay before the range start — so an afternoon question got an empty
1061
+ // calendar and read as "fully booked".
1062
+ if (close <= from || open >= to)
1063
+ continue;
1014
1064
  for (let start = open.getTime(); start + SLOT_MINUTES * 60000 <= close.getTime(); start += step) {
1015
1065
  const startAt = new Date(start);
1016
1066
  const endAt = new Date(start + SLOT_MINUTES * 60000);
@@ -1030,6 +1080,10 @@ export const mockBookingAvailability = (productId, from, to, resourceId) => {
1030
1080
  booked: resources.reduce((sum, entry) => sum + entry.booked, 0),
1031
1081
  free: resources.reduce((sum, entry) => sum + entry.free, 0),
1032
1082
  resources,
1083
+ // The shop day, as the real host stamps it. The mock keeps one shop on the machine's
1084
+ // own clock, so here that is the machine's day — the point is that the FIELD is there,
1085
+ // so a screen built against the mock never learns to work the day out for itself.
1086
+ dayKey: shopDayOf(startAt),
1033
1087
  });
1034
1088
  }
1035
1089
  }
@@ -1039,6 +1093,8 @@ export const mockBookingAvailability = (productId, from, to, resourceId) => {
1039
1093
  bookingType: BookingType.APPOINTMENT,
1040
1094
  ratePeriod: BookingRatePeriod.SLOT,
1041
1095
  slots,
1096
+ timeZone: MOCK_SHOP_TIME_ZONE,
1097
+ days: [...new Set(slots.map((slot) => slot.dayKey))],
1042
1098
  };
1043
1099
  };
1044
1100
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@final-commerce/command-frame",
3
- "version": "0.8.0-staging.1",
3
+ "version": "0.8.0-staging.2",
4
4
  "description": "Commands Frame library",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -57,7 +57,7 @@
57
57
  "ignoreBranchesMissingTickets": true
58
58
  },
59
59
  "dependencies": {
60
- "@final-commerce/common": "2.4.0-staging.1"
60
+ "@final-commerce/common": "2.4.0-staging.2"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@commitlint/cli": "^19.0.0",
@@ -73,5 +73,8 @@
73
73
  "typescript-eslint": "^8.0.0",
74
74
  "vitest": "^4.0.0"
75
75
  },
76
+ "overrides": {
77
+ "js-yaml": "^4.3.2"
78
+ },
76
79
  "prettier": "@final-commerce/common/prettier"
77
80
  }