From 276eaf6b3c7d3d20d410f61ce1fa3599c6e6e3bb Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Wed, 25 Mar 2026 15:51:41 +0200 Subject: [PATCH 1/7] feat: add LocationService methods for setting opening hours Add setOpeningHours and setOpeningHoursExceptions to LocationService, enabling configuration of weekly opening hours and date-specific exceptions via the V2 API. --- .../src/lib/model/opening-hours.ts | 58 ++++++++++ .../location/location.service.spec.ts | 105 ++++++++++++++++++ .../lib/services/location/location.service.ts | 62 ++++++++++- .../src/lib/services/location/location.ts | 28 +++++ .../javascript-api/src/public-api/model.ts | 7 ++ 5 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 packages/javascript-api/src/lib/model/opening-hours.ts diff --git a/packages/javascript-api/src/lib/model/opening-hours.ts b/packages/javascript-api/src/lib/model/opening-hours.ts new file mode 100644 index 00000000..4be535ed --- /dev/null +++ b/packages/javascript-api/src/lib/model/opening-hours.ts @@ -0,0 +1,58 @@ +/** + * A time of day represented as hours and minutes. + */ +export interface OpeningHoursTime { + /** Hour of the day (0-23) */ + hours: number; + /** Minute of the hour (0-59) */ + minutes: number; +} + +/** + * A time range during which a location is open for business. + */ +export interface OpeningHoursRange { + opens: OpeningHoursTime; + closes: OpeningHoursTime; +} + +/** + * Opening hours configuration for a single day of the week. + * + * - Set `businessHours` to specify time ranges when the location is open. + * - Set `closed` to `true` to mark the day as closed. + * - Leave both unset to indicate the location is open all day. + * + * `businessHours` and `closed` are mutually exclusive. + */ +export interface DayOpeningHours { + businessHours?: OpeningHoursRange[]; + closed?: boolean; +} + +/** + * Weekly opening hours for a location, with an entry for each day of the week. + */ +export interface OpeningHours { + mon: DayOpeningHours; + tue: DayOpeningHours; + wed: DayOpeningHours; + thu: DayOpeningHours; + fri: DayOpeningHours; + sat: DayOpeningHours; + sun: DayOpeningHours; +} + +/** + * A date-specific exception to the regular opening hours schedule. + * + * Requires exactly one of `closed` or `businessHours`. + */ +export interface OpeningHoursException { + /** ISO date string, e.g. "2020-05-13" */ + date: string; + closed?: boolean; + /** Reason for closure, max 30 characters */ + closedReason?: string; + businessHours?: OpeningHoursRange[]; +} diff --git a/packages/javascript-api/src/lib/services/location/location.service.spec.ts b/packages/javascript-api/src/lib/services/location/location.service.spec.ts index d5f3b85c..a1da994a 100644 --- a/packages/javascript-api/src/lib/services/location/location.service.spec.ts +++ b/packages/javascript-api/src/lib/services/location/location.service.spec.ts @@ -147,6 +147,111 @@ describe('Location service', function () { }); }); + describe('setOpeningHours()', function () { + const OPENING_HOURS = { + mon: { + businessHours: [ + { + opens: { hours: 9, minutes: 0 }, + closes: { hours: 17, minutes: 30 }, + }, + ], + }, + tue: { + businessHours: [ + { + opens: { hours: 9, minutes: 0 }, + closes: { hours: 17, minutes: 30 }, + }, + ], + }, + wed: {}, + thu: {}, + fri: {}, + sat: { closed: true }, + sun: { closed: true }, + }; + + beforeEach(function () { + requestStub.resolves({}); + }); + + it('calls ApiBase.request with correct URL, method, body and headers using numeric ID', async function () { + await LocationService.setOpeningHours(LOCATION_ID, OPENING_HOURS); + expect( + requestStub.calledWith(`locations/${LOCATION_ID}/opening-hours`, { + method: 'PUT', + body: JSON.stringify(OPENING_HOURS), + headers: { 'X-Qminder-API-Version': '2020-09-01' }, + }), + ).toBeTruthy(); + }); + + it('accepts a location object with id', async function () { + await LocationService.setOpeningHours( + { id: LOCATION_ID }, + OPENING_HOURS, + ); + expect( + requestStub.calledWith(`locations/${LOCATION_ID}/opening-hours`, { + method: 'PUT', + body: JSON.stringify(OPENING_HOURS), + headers: { 'X-Qminder-API-Version': '2020-09-01' }, + }), + ).toBeTruthy(); + }); + }); + + describe('setOpeningHoursExceptions()', function () { + const EXCEPTIONS = [ + { date: '2020-05-13', closed: true, closedReason: 'Birthday' }, + { + date: '2020-12-25', + businessHours: [ + { + opens: { hours: 10, minutes: 0 }, + closes: { hours: 14, minutes: 0 }, + }, + ], + }, + ]; + + beforeEach(function () { + requestStub.resolves({}); + }); + + it('calls ApiBase.request with correct URL, method, body and headers using numeric ID', async function () { + await LocationService.setOpeningHoursExceptions(LOCATION_ID, EXCEPTIONS); + expect( + requestStub.calledWith( + `locations/${LOCATION_ID}/opening-hours/exceptions`, + { + method: 'PUT', + body: JSON.stringify(EXCEPTIONS), + headers: { 'X-Qminder-API-Version': '2020-09-01' }, + }, + ), + ).toBeTruthy(); + }); + + it('accepts a location object with id', async function () { + await LocationService.setOpeningHoursExceptions( + { id: LOCATION_ID }, + EXCEPTIONS, + ); + expect( + requestStub.calledWith( + `locations/${LOCATION_ID}/opening-hours/exceptions`, + { + method: 'PUT', + body: JSON.stringify(EXCEPTIONS), + headers: { 'X-Qminder-API-Version': '2020-09-01' }, + }, + ), + ).toBeTruthy(); + }); + }); + afterEach(function () { requestStub.restore(); }); diff --git a/packages/javascript-api/src/lib/services/location/location.service.ts b/packages/javascript-api/src/lib/services/location/location.service.ts index a18abc11..49030955 100644 --- a/packages/javascript-api/src/lib/services/location/location.service.ts +++ b/packages/javascript-api/src/lib/services/location/location.service.ts @@ -1,4 +1,10 @@ -import { details, getDesks, list } from './location.js'; +import { + details, + getDesks, + list, + setOpeningHours, + setOpeningHoursExceptions, +} from './location.js'; /** * The LocationService allows you to get data about Locations. @@ -74,4 +80,58 @@ export const LocationService = { * @returns a Promise that resolves to the list of desks in this location */ getDesks, + + /** + * Set the weekly opening hours for a location. + * + * Each day can have `businessHours` (array of time ranges), `closed: true`, or + * neither (open all day). `businessHours` and `closed` are mutually exclusive. + * + * Calls the following HTTP API: `PUT /locations//opening-hours` + * + * For example: + * + * ```javascript + * import { Qminder } from 'qminder-api'; + * Qminder.setKey('API_KEY_HERE'); + * + * await Qminder.Location.setOpeningHours(1234, { + * mon: { businessHours: [{ opens: { hours: 9, minutes: 0 }, closes: { hours: 17, minutes: 0 } }] }, + * tue: { businessHours: [{ opens: { hours: 9, minutes: 0 }, closes: { hours: 17, minutes: 0 } }] }, + * wed: {}, + * thu: {}, + * fri: {}, + * sat: { closed: true }, + * sun: { closed: true }, + * }); + * ``` + * + * @param location the location or location ID + * @param openingHours the weekly opening hours configuration + */ + setOpeningHours, + + /** + * Set date-specific exceptions to the regular opening hours schedule. + * + * Each exception requires a `date` and exactly one of `closed: true` or `businessHours`. + * An optional `closedReason` (max 30 characters) can be provided when closed. + * + * Calls the following HTTP API: `PUT /locations//opening-hours/exceptions` + * + * For example: + * + * ```javascript + * import { Qminder } from 'qminder-api'; + * Qminder.setKey('API_KEY_HERE'); + * + * await Qminder.Location.setOpeningHoursExceptions(1234, [ + * { date: '2020-05-13', closed: true, closedReason: 'Holiday' }, + * ]); + * ``` + * + * @param location the location or location ID + * @param exceptions the list of opening hours exceptions + */ + setOpeningHoursExceptions, }; diff --git a/packages/javascript-api/src/lib/services/location/location.ts b/packages/javascript-api/src/lib/services/location/location.ts index 313c7128..59061bf9 100644 --- a/packages/javascript-api/src/lib/services/location/location.ts +++ b/packages/javascript-api/src/lib/services/location/location.ts @@ -1,5 +1,9 @@ import { Desk } from '../../model/desk.js'; import { Location } from '../../model/location.js'; +import { + OpeningHours, + OpeningHoursException, +} from '../../model/opening-hours.js'; import { extractId, IdOrObject } from '../../util/id-or-object.js'; import { ApiBase } from '../api-base/api-base.js'; @@ -27,3 +31,27 @@ export function getDesks(location: IdOrObject): Promise { }, ); } + +export async function setOpeningHours( + location: IdOrObject, + openingHours: OpeningHours, +): Promise { + const locationId = extractId(location); + await ApiBase.request(`locations/${locationId}/opening-hours`, { + method: 'PUT', + body: JSON.stringify(openingHours), + headers: { 'X-Qminder-API-Version': '2020-09-01' }, + }); +} + +export async function setOpeningHoursExceptions( + location: IdOrObject, + exceptions: OpeningHoursException[], +): Promise { + const locationId = extractId(location); + await ApiBase.request(`locations/${locationId}/opening-hours/exceptions`, { + method: 'PUT', + body: JSON.stringify(exceptions), + headers: { 'X-Qminder-API-Version': '2020-09-01' }, + }); +} diff --git a/packages/javascript-api/src/public-api/model.ts b/packages/javascript-api/src/public-api/model.ts index 282b2f95..4c6b4d19 100644 --- a/packages/javascript-api/src/public-api/model.ts +++ b/packages/javascript-api/src/public-api/model.ts @@ -14,6 +14,13 @@ export { TicketExtraText } from '../lib/model/ticket/ticket-extra-text.js'; export { TicketExtraUrl } from '../lib/model/ticket/ticket-extra-url.js'; export { TicketExtraOption } from '../lib/model/ticket/ticket-extra-option.js'; export { User } from '../lib/model/user.js'; +export { + OpeningHoursTime, + OpeningHoursRange, + DayOpeningHours, + OpeningHours, + OpeningHoursException, +} from '../lib/model/opening-hours.js'; export { Webhook } from '../lib/model/webhook.js'; export { SimpleError } from '../lib/model/errors/simple-error.js'; export { ComplexError } from '../lib/model/errors/complex-error.js'; From 73683cee38c2df0c6fd32592054150e93fdcaafb Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Wed, 25 Mar 2026 15:52:42 +0200 Subject: [PATCH 2/7] style: fix prettier formatting in location service spec --- .../src/lib/services/location/location.service.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/javascript-api/src/lib/services/location/location.service.spec.ts b/packages/javascript-api/src/lib/services/location/location.service.spec.ts index a1da994a..d037bba1 100644 --- a/packages/javascript-api/src/lib/services/location/location.service.spec.ts +++ b/packages/javascript-api/src/lib/services/location/location.service.spec.ts @@ -188,10 +188,7 @@ describe('Location service', function () { }); it('accepts a location object with id', async function () { - await LocationService.setOpeningHours( - { id: LOCATION_ID }, - OPENING_HOURS, - ); + await LocationService.setOpeningHours({ id: LOCATION_ID }, OPENING_HOURS); expect( requestStub.calledWith(`locations/${LOCATION_ID}/opening-hours`, { method: 'PUT', From c33cb5764406b7adb95fd7c4bb46dd6cd837cc80 Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Wed, 25 Mar 2026 15:54:52 +0200 Subject: [PATCH 3/7] refactor: split opening hours types into dedicated files Follow the existing one-interface-per-file convention used throughout the model directory. --- .../src/lib/model/day-opening-hours.ts | 15 ++++++ .../src/lib/model/opening-hours-exception.ts | 15 ++++++ .../src/lib/model/opening-hours-range.ts | 9 ++++ .../src/lib/model/opening-hours-time.ts | 9 ++++ .../src/lib/model/opening-hours.ts | 46 +------------------ .../src/lib/services/location/location.ts | 6 +-- .../javascript-api/src/public-api/model.ts | 12 ++--- 7 files changed, 56 insertions(+), 56 deletions(-) create mode 100644 packages/javascript-api/src/lib/model/day-opening-hours.ts create mode 100644 packages/javascript-api/src/lib/model/opening-hours-exception.ts create mode 100644 packages/javascript-api/src/lib/model/opening-hours-range.ts create mode 100644 packages/javascript-api/src/lib/model/opening-hours-time.ts diff --git a/packages/javascript-api/src/lib/model/day-opening-hours.ts b/packages/javascript-api/src/lib/model/day-opening-hours.ts new file mode 100644 index 00000000..3381da15 --- /dev/null +++ b/packages/javascript-api/src/lib/model/day-opening-hours.ts @@ -0,0 +1,15 @@ +import { OpeningHoursRange } from './opening-hours-range.js'; + +/** + * Opening hours configuration for a single day of the week. + * + * - Set `businessHours` to specify time ranges when the location is open. + * - Set `closed` to `true` to mark the day as closed. + * - Leave both unset to indicate the location is open all day. + * + * `businessHours` and `closed` are mutually exclusive. + */ +export interface DayOpeningHours { + businessHours?: OpeningHoursRange[]; + closed?: boolean; +} diff --git a/packages/javascript-api/src/lib/model/opening-hours-exception.ts b/packages/javascript-api/src/lib/model/opening-hours-exception.ts new file mode 100644 index 00000000..7fa07d36 --- /dev/null +++ b/packages/javascript-api/src/lib/model/opening-hours-exception.ts @@ -0,0 +1,15 @@ +import { OpeningHoursRange } from './opening-hours-range.js'; + +/** + * A date-specific exception to the regular opening hours schedule. + * + * Requires exactly one of `closed` or `businessHours`. + */ +export interface OpeningHoursException { + /** ISO date string, e.g. "2020-05-13" */ + date: string; + closed?: boolean; + /** Reason for closure, max 30 characters */ + closedReason?: string; + businessHours?: OpeningHoursRange[]; +} diff --git a/packages/javascript-api/src/lib/model/opening-hours-range.ts b/packages/javascript-api/src/lib/model/opening-hours-range.ts new file mode 100644 index 00000000..132ac375 --- /dev/null +++ b/packages/javascript-api/src/lib/model/opening-hours-range.ts @@ -0,0 +1,9 @@ +import { OpeningHoursTime } from './opening-hours-time.js'; + +/** + * A time range during which a location is open for business. + */ +export interface OpeningHoursRange { + opens: OpeningHoursTime; + closes: OpeningHoursTime; +} diff --git a/packages/javascript-api/src/lib/model/opening-hours-time.ts b/packages/javascript-api/src/lib/model/opening-hours-time.ts new file mode 100644 index 00000000..c94c9c95 --- /dev/null +++ b/packages/javascript-api/src/lib/model/opening-hours-time.ts @@ -0,0 +1,9 @@ +/** + * A time of day represented as hours and minutes. + */ +export interface OpeningHoursTime { + /** Hour of the day (0-23) */ + hours: number; + /** Minute of the hour (0-59) */ + minutes: number; +} diff --git a/packages/javascript-api/src/lib/model/opening-hours.ts b/packages/javascript-api/src/lib/model/opening-hours.ts index 4be535ed..d38ae75b 100644 --- a/packages/javascript-api/src/lib/model/opening-hours.ts +++ b/packages/javascript-api/src/lib/model/opening-hours.ts @@ -1,34 +1,4 @@ -/** - * A time of day represented as hours and minutes. - */ -export interface OpeningHoursTime { - /** Hour of the day (0-23) */ - hours: number; - /** Minute of the hour (0-59) */ - minutes: number; -} - -/** - * A time range during which a location is open for business. - */ -export interface OpeningHoursRange { - opens: OpeningHoursTime; - closes: OpeningHoursTime; -} - -/** - * Opening hours configuration for a single day of the week. - * - * - Set `businessHours` to specify time ranges when the location is open. - * - Set `closed` to `true` to mark the day as closed. - * - Leave both unset to indicate the location is open all day. - * - * `businessHours` and `closed` are mutually exclusive. - */ -export interface DayOpeningHours { - businessHours?: OpeningHoursRange[]; - closed?: boolean; -} +import { DayOpeningHours } from './day-opening-hours.js'; /** * Weekly opening hours for a location, with an entry for each day of the week. @@ -42,17 +12,3 @@ export interface OpeningHours { sat: DayOpeningHours; sun: DayOpeningHours; } - -/** - * A date-specific exception to the regular opening hours schedule. - * - * Requires exactly one of `closed` or `businessHours`. - */ -export interface OpeningHoursException { - /** ISO date string, e.g. "2020-05-13" */ - date: string; - closed?: boolean; - /** Reason for closure, max 30 characters */ - closedReason?: string; - businessHours?: OpeningHoursRange[]; -} diff --git a/packages/javascript-api/src/lib/services/location/location.ts b/packages/javascript-api/src/lib/services/location/location.ts index 59061bf9..5b21ab03 100644 --- a/packages/javascript-api/src/lib/services/location/location.ts +++ b/packages/javascript-api/src/lib/services/location/location.ts @@ -1,9 +1,7 @@ import { Desk } from '../../model/desk.js'; import { Location } from '../../model/location.js'; -import { - OpeningHours, - OpeningHoursException, -} from '../../model/opening-hours.js'; +import { OpeningHours } from '../../model/opening-hours.js'; +import { OpeningHoursException } from '../../model/opening-hours-exception.js'; import { extractId, IdOrObject } from '../../util/id-or-object.js'; import { ApiBase } from '../api-base/api-base.js'; diff --git a/packages/javascript-api/src/public-api/model.ts b/packages/javascript-api/src/public-api/model.ts index 4c6b4d19..95e1f81b 100644 --- a/packages/javascript-api/src/public-api/model.ts +++ b/packages/javascript-api/src/public-api/model.ts @@ -14,13 +14,11 @@ export { TicketExtraText } from '../lib/model/ticket/ticket-extra-text.js'; export { TicketExtraUrl } from '../lib/model/ticket/ticket-extra-url.js'; export { TicketExtraOption } from '../lib/model/ticket/ticket-extra-option.js'; export { User } from '../lib/model/user.js'; -export { - OpeningHoursTime, - OpeningHoursRange, - DayOpeningHours, - OpeningHours, - OpeningHoursException, -} from '../lib/model/opening-hours.js'; +export { DayOpeningHours } from '../lib/model/day-opening-hours.js'; +export { OpeningHours } from '../lib/model/opening-hours.js'; +export { OpeningHoursException } from '../lib/model/opening-hours-exception.js'; +export { OpeningHoursRange } from '../lib/model/opening-hours-range.js'; +export { OpeningHoursTime } from '../lib/model/opening-hours-time.js'; export { Webhook } from '../lib/model/webhook.js'; export { SimpleError } from '../lib/model/errors/simple-error.js'; export { ComplexError } from '../lib/model/errors/complex-error.js'; From 3255d4e5d791c6a6042c3cab92624b77760500aa Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Wed, 25 Mar 2026 15:56:07 +0200 Subject: [PATCH 4/7] test: remove trivial duplicate tests for opening hours methods --- .../location/location.service.spec.ts | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/packages/javascript-api/src/lib/services/location/location.service.spec.ts b/packages/javascript-api/src/lib/services/location/location.service.spec.ts index d037bba1..1d6521b2 100644 --- a/packages/javascript-api/src/lib/services/location/location.service.spec.ts +++ b/packages/javascript-api/src/lib/services/location/location.service.spec.ts @@ -176,7 +176,7 @@ describe('Location service', function () { requestStub.resolves({}); }); - it('calls ApiBase.request with correct URL, method, body and headers using numeric ID', async function () { + it('calls ApiBase.request with correct URL, method, body and headers', async function () { await LocationService.setOpeningHours(LOCATION_ID, OPENING_HOURS); expect( requestStub.calledWith(`locations/${LOCATION_ID}/opening-hours`, { @@ -186,17 +186,6 @@ describe('Location service', function () { }), ).toBeTruthy(); }); - - it('accepts a location object with id', async function () { - await LocationService.setOpeningHours({ id: LOCATION_ID }, OPENING_HOURS); - expect( - requestStub.calledWith(`locations/${LOCATION_ID}/opening-hours`, { - method: 'PUT', - body: JSON.stringify(OPENING_HOURS), - headers: { 'X-Qminder-API-Version': '2020-09-01' }, - }), - ).toBeTruthy(); - }); }); describe('setOpeningHoursExceptions()', function () { @@ -217,7 +206,7 @@ describe('Location service', function () { requestStub.resolves({}); }); - it('calls ApiBase.request with correct URL, method, body and headers using numeric ID', async function () { + it('calls ApiBase.request with correct URL, method, body and headers', async function () { await LocationService.setOpeningHoursExceptions(LOCATION_ID, EXCEPTIONS); expect( requestStub.calledWith( @@ -230,23 +219,6 @@ describe('Location service', function () { ), ).toBeTruthy(); }); - - it('accepts a location object with id', async function () { - await LocationService.setOpeningHoursExceptions( - { id: LOCATION_ID }, - EXCEPTIONS, - ); - expect( - requestStub.calledWith( - `locations/${LOCATION_ID}/opening-hours/exceptions`, - { - method: 'PUT', - body: JSON.stringify(EXCEPTIONS), - headers: { 'X-Qminder-API-Version': '2020-09-01' }, - }, - ), - ).toBeTruthy(); - }); }); afterEach(function () { From 5089d02d74fbf5dc5670937bafeb921fed239931 Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Wed, 25 Mar 2026 15:57:58 +0200 Subject: [PATCH 5/7] docs: trim verbose JSDoc comments from opening hours types and service --- .../src/lib/model/day-opening-hours.ts | 8 +--- .../src/lib/model/opening-hours-exception.ts | 4 +- .../src/lib/model/opening-hours-range.ts | 3 -- .../src/lib/model/opening-hours-time.ts | 5 --- .../src/lib/model/opening-hours.ts | 3 -- .../lib/services/location/location.service.ts | 40 ------------------- 6 files changed, 2 insertions(+), 61 deletions(-) diff --git a/packages/javascript-api/src/lib/model/day-opening-hours.ts b/packages/javascript-api/src/lib/model/day-opening-hours.ts index 3381da15..67777e2f 100644 --- a/packages/javascript-api/src/lib/model/day-opening-hours.ts +++ b/packages/javascript-api/src/lib/model/day-opening-hours.ts @@ -1,13 +1,7 @@ import { OpeningHoursRange } from './opening-hours-range.js'; /** - * Opening hours configuration for a single day of the week. - * - * - Set `businessHours` to specify time ranges when the location is open. - * - Set `closed` to `true` to mark the day as closed. - * - Leave both unset to indicate the location is open all day. - * - * `businessHours` and `closed` are mutually exclusive. + * `businessHours` and `closed` are mutually exclusive. Neither means open all day. */ export interface DayOpeningHours { businessHours?: OpeningHoursRange[]; diff --git a/packages/javascript-api/src/lib/model/opening-hours-exception.ts b/packages/javascript-api/src/lib/model/opening-hours-exception.ts index 7fa07d36..d43029f5 100644 --- a/packages/javascript-api/src/lib/model/opening-hours-exception.ts +++ b/packages/javascript-api/src/lib/model/opening-hours-exception.ts @@ -1,15 +1,13 @@ import { OpeningHoursRange } from './opening-hours-range.js'; /** - * A date-specific exception to the regular opening hours schedule. - * * Requires exactly one of `closed` or `businessHours`. */ export interface OpeningHoursException { /** ISO date string, e.g. "2020-05-13" */ date: string; closed?: boolean; - /** Reason for closure, max 30 characters */ + /** Max 30 characters */ closedReason?: string; businessHours?: OpeningHoursRange[]; } diff --git a/packages/javascript-api/src/lib/model/opening-hours-range.ts b/packages/javascript-api/src/lib/model/opening-hours-range.ts index 132ac375..88418ee3 100644 --- a/packages/javascript-api/src/lib/model/opening-hours-range.ts +++ b/packages/javascript-api/src/lib/model/opening-hours-range.ts @@ -1,8 +1,5 @@ import { OpeningHoursTime } from './opening-hours-time.js'; -/** - * A time range during which a location is open for business. - */ export interface OpeningHoursRange { opens: OpeningHoursTime; closes: OpeningHoursTime; diff --git a/packages/javascript-api/src/lib/model/opening-hours-time.ts b/packages/javascript-api/src/lib/model/opening-hours-time.ts index c94c9c95..0c364479 100644 --- a/packages/javascript-api/src/lib/model/opening-hours-time.ts +++ b/packages/javascript-api/src/lib/model/opening-hours-time.ts @@ -1,9 +1,4 @@ -/** - * A time of day represented as hours and minutes. - */ export interface OpeningHoursTime { - /** Hour of the day (0-23) */ hours: number; - /** Minute of the hour (0-59) */ minutes: number; } diff --git a/packages/javascript-api/src/lib/model/opening-hours.ts b/packages/javascript-api/src/lib/model/opening-hours.ts index d38ae75b..eb5c4eac 100644 --- a/packages/javascript-api/src/lib/model/opening-hours.ts +++ b/packages/javascript-api/src/lib/model/opening-hours.ts @@ -1,8 +1,5 @@ import { DayOpeningHours } from './day-opening-hours.js'; -/** - * Weekly opening hours for a location, with an entry for each day of the week. - */ export interface OpeningHours { mon: DayOpeningHours; tue: DayOpeningHours; diff --git a/packages/javascript-api/src/lib/services/location/location.service.ts b/packages/javascript-api/src/lib/services/location/location.service.ts index 49030955..5f19e679 100644 --- a/packages/javascript-api/src/lib/services/location/location.service.ts +++ b/packages/javascript-api/src/lib/services/location/location.service.ts @@ -84,54 +84,14 @@ export const LocationService = { /** * Set the weekly opening hours for a location. * - * Each day can have `businessHours` (array of time ranges), `closed: true`, or - * neither (open all day). `businessHours` and `closed` are mutually exclusive. - * * Calls the following HTTP API: `PUT /locations//opening-hours` - * - * For example: - * - * ```javascript - * import { Qminder } from 'qminder-api'; - * Qminder.setKey('API_KEY_HERE'); - * - * await Qminder.Location.setOpeningHours(1234, { - * mon: { businessHours: [{ opens: { hours: 9, minutes: 0 }, closes: { hours: 17, minutes: 0 } }] }, - * tue: { businessHours: [{ opens: { hours: 9, minutes: 0 }, closes: { hours: 17, minutes: 0 } }] }, - * wed: {}, - * thu: {}, - * fri: {}, - * sat: { closed: true }, - * sun: { closed: true }, - * }); - * ``` - * - * @param location the location or location ID - * @param openingHours the weekly opening hours configuration */ setOpeningHours, /** * Set date-specific exceptions to the regular opening hours schedule. * - * Each exception requires a `date` and exactly one of `closed: true` or `businessHours`. - * An optional `closedReason` (max 30 characters) can be provided when closed. - * * Calls the following HTTP API: `PUT /locations//opening-hours/exceptions` - * - * For example: - * - * ```javascript - * import { Qminder } from 'qminder-api'; - * Qminder.setKey('API_KEY_HERE'); - * - * await Qminder.Location.setOpeningHoursExceptions(1234, [ - * { date: '2020-05-13', closed: true, closedReason: 'Holiday' }, - * ]); - * ``` - * - * @param location the location or location ID - * @param exceptions the list of opening hours exceptions */ setOpeningHoursExceptions, }; From 03a40096405225c8e97953fa74c57fcd1cfadc11 Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Wed, 25 Mar 2026 16:43:36 +0200 Subject: [PATCH 6/7] refactor: enforce mutually exclusive fields with discriminated unions Use discriminated union types for DayOpeningHours and OpeningHoursException so the compiler rejects invalid combinations of closed and businessHours. Extract shared V2_HEADERS constant and tighten test stubs with withArgs. --- .../src/lib/model/day-opening-hours.ts | 8 ++++---- .../src/lib/model/opening-hours-exception.ts | 17 +++++++++++------ .../services/location/location.service.spec.ts | 16 +++++++++++----- .../src/lib/services/location/location.ts | 6 ++++-- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/javascript-api/src/lib/model/day-opening-hours.ts b/packages/javascript-api/src/lib/model/day-opening-hours.ts index 67777e2f..48a392e1 100644 --- a/packages/javascript-api/src/lib/model/day-opening-hours.ts +++ b/packages/javascript-api/src/lib/model/day-opening-hours.ts @@ -3,7 +3,7 @@ import { OpeningHoursRange } from './opening-hours-range.js'; /** * `businessHours` and `closed` are mutually exclusive. Neither means open all day. */ -export interface DayOpeningHours { - businessHours?: OpeningHoursRange[]; - closed?: boolean; -} +export type DayOpeningHours = + | { businessHours: OpeningHoursRange[]; closed?: undefined } + | { closed: true; businessHours?: undefined } + | Record; diff --git a/packages/javascript-api/src/lib/model/opening-hours-exception.ts b/packages/javascript-api/src/lib/model/opening-hours-exception.ts index d43029f5..46af69d4 100644 --- a/packages/javascript-api/src/lib/model/opening-hours-exception.ts +++ b/packages/javascript-api/src/lib/model/opening-hours-exception.ts @@ -1,13 +1,18 @@ import { OpeningHoursRange } from './opening-hours-range.js'; -/** - * Requires exactly one of `closed` or `businessHours`. - */ -export interface OpeningHoursException { +interface OpeningHoursExceptionBase { /** ISO date string, e.g. "2020-05-13" */ date: string; - closed?: boolean; /** Max 30 characters */ closedReason?: string; - businessHours?: OpeningHoursRange[]; } + +/** + * Requires exactly one of `closed` or `businessHours`. + */ +export type OpeningHoursException = + | (OpeningHoursExceptionBase & { closed: true; businessHours?: undefined }) + | (OpeningHoursExceptionBase & { + businessHours: OpeningHoursRange[]; + closed?: undefined; + }); diff --git a/packages/javascript-api/src/lib/services/location/location.service.spec.ts b/packages/javascript-api/src/lib/services/location/location.service.spec.ts index 1d6521b2..25594ff5 100644 --- a/packages/javascript-api/src/lib/services/location/location.service.spec.ts +++ b/packages/javascript-api/src/lib/services/location/location.service.spec.ts @@ -168,12 +168,14 @@ describe('Location service', function () { wed: {}, thu: {}, fri: {}, - sat: { closed: true }, - sun: { closed: true }, + sat: { closed: true as const }, + sun: { closed: true as const }, }; beforeEach(function () { - requestStub.resolves({}); + requestStub + .withArgs(`locations/${LOCATION_ID}/opening-hours`) + .resolves({}); }); it('calls ApiBase.request with correct URL, method, body and headers', async function () { @@ -186,11 +188,12 @@ describe('Location service', function () { }), ).toBeTruthy(); }); + }); describe('setOpeningHoursExceptions()', function () { const EXCEPTIONS = [ - { date: '2020-05-13', closed: true, closedReason: 'Birthday' }, + { date: '2020-05-13', closed: true as const, closedReason: 'Birthday' }, { date: '2020-12-25', businessHours: [ @@ -203,7 +206,9 @@ describe('Location service', function () { ]; beforeEach(function () { - requestStub.resolves({}); + requestStub + .withArgs(`locations/${LOCATION_ID}/opening-hours/exceptions`) + .resolves({}); }); it('calls ApiBase.request with correct URL, method, body and headers', async function () { @@ -219,6 +224,7 @@ describe('Location service', function () { ), ).toBeTruthy(); }); + }); afterEach(function () { diff --git a/packages/javascript-api/src/lib/services/location/location.ts b/packages/javascript-api/src/lib/services/location/location.ts index 5b21ab03..f09cac66 100644 --- a/packages/javascript-api/src/lib/services/location/location.ts +++ b/packages/javascript-api/src/lib/services/location/location.ts @@ -5,6 +5,8 @@ import { OpeningHoursException } from '../../model/opening-hours-exception.js'; import { extractId, IdOrObject } from '../../util/id-or-object.js'; import { ApiBase } from '../api-base/api-base.js'; +const V2_HEADERS = { 'X-Qminder-API-Version': '2020-09-01' } as const; + export function list(): Promise { return ApiBase.request('v1/locations/').then( (locations: { data: Location[] }) => { @@ -38,7 +40,7 @@ export async function setOpeningHours( await ApiBase.request(`locations/${locationId}/opening-hours`, { method: 'PUT', body: JSON.stringify(openingHours), - headers: { 'X-Qminder-API-Version': '2020-09-01' }, + headers: V2_HEADERS, }); } @@ -50,6 +52,6 @@ export async function setOpeningHoursExceptions( await ApiBase.request(`locations/${locationId}/opening-hours/exceptions`, { method: 'PUT', body: JSON.stringify(exceptions), - headers: { 'X-Qminder-API-Version': '2020-09-01' }, + headers: V2_HEADERS, }); } From e5ca7319bfc7a9821466dee36d9f1fb6684a31af Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Wed, 25 Mar 2026 16:46:13 +0200 Subject: [PATCH 7/7] style: fix prettier formatting in location service spec --- .../src/lib/services/location/location.service.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/javascript-api/src/lib/services/location/location.service.spec.ts b/packages/javascript-api/src/lib/services/location/location.service.spec.ts index 25594ff5..e069834c 100644 --- a/packages/javascript-api/src/lib/services/location/location.service.spec.ts +++ b/packages/javascript-api/src/lib/services/location/location.service.spec.ts @@ -188,7 +188,6 @@ describe('Location service', function () { }), ).toBeTruthy(); }); - }); describe('setOpeningHoursExceptions()', function () { @@ -224,7 +223,6 @@ describe('Location service', function () { ), ).toBeTruthy(); }); - }); afterEach(function () {