@markwharton/eh-payroll 2.1.0 → 2.1.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.
package/README.md CHANGED
@@ -139,6 +139,8 @@ const client = new EHClient({
139
139
  });
140
140
  ```
141
141
 
142
+ `EHConfig` extends `ClientConfig` from api-core, which provides the `baseUrl`, `onRequest`, and `retry` fields. `apiKey`, `businessId`, `region`, `cache`, and `rateLimitPerSecond` are EH-specific.
143
+
142
144
  ### Cache TTLs
143
145
 
144
146
  | Cache Key | Default TTL |
@@ -154,7 +156,7 @@ const client = new EHClient({
154
156
 
155
157
  ### Rate Limiting
156
158
 
157
- The client enforces a sliding-window rate limit of 5 requests per second (the [API limit](https://api.keypay.com.au/australia/guides/Usage.html)). All outbound requests pass through the rate limiter automatically. Set `rateLimitPerSecond: 0` in config to disable (useful for tests).
159
+ The client enforces a sliding-window rate limit of 5 requests per second (the [API limit](https://api.keypay.com.au/australia/guides/Usage.html)). All outbound requests pass through the rate limiter automatically. Set `rateLimitPerSecond: 0` in config to disable (useful for tests). The `RateLimiter` class is provided by api-core and operates per-instance (see Known Limitations in the root README).
158
160
 
159
161
  ### Retry
160
162
 
package/dist/client.d.ts CHANGED
@@ -52,6 +52,18 @@ export declare class EHClient {
52
52
  * Fetch a URL and parse the response, with standardized error handling.
53
53
  */
54
54
  private fetchAndParse;
55
+ /**
56
+ * Fetch all pages of a paginated endpoint.
57
+ *
58
+ * Payroll API pagination uses either OData ($skip/$top) or PascalCase
59
+ * (CurrentPage/PageSize). The caller provides a buildUrl callback that
60
+ * receives the current skip offset and returns the full URL with
61
+ * pagination parameters set.
62
+ *
63
+ * Consumer always receives the complete array — pageSize controls
64
+ * the internal batch size (items per API call).
65
+ */
66
+ private fetchPaginated;
55
67
  /**
56
68
  * Validate the API key by calling GET /user
57
69
  */
package/dist/client.js CHANGED
@@ -8,9 +8,9 @@
8
8
  */
9
9
  import { AU_EMPLOYEE_OPERATIONAL_FIELDS, AU_EMPLOYEE_FIELDS, LOCATION_FIELDS, EMPLOYEE_GROUP_FIELDS, ROSTER_SHIFT_FIELDS, KIOSK_FIELDS, KIOSK_EMPLOYEE_FIELDS, } from './types.js';
10
10
  import { buildBasicAuthHeader } from './utils.js';
11
- import { parseEHErrorResponse } from './errors.js';
11
+ import { parseEHPayrollErrorResponse } from './errors.js';
12
12
  import { EH_API_BASE, EH_REGION_URLS } from './constants.js';
13
- import { TTLCache, pickFields, RateLimiter, getErrorMessage, fetchWithRetry, resolveRetryConfig, ok, err } from '@markwharton/api-core';
13
+ import { TTLCache, pickFields, RateLimiter, getErrorMessage, fetchWithRetry, resolveRetryConfig, ok, okVoid, err } from '@markwharton/api-core';
14
14
  /** Default page size for paginated endpoints */
15
15
  const DEFAULT_PAGE_SIZE = 100;
16
16
  // ============================================================================
@@ -114,7 +114,7 @@ export class EHClient {
114
114
  const response = await this.fetch(url, fetchOptions);
115
115
  if (!response.ok) {
116
116
  const errorText = await response.text();
117
- const { message } = parseEHErrorResponse(errorText, response.status);
117
+ const { message } = parseEHPayrollErrorResponse(errorText, response.status);
118
118
  return err(message, response.status);
119
119
  }
120
120
  return ok(await parse(response));
@@ -123,6 +123,42 @@ export class EHClient {
123
123
  return err(getErrorMessage(error), 0);
124
124
  }
125
125
  }
126
+ /**
127
+ * Fetch all pages of a paginated endpoint.
128
+ *
129
+ * Payroll API pagination uses either OData ($skip/$top) or PascalCase
130
+ * (CurrentPage/PageSize). The caller provides a buildUrl callback that
131
+ * receives the current skip offset and returns the full URL with
132
+ * pagination parameters set.
133
+ *
134
+ * Consumer always receives the complete array — pageSize controls
135
+ * the internal batch size (items per API call).
136
+ */
137
+ async fetchPaginated(buildUrl, fields, pageSize = DEFAULT_PAGE_SIZE) {
138
+ try {
139
+ const allItems = [];
140
+ let skip = 0;
141
+ while (true) {
142
+ const url = buildUrl(skip);
143
+ const response = await this.fetch(url);
144
+ if (!response.ok) {
145
+ const errorText = await response.text();
146
+ const { message } = parseEHPayrollErrorResponse(errorText, response.status);
147
+ return err(message, response.status);
148
+ }
149
+ const page = (await response.json())
150
+ .map(item => pickFields(item, fields));
151
+ allItems.push(...page);
152
+ if (page.length < pageSize)
153
+ break;
154
+ skip += pageSize;
155
+ }
156
+ return ok(allItems);
157
+ }
158
+ catch (error) {
159
+ return err(getErrorMessage(error), 0);
160
+ }
161
+ }
126
162
  // ============================================================================
127
163
  // Validation
128
164
  // ============================================================================
@@ -134,7 +170,7 @@ export class EHClient {
134
170
  try {
135
171
  const response = await this.fetch(url);
136
172
  if (response.ok) {
137
- return { ok: true };
173
+ return okVoid();
138
174
  }
139
175
  if (response.status === 401 || response.status === 403) {
140
176
  return err('Invalid or expired API key', response.status);
@@ -161,37 +197,17 @@ export class EHClient {
161
197
  if (options?.includePii)
162
198
  parts.push('pii');
163
199
  const cacheKey = parts.join(':');
164
- return this.cached(cacheKey, this.cacheTtl.employeesTtl, async () => {
200
+ return this.cached(cacheKey, this.cacheTtl.employeesTtl, () => {
165
201
  const params = new URLSearchParams();
166
202
  if (options?.payScheduleId != null)
167
203
  params.set('filter.payScheduleId', String(options.payScheduleId));
168
204
  if (options?.locationId != null)
169
205
  params.set('filter.locationId', String(options.locationId));
170
206
  params.set('$top', String(DEFAULT_PAGE_SIZE));
171
- try {
172
- const allEmployees = [];
173
- let skip = 0;
174
- while (true) {
175
- params.set('$skip', String(skip));
176
- const url = `${this.baseUrl}/business/${this.businessId}/employee/unstructured?${params}`;
177
- const response = await this.fetch(url);
178
- if (!response.ok) {
179
- const errorText = await response.text();
180
- const { message } = parseEHErrorResponse(errorText, response.status);
181
- return err(message, response.status);
182
- }
183
- const page = (await response.json())
184
- .map(item => pickFields(item, fields));
185
- allEmployees.push(...page);
186
- if (page.length < DEFAULT_PAGE_SIZE)
187
- break;
188
- skip += DEFAULT_PAGE_SIZE;
189
- }
190
- return ok(allEmployees);
191
- }
192
- catch (error) {
193
- return err(getErrorMessage(error), 0);
194
- }
207
+ return this.fetchPaginated((skip) => {
208
+ params.set('$skip', String(skip));
209
+ return `${this.baseUrl}/business/${this.businessId}/employee/unstructured?${params}`;
210
+ }, fields);
195
211
  });
196
212
  }
197
213
  /**
@@ -228,34 +244,12 @@ export class EHClient {
228
244
  * Get all business locations
229
245
  */
230
246
  async getLocations() {
231
- return this.cached('locations', this.cacheTtl.locationsTtl, async () => {
232
- const params = new URLSearchParams({
233
- '$top': String(DEFAULT_PAGE_SIZE),
234
- });
235
- try {
236
- const allLocations = [];
237
- let skip = 0;
238
- while (true) {
239
- params.set('$skip', String(skip));
240
- const url = `${this.baseUrl}/business/${this.businessId}/location?${params}`;
241
- const response = await this.fetch(url);
242
- if (!response.ok) {
243
- const errorText = await response.text();
244
- const { message } = parseEHErrorResponse(errorText, response.status);
245
- return err(message, response.status);
246
- }
247
- const page = (await response.json())
248
- .map(item => pickFields(item, LOCATION_FIELDS));
249
- allLocations.push(...page);
250
- if (page.length < DEFAULT_PAGE_SIZE)
251
- break;
252
- skip += DEFAULT_PAGE_SIZE;
253
- }
254
- return ok(allLocations);
255
- }
256
- catch (error) {
257
- return err(getErrorMessage(error), 0);
258
- }
247
+ return this.cached('locations', this.cacheTtl.locationsTtl, () => {
248
+ const params = new URLSearchParams({ '$top': String(DEFAULT_PAGE_SIZE) });
249
+ return this.fetchPaginated((skip) => {
250
+ params.set('$skip', String(skip));
251
+ return `${this.baseUrl}/business/${this.businessId}/location?${params}`;
252
+ }, LOCATION_FIELDS);
259
253
  });
260
254
  }
261
255
  // ============================================================================
@@ -265,34 +259,12 @@ export class EHClient {
265
259
  * Get all employee groups
266
260
  */
267
261
  async getEmployeeGroups() {
268
- return this.cached('employeeGroups', this.cacheTtl.groupsTtl, async () => {
269
- const params = new URLSearchParams({
270
- '$top': String(DEFAULT_PAGE_SIZE),
271
- });
272
- try {
273
- const allGroups = [];
274
- let skip = 0;
275
- while (true) {
276
- params.set('$skip', String(skip));
277
- const url = `${this.baseUrl}/business/${this.businessId}/employeegroup?${params}`;
278
- const response = await this.fetch(url);
279
- if (!response.ok) {
280
- const errorText = await response.text();
281
- const { message } = parseEHErrorResponse(errorText, response.status);
282
- return err(message, response.status);
283
- }
284
- const page = (await response.json())
285
- .map(item => pickFields(item, EMPLOYEE_GROUP_FIELDS));
286
- allGroups.push(...page);
287
- if (page.length < DEFAULT_PAGE_SIZE)
288
- break;
289
- skip += DEFAULT_PAGE_SIZE;
290
- }
291
- return ok(allGroups);
292
- }
293
- catch (error) {
294
- return err(getErrorMessage(error), 0);
295
- }
262
+ return this.cached('employeegroups', this.cacheTtl.groupsTtl, () => {
263
+ const params = new URLSearchParams({ '$top': String(DEFAULT_PAGE_SIZE) });
264
+ return this.fetchPaginated((skip) => {
265
+ params.set('$skip', String(skip));
266
+ return `${this.baseUrl}/business/${this.businessId}/employeegroup?${params}`;
267
+ }, EMPLOYEE_GROUP_FIELDS);
296
268
  });
297
269
  }
298
270
  // ============================================================================
@@ -332,7 +304,7 @@ export class EHClient {
332
304
  if (options?.includeWarnings)
333
305
  parts.push('iw');
334
306
  const cacheKey = parts.join(':');
335
- return this.cached(cacheKey, this.cacheTtl.rosterShiftsTtl, async () => {
307
+ return this.cached(cacheKey, this.cacheTtl.rosterShiftsTtl, () => {
336
308
  // Roster shift query params are PascalCase per the Swagger spec, unlike
337
309
  // all other endpoints which use camelCase. See: https://api.keypay.com.au/swagger-au.json
338
310
  const params = new URLSearchParams({
@@ -372,31 +344,10 @@ export class EHClient {
372
344
  params.set('ExcludeShiftsOverlappingFromDate', 'true');
373
345
  if (options?.includeWarnings)
374
346
  params.set('IncludeWarnings', 'true');
375
- try {
376
- const allShifts = [];
377
- let currentPage = 1;
378
- // Paginate using PageSize/CurrentPage until we get fewer than PageSize results
379
- while (true) {
380
- params.set('CurrentPage', String(currentPage));
381
- const url = `${this.baseUrl}/business/${this.businessId}/rostershift?${params}`;
382
- const response = await this.fetch(url);
383
- if (!response.ok) {
384
- const errorText = await response.text();
385
- const { message } = parseEHErrorResponse(errorText, response.status);
386
- return err(message, response.status);
387
- }
388
- const page = (await response.json())
389
- .map(item => pickFields(item, ROSTER_SHIFT_FIELDS));
390
- allShifts.push(...page);
391
- if (page.length < DEFAULT_PAGE_SIZE)
392
- break;
393
- currentPage++;
394
- }
395
- return ok(allShifts);
396
- }
397
- catch (error) {
398
- return err(getErrorMessage(error), 0);
399
- }
347
+ return this.fetchPaginated((skip) => {
348
+ params.set('CurrentPage', String(skip / DEFAULT_PAGE_SIZE + 1));
349
+ return `${this.baseUrl}/business/${this.businessId}/rostershift?${params}`;
350
+ }, ROSTER_SHIFT_FIELDS);
400
351
  });
401
352
  }
402
353
  // ============================================================================
@@ -406,34 +357,12 @@ export class EHClient {
406
357
  * Get all kiosks for the business
407
358
  */
408
359
  async getKiosks() {
409
- return this.cached('kiosks', this.cacheTtl.kiosksTtl, async () => {
410
- const params = new URLSearchParams({
411
- '$top': String(DEFAULT_PAGE_SIZE),
412
- });
413
- try {
414
- const allKiosks = [];
415
- let skip = 0;
416
- while (true) {
417
- params.set('$skip', String(skip));
418
- const url = `${this.baseUrl}/business/${this.businessId}/kiosk?${params}`;
419
- const response = await this.fetch(url);
420
- if (!response.ok) {
421
- const errorText = await response.text();
422
- const { message } = parseEHErrorResponse(errorText, response.status);
423
- return err(message, response.status);
424
- }
425
- const page = (await response.json())
426
- .map(item => pickFields(item, KIOSK_FIELDS));
427
- allKiosks.push(...page);
428
- if (page.length < DEFAULT_PAGE_SIZE)
429
- break;
430
- skip += DEFAULT_PAGE_SIZE;
431
- }
432
- return ok(allKiosks);
433
- }
434
- catch (error) {
435
- return err(getErrorMessage(error), 0);
436
- }
360
+ return this.cached('kiosks', this.cacheTtl.kiosksTtl, () => {
361
+ const params = new URLSearchParams({ '$top': String(DEFAULT_PAGE_SIZE) });
362
+ return this.fetchPaginated((skip) => {
363
+ params.set('$skip', String(skip));
364
+ return `${this.baseUrl}/business/${this.businessId}/kiosk?${params}`;
365
+ }, KIOSK_FIELDS);
437
366
  });
438
367
  }
439
368
  /**
package/dist/errors.d.ts CHANGED
@@ -11,7 +11,7 @@ import type { ParsedError } from '@markwharton/api-core';
11
11
  /**
12
12
  * Parsed EH error response
13
13
  */
14
- export type EHParsedError = ParsedError;
14
+ export type EHPayrollParsedError = ParsedError;
15
15
  /**
16
16
  * Parse EH API error response text into a human-readable message.
17
17
  *
@@ -19,19 +19,19 @@ export type EHParsedError = ParsedError;
19
19
  * JSON error formats with no API-specific extensions.
20
20
  *
21
21
  * @param errorText - Raw error response text
22
- * @param statusCode - HTTP status code
22
+ * @param status - HTTP status code
23
23
  * @returns Parsed error with message
24
24
  */
25
- export declare function parseEHErrorResponse(errorText: string, statusCode: number): EHParsedError;
25
+ export declare function parseEHPayrollErrorResponse(errorText: string, status: number): EHPayrollParsedError;
26
26
  /**
27
27
  * Custom error class for EH API errors
28
28
  */
29
- export declare class EHError extends ApiError {
29
+ export declare class EHPayrollError extends ApiError {
30
30
  constructor(message: string, status: number, options?: {
31
31
  rawResponse?: string;
32
32
  });
33
33
  /**
34
- * Create an EHError from an API response
34
+ * Create an EHPayrollError from an API response
35
35
  */
36
- static fromResponse(statusCode: number, responseText: string): EHError;
36
+ static fromResponse(status: number, responseText: string): EHPayrollError;
37
37
  }
package/dist/errors.js CHANGED
@@ -14,26 +14,26 @@ import { ApiError, parseJsonErrorResponse } from '@markwharton/api-core';
14
14
  * JSON error formats with no API-specific extensions.
15
15
  *
16
16
  * @param errorText - Raw error response text
17
- * @param statusCode - HTTP status code
17
+ * @param status - HTTP status code
18
18
  * @returns Parsed error with message
19
19
  */
20
- export function parseEHErrorResponse(errorText, statusCode) {
21
- return parseJsonErrorResponse(errorText, statusCode);
20
+ export function parseEHPayrollErrorResponse(errorText, status) {
21
+ return parseJsonErrorResponse(errorText, status);
22
22
  }
23
23
  /**
24
24
  * Custom error class for EH API errors
25
25
  */
26
- export class EHError extends ApiError {
26
+ export class EHPayrollError extends ApiError {
27
27
  constructor(message, status, options) {
28
28
  super(message, status, options);
29
- this.name = 'EHError';
29
+ this.name = 'EHPayrollError';
30
30
  }
31
31
  /**
32
- * Create an EHError from an API response
32
+ * Create an EHPayrollError from an API response
33
33
  */
34
- static fromResponse(statusCode, responseText) {
35
- const parsed = parseEHErrorResponse(responseText, statusCode);
36
- return new EHError(parsed.message, statusCode, {
34
+ static fromResponse(status, responseText) {
35
+ const parsed = parseEHPayrollErrorResponse(responseText, status);
36
+ return new EHPayrollError(parsed.message, status, {
37
37
  rawResponse: responseText,
38
38
  });
39
39
  }
package/dist/index.d.ts CHANGED
@@ -25,8 +25,8 @@ export { AU_EMPLOYEE_OPERATIONAL_FIELDS, AU_EMPLOYEE_PII_FIELDS, AU_EMPLOYEE_FIE
25
25
  export type { EHAuEmployee } from './employee-types.generated.js';
26
26
  export { buildBasicAuthHeader } from './utils.js';
27
27
  export { ok, err, getErrorMessage, pickFields, RateLimiter } from '@markwharton/api-core';
28
- export type { Result, RetryConfig, OnRequestCallback, BaseClientConfig } from '@markwharton/api-core';
28
+ export type { Result, RetryConfig, OnRequestCallback, ClientConfig } from '@markwharton/api-core';
29
29
  export { EH_API_BASE, EH_REGION_URLS } from './constants.js';
30
30
  export type { EHRegion } from './constants.js';
31
- export { EHError, parseEHErrorResponse } from './errors.js';
32
- export type { EHParsedError } from './errors.js';
31
+ export { EHPayrollError, parseEHPayrollErrorResponse } from './errors.js';
32
+ export type { EHPayrollParsedError } from './errors.js';
package/dist/index.js CHANGED
@@ -30,4 +30,4 @@ export { ok, err, getErrorMessage, pickFields, RateLimiter } from '@markwharton/
30
30
  // Constants
31
31
  export { EH_API_BASE, EH_REGION_URLS } from './constants.js';
32
32
  // Errors
33
- export { EHError, parseEHErrorResponse } from './errors.js';
33
+ export { EHPayrollError, parseEHPayrollErrorResponse } from './errors.js';
package/dist/types.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Based on the API reference and KeyPay .NET SDK models.
6
6
  */
7
7
  import type { EHRegion } from './constants.js';
8
- import type { RetryConfig, BaseClientConfig } from '@markwharton/api-core';
8
+ import type { RetryConfig, ClientConfig } from '@markwharton/api-core';
9
9
  /**
10
10
  * Cache configuration for EHClient
11
11
  *
@@ -35,7 +35,7 @@ export type EHRetryConfig = RetryConfig;
35
35
  /**
36
36
  * Employment Hero Payroll configuration for API access
37
37
  */
38
- export interface EHConfig extends BaseClientConfig {
38
+ export interface EHConfig extends ClientConfig {
39
39
  /** API key for authentication (used as Basic Auth username) */
40
40
  apiKey: string;
41
41
  /** Business ID to operate on */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/eh-payroll",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Employment Hero Payroll API client",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",