@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/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
+ }
@@ -0,0 +1,9 @@
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
+ export {};
@@ -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
+ }