@markwharton/eh-payroll 2.2.0 → 2.3.0

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
@@ -43,6 +43,14 @@ const pubResult = await client.getRosterShifts('2026-02-03', '2026-02-09', {
43
43
  });
44
44
  if (pubResult.ok) console.log(pubResult.data); // EHRosterShift[]
45
45
 
46
+ // Get leave requests for a date range (all filters server-side)
47
+ const leaveResult = await client.getLeaveRequests({
48
+ fromDate: '2026-01-01',
49
+ toDate: '2026-03-31',
50
+ status: 'Approved',
51
+ });
52
+ if (leaveResult.ok) console.log(leaveResult.data); // EHLeaveRequest[]
53
+
46
54
  // Get business locations
47
55
  const locResult = await client.getLocations();
48
56
  if (locResult.ok) console.log(locResult.data); // EHLocation[]
@@ -110,15 +118,35 @@ const employees = result.data;
110
118
  | `getKiosks()` | — | `Result<EHKiosk[]>` |
111
119
  | `getKioskStaff(kioskId, options?)` | `number, EHKioskStaffOptions?` | `Result<EHKioskEmployee[]>` |
112
120
  | `getReportFields()` | — | `Result<EHReportField[]>` |
121
+ | `getLeaveRequests(options?)` | `EHLeaveRequestOptions?` | `Result<EHLeaveRequest[]>` |
113
122
  | `getEmployeeDetailsReport(options?)` | `EHEmployeeDetailsReportOptions?` | `Result<Record<string, unknown>[]>` |
114
123
 
124
+ ### `getLeaveRequests()`
125
+
126
+ Returns leave requests with all filters applied server-side. Unlike paginated endpoints, this returns a flat array.
127
+
128
+ **`EHLeaveRequestOptions`:**
129
+
130
+ | Option | Type | Description |
131
+ |--------|------|-------------|
132
+ | `fromDate` | `string` | Start date filter (YYYY-MM-DD) |
133
+ | `toDate` | `string` | End date filter (YYYY-MM-DD) |
134
+ | `status` | `EHLeaveRequestStatus` | `'Approved'`, `'Pending'`, `'Rejected'`, or `'Cancelled'` |
135
+ | `employeeId` | `number` | Filter by employee ID |
136
+ | `leaveCategoryId` | `number` | Filter by leave category ID |
137
+ | `locationId` | `number` | Filter by location ID |
138
+
139
+ ### `getEmployeeDetailsReport()`
140
+
141
+ Returns all employees with the requested columns in a single API call. The response is dynamic — field keys depend on `selectedColumns`. Results are cached with `employeeDetailsReportTtl` (default: 2 min).
142
+
115
143
  ### `getRosterShifts()`
116
144
 
117
145
  Automatically paginates through all results (100 per page). Returns the complete set of roster shifts for the requested date range. All filter options are applied server-side, reducing the number of results and pages fetched.
118
146
 
119
147
  ### Query Parameter Casing
120
148
 
121
- The [KeyPay Swagger spec](https://api.keypay.com.au/swagger-au.json) uses inconsistent query parameter casing across endpoints. Roster shifts use PascalCase (`EmployeeId`, `ShiftStatus`), while all other endpoints use camelCase (`selectedColumns`, `filter.locationId`). The library accepts camelCase options throughout and converts to PascalCase for roster shifts internally.
149
+ The [KeyPay Swagger spec](https://api.keypay.com.au/swagger-au.json) uses inconsistent query parameter casing across endpoints. Roster shifts and leave requests use PascalCase (`EmployeeId`, `ShiftStatus`, `FromDate`), while other endpoints use camelCase (`selectedColumns`, `filter.locationId`). The library accepts camelCase options throughout and converts to PascalCase internally where needed.
122
150
 
123
151
  ## Configuration
124
152
 
@@ -151,9 +179,11 @@ const client = new EHClient({
151
179
  | Employee groups | 5 min |
152
180
  | Standard hours | 5 min |
153
181
  | Roster shifts | 2 min |
182
+ | Leave requests | 2 min |
154
183
  | Kiosks | 5 min |
155
184
  | Kiosk staff | 1 min |
156
185
  | Report fields | 10 min |
186
+ | Employee details report | 2 min |
157
187
 
158
188
  Failed API results (`ok: false`) are never cached — transient errors won't persist for the full TTL. See the [root README Cache System section](../../README.md#cache-system) for the full cache architecture (layered stores, PII handling, request coalescing).
159
189
 
package/dist/client.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @see https://api.keypay.com.au/
8
8
  */
9
- import type { EHConfig, EHEmployeeOptions, EHSingleEmployeeOptions, EHStandardHours, EHLocation, EHEmployeeGroup, EHRosterShift, EHRosterShiftOptions, EHKiosk, EHKioskEmployee, EHKioskStaffOptions, EHReportField, EHEmployeeDetailsReportOptions } from './types.js';
9
+ import type { EHConfig, EHLeaveRequest, EHLeaveRequestOptions, EHEmployeeOptions, EHSingleEmployeeOptions, EHStandardHours, EHLocation, EHEmployeeGroup, EHRosterShift, EHRosterShiftOptions, EHKiosk, EHKioskEmployee, EHKioskStaffOptions, EHReportField, EHEmployeeDetailsReportOptions } from './types.js';
10
10
  import type { EHAuEmployee } from './employee-types.generated.js';
11
11
  import type { Result } from '@markwharton/api-core';
12
12
  /**
@@ -43,6 +43,10 @@ export declare class EHClient {
43
43
  * Clear all cached API responses.
44
44
  */
45
45
  clearCache(): void;
46
+ /**
47
+ * Build a URL for a business-scoped endpoint.
48
+ */
49
+ private businessUrl;
46
50
  /**
47
51
  * Make an authenticated request to the EH API
48
52
  *
@@ -70,6 +74,15 @@ export declare class EHClient {
70
74
  * Validate the API key by calling GET /user
71
75
  */
72
76
  validateApiKey(): Promise<Result<void>>;
77
+ /**
78
+ * Get leave requests (server-side filtering)
79
+ *
80
+ * All filter options are passed as query parameters to the API.
81
+ * The endpoint returns a flat array (not paginated).
82
+ *
83
+ * @see https://api.keypay.com.au/australia/reference/leave-requests/au-business-hours-leave-request--list-leave-requests.html
84
+ */
85
+ getLeaveRequests(options?: EHLeaveRequestOptions): Promise<Result<EHLeaveRequest[]>>;
73
86
  /**
74
87
  * Get all employees (unstructured format)
75
88
  */
package/dist/client.js CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @see https://api.keypay.com.au/
8
8
  */
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';
9
+ import { AU_EMPLOYEE_OPERATIONAL_FIELDS, AU_EMPLOYEE_FIELDS, LEAVE_REQUEST_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
11
  import { parseEHPayrollErrorResponse } from './errors.js';
12
12
  import { EH_API_BASE, EH_REGION_URLS } from './constants.js';
@@ -42,6 +42,7 @@ export class EHClient {
42
42
  }
43
43
  this.cacheTtl = {
44
44
  employeesTtl: config.cache?.employeesTtl ?? 300000,
45
+ leaveRequestsTtl: config.cache?.leaveRequestsTtl ?? 120000,
45
46
  locationsTtl: config.cache?.locationsTtl ?? 300000,
46
47
  groupsTtl: config.cache?.groupsTtl ?? 300000,
47
48
  standardHoursTtl: config.cache?.standardHoursTtl ?? 300000,
@@ -49,6 +50,7 @@ export class EHClient {
49
50
  kiosksTtl: config.cache?.kiosksTtl ?? 300000,
50
51
  kioskStaffTtl: config.cache?.kioskStaffTtl ?? 60000,
51
52
  reportFieldsTtl: config.cache?.reportFieldsTtl ?? 600000,
53
+ employeeDetailsReportTtl: config.cache?.employeeDetailsReportTtl ?? 120000,
52
54
  };
53
55
  // Initialize retry config with defaults if provided
54
56
  this.retryConfig = resolveRetryConfig(config.retry);
@@ -77,6 +79,14 @@ export class EHClient {
77
79
  clearCache() {
78
80
  this.cache?.clear();
79
81
  }
82
+ /**
83
+ * Build a URL for a business-scoped endpoint.
84
+ */
85
+ businessUrl(path, params) {
86
+ const base = `${this.baseUrl}/business/${this.businessId}/${path}`;
87
+ const qs = params?.toString();
88
+ return qs ? `${base}?${qs}` : base;
89
+ }
80
90
  /**
81
91
  * Make an authenticated request to the EH API
82
92
  *
@@ -186,6 +196,53 @@ export class EHClient {
186
196
  }
187
197
  }
188
198
  // ============================================================================
199
+ // Leave Requests
200
+ // ============================================================================
201
+ /**
202
+ * Get leave requests (server-side filtering)
203
+ *
204
+ * All filter options are passed as query parameters to the API.
205
+ * The endpoint returns a flat array (not paginated).
206
+ *
207
+ * @see https://api.keypay.com.au/australia/reference/leave-requests/au-business-hours-leave-request--list-leave-requests.html
208
+ */
209
+ async getLeaveRequests(options) {
210
+ const parts = ['leaverequests'];
211
+ if (options?.fromDate)
212
+ parts.push(`from:${options.fromDate}`);
213
+ if (options?.toDate)
214
+ parts.push(`to:${options.toDate}`);
215
+ if (options?.status)
216
+ parts.push(`s:${options.status}`);
217
+ if (options?.employeeId != null)
218
+ parts.push(`eid:${options.employeeId}`);
219
+ if (options?.leaveCategoryId != null)
220
+ parts.push(`lcid:${options.leaveCategoryId}`);
221
+ if (options?.locationId != null)
222
+ parts.push(`lid:${options.locationId}`);
223
+ const cacheKey = parts.join(':');
224
+ return this.cached(cacheKey, this.cacheTtl.leaveRequestsTtl, async () => {
225
+ const params = new URLSearchParams();
226
+ if (options?.fromDate)
227
+ params.set('FromDate', options.fromDate);
228
+ if (options?.toDate)
229
+ params.set('ToDate', options.toDate);
230
+ if (options?.status)
231
+ params.set('Status', options.status);
232
+ if (options?.employeeId != null)
233
+ params.set('EmployeeId', String(options.employeeId));
234
+ if (options?.leaveCategoryId != null)
235
+ params.set('LeaveCategoryId', String(options.leaveCategoryId));
236
+ if (options?.locationId != null)
237
+ params.set('LocationId', String(options.locationId));
238
+ const url = this.businessUrl('leaverequest', params);
239
+ return this.fetchAndParse(url, async (r) => {
240
+ return (await r.json())
241
+ .map(item => pickFields(item, LEAVE_REQUEST_FIELDS));
242
+ });
243
+ });
244
+ }
245
+ // ============================================================================
189
246
  // Employees
190
247
  // ============================================================================
191
248
  /**
@@ -386,10 +443,7 @@ export class EHClient {
386
443
  if (options?.restrictCurrentShiftsToCurrentKioskLocation) {
387
444
  params.set('restrictCurrentShiftsToCurrentKioskLocation', 'true');
388
445
  }
389
- const queryString = params.toString();
390
- const url = queryString
391
- ? `${this.baseUrl}/business/${this.businessId}/kiosk/${kioskId}/staff?${queryString}`
392
- : `${this.baseUrl}/business/${this.businessId}/kiosk/${kioskId}/staff`;
446
+ const url = this.businessUrl(`kiosk/${kioskId}/staff`, params);
393
447
  return this.fetchAndParse(url, async (r) => {
394
448
  return (await r.json())
395
449
  .map(item => pickFields(item, KIOSK_EMPLOYEE_FIELDS));
@@ -420,26 +474,37 @@ export class EHClient {
420
474
  * The response is dynamic (JObject[]) — field keys depend on selectedColumns.
421
475
  */
422
476
  async getEmployeeDetailsReport(options) {
423
- const params = new URLSearchParams();
424
- if (options?.selectedColumns) {
425
- for (const col of options.selectedColumns) {
426
- params.append('selectedColumns', col);
427
- }
428
- }
477
+ const parts = ['report:employeedetails'];
478
+ if (options?.selectedColumns?.length)
479
+ parts.push(`cols:${options.selectedColumns.join(',')}`);
429
480
  if (options?.locationId != null)
430
- params.set('locationId', String(options.locationId));
481
+ parts.push(`loc:${options.locationId}`);
431
482
  if (options?.employingEntityId != null)
432
- params.set('employingEntityId', String(options.employingEntityId));
483
+ parts.push(`ee:${options.employingEntityId}`);
433
484
  if (options?.includeActive != null)
434
- params.set('includeActive', String(options.includeActive));
485
+ parts.push(`a:${options.includeActive}`);
435
486
  if (options?.includeInactive != null)
436
- params.set('includeInactive', String(options.includeInactive));
437
- const queryString = params.toString();
438
- const url = queryString
439
- ? `${this.baseUrl}/business/${this.businessId}/report/employeedetails?${queryString}`
440
- : `${this.baseUrl}/business/${this.businessId}/report/employeedetails`;
441
- return this.fetchAndParse(url, async (r) => {
442
- return await r.json();
487
+ parts.push(`i:${options.includeInactive}`);
488
+ const cacheKey = parts.join(':');
489
+ return this.cached(cacheKey, this.cacheTtl.employeeDetailsReportTtl, async () => {
490
+ const params = new URLSearchParams();
491
+ if (options?.selectedColumns) {
492
+ for (const col of options.selectedColumns) {
493
+ params.append('selectedColumns', col);
494
+ }
495
+ }
496
+ if (options?.locationId != null)
497
+ params.set('locationId', String(options.locationId));
498
+ if (options?.employingEntityId != null)
499
+ params.set('employingEntityId', String(options.employingEntityId));
500
+ if (options?.includeActive != null)
501
+ params.set('includeActive', String(options.includeActive));
502
+ if (options?.includeInactive != null)
503
+ params.set('includeInactive', String(options.includeInactive));
504
+ const url = this.businessUrl('report/employeedetails', params);
505
+ return this.fetchAndParse(url, async (r) => {
506
+ return await r.json();
507
+ });
443
508
  });
444
509
  }
445
510
  }
package/dist/index.d.ts CHANGED
@@ -20,8 +20,8 @@
20
20
  * ```
21
21
  */
22
22
  export { EHClient } from './client.js';
23
- export type { EHConfig, EHCacheConfig, EHRetryConfig, EHEmployee, EHEmployeeOptions, EHSingleEmployeeOptions, EHStandardHours, EHLocation, EHEmployeeGroup, EHRosterShift, EHRosterShiftOptions, EHAttendanceStatus, EHKiosk, EHKioskEmployee, EHKioskStaffOptions, EHReportField, EHEmployeeDetailsReportOptions, } from './types.js';
24
- export { AU_EMPLOYEE_OPERATIONAL_FIELDS, AU_EMPLOYEE_PII_FIELDS, AU_EMPLOYEE_FIELDS, LOCATION_FIELDS, EMPLOYEE_GROUP_FIELDS, ROSTER_SHIFT_FIELDS, KIOSK_FIELDS, KIOSK_EMPLOYEE_FIELDS, } from './types.js';
23
+ export type { EHConfig, EHCacheConfig, EHRetryConfig, EHLeaveRequest, EHLeaveRequestOptions, EHLeaveRequestStatus, EHEmployee, EHEmployeeOptions, EHSingleEmployeeOptions, EHStandardHours, EHLocation, EHEmployeeGroup, EHRosterShift, EHRosterShiftOptions, EHAttendanceStatus, EHKiosk, EHKioskEmployee, EHKioskStaffOptions, EHReportField, EHEmployeeDetailsReportOptions, } from './types.js';
24
+ export { AU_EMPLOYEE_OPERATIONAL_FIELDS, AU_EMPLOYEE_PII_FIELDS, AU_EMPLOYEE_FIELDS, LEAVE_REQUEST_FIELDS, LOCATION_FIELDS, EMPLOYEE_GROUP_FIELDS, ROSTER_SHIFT_FIELDS, KIOSK_FIELDS, KIOSK_EMPLOYEE_FIELDS, } from './types.js';
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, TTLCache, MemoryCacheStore, LayeredCache } from '@markwharton/api-core';
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@
22
22
  // Main client
23
23
  export { EHClient } from './client.js';
24
24
  // Field key constants (whitelists for pickFields)
25
- export { AU_EMPLOYEE_OPERATIONAL_FIELDS, AU_EMPLOYEE_PII_FIELDS, AU_EMPLOYEE_FIELDS, LOCATION_FIELDS, EMPLOYEE_GROUP_FIELDS, ROSTER_SHIFT_FIELDS, KIOSK_FIELDS, KIOSK_EMPLOYEE_FIELDS, } from './types.js';
25
+ export { AU_EMPLOYEE_OPERATIONAL_FIELDS, AU_EMPLOYEE_PII_FIELDS, AU_EMPLOYEE_FIELDS, LEAVE_REQUEST_FIELDS, LOCATION_FIELDS, EMPLOYEE_GROUP_FIELDS, ROSTER_SHIFT_FIELDS, KIOSK_FIELDS, KIOSK_EMPLOYEE_FIELDS, } from './types.js';
26
26
  // Utilities
27
27
  export { buildBasicAuthHeader } from './utils.js';
28
28
  // Re-exported from @markwharton/api-core
package/dist/types.d.ts CHANGED
@@ -15,6 +15,8 @@ import type { RetryConfig, ClientConfig } from '@markwharton/api-core';
15
15
  export interface EHCacheConfig {
16
16
  /** TTL for employee list (default: 300000 = 5 min) */
17
17
  employeesTtl?: number;
18
+ /** TTL for leave requests (default: 120000 = 2 min) */
19
+ leaveRequestsTtl?: number;
18
20
  /** TTL for locations (default: 300000 = 5 min) */
19
21
  locationsTtl?: number;
20
22
  /** TTL for employee groups (default: 300000 = 5 min) */
@@ -29,6 +31,8 @@ export interface EHCacheConfig {
29
31
  kioskStaffTtl?: number;
30
32
  /** TTL for report fields (default: 600000 = 10 min) */
31
33
  reportFieldsTtl?: number;
34
+ /** TTL for employee details report (default: 120000 = 2 min) */
35
+ employeeDetailsReportTtl?: number;
32
36
  }
33
37
  /** @deprecated Use `RetryConfig` from `@markwharton/api-core` directly. */
34
38
  export type EHRetryConfig = RetryConfig;
@@ -172,6 +176,57 @@ export interface EHKioskEmployee {
172
176
  /** Last recorded time in UTC */
173
177
  recordedTimeUtc: string | null;
174
178
  }
179
+ /**
180
+ * Leave request from the Payroll API
181
+ *
182
+ * From GET /business/{id}/leaverequest
183
+ * Field whitelist from Swagger HourLeaveRequestResponseModel.
184
+ */
185
+ export interface EHLeaveRequest {
186
+ /** Leave request ID */
187
+ id: number;
188
+ /** Employee ID */
189
+ employeeId: number;
190
+ /** Leave category ID */
191
+ leaveCategoryId: number;
192
+ /** Employee display name */
193
+ employee: string;
194
+ /** Leave category name */
195
+ leaveCategory: string;
196
+ /** Start date (ISO 8601 datetime) */
197
+ fromDate: string;
198
+ /** End date (ISO 8601 datetime) */
199
+ toDate: string;
200
+ /** Total hours requested */
201
+ totalHours: number;
202
+ /** Hours actually applied */
203
+ hoursApplied: number;
204
+ /** Notes on the leave request */
205
+ notes: string | null;
206
+ /** Status: Approved, Pending, Rejected, Cancelled */
207
+ status: string;
208
+ }
209
+ /** Leave request status values */
210
+ export type EHLeaveRequestStatus = 'Approved' | 'Pending' | 'Rejected' | 'Cancelled';
211
+ /**
212
+ * Options for getLeaveRequests
213
+ *
214
+ * All filters are passed server-side to the API.
215
+ */
216
+ export interface EHLeaveRequestOptions {
217
+ /** Filter by start date (ISO 8601 date-time → API FromDate) */
218
+ fromDate?: string;
219
+ /** Filter by end date (ISO 8601 date-time → API ToDate) */
220
+ toDate?: string;
221
+ /** Filter by status (→ API Status) */
222
+ status?: EHLeaveRequestStatus;
223
+ /** Filter by employee ID (→ API EmployeeId) */
224
+ employeeId?: number;
225
+ /** Filter by leave category ID (→ API LeaveCategoryId) */
226
+ leaveCategoryId?: number;
227
+ /** Filter by location ID (→ API LocationId) */
228
+ locationId?: number;
229
+ }
175
230
  /**
176
231
  * Options for getEmployees
177
232
  */
@@ -291,6 +346,8 @@ export interface EHEmployeeDetailsReportOptions {
291
346
  /** Include inactive employees (default: false) */
292
347
  includeInactive?: boolean;
293
348
  }
349
+ /** Whitelisted fields for EHLeaveRequest */
350
+ export declare const LEAVE_REQUEST_FIELDS: readonly ["id", "employeeId", "leaveCategoryId", "employee", "leaveCategory", "fromDate", "toDate", "totalHours", "hoursApplied", "notes", "status"];
294
351
  /** Whitelisted fields for EHLocation */
295
352
  export declare const LOCATION_FIELDS: readonly ["id", "parentId", "name", "externalId", "source", "fullyQualifiedName", "isGlobal", "state", "country"];
296
353
  /** Whitelisted fields for EHKiosk */
package/dist/types.js CHANGED
@@ -7,6 +7,11 @@
7
7
  // ============================================================================
8
8
  // Field Key Constants (whitelists for pickFields)
9
9
  // ============================================================================
10
+ /** Whitelisted fields for EHLeaveRequest */
11
+ export const LEAVE_REQUEST_FIELDS = [
12
+ 'id', 'employeeId', 'leaveCategoryId', 'employee', 'leaveCategory',
13
+ 'fromDate', 'toDate', 'totalHours', 'hoursApplied', 'notes', 'status',
14
+ ];
10
15
  /** Whitelisted fields for EHLocation */
11
16
  export const LOCATION_FIELDS = [
12
17
  'id', 'parentId', 'name', 'externalId', 'source',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/eh-payroll",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Employment Hero Payroll API client",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",