From 6bc260aef848f4b5055956bad4ab3b6d1bae98e4 Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Mon, 30 Mar 2026 15:13:50 +0300 Subject: [PATCH 1/4] feat: update LineService.create to use JSON endpoint with version headers Migrate LineService.create() from the legacy form-urlencoded endpoint to the JSON endpoint with X-Qminder-API-Version header. The new endpoint supports additional fields: color, disabled, translations, and appointmentSettings. BREAKING CHANGE: create() now accepts LineCreationRequest (with named colors) instead of partial Line, and returns LineCreatedResponse ({ id: string }) instead of Line. --- .../model/line/line-appointment-settings.ts | 4 + .../src/lib/model/line/line-color.ts | 11 +++ .../lib/model/line/line-created-response.ts | 3 + .../lib/model/line/line-creation-request.ts | 11 +++ .../src/lib/model/line/line-translation.ts | 4 + .../lib/services/line/line.service.spec.ts | 77 +++++++++++++++++++ .../src/lib/services/line/line.service.ts | 16 ++-- .../src/lib/services/line/line.ts | 33 ++++++-- .../javascript-api/src/public-api/model.ts | 5 ++ 9 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 packages/javascript-api/src/lib/model/line/line-appointment-settings.ts create mode 100644 packages/javascript-api/src/lib/model/line/line-color.ts create mode 100644 packages/javascript-api/src/lib/model/line/line-created-response.ts create mode 100644 packages/javascript-api/src/lib/model/line/line-creation-request.ts create mode 100644 packages/javascript-api/src/lib/model/line/line-translation.ts diff --git a/packages/javascript-api/src/lib/model/line/line-appointment-settings.ts b/packages/javascript-api/src/lib/model/line/line-appointment-settings.ts new file mode 100644 index 00000000..48485e6a --- /dev/null +++ b/packages/javascript-api/src/lib/model/line/line-appointment-settings.ts @@ -0,0 +1,4 @@ +export interface LineAppointmentSettings { + enabled: boolean; + duration: 15 | 30 | 45 | 60 | 90 | 120 | 180; +} diff --git a/packages/javascript-api/src/lib/model/line/line-color.ts b/packages/javascript-api/src/lib/model/line/line-color.ts new file mode 100644 index 00000000..c8584cb2 --- /dev/null +++ b/packages/javascript-api/src/lib/model/line/line-color.ts @@ -0,0 +1,11 @@ +export type LineColor = + | 'VIOLET' + | 'TEAL' + | 'MINT' + | 'CORAL' + | 'YELLOW' + | 'ROSE' + | 'INDIGO' + | 'BLUE' + | 'LAVENDER' + | 'MARSHMALLOW'; diff --git a/packages/javascript-api/src/lib/model/line/line-created-response.ts b/packages/javascript-api/src/lib/model/line/line-created-response.ts new file mode 100644 index 00000000..0a40fa90 --- /dev/null +++ b/packages/javascript-api/src/lib/model/line/line-created-response.ts @@ -0,0 +1,3 @@ +export interface LineCreatedResponse { + id: string; +} diff --git a/packages/javascript-api/src/lib/model/line/line-creation-request.ts b/packages/javascript-api/src/lib/model/line/line-creation-request.ts new file mode 100644 index 00000000..b4dcd4b9 --- /dev/null +++ b/packages/javascript-api/src/lib/model/line/line-creation-request.ts @@ -0,0 +1,11 @@ +import { LineAppointmentSettings } from './line-appointment-settings.js'; +import { LineColor } from './line-color.js'; +import { LineTranslation } from './line-translation.js'; + +export interface LineCreationRequest { + name: string; + color: LineColor; + disabled?: boolean; + translations?: LineTranslation[]; + appointmentSettings?: LineAppointmentSettings; +} diff --git a/packages/javascript-api/src/lib/model/line/line-translation.ts b/packages/javascript-api/src/lib/model/line/line-translation.ts new file mode 100644 index 00000000..05f3bd61 --- /dev/null +++ b/packages/javascript-api/src/lib/model/line/line-translation.ts @@ -0,0 +1,4 @@ +export interface LineTranslation { + languageCode: string; + name?: string; +} diff --git a/packages/javascript-api/src/lib/services/line/line.service.spec.ts b/packages/javascript-api/src/lib/services/line/line.service.spec.ts index 39de9e4e..d8c1644f 100644 --- a/packages/javascript-api/src/lib/services/line/line.service.spec.ts +++ b/packages/javascript-api/src/lib/services/line/line.service.spec.ts @@ -1,5 +1,7 @@ import * as sinon from 'sinon'; import { Line } from '../../model/line'; +import { LineCreationRequest } from '../../model/line/line-creation-request'; +import { ResponseValidationError } from '../../model/errors/response-validation-error'; import { Qminder } from '../../qminder'; import { LineService } from './line.service'; @@ -72,6 +74,81 @@ describe('Line service', function () { }); }); + describe('create()', function () { + const SUCCESSFUL_RESPONSE = { id: '12345' }; + + it('sends the request to the correct URL with JSON body and version headers', async function () { + requestStub.resolves(SUCCESSFUL_RESPONSE); + const request: LineCreationRequest = { + name: 'Priority Service', + color: 'TEAL', + }; + const result = await LineService.create(LOCATION_ID, request); + expect( + requestStub.calledWith(`locations/${LOCATION_ID}/lines`, { + method: 'POST', + body: JSON.stringify(request), + headers: { 'X-Qminder-API-Version': '2020-09-01' }, + }), + ).toBeTruthy(); + expect(result).toEqual(SUCCESSFUL_RESPONSE); + }); + + it('sends optional fields when provided', async function () { + requestStub.resolves(SUCCESSFUL_RESPONSE); + const request: LineCreationRequest = { + name: 'Priority Service', + color: 'VIOLET', + disabled: true, + translations: [{ languageCode: 'et', name: 'Eelisteenindus' }], + appointmentSettings: { enabled: true, duration: 30 }, + }; + await LineService.create(LOCATION_ID, request); + expect( + requestStub.calledWith(`locations/${LOCATION_ID}/lines`, { + method: 'POST', + body: JSON.stringify(request), + headers: { 'X-Qminder-API-Version': '2020-09-01' }, + }), + ).toBeTruthy(); + }); + + it('throws when response does not contain id', async function () { + requestStub.resolves({}); + const request: LineCreationRequest = { + name: 'Priority Service', + color: 'TEAL', + }; + await expect(LineService.create(LOCATION_ID, request)).rejects.toThrow( + new ResponseValidationError('Response does not contain "id"'), + ); + }); + + it('throws when location ID is missing', async function () { + await expect( + LineService.create(null as any, { name: 'Test', color: 'TEAL' }), + ).rejects.toThrow(); + }); + + it('throws when line parameter is missing', async function () { + await expect( + LineService.create(LOCATION_ID, null as any), + ).rejects.toThrow(); + }); + + it('throws when line name is missing', async function () { + await expect( + LineService.create(LOCATION_ID, { color: 'TEAL' } as any), + ).rejects.toThrow(); + }); + + it('throws when line color is missing', async function () { + await expect( + LineService.create(LOCATION_ID, { name: 'Test' } as any), + ).rejects.toThrow(); + }); + }); + describe('update()', function () { beforeEach(function (done) { requestStub.withArgs('v1/lines/71490').resolves({}); diff --git a/packages/javascript-api/src/lib/services/line/line.service.ts b/packages/javascript-api/src/lib/services/line/line.service.ts index 75178bc9..2992129e 100644 --- a/packages/javascript-api/src/lib/services/line/line.service.ts +++ b/packages/javascript-api/src/lib/services/line/line.service.ts @@ -58,20 +58,24 @@ export const LineService = { details, /** - * Create a new Line and return its details. + * Create a new Line and return its ID. * * Calls the following HTTP API: `POST /locations//lines` * * For example: * * ```javascript - * const line: Line = await Qminder.Line.create(950, { name: 'Priority Service' }); - * console.log(line.id); // 1425 + * const response = await Qminder.Line.create(950, { + * name: 'Priority Service', + * color: 'TEAL', + * translations: [{ languageCode: 'et', name: 'Eelisteenindus' }], + * appointmentSettings: { enabled: true, duration: 30 }, + * }); + * console.log(response.id); // "1425" * ``` * @param location the location to add the line under - * @param line the parameters of the new line - must include the line name - * @returns a Promise that resolves to a new Line object, created according - * to the parameters. + * @param line the parameters of the new line - must include name and color + * @returns a Promise that resolves to a LineCreatedResponse containing the new line's ID. */ create, diff --git a/packages/javascript-api/src/lib/services/line/line.ts b/packages/javascript-api/src/lib/services/line/line.ts index b167fdfb..27944c35 100644 --- a/packages/javascript-api/src/lib/services/line/line.ts +++ b/packages/javascript-api/src/lib/services/line/line.ts @@ -1,9 +1,12 @@ import { Line } from '../../model/line.js'; +import { LineCreatedResponse } from '../../model/line/line-created-response.js'; +import { LineCreationRequest } from '../../model/line/line-creation-request.js'; import { Location } from '../../model/location.js'; import { extractId, IdOrObject } from '../../util/id-or-object.js'; import { ApiBase, SuccessResponse } from '../api-base/api-base.js'; +import { V2_HEADERS } from '../v2-headers.js'; +import { ResponseValidationError } from '../../model/errors/response-validation-error.js'; -type LineCreateParameters = Partial> & Pick; type LineUpdateParameters = Pick & Partial>; @@ -25,10 +28,10 @@ export function details(line: IdOrObject): Promise { return ApiBase.request(`v1/lines/${lineId}/`); } -export function create( +export async function create( location: IdOrObject, - line: LineCreateParameters, -): Promise { + line: LineCreationRequest, +): Promise { const locationId = extractId(location); if (!locationId || typeof locationId !== 'string') { throw new Error('Location ID invalid or missing.'); @@ -39,10 +42,24 @@ export function create( if (!line.name || typeof line.name !== 'string') { throw new Error('Cannot create a line without a line name.'); } - return ApiBase.request(`v1/locations/${locationId}/lines`, { - body: line, - method: 'POST', - }) as Promise; + if (!line.color || typeof line.color !== 'string') { + throw new Error('Cannot create a line without a color.'); + } + + const result: LineCreatedResponse = await ApiBase.request( + `locations/${locationId}/lines`, + { + method: 'POST', + body: JSON.stringify(line), + headers: V2_HEADERS, + }, + ); + + if (!result.id) { + throw new ResponseValidationError('Response does not contain "id"'); + } + + return result; } export function update(line: LineUpdateParameters): Promise { diff --git a/packages/javascript-api/src/public-api/model.ts b/packages/javascript-api/src/public-api/model.ts index dca6a840..45a378a3 100644 --- a/packages/javascript-api/src/public-api/model.ts +++ b/packages/javascript-api/src/public-api/model.ts @@ -5,6 +5,11 @@ export { Desk } from '../lib/model/desk.js'; export { Device } from '../lib/model/device.js'; export { Id } from '../lib/model/id.js'; export { Line } from '../lib/model/line.js'; +export { LineColor } from '../lib/model/line/line-color.js'; +export { LineTranslation } from '../lib/model/line/line-translation.js'; +export { LineAppointmentSettings } from '../lib/model/line/line-appointment-settings.js'; +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 { TicketExtra } from '../lib/model/ticket/ticket-extra.js'; export { TicketLabel } from '../lib/model/ticket/ticket-label.js'; From 626c78793a55e34c964662f0f898964adf5f9605 Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Mon, 30 Mar 2026 15:20:38 +0300 Subject: [PATCH 2/4] fix: use French instead of Estonian in translation examples --- .../javascript-api/src/lib/services/line/line.service.spec.ts | 2 +- packages/javascript-api/src/lib/services/line/line.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/javascript-api/src/lib/services/line/line.service.spec.ts b/packages/javascript-api/src/lib/services/line/line.service.spec.ts index d8c1644f..2bdb0338 100644 --- a/packages/javascript-api/src/lib/services/line/line.service.spec.ts +++ b/packages/javascript-api/src/lib/services/line/line.service.spec.ts @@ -100,7 +100,7 @@ describe('Line service', function () { name: 'Priority Service', color: 'VIOLET', disabled: true, - translations: [{ languageCode: 'et', name: 'Eelisteenindus' }], + translations: [{ languageCode: 'fr', name: 'Service Prioritaire' }], appointmentSettings: { enabled: true, duration: 30 }, }; await LineService.create(LOCATION_ID, request); diff --git a/packages/javascript-api/src/lib/services/line/line.service.ts b/packages/javascript-api/src/lib/services/line/line.service.ts index 2992129e..b6ca6f05 100644 --- a/packages/javascript-api/src/lib/services/line/line.service.ts +++ b/packages/javascript-api/src/lib/services/line/line.service.ts @@ -68,7 +68,7 @@ export const LineService = { * const response = await Qminder.Line.create(950, { * name: 'Priority Service', * color: 'TEAL', - * translations: [{ languageCode: 'et', name: 'Eelisteenindus' }], + * translations: [{ languageCode: 'fr', name: 'Service Prioritaire' }], * appointmentSettings: { enabled: true, duration: 30 }, * }); * console.log(response.id); // "1425" From 9177803073cd94b2d411226755ce2d36e915665b Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Mon, 30 Mar 2026 15:22:09 +0300 Subject: [PATCH 3/4] refactor: remove trivial null-guard tests from create() --- .../lib/services/line/line.service.spec.ts | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/packages/javascript-api/src/lib/services/line/line.service.spec.ts b/packages/javascript-api/src/lib/services/line/line.service.spec.ts index 2bdb0338..79bd9f99 100644 --- a/packages/javascript-api/src/lib/services/line/line.service.spec.ts +++ b/packages/javascript-api/src/lib/services/line/line.service.spec.ts @@ -124,30 +124,7 @@ describe('Line service', function () { ); }); - it('throws when location ID is missing', async function () { - await expect( - LineService.create(null as any, { name: 'Test', color: 'TEAL' }), - ).rejects.toThrow(); - }); - - it('throws when line parameter is missing', async function () { - await expect( - LineService.create(LOCATION_ID, null as any), - ).rejects.toThrow(); - }); - - it('throws when line name is missing', async function () { - await expect( - LineService.create(LOCATION_ID, { color: 'TEAL' } as any), - ).rejects.toThrow(); - }); - - it('throws when line color is missing', async function () { - await expect( - LineService.create(LOCATION_ID, { name: 'Test' } as any), - ).rejects.toThrow(); - }); - }); +}); describe('update()', function () { beforeEach(function (done) { From 00b6e78fc1229c7c571a0302661687bb899c941b Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Mon, 30 Mar 2026 16:26:05 +0300 Subject: [PATCH 4/4] fix: format and improve create() test assertions --- .../lib/services/line/line.service.spec.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/javascript-api/src/lib/services/line/line.service.spec.ts b/packages/javascript-api/src/lib/services/line/line.service.spec.ts index 79bd9f99..a817167f 100644 --- a/packages/javascript-api/src/lib/services/line/line.service.spec.ts +++ b/packages/javascript-api/src/lib/services/line/line.service.spec.ts @@ -84,13 +84,14 @@ describe('Line service', function () { color: 'TEAL', }; const result = await LineService.create(LOCATION_ID, request); - expect( - requestStub.calledWith(`locations/${LOCATION_ID}/lines`, { + expect(requestStub.firstCall.args).toEqual([ + `locations/${LOCATION_ID}/lines`, + { method: 'POST', body: JSON.stringify(request), headers: { 'X-Qminder-API-Version': '2020-09-01' }, - }), - ).toBeTruthy(); + }, + ]); expect(result).toEqual(SUCCESSFUL_RESPONSE); }); @@ -104,13 +105,14 @@ describe('Line service', function () { appointmentSettings: { enabled: true, duration: 30 }, }; await LineService.create(LOCATION_ID, request); - expect( - requestStub.calledWith(`locations/${LOCATION_ID}/lines`, { + expect(requestStub.firstCall.args).toEqual([ + `locations/${LOCATION_ID}/lines`, + { method: 'POST', body: JSON.stringify(request), headers: { 'X-Qminder-API-Version': '2020-09-01' }, - }), - ).toBeTruthy(); + }, + ]); }); it('throws when response does not contain id', async function () { @@ -123,8 +125,7 @@ describe('Line service', function () { new ResponseValidationError('Response does not contain "id"'), ); }); - -}); + }); describe('update()', function () { beforeEach(function (done) {