HR integration

Introduction

4HSE provides tools for managing personnel records. However it’s becoming increasingly common for well-established companies to want to continue using their HR software and integrate it with a security management system like 4HSE. This challenge arises both during the project onboarding phase and in subsequent daily operational phases where maintaining a solid alignment between the two systems is crucial.

We will explore how to develop software that leveraging the APIs provided by 4hse allows any HR software to integrate with 4HSE keeping personnel records data consistently synchronized.

We won’t focus on a specific HR software but aim for a generic approach to the problem by developing an integration script that can be easily adapted to various HR software solutions in the market.

Prerequisites

What do we need to develop an HR integration script?

  • Data source: We will use a simple CSV file as a data source, which we imagine to be exported periodically from an HR software database to align with 4hse every 24 hours.
  • project_id: We need to have an active project in 4hse and know its ID. You can find the ID in the “info” section of your project window.
  • access_token: We need an active user with admin role for the mentioned project, as this user will be the one executing the synchronization. To authenticate this user via API, we need its access token. The token is provided by 4hse upon client request.

Data Source

To prepare the CSV file extracted from the data source, we must always consider these simple rules:

  1. The file follows a column layout predefined by 4hse.
  2. The file always contains the entire dataset of employees, updated.
  3. The field values for each individual employee in the dataset will overwrite the values for the same employee in 4hse.
  4. Employees present in 4hse but not in the source will cause the historical tracking of those present in 4hse.
  5. Employees present in the source but not in 4hse will be added to 4hse.

The CSV file consists of the following columns:

Among these fields, it is necessary to identify a key field, i.e., a field whose value uniquely identifies an employee. It can be any of these fields, usually the code or the tax code tax_code.

4HSE REST API

4hse provides a standard OpenAPI REST API for executing and monitoring synchronization. The create endpoint receives a request containing an input CSV and creates a task at the 4hse processing servers. The service responds immediately upon successful task creation, providing the unique task identifier. The task processing will be executed as soon as possible. The view endpoint provides the processing status of the task identified by its ID.

Complete documentation of the APIs can be found here: PeopleSync API.

Example

In this example, we want to establish periodic synchronization between the data extracted from our HR and the “My Company” project in 4hse. Suppose the CSV file employees-1.csv has been extracted from our HR software and appropriately transformed to follow the CSV column structure provided by 4hse. The file contains 7 employees.

  • ACCESS_TOKEN: It is the token that identifies the 4hse user who will perform the operation, an admin of the project.
  • PROJECT_ID: It is the ID of the project we have just created.
  • PRIMARY_KEY: It is the column in the CSV that uniquely identifies an employee. We have chosen “tax_code.”
  • EMULATION: It allows us to execute the request in emulation mode. This means we can perform the sync and obtain the results without actually making the changes. This allows us to test the outcome of the operation before applying it. We will initially set this value to TRUE.
  • MAX_CHANGES: It is a security parameter that controls the maximum number of changes applicable in the request. If the total number of changes made exceeds this value, we will be alerted, and the request will be rejected. We initially set it to 150.

Now let’s define the createTask method, which will be used to create the synchronization task at the people-sync service of 4hse. The creation of the task is a simple HTTP POST request to the server, which responds with a task_id. The task ID is an identifier for the task that allows us to query the server to know its progress (queued, executed, etc.).

The method only requires the input of the file name to synchronize and provides the output of the created task ID.

private static String createTask(String filePath) throws IOException {
 ...
}

 

The first thing to do is to instantiate an HTTP client for sending the request.

private static String createTask(String filePath) throws IOException {
 HttpClient httpClient = HttpClients.createDefault();
 HttpPost httpPost = new HttpPost("http://localhost:8081/people-sync/create");
}

 

Since we need to send a file, the data must be transmitted in multipart format. Let’s populate the body of the multipart request by providing the file and the remaining input parameters:

private static String createTask(String filePath) throws IOException {
 HttpClient httpClient = HttpClients.createDefault();
 HttpPost httpPost = new HttpPost("http://localhost:8081/people-sync/create");

 MultipartEntityBuilder builder = MultipartEntityBuilder.create();

 // Add file parameter
 builder.addBinaryBody("file", new File(filePath), 
ContentType.APPLICATION_OCTET_STREAM, "file.csv");

 // Add other parameters
 builder.addTextBody("access-token", ACCESS_TOKEN);
 builder.addTextBody("project_id", PROJECT_ID);
 builder.addTextBody("pk", PRIMARY_KEY);
 builder.addTextBody("emulation", String.valueOf(EMULATION));
 builder.addTextBody("max_changes", String.valueOf(MAX_CHANGES));

 HttpEntity multipart = builder.build();
 httpPost.setEntity(multipart);
}

 

Finally, we execute the request. In case of success, the server will respond with a JSON that needs to be parsed to obtain the task_id, which we will return as output. In case of failure, we print the error on the screen.

private static String createTask(String filePath) throws IOException {
 HttpClient httpClient = HttpClients.createDefault();
 HttpPost httpPost = new HttpPost("http://localhost:8081/people-sync/create");

 MultipartEntityBuilder builder = MultipartEntityBuilder.create();

 // Add file parameter
 builder.addBinaryBody("file", new File(filePath), 
ContentType.APPLICATION_OCTET_STREAM,"file.csv");

 // Add other parameters
 builder.addTextBody("access-token", ACCESS_TOKEN);
 builder.addTextBody("project_id", PROJECT_ID);
 builder.addTextBody("pk", PRIMARY_KEY);
 builder.addTextBody("emulation", String.valueOf(EMULATION));
 builder.addTextBody("max_changes", String.valueOf(MAX_CHANGES));

 HttpEntity multipart = builder.build();
 httpPost.setEntity(multipart);

 // Execute the request
 HttpResponse response = httpClient.execute(httpPost);

 // Parse the response JSON
 JSONParser parser = new JSONParser();
 try (InputStream content = response.getEntity().getContent()) {
 JSONObject jsonResponse = (JSONObject) parser.parse(new InputStreamReader(content));
 return (String) jsonResponse.get("task_id");
 } catch (Exception e) {
 e.printStackTrace();
 return null;
 }
}

Now, let’s define a method readTask that, using the task_id, will obtain the current status of the task and print it on the screen.

private static void readTask(String taskId) throws IOException {
 ...
}

Similarly to the previous method, we instantiate an HTTP client that, this time, makes a request using the GET method, providing the task_id and the access-token for user identification.

private static void readTask(String taskId) throws IOException {
 HttpClient httpClient = HttpClients.createDefault();
 HttpGet httpGet = new HttpGet("http://localhost:8081/people-sync/view?id=" 
+ taskId + "&access-token=" + ACCESS_TOKEN); }

We execute the request, parse the JSON response, and print the task_id on the screen.

private static void readTask(String taskId) throws IOException {
 HttpClient httpClient = HttpClients.createDefault();
 HttpGet httpGet = new HttpGet("http://localhost:8081/people-sync/view?id=" + taskId + 
"&access-token=" + ACCESS_TOKEN);

 // Execute the request
 HttpResponse response = httpClient.execute(httpGet);

 // Parse and print the response JSON
 JSONParser parser = new JSONParser();
 try (InputStream content = response.getEntity().getContent()) {
 JSONObject jsonResponse = (JSONObject) parser.parse(new InputStreamReader(content));
 System.out.println("Task: " + jsonResponse.toJSONString());
 } catch (Exception e) {
 e.printStackTrace();
 }
}

Finally, we define the main method, which will be the entry point of our script. The available commands for the script will be:

  • create to create the task
    java -jar your-jar-file.jar create filename.csv
  • read to read the task
    java -jar your-jar-file.jar read task-id

We then check that the input parameters are provided; otherwise, we display a message on the screen.

public static void main(String[] args) {
 String command = args[0]; //create or read

 if (args.length < 2) {
 System.out.println("Usage:");
 System.out.println("java -jar your-jar-file.jar create file.csv");
 System.out.println("java -jar your-jar-file.jar read task-id");
 return;
 }
}

We then check if the first parameter passed to the command is create or read and call the respective methods, passing the CSV file name as an argument for creation or the task ID for reading.

public static void main(String[] args) {
 String command = args[0]; //create or read

 if (args.length < 2) {
 System.out.println("Usage:");
 System.out.println("java -jar your-jar-file.jar create file.csv");
 System.out.println("java -jar your-jar-file.jar read task-id");
 return;
 }

 try {
 if ("create".equals(args[0])) {
 String taskId = createTask(args[1]);
 System.out.println("Created Task ID: " + taskId);
 }
 else if ("read".equals(args[0])) {
 readTask(args[1]);
 }
 else {
 System.out.println("Action must be 'create' or 'read'");
 }
 } catch (IOException e) {
 e.printStackTrace();
 }
}

Now let’s run the script by initiating the task creation with the command:

java -jar target/my-java-project-1.0-SNAPSHOT-jar-with-dependencies.jar 
create employees2-1.csv

In response, we obtain the ID of the created task:

Created Task ID: be9eec92-621c-31b1-a525-7df391f599bf

After waiting for a few seconds, we execute the command to read the status of the created task:

java -jar target/my-java-project-1.0-SNAPSHOT-jar-with-dependencies.jar 
read be9eec92-621c-31b1-a525-7df391f599bf

In response, we get the complete report of the server’s actions:

  • task_id: task ID
  • entity_id: project ID
  • user_id: the user who created the task
  • status: indicates whether the task was executed successfully or not
  • created_atupdated_at: provide temporal references to the creation and update of the task
  • response.result: is a history of operations performed for each record in our CSV file. In this case, as they were new employees, each employee, identified by their tax_code, was added to the project.
  • response.changes: is a summary of all operations performed. In this case, the task made a total of 7 changes, all of which were “additions.”
  • log: the server log recorded during the task execution. It is useful for identifying problems in case of errors.
{
 "task_id": "a386a5bf-5bc4-3be7-9392-c81c69918bfc",
 "entity_id": "41ec7395-eeda-4c8e-bc73-5585d48e5161",
 "user_id": "john@4hse.com",
 "status": "DONE",
 "created_at": "2024-01-11 08:14:53",
 "updated_at": "2024-01-11 08:15:07",
 "response": {
 "result": {
 "ABC123456": [
 {
 "action": "add",
 "id": "ABC123456"
 }
 ],
 "PQR123456": [
 {
 "action": "add",
 "id": "PQR123456"
 }
 ],
 "DEF789012": [
 {
 "action": "add",
 "id": "DEF789012"
 }
 ],
 "STU789012": [
 {
 "action": "add",
 "id": "STU789012"
 }
 ],
 "MNO567890": [
 {
 "action": "add",
 "id": "MNO567890"
 }
 ],
 "GHI345678": [
 {
 "action": "add",
 "id": "GHI345678"
 }
 ],
 "JKL901234": [
 {
 "action": "add",
 "id": "JKL901234"
 }
 ]
 },
 "log": [
 {
 "level": "INFO",
 "text": "Task a386a5bf-5bc4-3be7-9392-c81c69918bfc executed",
 "timestamp": 1704960907
 },
 {
 "level": "INFO",
 "text": "Executing task a386a5bf-5bc4-3be7-9392-c81c69918bfc in emulation mode",
 "timestamp": 1704960906
 }
 ],
 "changes": {
 "add": 7,
 "total": 7,
 "activate": 0,
 "update": 0,
 "deactivate": 0
 }
 }
}

At this point, let’s change the emulation parameter to false and rerun the create command.

private final static boolean EMULATION = false;

We will receive the same response as before, but in this case, the changes have been actually executed, and therefore, we will have 7 employees in the “La mia azienda” project.

Now, let’s assume that in the company, 2 people have retired. Consequently, our HR has created a new file employees2-2.csv containing only the remaining 5 individuals. Additionally, for “John Doe,” the NOTE field has been modified. In total, there are 3 differences compared to the employees2-1.csv file.

Let’s modify our script, specifically the MAX_CHANGES parameter, reducing it to 2.

private final static int MAX_CHANGES = 2;

Let’s run the create command again, this time sending the employees2-2.csv file:

java -jar target/my-java-project-1.0-SNAPSHOT-jar-with-dependencies.jar create 
employees2-2.csv

Read the status of the newly created task, and we encounter an error as the actual number of changes (3) exceeds the maximum allowed by this request (2).

{
 "task_id": "b9819d5e-a0a7-34e0-844d-cde54ed91858",
 "entity_id": "41ec7395-eeda-4c8e-bc73-5585d48e5161",
 "user_id": "john@4hse.com",
 "status": "FAILED",
 "created_at": "2024-01-11 09:19:03",
 "updated_at": "2024-01-11 09:19:21",
 "response": {
 "log": [
 {
 "level": "ERROR",
 "text": "Out of max changes",
 "timestamp": 1704964761
 }
 ]
 }
}

Let’s modify the value of MAX_CHANGES again, setting it to 150.

private final static int MAX_CHANGES = 150;

Let’s rerun the task creation command and check its status:

{
 "task_id": "b7ede071-e5c2-37c9-8075-9cd1b5580f26",
 "entity_id": "41ec7395-eeda-4c8e-bc73-5585d48e5161",
 "user_id": "john@4hse.com",
 "created_at": "2024-01-11 08:22:36",
 "updated_at": "2024-01-11 08:23:16",
 "status": "DONE"
 "response": {
 "result": {
 "ABC123456": [
 {
 "changes": {
 "note": "Another note for John Doe"
 },
 "action": "update",
 "id": "ABC123456"
 }
 ],
 "PQR123456": [
 {
 "action": "deactivate",
 "periods": [
 {
 "end_date": "2024-01-11 00:00:00",
 "start_date": "1970-01-01 00:00:00"
 }
 ],
 "id": "PQR123456"
 }
 ],
 "STU789012": [
 {
 "action": "deactivate",
 "periods": [
 {
 "end_date": "2024-01-11 00:00:00",
 "start_date": "1970-01-01 00:00:00"
 }
 ],
 "id": "STU789012"
 }
 ]
 },
 "log": [
 {
 "level": "INFO",
 "text": "Task b7ede071-e5c2-37c9-8075-9cd1b5580f26 executed",
 "timestamp": 1704961396
 },
 {
 "level": "INFO",
 "text": "Executing task b7ede071-e5c2-37c9-8075-9cd1b5580f26 in emulation mode",
 "timestamp": 1704961396
 }
 ],
 "changes": {
 "add": 0,
 "total": 3,
 "activate": 0,
 "update": 1,
 "deactivate": 2
 }
 }
}

The response shows the three expected changes. The two individuals who retired have been historically recorded, and the notes for the relevant employee have been modified.

Conclusions

We have seen how easy it is to synchronize your HR database with 4hse, keeping both systems perfectly aligned. In the next tutorial, we will further extend this example script to leverage more in-depth synchronization mechanisms.

Sources: https://github.com/4hse/example-people-sync