@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/dist/client.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
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 { buildBasicAuthHeader } from './utils.js';
|
|
10
|
+
import { parseEHErrorResponse } from './errors.js';
|
|
11
|
+
import { EH_API_BASE, EH_REGION_URLS } from './constants.js';
|
|
12
|
+
import { TTLCache, getErrorMessage, fetchWithRetry } from '@markwharton/api-core';
|
|
13
|
+
import { RateLimiter } from './rate-limiter.js';
|
|
14
|
+
/** Default page size for paginated endpoints */
|
|
15
|
+
const DEFAULT_PAGE_SIZE = 100;
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Client
|
|
18
|
+
// ============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Employment Hero Payroll API Client
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const client = new EHClient({ apiKey: 'xxx', businessId: 123 });
|
|
25
|
+
*
|
|
26
|
+
* // Validate credentials
|
|
27
|
+
* await client.validateApiKey();
|
|
28
|
+
*
|
|
29
|
+
* // Get employees
|
|
30
|
+
* const { employees } = await client.getEmployees();
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class EHClient {
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.apiKey = config.apiKey;
|
|
36
|
+
this.businessId = config.businessId;
|
|
37
|
+
this.baseUrl = config.baseUrl ?? (config.region ? EH_REGION_URLS[config.region] : EH_API_BASE);
|
|
38
|
+
this.onRequest = config.onRequest;
|
|
39
|
+
// Initialize cache if configured
|
|
40
|
+
if (config.cache) {
|
|
41
|
+
this.cache = new TTLCache();
|
|
42
|
+
}
|
|
43
|
+
this.cacheTtl = {
|
|
44
|
+
employeesTtl: config.cache?.employeesTtl ?? 300000,
|
|
45
|
+
groupsTtl: config.cache?.groupsTtl ?? 300000,
|
|
46
|
+
standardHoursTtl: config.cache?.standardHoursTtl ?? 300000,
|
|
47
|
+
rosterShiftsTtl: config.cache?.rosterShiftsTtl ?? 120000,
|
|
48
|
+
reportFieldsTtl: config.cache?.reportFieldsTtl ?? 600000,
|
|
49
|
+
};
|
|
50
|
+
// Initialize retry config with defaults if provided
|
|
51
|
+
if (config.retry) {
|
|
52
|
+
this.retryConfig = {
|
|
53
|
+
maxRetries: config.retry.maxRetries ?? 3,
|
|
54
|
+
initialDelayMs: config.retry.initialDelayMs ?? 1000,
|
|
55
|
+
maxDelayMs: config.retry.maxDelayMs ?? 10000,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Initialize rate limiter (default: 5 req/s per API spec)
|
|
59
|
+
const rateLimitPerSecond = config.rateLimitPerSecond ?? 5;
|
|
60
|
+
if (rateLimitPerSecond > 0) {
|
|
61
|
+
this.rateLimiter = new RateLimiter(rateLimitPerSecond);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Route through cache if enabled, otherwise call factory directly.
|
|
66
|
+
*/
|
|
67
|
+
async cached(key, ttlMs, factory) {
|
|
68
|
+
if (this.cache) {
|
|
69
|
+
return this.cache.get(key, ttlMs, factory);
|
|
70
|
+
}
|
|
71
|
+
return factory();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Clear all cached API responses.
|
|
75
|
+
*/
|
|
76
|
+
clearCache() {
|
|
77
|
+
this.cache?.clear();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Make an authenticated request to the EH API
|
|
81
|
+
*
|
|
82
|
+
* When retry is configured, automatically retries on HTTP 429 and 503
|
|
83
|
+
* with exponential backoff. Respects the Retry-After header when present.
|
|
84
|
+
*/
|
|
85
|
+
async fetch(url, options = {}) {
|
|
86
|
+
const { method = 'GET', body, description } = options;
|
|
87
|
+
// Acquire rate limit token before making request
|
|
88
|
+
if (this.rateLimiter) {
|
|
89
|
+
await this.rateLimiter.acquire();
|
|
90
|
+
}
|
|
91
|
+
// Notify listener of request (for debugging)
|
|
92
|
+
this.onRequest?.({ method, url, description });
|
|
93
|
+
return fetchWithRetry(url, {
|
|
94
|
+
method,
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: buildBasicAuthHeader(this.apiKey),
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
Accept: 'application/json',
|
|
99
|
+
},
|
|
100
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
101
|
+
}, {
|
|
102
|
+
retry: this.retryConfig,
|
|
103
|
+
onRetry: ({ attempt, maxRetries, delayMs, status }) => {
|
|
104
|
+
this.onRequest?.({
|
|
105
|
+
method,
|
|
106
|
+
url,
|
|
107
|
+
description: `Retry ${attempt}/${maxRetries} after ${delayMs}ms (HTTP ${status})`,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Fetch a URL and parse the response, with standardized error handling.
|
|
114
|
+
*/
|
|
115
|
+
async fetchAndParse(url, parse, fetchOptions) {
|
|
116
|
+
try {
|
|
117
|
+
const response = await this.fetch(url, fetchOptions);
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const errorText = await response.text();
|
|
120
|
+
const { message } = parseEHErrorResponse(errorText, response.status);
|
|
121
|
+
return { error: { message, statusCode: response.status } };
|
|
122
|
+
}
|
|
123
|
+
return { data: await parse(response) };
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return { error: { message: getErrorMessage(error), statusCode: 0 } };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Validation
|
|
131
|
+
// ============================================================================
|
|
132
|
+
/**
|
|
133
|
+
* Validate the API key by calling GET /user
|
|
134
|
+
*/
|
|
135
|
+
async validateApiKey() {
|
|
136
|
+
const url = `${this.baseUrl}/user`;
|
|
137
|
+
try {
|
|
138
|
+
const response = await this.fetch(url);
|
|
139
|
+
if (response.ok) {
|
|
140
|
+
return { valid: true };
|
|
141
|
+
}
|
|
142
|
+
if (response.status === 401 || response.status === 403) {
|
|
143
|
+
return { valid: false, error: 'Invalid or expired API key' };
|
|
144
|
+
}
|
|
145
|
+
return { valid: false, error: `Unexpected response: HTTP ${response.status}` };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return { valid: false, error: getErrorMessage(error) || 'Connection failed' };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Employees
|
|
153
|
+
// ============================================================================
|
|
154
|
+
/**
|
|
155
|
+
* Get all employees (unstructured format)
|
|
156
|
+
*/
|
|
157
|
+
async getEmployees(options) {
|
|
158
|
+
const parts = ['employees'];
|
|
159
|
+
if (options?.payScheduleId != null)
|
|
160
|
+
parts.push(`ps:${options.payScheduleId}`);
|
|
161
|
+
if (options?.locationId != null)
|
|
162
|
+
parts.push(`loc:${options.locationId}`);
|
|
163
|
+
const cacheKey = parts.join(':');
|
|
164
|
+
return this.cached(cacheKey, this.cacheTtl.employeesTtl, async () => {
|
|
165
|
+
const params = new URLSearchParams();
|
|
166
|
+
if (options?.payScheduleId != null)
|
|
167
|
+
params.set('filter.payScheduleId', String(options.payScheduleId));
|
|
168
|
+
if (options?.locationId != null)
|
|
169
|
+
params.set('filter.locationId', String(options.locationId));
|
|
170
|
+
const queryString = params.toString();
|
|
171
|
+
const url = queryString
|
|
172
|
+
? `${this.baseUrl}/business/${this.businessId}/employee/unstructured?${queryString}`
|
|
173
|
+
: `${this.baseUrl}/business/${this.businessId}/employee/unstructured`;
|
|
174
|
+
const { data, error } = await this.fetchAndParse(url, async (r) => {
|
|
175
|
+
return await r.json();
|
|
176
|
+
});
|
|
177
|
+
return error ? { error } : { employees: data };
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get a single employee by ID
|
|
182
|
+
*/
|
|
183
|
+
async getEmployee(employeeId) {
|
|
184
|
+
return this.cached(`employee:${employeeId}`, this.cacheTtl.employeesTtl, async () => {
|
|
185
|
+
const url = `${this.baseUrl}/business/${this.businessId}/employee/unstructured/${employeeId}`;
|
|
186
|
+
const { data, error } = await this.fetchAndParse(url, async (r) => {
|
|
187
|
+
return await r.json();
|
|
188
|
+
});
|
|
189
|
+
return error ? { error } : { employee: data };
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// Standard Hours
|
|
194
|
+
// ============================================================================
|
|
195
|
+
/**
|
|
196
|
+
* Get standard hours for an employee (includes FTE value)
|
|
197
|
+
*/
|
|
198
|
+
async getStandardHours(employeeId) {
|
|
199
|
+
return this.cached(`standardhours:${employeeId}`, this.cacheTtl.standardHoursTtl, async () => {
|
|
200
|
+
const url = `${this.baseUrl}/business/${this.businessId}/employee/${employeeId}/standardhours`;
|
|
201
|
+
const { data, error } = await this.fetchAndParse(url, async (r) => {
|
|
202
|
+
return await r.json();
|
|
203
|
+
}, { description: `Get standard hours for employee ${employeeId}` });
|
|
204
|
+
return error ? { error } : { standardHours: data };
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Employee Groups
|
|
209
|
+
// ============================================================================
|
|
210
|
+
/**
|
|
211
|
+
* Get all employee groups
|
|
212
|
+
*/
|
|
213
|
+
async getEmployeeGroups() {
|
|
214
|
+
return this.cached('groups', this.cacheTtl.groupsTtl, async () => {
|
|
215
|
+
const url = `${this.baseUrl}/business/${this.businessId}/employeegroup`;
|
|
216
|
+
const { data, error } = await this.fetchAndParse(url, async (r) => {
|
|
217
|
+
return await r.json();
|
|
218
|
+
});
|
|
219
|
+
return error ? { error } : { groups: data };
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// Roster Shifts
|
|
224
|
+
// ============================================================================
|
|
225
|
+
/**
|
|
226
|
+
* Get roster shifts for a date range
|
|
227
|
+
*
|
|
228
|
+
* @param fromDate - Start date (YYYY-MM-DD)
|
|
229
|
+
* @param toDate - End date (YYYY-MM-DD)
|
|
230
|
+
* @param options - Optional filters
|
|
231
|
+
*/
|
|
232
|
+
async getRosterShifts(fromDate, toDate, options) {
|
|
233
|
+
const parts = [`rostershifts:${fromDate}:${toDate}`];
|
|
234
|
+
if (options?.employeeId != null)
|
|
235
|
+
parts.push(`eid:${options.employeeId}`);
|
|
236
|
+
if (options?.locationId != null)
|
|
237
|
+
parts.push(`lid:${options.locationId}`);
|
|
238
|
+
if (options?.employeeGroupId != null)
|
|
239
|
+
parts.push(`gid:${options.employeeGroupId}`);
|
|
240
|
+
if (options?.shiftStatus)
|
|
241
|
+
parts.push(`ss:${options.shiftStatus}`);
|
|
242
|
+
if (options?.shiftStatuses)
|
|
243
|
+
parts.push(`sss:${options.shiftStatuses.join(',')}`);
|
|
244
|
+
if (options?.selectedLocations)
|
|
245
|
+
parts.push(`sl:${options.selectedLocations.join(',')}`);
|
|
246
|
+
if (options?.selectedEmployees)
|
|
247
|
+
parts.push(`se:${options.selectedEmployees.join(',')}`);
|
|
248
|
+
if (options?.selectedRoles)
|
|
249
|
+
parts.push(`sr:${options.selectedRoles.join(',')}`);
|
|
250
|
+
if (options?.selectAllRoles)
|
|
251
|
+
parts.push('sar');
|
|
252
|
+
if (options?.unassignedShiftsOnly)
|
|
253
|
+
parts.push('uso');
|
|
254
|
+
if (options?.excludeShiftsOverlappingFromDate)
|
|
255
|
+
parts.push('exo');
|
|
256
|
+
if (options?.includeWarnings)
|
|
257
|
+
parts.push('iw');
|
|
258
|
+
const cacheKey = parts.join(':');
|
|
259
|
+
return this.cached(cacheKey, this.cacheTtl.rosterShiftsTtl, async () => {
|
|
260
|
+
// Roster shift query params are PascalCase per the Swagger spec, unlike
|
|
261
|
+
// all other endpoints which use camelCase. See: https://api.keypay.com.au/swagger-au.json
|
|
262
|
+
const params = new URLSearchParams({
|
|
263
|
+
FromDate: fromDate,
|
|
264
|
+
ToDate: toDate,
|
|
265
|
+
PageSize: String(DEFAULT_PAGE_SIZE),
|
|
266
|
+
});
|
|
267
|
+
if (options?.employeeId != null)
|
|
268
|
+
params.set('EmployeeId', String(options.employeeId));
|
|
269
|
+
if (options?.locationId != null)
|
|
270
|
+
params.set('LocationId', String(options.locationId));
|
|
271
|
+
if (options?.employeeGroupId != null)
|
|
272
|
+
params.set('EmployeeGroupId', String(options.employeeGroupId));
|
|
273
|
+
if (options?.selectAllRoles)
|
|
274
|
+
params.set('SelectAllRoles', 'true');
|
|
275
|
+
if (options?.shiftStatus)
|
|
276
|
+
params.set('ShiftStatus', options.shiftStatus);
|
|
277
|
+
if (options?.shiftStatuses) {
|
|
278
|
+
for (const s of options.shiftStatuses)
|
|
279
|
+
params.append('ShiftStatuses', s);
|
|
280
|
+
}
|
|
281
|
+
if (options?.selectedLocations) {
|
|
282
|
+
for (const l of options.selectedLocations)
|
|
283
|
+
params.append('SelectedLocations', l);
|
|
284
|
+
}
|
|
285
|
+
if (options?.selectedEmployees) {
|
|
286
|
+
for (const e of options.selectedEmployees)
|
|
287
|
+
params.append('SelectedEmployees', e);
|
|
288
|
+
}
|
|
289
|
+
if (options?.selectedRoles) {
|
|
290
|
+
for (const r of options.selectedRoles)
|
|
291
|
+
params.append('SelectedRoles', r);
|
|
292
|
+
}
|
|
293
|
+
if (options?.unassignedShiftsOnly)
|
|
294
|
+
params.set('UnassignedShiftsOnly', 'true');
|
|
295
|
+
if (options?.excludeShiftsOverlappingFromDate)
|
|
296
|
+
params.set('ExcludeShiftsOverlappingFromDate', 'true');
|
|
297
|
+
if (options?.includeWarnings)
|
|
298
|
+
params.set('IncludeWarnings', 'true');
|
|
299
|
+
try {
|
|
300
|
+
const allShifts = [];
|
|
301
|
+
let currentPage = 1;
|
|
302
|
+
// Paginate using PageSize/CurrentPage until we get fewer than PageSize results
|
|
303
|
+
while (true) {
|
|
304
|
+
params.set('CurrentPage', String(currentPage));
|
|
305
|
+
const url = `${this.baseUrl}/business/${this.businessId}/rostershift?${params}`;
|
|
306
|
+
const response = await this.fetch(url);
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
const errorText = await response.text();
|
|
309
|
+
const { message } = parseEHErrorResponse(errorText, response.status);
|
|
310
|
+
return { error: { message, statusCode: response.status } };
|
|
311
|
+
}
|
|
312
|
+
const page = await response.json();
|
|
313
|
+
allShifts.push(...page);
|
|
314
|
+
if (page.length < DEFAULT_PAGE_SIZE)
|
|
315
|
+
break;
|
|
316
|
+
currentPage++;
|
|
317
|
+
}
|
|
318
|
+
return { shifts: allShifts };
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
return { error: { message: getErrorMessage(error), statusCode: 0 } };
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// Time & Attendance (Kiosk)
|
|
327
|
+
// ============================================================================
|
|
328
|
+
/**
|
|
329
|
+
* Get kiosk staff for time and attendance
|
|
330
|
+
*
|
|
331
|
+
* @param kioskId - Kiosk identifier
|
|
332
|
+
* @param options - Optional query parameters
|
|
333
|
+
*/
|
|
334
|
+
async getKioskStaff(kioskId, options) {
|
|
335
|
+
const params = new URLSearchParams();
|
|
336
|
+
if (options?.restrictCurrentShiftsToCurrentKioskLocation) {
|
|
337
|
+
params.set('restrictCurrentShiftsToCurrentKioskLocation', 'true');
|
|
338
|
+
}
|
|
339
|
+
const queryString = params.toString();
|
|
340
|
+
const url = queryString
|
|
341
|
+
? `${this.baseUrl}/business/${this.businessId}/kiosk/${kioskId}/staff?${queryString}`
|
|
342
|
+
: `${this.baseUrl}/business/${this.businessId}/kiosk/${kioskId}/staff`;
|
|
343
|
+
const { data, error } = await this.fetchAndParse(url, async (r) => {
|
|
344
|
+
return await r.json();
|
|
345
|
+
});
|
|
346
|
+
return error ? { error } : { staff: data };
|
|
347
|
+
}
|
|
348
|
+
// ============================================================================
|
|
349
|
+
// Employee Details Report
|
|
350
|
+
// ============================================================================
|
|
351
|
+
/**
|
|
352
|
+
* Get available fields for the Employee Details Report
|
|
353
|
+
*
|
|
354
|
+
* Returns the list of columns that can be requested via getEmployeeDetailsReport().
|
|
355
|
+
* Use this to discover what data is available for the business.
|
|
356
|
+
*/
|
|
357
|
+
async getReportFields() {
|
|
358
|
+
return this.cached('reportfields', this.cacheTtl.reportFieldsTtl, async () => {
|
|
359
|
+
const url = `${this.baseUrl}/business/${this.businessId}/report/employeedetails/fields`;
|
|
360
|
+
const { data, error } = await this.fetchAndParse(url, async (r) => {
|
|
361
|
+
return await r.json();
|
|
362
|
+
});
|
|
363
|
+
return error ? { error } : { fields: data };
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Get Employee Details Report
|
|
368
|
+
*
|
|
369
|
+
* Returns all employees with the requested columns in a single API call.
|
|
370
|
+
* The response is dynamic (JObject[]) — field keys depend on selectedColumns.
|
|
371
|
+
*/
|
|
372
|
+
async getEmployeeDetailsReport(options) {
|
|
373
|
+
const params = new URLSearchParams();
|
|
374
|
+
if (options?.selectedColumns) {
|
|
375
|
+
for (const col of options.selectedColumns) {
|
|
376
|
+
params.append('selectedColumns', col);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (options?.locationId != null)
|
|
380
|
+
params.set('locationId', String(options.locationId));
|
|
381
|
+
if (options?.employingEntityId != null)
|
|
382
|
+
params.set('employingEntityId', String(options.employingEntityId));
|
|
383
|
+
if (options?.includeActive != null)
|
|
384
|
+
params.set('includeActive', String(options.includeActive));
|
|
385
|
+
if (options?.includeInactive != null)
|
|
386
|
+
params.set('includeInactive', String(options.includeInactive));
|
|
387
|
+
const queryString = params.toString();
|
|
388
|
+
const url = queryString
|
|
389
|
+
? `${this.baseUrl}/business/${this.businessId}/report/employeedetails?${queryString}`
|
|
390
|
+
: `${this.baseUrl}/business/${this.businessId}/report/employeedetails`;
|
|
391
|
+
const { data, error } = await this.fetchAndParse(url, async (r) => {
|
|
392
|
+
return await r.json();
|
|
393
|
+
});
|
|
394
|
+
return error ? { error } : { records: data };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Employment Hero Payroll Constants
|
|
3
|
+
*/
|
|
4
|
+
/** Supported API regions */
|
|
5
|
+
export type EHRegion = 'au' | 'nz' | 'uk' | 'sg' | 'my';
|
|
6
|
+
/** Base URLs for each region */
|
|
7
|
+
export declare const EH_REGION_URLS: Record<EHRegion, string>;
|
|
8
|
+
/** Default EH Payroll API base URL (AU) */
|
|
9
|
+
export declare const EH_API_BASE: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Employment Hero Payroll Constants
|
|
3
|
+
*/
|
|
4
|
+
/** Base URLs for each region */
|
|
5
|
+
export const EH_REGION_URLS = {
|
|
6
|
+
au: 'https://api.yourpayroll.com.au/api/v2',
|
|
7
|
+
nz: 'https://apinz.yourpayroll.io/api/v2',
|
|
8
|
+
uk: 'https://api.yourpayroll.co.uk/api/v2',
|
|
9
|
+
sg: 'https://apisg.yourpayroll.io/api/v2',
|
|
10
|
+
my: 'https://apimy.yourpayroll.io/api/v2',
|
|
11
|
+
};
|
|
12
|
+
/** Default EH Payroll API base URL (AU) */
|
|
13
|
+
export const EH_API_BASE = EH_REGION_URLS.au;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-generated AU employee type.
|
|
3
|
+
*
|
|
4
|
+
* Generated from KeyPay AU Swagger spec. DO NOT EDIT MANUALLY.
|
|
5
|
+
* Run: npx tsx scripts/generate-types.ts
|
|
6
|
+
*
|
|
7
|
+
* Common fields: 69, AU-specific: 70
|
|
8
|
+
*/
|
|
9
|
+
/** Common fields shared across all 5 regions (69 fields) */
|
|
10
|
+
export interface EHEmployeeCommon {
|
|
11
|
+
anniversaryDate: string | null;
|
|
12
|
+
automaticallyPayEmployee: string | null;
|
|
13
|
+
bankAccount1_AccountName: string | null;
|
|
14
|
+
bankAccount1_AccountNumber: string | null;
|
|
15
|
+
bankAccount1_AllocatedPercentage: number | null;
|
|
16
|
+
bankAccount1_FixedAmount: number | null;
|
|
17
|
+
bankAccount2_AccountName: string | null;
|
|
18
|
+
bankAccount2_AccountNumber: string | null;
|
|
19
|
+
bankAccount2_AllocatedPercentage: number | null;
|
|
20
|
+
bankAccount2_FixedAmount: number | null;
|
|
21
|
+
bankAccount3_AccountName: string | null;
|
|
22
|
+
bankAccount3_AccountNumber: string | null;
|
|
23
|
+
bankAccount3_AllocatedPercentage: number | null;
|
|
24
|
+
bankAccount3_FixedAmount: number | null;
|
|
25
|
+
dateCreated: string | null;
|
|
26
|
+
dateOfBirth: string | null;
|
|
27
|
+
emailAddress: string | null;
|
|
28
|
+
emergencyContact1_Address: string | null;
|
|
29
|
+
emergencyContact1_AlternateContactNumber: string | null;
|
|
30
|
+
emergencyContact1_ContactNumber: string | null;
|
|
31
|
+
emergencyContact1_Name: string | null;
|
|
32
|
+
emergencyContact1_Relationship: string | null;
|
|
33
|
+
emergencyContact2_Address: string | null;
|
|
34
|
+
emergencyContact2_AlternateContactNumber: string | null;
|
|
35
|
+
emergencyContact2_ContactNumber: string | null;
|
|
36
|
+
emergencyContact2_Name: string | null;
|
|
37
|
+
emergencyContact2_Relationship: string | null;
|
|
38
|
+
endDate: string | null;
|
|
39
|
+
externalId: string | null;
|
|
40
|
+
firstName: string | null;
|
|
41
|
+
gender: string | null;
|
|
42
|
+
homePhone: string | null;
|
|
43
|
+
hoursPerWeek: number | null;
|
|
44
|
+
id: number | null;
|
|
45
|
+
isEnabledForTimesheets: string | null;
|
|
46
|
+
jobTitle: string | null;
|
|
47
|
+
leaveAccrualStartDateType: 'EmployeeStartDate' | 'SpecifiedDate' | 'CalendarYear' | 'CategorySpecificDate' | null;
|
|
48
|
+
leaveTemplate: string | null;
|
|
49
|
+
leaveYearStart: string | null;
|
|
50
|
+
locations: string | null;
|
|
51
|
+
middleName: string | null;
|
|
52
|
+
mobilePhone: string | null;
|
|
53
|
+
overrideTemplateRate: string | null;
|
|
54
|
+
payConditionRuleSet: string | null;
|
|
55
|
+
payRateTemplate: string | null;
|
|
56
|
+
paySchedule: string | null;
|
|
57
|
+
paySlipNotificationType: string | null;
|
|
58
|
+
postalAddressLine2: string | null;
|
|
59
|
+
postalCountry: string | null;
|
|
60
|
+
postalPostCode: string | null;
|
|
61
|
+
postalStreetAddress: string | null;
|
|
62
|
+
preferredName: string | null;
|
|
63
|
+
primaryLocation: string | null;
|
|
64
|
+
primaryPayCategory: string | null;
|
|
65
|
+
rate: number | null;
|
|
66
|
+
rateUnit: string | null;
|
|
67
|
+
reportingDimensionValues: string | null;
|
|
68
|
+
residentialAddressLine2: string | null;
|
|
69
|
+
residentialCountry: string | null;
|
|
70
|
+
residentialPostCode: string | null;
|
|
71
|
+
residentialStreetAddress: string | null;
|
|
72
|
+
rosteringNotificationChoices: string | null;
|
|
73
|
+
startDate: string | null;
|
|
74
|
+
status: 'Active' | 'Terminated' | 'Incomplete' | null;
|
|
75
|
+
surname: string | null;
|
|
76
|
+
tags: string | null;
|
|
77
|
+
title: string | null;
|
|
78
|
+
workPhone: string | null;
|
|
79
|
+
workTypes: string | null;
|
|
80
|
+
}
|
|
81
|
+
/** AU region employee (70 region-specific fields) */
|
|
82
|
+
export interface EHAuEmployee extends EHEmployeeCommon {
|
|
83
|
+
australianResident: boolean;
|
|
84
|
+
automaticallyApplyPublicHolidayNotWorkedEarningsLines: boolean;
|
|
85
|
+
awardId: number | null;
|
|
86
|
+
bankAccount1_BSB: string | null;
|
|
87
|
+
bankAccount2_BSB: string | null;
|
|
88
|
+
bankAccount3_BSB: string | null;
|
|
89
|
+
businessAwardPackage: string | null;
|
|
90
|
+
claimMedicareLevyReduction: boolean;
|
|
91
|
+
claimTaxFreeThreshold: boolean;
|
|
92
|
+
closelyHeldEmployee: boolean;
|
|
93
|
+
closelyHeldReporting: 'PerQuarter' | 'PerPayRun' | null;
|
|
94
|
+
contractorABN: string | null;
|
|
95
|
+
dateTaxFileDeclarationReported: string | null;
|
|
96
|
+
dateTaxFileDeclarationSigned: string | null;
|
|
97
|
+
disableAutoProgression: boolean;
|
|
98
|
+
dvlPaySlipDescription: string | null;
|
|
99
|
+
employingEntityABN: string | null;
|
|
100
|
+
employingEntityId: string | null;
|
|
101
|
+
employmentAgreement: string | null;
|
|
102
|
+
employmentAgreementId: number | null;
|
|
103
|
+
employmentType: string | null;
|
|
104
|
+
hasApprovedWorkingHolidayVisa: boolean;
|
|
105
|
+
hasWithholdingVariation: boolean;
|
|
106
|
+
hoursPerDay: number | null;
|
|
107
|
+
includeInPortableLongServiceLeaveReport: boolean;
|
|
108
|
+
isExemptFromFloodLevy: boolean;
|
|
109
|
+
isExemptFromPayrollTax: boolean;
|
|
110
|
+
isSeasonalWorker: boolean;
|
|
111
|
+
maximumQuarterlySuperContributionsBase: number | null;
|
|
112
|
+
medicareLevyExemption: string | null;
|
|
113
|
+
medicareLevyReductionDependentCount: number | null;
|
|
114
|
+
medicareLevyReductionSpouse: boolean;
|
|
115
|
+
medicareLevySurchargeWithholdingTier: 'Tier1' | 'Tier2' | 'Tier3' | null;
|
|
116
|
+
otherTaxOffset: boolean;
|
|
117
|
+
portableLongServiceLeaveId: string | null;
|
|
118
|
+
postalAddressIsOverseas: boolean;
|
|
119
|
+
postalState: string | null;
|
|
120
|
+
postalSuburb: string | null;
|
|
121
|
+
previousSurname: string | null;
|
|
122
|
+
residentialAddressIsOverseas: boolean;
|
|
123
|
+
residentialState: string | null;
|
|
124
|
+
residentialSuburb: string | null;
|
|
125
|
+
seniorsTaxOffset: boolean;
|
|
126
|
+
singleTouchPayroll: 'CloselyHeld' | 'ForeignEmployment' | 'InboundAssignee' | 'LabourHire' | 'OtherSpecifiedPayments' | null;
|
|
127
|
+
stslDebt: boolean;
|
|
128
|
+
superFund1_AllocatedPercentage: number | null;
|
|
129
|
+
superFund1_EmployerNominatedFund: boolean;
|
|
130
|
+
superFund1_FixedAmount: number | null;
|
|
131
|
+
superFund1_FundName: string | null;
|
|
132
|
+
superFund1_MemberNumber: string | null;
|
|
133
|
+
superFund1_ProductCode: string | null;
|
|
134
|
+
superFund2_AllocatedPercentage: number | null;
|
|
135
|
+
superFund2_EmployerNominatedFund: boolean;
|
|
136
|
+
superFund2_FixedAmount: number | null;
|
|
137
|
+
superFund2_FundName: string | null;
|
|
138
|
+
superFund2_MemberNumber: string | null;
|
|
139
|
+
superFund2_ProductCode: string | null;
|
|
140
|
+
superFund3_AllocatedPercentage: number | null;
|
|
141
|
+
superFund3_EmployerNominatedFund: boolean;
|
|
142
|
+
superFund3_FixedAmount: number | null;
|
|
143
|
+
superFund3_FundName: string | null;
|
|
144
|
+
superFund3_MemberNumber: string | null;
|
|
145
|
+
superFund3_ProductCode: string | null;
|
|
146
|
+
superThresholdAmount: number | null;
|
|
147
|
+
taxCategory: 'Actor_WithTaxFreeThreshold' | 'Actor_NoTaxFreeThreshold' | 'Actor_LimitedPerformancePerWeek' | 'Actor_Promotional' | 'HorticulturalistShearer_WithTaxFreeThreshold' | 'HorticulturalistShearer_ForeignResident' | 'SeniorPensioner_Single' | 'SeniorPensioner_Married' | 'SeniorPensioner_SeparatedCoupleIllness' | 'ATODefined_DeathBeneficiary' | 'ATODefined_DownwardVariation' | 'ATODefined_NonEmployee' | 'DailyCasual' | null;
|
|
148
|
+
taxFileNumber: string | null;
|
|
149
|
+
taxVariation: number | null;
|
|
150
|
+
terminationReason: string | null;
|
|
151
|
+
workingHolidayVisaCountry: string | null;
|
|
152
|
+
workingHolidayVisaStartDate: string | null;
|
|
153
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Employment Hero Payroll Error Handling
|
|
3
|
+
*
|
|
4
|
+
* EH/KeyPay API returns errors in various formats:
|
|
5
|
+
* - {"message":"..."} or {"error":"..."}
|
|
6
|
+
* - Plain text
|
|
7
|
+
* - HTML error pages (for server errors)
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Parsed EH error response
|
|
11
|
+
*/
|
|
12
|
+
export interface EHParsedError {
|
|
13
|
+
/** Human-readable error message */
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse EH API error response text into a human-readable message.
|
|
18
|
+
*
|
|
19
|
+
* @param errorText - Raw error response text
|
|
20
|
+
* @param statusCode - HTTP status code
|
|
21
|
+
* @returns Parsed error with message
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseEHErrorResponse(errorText: string, statusCode: number): EHParsedError;
|
|
24
|
+
/**
|
|
25
|
+
* Custom error class for EH API errors
|
|
26
|
+
*/
|
|
27
|
+
export declare class EHError extends Error {
|
|
28
|
+
/** HTTP status code */
|
|
29
|
+
statusCode: number;
|
|
30
|
+
/** Raw error response */
|
|
31
|
+
rawResponse?: string;
|
|
32
|
+
constructor(message: string, statusCode: number, options?: {
|
|
33
|
+
rawResponse?: string;
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Create an EHError from an API response
|
|
37
|
+
*/
|
|
38
|
+
static fromResponse(statusCode: number, responseText: string): EHError;
|
|
39
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Employment Hero Payroll Error Handling
|
|
3
|
+
*
|
|
4
|
+
* EH/KeyPay API returns errors in various formats:
|
|
5
|
+
* - {"message":"..."} or {"error":"..."}
|
|
6
|
+
* - Plain text
|
|
7
|
+
* - HTML error pages (for server errors)
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Parse EH API error response text into a human-readable message.
|
|
11
|
+
*
|
|
12
|
+
* @param errorText - Raw error response text
|
|
13
|
+
* @param statusCode - HTTP status code
|
|
14
|
+
* @returns Parsed error with message
|
|
15
|
+
*/
|
|
16
|
+
export function parseEHErrorResponse(errorText, statusCode) {
|
|
17
|
+
try {
|
|
18
|
+
const errorJson = JSON.parse(errorText);
|
|
19
|
+
// Common EH error format
|
|
20
|
+
if (errorJson.message) {
|
|
21
|
+
return { message: errorJson.message };
|
|
22
|
+
}
|
|
23
|
+
if (errorJson.error) {
|
|
24
|
+
return { message: errorJson.error };
|
|
25
|
+
}
|
|
26
|
+
return { message: `HTTP ${statusCode}` };
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Not JSON, return as-is or fallback
|
|
30
|
+
return { message: errorText || `HTTP ${statusCode}` };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Custom error class for EH API errors
|
|
35
|
+
*/
|
|
36
|
+
export class EHError extends Error {
|
|
37
|
+
constructor(message, statusCode, options) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'EHError';
|
|
40
|
+
this.statusCode = statusCode;
|
|
41
|
+
this.rawResponse = options?.rawResponse;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create an EHError from an API response
|
|
45
|
+
*/
|
|
46
|
+
static fromResponse(statusCode, responseText) {
|
|
47
|
+
const parsed = parseEHErrorResponse(responseText, statusCode);
|
|
48
|
+
return new EHError(parsed.message, statusCode, {
|
|
49
|
+
rawResponse: responseText,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|