@markwharton/eh-payroll 1.0.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 +126 -0
- package/dist/cache.d.ts +38 -0
- package/dist/cache.js +73 -0
- package/dist/client.d.ts +130 -0
- package/dist/client.js +396 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/employee-types.generated.d.ts +153 -0
- package/dist/employee-types.generated.js +9 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +52 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +32 -0
- package/dist/rate-limiter.d.ts +33 -0
- package/dist/rate-limiter.js +63 -0
- package/dist/types.d.ts +241 -0
- package/dist/types.js +7 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +14 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @markwharton/eh-payroll
|
|
2
|
+
|
|
3
|
+
Employment Hero Payroll API client for employee data, roster shifts, and time & attendance.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @markwharton/eh-payroll
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { EHClient } from '@markwharton/eh-payroll';
|
|
15
|
+
|
|
16
|
+
const client = new EHClient({
|
|
17
|
+
apiKey: 'xxx',
|
|
18
|
+
businessId: 123,
|
|
19
|
+
region: 'au', // 'au' | 'nz' | 'uk' | 'sg' | 'my' (default: 'au')
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Validate credentials
|
|
23
|
+
await client.validateApiKey();
|
|
24
|
+
|
|
25
|
+
// Get all employees
|
|
26
|
+
const { employees } = await client.getEmployees();
|
|
27
|
+
|
|
28
|
+
// Get employees filtered by location
|
|
29
|
+
const { employees: filtered } = await client.getEmployees({ locationId: 5 });
|
|
30
|
+
|
|
31
|
+
// Get roster shifts for a date range (auto-paginates)
|
|
32
|
+
const { shifts } = await client.getRosterShifts('2026-02-03', '2026-02-09');
|
|
33
|
+
|
|
34
|
+
// Get roster shifts with filters
|
|
35
|
+
const { shifts: published } = await client.getRosterShifts('2026-02-03', '2026-02-09', {
|
|
36
|
+
employeeId: 42,
|
|
37
|
+
shiftStatus: 'Published',
|
|
38
|
+
selectAllRoles: true
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Get kiosk staff (time and attendance)
|
|
42
|
+
const { staff } = await client.getKioskStaff(kioskId);
|
|
43
|
+
|
|
44
|
+
// Get employee groups
|
|
45
|
+
const { groups } = await client.getEmployeeGroups();
|
|
46
|
+
|
|
47
|
+
// Get standard hours for an employee (includes FTE value)
|
|
48
|
+
const { standardHours } = await client.getStandardHours(employeeId);
|
|
49
|
+
|
|
50
|
+
// Discover available report columns
|
|
51
|
+
const { fields } = await client.getReportFields();
|
|
52
|
+
|
|
53
|
+
// Run Employee Details Report with selected columns
|
|
54
|
+
const { records } = await client.getEmployeeDetailsReport({
|
|
55
|
+
selectedColumns: ['FirstName', 'Surname', 'ExternalId']
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API Reference
|
|
60
|
+
|
|
61
|
+
All methods return `{ data?, error? }` result objects rather than throwing exceptions.
|
|
62
|
+
|
|
63
|
+
| Method | Parameters | Returns |
|
|
64
|
+
|--------|-----------|---------|
|
|
65
|
+
| `validateApiKey()` | — | `{ valid, error? }` |
|
|
66
|
+
| `getEmployees(options?)` | `EHEmployeeOptions?` | `{ employees?, error? }` |
|
|
67
|
+
| `getEmployee(employeeId)` | `number` | `{ employee?, error? }` |
|
|
68
|
+
| `getStandardHours(employeeId)` | `number` | `{ standardHours?, error? }` |
|
|
69
|
+
| `getEmployeeGroups()` | — | `{ groups?, error? }` |
|
|
70
|
+
| `getRosterShifts(from, to, options?)` | `string, string, EHRosterShiftOptions?` | `{ shifts?, error? }` |
|
|
71
|
+
| `getKioskStaff(kioskId, options?)` | `number, EHKioskStaffOptions?` | `{ staff?, error? }` |
|
|
72
|
+
| `getReportFields()` | — | `{ fields?, error? }` |
|
|
73
|
+
| `getEmployeeDetailsReport(options?)` | `EHEmployeeDetailsReportOptions?` | `{ records?, error? }` |
|
|
74
|
+
|
|
75
|
+
### `getRosterShifts()`
|
|
76
|
+
|
|
77
|
+
Automatically paginates through all results (100 per page). Returns the complete set of shifts for the requested date range. All filter options are applied server-side, reducing the number of results and pages fetched.
|
|
78
|
+
|
|
79
|
+
### Query Parameter Casing
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const client = new EHClient({
|
|
87
|
+
apiKey: 'your-api-key', // Required: used as Basic Auth username
|
|
88
|
+
businessId: 12345, // Required: business ID
|
|
89
|
+
region: 'au', // Optional: au, nz, uk, sg, my (default: au)
|
|
90
|
+
baseUrl: '...', // Optional: override base URL (takes priority over region)
|
|
91
|
+
rateLimitPerSecond: 5, // Optional: rate limit (default 5, set 0 to disable)
|
|
92
|
+
onRequest: ({ method, url, description }) => { ... }, // Optional: debug callback
|
|
93
|
+
cache: {}, // Optional: enable TTL caching (defaults below)
|
|
94
|
+
retry: { // Optional: retry on 429/503
|
|
95
|
+
maxRetries: 3,
|
|
96
|
+
initialDelayMs: 1000,
|
|
97
|
+
maxDelayMs: 10000,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Cache TTLs
|
|
103
|
+
|
|
104
|
+
| Cache Key | Default TTL |
|
|
105
|
+
|-----------|-------------|
|
|
106
|
+
| Employees | 5 min |
|
|
107
|
+
| Employee groups | 5 min |
|
|
108
|
+
| Standard hours | 5 min |
|
|
109
|
+
| Roster shifts | 2 min |
|
|
110
|
+
| Report fields | 10 min |
|
|
111
|
+
|
|
112
|
+
### Rate Limiting
|
|
113
|
+
|
|
114
|
+
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).
|
|
115
|
+
|
|
116
|
+
### Retry
|
|
117
|
+
|
|
118
|
+
Automatically retries on HTTP 429 (Too Many Requests) and 503 (Service Unavailable) with exponential backoff. Respects the `Retry-After` header when present.
|
|
119
|
+
|
|
120
|
+
## Authentication
|
|
121
|
+
|
|
122
|
+
Uses HTTP Basic Authentication with the API key as the username and an empty password, per the [KeyPay API reference](https://api.keypay.com.au/).
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple in-memory TTL cache with request coalescing
|
|
3
|
+
*
|
|
4
|
+
* Provides per-instance memoization for EHClient API responses.
|
|
5
|
+
* In serverless environments (Azure Functions, Static Web Apps),
|
|
6
|
+
* module-level state persists across warm invocations within the
|
|
7
|
+
* same instance — this cache leverages that behavior.
|
|
8
|
+
*
|
|
9
|
+
* Request coalescing: when multiple concurrent callers request the
|
|
10
|
+
* same expired key, only one factory call is made. All callers
|
|
11
|
+
* receive the same resolved value (or the same rejection).
|
|
12
|
+
*
|
|
13
|
+
* Not a distributed cache: each instance has its own cache.
|
|
14
|
+
* Cold starts and instance recycling naturally clear stale data.
|
|
15
|
+
*/
|
|
16
|
+
export declare class TTLCache {
|
|
17
|
+
private store;
|
|
18
|
+
private inflight;
|
|
19
|
+
/**
|
|
20
|
+
* Get a cached value, or call the factory to populate it.
|
|
21
|
+
*
|
|
22
|
+
* If a factory call is already in progress for this key,
|
|
23
|
+
* returns the existing promise instead of starting a duplicate.
|
|
24
|
+
*
|
|
25
|
+
* @param key - Cache key
|
|
26
|
+
* @param ttlMs - Time-to-live in milliseconds
|
|
27
|
+
* @param factory - Async function to produce the value on cache miss
|
|
28
|
+
*/
|
|
29
|
+
get<T>(key: string, ttlMs: number, factory: () => Promise<T>): Promise<T>;
|
|
30
|
+
/**
|
|
31
|
+
* Invalidate cache entries matching a key prefix.
|
|
32
|
+
*/
|
|
33
|
+
invalidate(prefix: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Clear all cached data and in-flight requests.
|
|
36
|
+
*/
|
|
37
|
+
clear(): void;
|
|
38
|
+
}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple in-memory TTL cache with request coalescing
|
|
3
|
+
*
|
|
4
|
+
* Provides per-instance memoization for EHClient API responses.
|
|
5
|
+
* In serverless environments (Azure Functions, Static Web Apps),
|
|
6
|
+
* module-level state persists across warm invocations within the
|
|
7
|
+
* same instance — this cache leverages that behavior.
|
|
8
|
+
*
|
|
9
|
+
* Request coalescing: when multiple concurrent callers request the
|
|
10
|
+
* same expired key, only one factory call is made. All callers
|
|
11
|
+
* receive the same resolved value (or the same rejection).
|
|
12
|
+
*
|
|
13
|
+
* Not a distributed cache: each instance has its own cache.
|
|
14
|
+
* Cold starts and instance recycling naturally clear stale data.
|
|
15
|
+
*/
|
|
16
|
+
export class TTLCache {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.store = new Map();
|
|
19
|
+
this.inflight = new Map();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get a cached value, or call the factory to populate it.
|
|
23
|
+
*
|
|
24
|
+
* If a factory call is already in progress for this key,
|
|
25
|
+
* returns the existing promise instead of starting a duplicate.
|
|
26
|
+
*
|
|
27
|
+
* @param key - Cache key
|
|
28
|
+
* @param ttlMs - Time-to-live in milliseconds
|
|
29
|
+
* @param factory - Async function to produce the value on cache miss
|
|
30
|
+
*/
|
|
31
|
+
async get(key, ttlMs, factory) {
|
|
32
|
+
const existing = this.store.get(key);
|
|
33
|
+
if (existing && existing.expiresAt > Date.now()) {
|
|
34
|
+
return existing.data;
|
|
35
|
+
}
|
|
36
|
+
const pending = this.inflight.get(key);
|
|
37
|
+
if (pending) {
|
|
38
|
+
return pending;
|
|
39
|
+
}
|
|
40
|
+
const promise = factory().then((data) => {
|
|
41
|
+
this.store.set(key, { data, expiresAt: Date.now() + ttlMs });
|
|
42
|
+
this.inflight.delete(key);
|
|
43
|
+
return data;
|
|
44
|
+
}, (err) => {
|
|
45
|
+
this.inflight.delete(key);
|
|
46
|
+
throw err;
|
|
47
|
+
});
|
|
48
|
+
this.inflight.set(key, promise);
|
|
49
|
+
return promise;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Invalidate cache entries matching a key prefix.
|
|
53
|
+
*/
|
|
54
|
+
invalidate(prefix) {
|
|
55
|
+
for (const key of this.store.keys()) {
|
|
56
|
+
if (key.startsWith(prefix)) {
|
|
57
|
+
this.store.delete(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const key of this.inflight.keys()) {
|
|
61
|
+
if (key.startsWith(prefix)) {
|
|
62
|
+
this.inflight.delete(key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Clear all cached data and in-flight requests.
|
|
68
|
+
*/
|
|
69
|
+
clear() {
|
|
70
|
+
this.store.clear();
|
|
71
|
+
this.inflight.clear();
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Employment Hero Payroll API Client
|
|
3
|
+
*
|
|
4
|
+
* Provides methods for interacting with the Employment Hero Payroll (KeyPay) API.
|
|
5
|
+
* Uses HTTP Basic Authentication (API key as username, empty password).
|
|
6
|
+
*
|
|
7
|
+
* @see https://api.keypay.com.au/
|
|
8
|
+
*/
|
|
9
|
+
import type { EHConfig, EHEmployeeOptions, EHStandardHours, EHEmployeeGroup, EHRosterShift, EHRosterShiftOptions, EHKioskEmployee, EHKioskStaffOptions, EHErrorInfo, EHReportField, EHEmployeeDetailsReportOptions } from './types.js';
|
|
10
|
+
import type { EHAuEmployee } from './employee-types.generated.js';
|
|
11
|
+
/**
|
|
12
|
+
* Employment Hero Payroll API Client
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const client = new EHClient({ apiKey: 'xxx', businessId: 123 });
|
|
17
|
+
*
|
|
18
|
+
* // Validate credentials
|
|
19
|
+
* await client.validateApiKey();
|
|
20
|
+
*
|
|
21
|
+
* // Get employees
|
|
22
|
+
* const { employees } = await client.getEmployees();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare class EHClient {
|
|
26
|
+
private readonly apiKey;
|
|
27
|
+
private readonly businessId;
|
|
28
|
+
private readonly baseUrl;
|
|
29
|
+
private readonly onRequest?;
|
|
30
|
+
private readonly cache?;
|
|
31
|
+
private readonly cacheTtl;
|
|
32
|
+
private readonly retryConfig?;
|
|
33
|
+
private readonly rateLimiter?;
|
|
34
|
+
constructor(config: EHConfig);
|
|
35
|
+
/**
|
|
36
|
+
* Route through cache if enabled, otherwise call factory directly.
|
|
37
|
+
*/
|
|
38
|
+
private cached;
|
|
39
|
+
/**
|
|
40
|
+
* Clear all cached API responses.
|
|
41
|
+
*/
|
|
42
|
+
clearCache(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Make an authenticated request to the EH API
|
|
45
|
+
*
|
|
46
|
+
* When retry is configured, automatically retries on HTTP 429 and 503
|
|
47
|
+
* with exponential backoff. Respects the Retry-After header when present.
|
|
48
|
+
*/
|
|
49
|
+
private fetch;
|
|
50
|
+
/**
|
|
51
|
+
* Fetch a URL and parse the response, with standardized error handling.
|
|
52
|
+
*/
|
|
53
|
+
private fetchAndParse;
|
|
54
|
+
/**
|
|
55
|
+
* Validate the API key by calling GET /user
|
|
56
|
+
*/
|
|
57
|
+
validateApiKey(): Promise<{
|
|
58
|
+
valid: boolean;
|
|
59
|
+
error?: string;
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Get all employees (unstructured format)
|
|
63
|
+
*/
|
|
64
|
+
getEmployees(options?: EHEmployeeOptions): Promise<{
|
|
65
|
+
employees?: EHAuEmployee[];
|
|
66
|
+
error?: EHErrorInfo;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Get a single employee by ID
|
|
70
|
+
*/
|
|
71
|
+
getEmployee(employeeId: number): Promise<{
|
|
72
|
+
employee?: EHAuEmployee;
|
|
73
|
+
error?: EHErrorInfo;
|
|
74
|
+
}>;
|
|
75
|
+
/**
|
|
76
|
+
* Get standard hours for an employee (includes FTE value)
|
|
77
|
+
*/
|
|
78
|
+
getStandardHours(employeeId: number): Promise<{
|
|
79
|
+
standardHours?: EHStandardHours;
|
|
80
|
+
error?: EHErrorInfo;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* Get all employee groups
|
|
84
|
+
*/
|
|
85
|
+
getEmployeeGroups(): Promise<{
|
|
86
|
+
groups?: EHEmployeeGroup[];
|
|
87
|
+
error?: EHErrorInfo;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Get roster shifts for a date range
|
|
91
|
+
*
|
|
92
|
+
* @param fromDate - Start date (YYYY-MM-DD)
|
|
93
|
+
* @param toDate - End date (YYYY-MM-DD)
|
|
94
|
+
* @param options - Optional filters
|
|
95
|
+
*/
|
|
96
|
+
getRosterShifts(fromDate: string, toDate: string, options?: EHRosterShiftOptions): Promise<{
|
|
97
|
+
shifts?: EHRosterShift[];
|
|
98
|
+
error?: EHErrorInfo;
|
|
99
|
+
}>;
|
|
100
|
+
/**
|
|
101
|
+
* Get kiosk staff for time and attendance
|
|
102
|
+
*
|
|
103
|
+
* @param kioskId - Kiosk identifier
|
|
104
|
+
* @param options - Optional query parameters
|
|
105
|
+
*/
|
|
106
|
+
getKioskStaff(kioskId: number, options?: EHKioskStaffOptions): Promise<{
|
|
107
|
+
staff?: EHKioskEmployee[];
|
|
108
|
+
error?: EHErrorInfo;
|
|
109
|
+
}>;
|
|
110
|
+
/**
|
|
111
|
+
* Get available fields for the Employee Details Report
|
|
112
|
+
*
|
|
113
|
+
* Returns the list of columns that can be requested via getEmployeeDetailsReport().
|
|
114
|
+
* Use this to discover what data is available for the business.
|
|
115
|
+
*/
|
|
116
|
+
getReportFields(): Promise<{
|
|
117
|
+
fields?: EHReportField[];
|
|
118
|
+
error?: EHErrorInfo;
|
|
119
|
+
}>;
|
|
120
|
+
/**
|
|
121
|
+
* Get Employee Details Report
|
|
122
|
+
*
|
|
123
|
+
* Returns all employees with the requested columns in a single API call.
|
|
124
|
+
* The response is dynamic (JObject[]) — field keys depend on selectedColumns.
|
|
125
|
+
*/
|
|
126
|
+
getEmployeeDetailsReport(options?: EHEmployeeDetailsReportOptions): Promise<{
|
|
127
|
+
records?: Record<string, unknown>[];
|
|
128
|
+
error?: EHErrorInfo;
|
|
129
|
+
}>;
|
|
130
|
+
}
|