From 4485719aa7b8fb0b092536442b6cd1b3afd69f3f Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Thu, 2 Apr 2026 13:36:30 +0300 Subject: [PATCH 1/2] feat: add LocationService.create method (#838) --- .../location/location-created-response.ts | 3 + .../location/location-creation-request.ts | 17 +++ .../location/location.service.spec.ts | 132 ++++++++++++++++++ .../lib/services/location/location.service.ts | 23 +++ .../src/lib/services/location/location.ts | 38 +++++ .../javascript-api/src/public-api/model.ts | 2 + 6 files changed, 215 insertions(+) create mode 100644 packages/javascript-api/src/lib/model/location/location-created-response.ts create mode 100644 packages/javascript-api/src/lib/model/location/location-creation-request.ts diff --git a/packages/javascript-api/src/lib/model/location/location-created-response.ts b/packages/javascript-api/src/lib/model/location/location-created-response.ts new file mode 100644 index 00000000..b824dbcd --- /dev/null +++ b/packages/javascript-api/src/lib/model/location/location-created-response.ts @@ -0,0 +1,3 @@ +export interface LocationCreatedResponse { + id: string; +} diff --git a/packages/javascript-api/src/lib/model/location/location-creation-request.ts b/packages/javascript-api/src/lib/model/location/location-creation-request.ts new file mode 100644 index 00000000..8cdc60a2 --- /dev/null +++ b/packages/javascript-api/src/lib/model/location/location-creation-request.ts @@ -0,0 +1,17 @@ +import { InputFieldCreationRequest } from '../input-field/input-field-creation-request.js'; +import { OpeningHours } from '../opening-hours.js'; +import { OpeningHoursException } from '../opening-hours-exception.js'; + +export interface LocationCreationRequest { + name: string; + latitude: number; + longitude: number; + address: string; + country: string; + openingHours?: { + regular?: OpeningHours; + exceptions?: OpeningHoursException[]; + }; + inputFields?: InputFieldCreationRequest[]; + languages?: string[]; +} 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 0154d355..5ea04036 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 @@ -1,9 +1,11 @@ import * as sinon from 'sinon'; import { Desk } from '../../model/desk'; +import { ResponseValidationError } from '../../model/errors/response-validation-error'; import { InputFieldCreationRequest } from '../../model/input-field/input-field-creation-request'; import { FirstNameFieldCreationRequest } from '../../model/input-field/first-name-field-creation-request'; import { NumericFieldCreationRequest } from '../../model/input-field/numeric-field-creation-request'; import { SelectFieldCreationRequest } from '../../model/input-field/select-field-creation-request'; +import { LocationCreationRequest } from '../../model/location/location-creation-request'; import { Qminder } from '../../qminder'; import { LocationService } from './location.service'; @@ -229,6 +231,136 @@ describe('Location service', function () { }); }); + describe('create()', function () { + const SUCCESSFUL_RESPONSE = { id: '12345' }; + const VALID_REQUEST: LocationCreationRequest = { + name: 'Main Office', + latitude: 59.4297, + longitude: 24.8149, + address: '123 Main St', + country: 'EE', + }; + + it('sends the request to the correct URL with JSON body and version headers', async function () { + requestStub.resolves(SUCCESSFUL_RESPONSE); + const result = await LocationService.create(VALID_REQUEST); + expect(requestStub.firstCall.args).toEqual([ + 'locations', + { + method: 'POST', + body: JSON.stringify(VALID_REQUEST), + headers: { 'X-Qminder-API-Version': '2020-09-01' }, + }, + ]); + expect(result).toEqual(SUCCESSFUL_RESPONSE); + }); + + it('sends optional fields when provided', async function () { + requestStub.resolves(SUCCESSFUL_RESPONSE); + const request: LocationCreationRequest = { + ...VALID_REQUEST, + openingHours: { + regular: { + mon: { + businessHours: [ + { + opens: { hours: 9, minutes: 0 }, + closes: { hours: 17, minutes: 0 }, + }, + ], + }, + tue: {}, + wed: {}, + thu: {}, + fri: {}, + sat: { closed: true as const }, + sun: { closed: true as const }, + }, + }, + inputFields: [], + languages: ['en', 'et'], + }; + await LocationService.create(request); + expect(requestStub.firstCall.args).toEqual([ + 'locations', + { + method: 'POST', + body: JSON.stringify(request), + headers: { 'X-Qminder-API-Version': '2020-09-01' }, + }, + ]); + }); + + it('throws ResponseValidationError when response does not contain id', async function () { + requestStub.resolves({}); + await expect(LocationService.create(VALID_REQUEST)).rejects.toThrow( + new ResponseValidationError('Response does not contain "id"'), + ); + }); + + it('throws when request is missing', async function () { + await expect(LocationService.create(null as any)).rejects.toThrow( + 'Location creation request invalid or missing.', + ); + }); + + it('throws when name is missing', async function () { + await expect( + LocationService.create({ ...VALID_REQUEST, name: '' }), + ).rejects.toThrow('Cannot create a location without a name.'); + }); + + it('throws when latitude is missing', async function () { + await expect( + LocationService.create({ ...VALID_REQUEST, latitude: undefined as any }), + ).rejects.toThrow('Cannot create a location without a valid latitude.'); + }); + + it('throws when latitude is NaN', async function () { + await expect( + LocationService.create({ ...VALID_REQUEST, latitude: NaN }), + ).rejects.toThrow('Cannot create a location without a valid latitude.'); + }); + + it('throws when longitude is missing', async function () { + await expect( + LocationService.create({ + ...VALID_REQUEST, + longitude: undefined as any, + }), + ).rejects.toThrow('Cannot create a location without a valid longitude.'); + }); + + it('throws when longitude is NaN', async function () { + await expect( + LocationService.create({ ...VALID_REQUEST, longitude: NaN }), + ).rejects.toThrow('Cannot create a location without a valid longitude.'); + }); + + it('throws when address is missing', async function () { + await expect( + LocationService.create({ ...VALID_REQUEST, address: '' }), + ).rejects.toThrow('Cannot create a location without an address.'); + }); + + it('throws when country is missing', async function () { + await expect( + LocationService.create({ ...VALID_REQUEST, country: '' }), + ).rejects.toThrow('Cannot create a location without a country.'); + }); + + it('accepts latitude 0 and longitude 0 as valid coordinates', async function () { + requestStub.resolves(SUCCESSFUL_RESPONSE); + const request: LocationCreationRequest = { + ...VALID_REQUEST, + latitude: 0, + longitude: 0, + }; + const result = await LocationService.create(request); + expect(result).toEqual(SUCCESSFUL_RESPONSE); + }); + }); + describe('createInputField()', function () { beforeEach(function () { requestStub.withArgs('input-fields').resolves({}); 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 00394d7f..197915a3 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,5 @@ import { + create, createInputField, details, getDesks, @@ -22,6 +23,28 @@ import { * ``` */ export const LocationService = { + /** + * Create a new Location and return its ID. + * + * Calls the following HTTP API: `POST /locations` + * + * For example: + * + * ```javascript + * const response = await Qminder.Location.create({ + * name: 'Main Office', + * latitude: 59.4297, + * longitude: 24.8149, + * address: '123 Main St', + * country: 'EE', + * }); + * console.log(response.id); // "12345" + * ``` + * @param request the parameters of the new location + * @returns a Promise that resolves to a LocationCreatedResponse containing the new location's ID. + */ + create, + /** * List all locations the API key has access to. * The API key belongs to a particular account and has access to all locations of the account. diff --git a/packages/javascript-api/src/lib/services/location/location.ts b/packages/javascript-api/src/lib/services/location/location.ts index 4730b6d8..36a52e71 100644 --- a/packages/javascript-api/src/lib/services/location/location.ts +++ b/packages/javascript-api/src/lib/services/location/location.ts @@ -1,5 +1,8 @@ import { Desk } from '../../model/desk.js'; +import { ResponseValidationError } from '../../model/errors/response-validation-error.js'; import { InputFieldCreationRequest } from '../../model/input-field/input-field-creation-request.js'; +import { LocationCreatedResponse } from '../../model/location/location-created-response.js'; +import { LocationCreationRequest } from '../../model/location/location-creation-request.js'; import { Location } from '../../model/location.js'; import { OpeningHours } from '../../model/opening-hours.js'; import { OpeningHoursException } from '../../model/opening-hours-exception.js'; @@ -56,6 +59,41 @@ export async function setOpeningHoursExceptions( }); } +export async function create( + request: LocationCreationRequest, +): Promise { + if (!request || typeof request !== 'object') { + throw new Error('Location creation request invalid or missing.'); + } + if (!request.name || typeof request.name !== 'string') { + throw new Error('Cannot create a location without a name.'); + } + if (typeof request.latitude !== 'number' || isNaN(request.latitude)) { + throw new Error('Cannot create a location without a valid latitude.'); + } + if (typeof request.longitude !== 'number' || isNaN(request.longitude)) { + throw new Error('Cannot create a location without a valid longitude.'); + } + if (!request.address || typeof request.address !== 'string') { + throw new Error('Cannot create a location without an address.'); + } + if (!request.country || typeof request.country !== 'string') { + throw new Error('Cannot create a location without a country.'); + } + + const result: LocationCreatedResponse = await ApiBase.request('locations', { + method: 'POST', + body: JSON.stringify(request), + headers: V2_HEADERS, + }); + + if (!result.id) { + throw new ResponseValidationError('Response does not contain "id"'); + } + + return result; +} + export async function createInputField( inputField: InputFieldCreationRequest, ): Promise { diff --git a/packages/javascript-api/src/public-api/model.ts b/packages/javascript-api/src/public-api/model.ts index 45a378a3..0b04d0cc 100644 --- a/packages/javascript-api/src/public-api/model.ts +++ b/packages/javascript-api/src/public-api/model.ts @@ -11,6 +11,8 @@ export { LineAppointmentSettings } from '../lib/model/line/line-appointment-sett export { LineCreationRequest } from '../lib/model/line/line-creation-request.js'; export { LineCreatedResponse } from '../lib/model/line/line-created-response.js'; export { Location } from '../lib/model/location.js'; +export { LocationCreationRequest } from '../lib/model/location/location-creation-request.js'; +export { LocationCreatedResponse } from '../lib/model/location/location-created-response.js'; export { TicketExtra } from '../lib/model/ticket/ticket-extra.js'; export { TicketLabel } from '../lib/model/ticket/ticket-label.js'; export { Ticket } from '../lib/model/ticket/ticket.js'; From 9237e8dd4fddbcccd0b685ab55428ad642fbd296 Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Thu, 2 Apr 2026 15:16:51 +0300 Subject: [PATCH 2/2] Fix CI: format location.service.spec.ts with Prettier --- .../src/lib/services/location/location.service.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 5ea04036..06a68de8 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 @@ -312,7 +312,10 @@ describe('Location service', function () { it('throws when latitude is missing', async function () { await expect( - LocationService.create({ ...VALID_REQUEST, latitude: undefined as any }), + LocationService.create({ + ...VALID_REQUEST, + latitude: undefined as any, + }), ).rejects.toThrow('Cannot create a location without a valid latitude.'); });