Introduzione
4HSE offre strumenti per la gestione delle anagrafiche del personale. Tuttavia, è sempre più comune che aziende consolidate desiderino continuare a usare il proprio software HR e integrarlo con un sistema di gestione della sicurezza come 4HSE. Questa esigenza si presenta sia nella fase di avvio del progetto che nelle successive fasi operative quotidiane, dove è fondamentale mantenere un allineamento solido tra i due sistemi.
Esploreremo come sviluppare un software che, sfruttando le API messe a disposizione da 4hse, consenta a qualsiasi software HR di integrarsi con 4HSE, mantenendo i dati anagrafici del personale sempre sincronizzati.
Non ci concentreremo su un software HR specifico, ma adotteremo un approccio generico al problema, sviluppando uno script di integrazione facilmente adattabile alle diverse soluzioni HR presenti sul mercato.
Prerequisiti
Cosa ci serve per sviluppare uno script di integrazione HR?
- Fonte dei dati: useremo un semplice file CSV come sorgente, che immaginiamo venga esportato periodicamente dal database del software HR per allinearsi con 4hse ogni 24 ore.
project_id
: Dobbiamo avere un progetto attivo in 4hse e conoscerne l’ID. Puoi trovare l’ID nella sezione “info” della finestra del progetto.access_token
: Ci serve un utente attivo con ruolo di amministratore per il progetto indicato, poiché sarà lui a eseguire la sincronizzazione. Per autenticare questo utente tramite API, abbiamo bisogno del suo access token. Il token viene fornito da 4hse su richiesta del cliente.
Fonte dei dati
Per preparare il file CSV estratto dalla fonte dati, dobbiamo sempre considerare queste semplici regole:
- Il file segue una struttura di colonne predefinita da 4hse.
- Il file contiene sempre l’intero dataset aggiornato dei dipendenti.
- I valori dei campi per ciascun dipendente nel dataset sovrascrivono i valori dello stesso dipendente in 4hse.
- I dipendenti presenti in 4hse ma non nella sorgente verranno storicizzati.
- I dipendenti presenti nella sorgente ma non in 4hse verranno aggiunti in 4hse.
The CSV file consists of the following columns:
field | required |
---|---|
code | no |
first_name | yes |
last_name | yes |
street | no |
locality | no |
postal_code | no |
region | no |
sex | no |
country | no |
birth_date | no |
birth_place | no |
tax_code | no |
note | no |
is_employee | yes |
Tra questi campi è necessario identificare un campo chiave, cioè un campo il cui valore identifica univocamente un dipendente. Può essere uno qualsiasi di questi campi, di solito il code
o il codice fiscale tax_code
.
REST API di 4HSE
4hse fornisce una REST API standard OpenAPI per eseguire e monitorare la sincronizzazione. L’endpoint create
riceve una richiesta contenente un file CSV di input e crea un’attività sui server di elaborazione di 4hse. Il servizio risponde immediatamente al successo della creazione dell’attività, fornendo l’identificativo univoco dell’attività. L’elaborazione dell’attività verrà eseguita il prima possibile. L’endpoint view
fornisce lo stato di elaborazione dell’attività identificata dal suo ID.
La documentazione completa delle API è disponibile qui: PeopleSync API.
Esempio
In questo esempio vogliamo stabilire una sincronizzazione periodica tra i dati estratti dal nostro gestionale HR e il progetto “La mia azienda” in 4hse. Supponiamo che il file CSV employees-1.csv
sia stato estratto dal nostro software HR e trasformato per seguire la struttura prevista da 4hse. Il file contiene 7 dipendenti.
code | first_name | last_name | street | locality | postal_code | region | sex | country | birth_date | birth_place | tax_code | note | is_employee |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | John | Doe | 123 Main St | Anytown | 12345 | CA | M | USA | 1980-05-15 | Anytown | ABC123456 | Note for John Doe | 1 |
2 | Jane | Smith | 456 Oak St | Someville | 67890 | NY | F | USA | 1985-08-21 | Someville | DEF789012 | Note for Jane Smith | 1 |
3 | Robert | Johnson | 789 Pine St | Another City | 54321 | TX | M | USA | 1972-11-03 | Another City | GHI345678 | Note for Robert Johnson | 1 |
4 | Emily | Davis | 101 Elm St | Yourtown | 98765 | FL | F | USA | 1992-04-18 | Yourtown | JKL901234 | Note for Emily Davis | 1 |
5 | Michael | Miller | 202 Cedar St | Cityville | 13579 | IL | M | USA | 1988-07-27 | Cityville | MNO567890 | Note for Michael Miller | 1 |
6 | Sophia | Brown | 303 Maple St | Smalltown | 24680 | GA | F | USA | 1995-02-09 | Smalltown | PQR123456 | Note for Sophia Brown | 1 |
7 | Daniel | Wilson | 404 Birch St | Newtown | 86420 | WA | M | USA | 1983-09-12 | Newtown | STU789012 | Note for Daniel Wilson | 1 |
ACCESS_TOKEN
: È il token che identifica l’utente di 4hse che eseguirà l’operazione, un amministratore del progetto.PROJECT_ID
: È l’ID del progetto che abbiamo appena creato.PRIMARY_KEY
: È la colonna nel CSV che identifica univocamente un dipendente. Abbiamo scelto “tax_code.”EMULATION
: Permette di eseguire la richiesta in modalità di emulazione. Questo significa che possiamo effettuare la sincronizzazione e ottenere i risultati senza applicare effettivamente le modifiche. Questo ci consente di testare l’esito dell’operazione prima di applicarla. Inizialmente imposteremo questo valore suTRUE
.MAX_CHANGES
: È un parametro di sicurezza che controlla il numero massimo di modifiche applicabili nella richiesta. Se il numero totale di modifiche effettuate supera questo valore, verremo avvisati e la richiesta verrà rifiutata. Inizialmente lo impostiamo a150
.
Ora definiamo il metodo createTask
, che verrà usato per creare l’attività di sincronizzazione presso il servizio people-sync
di 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 {
...
}
La prima cosa da fare è istanziare un client HTTP per inviare la richiesta:
private static String createTask(String filePath) throws IOException {
HttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("http://localhost:8081/people-sync/create");
}
Poiché dobbiamo inviare un file, i dati devono essere trasmessi in formato multipart.
Popoliamo il corpo della richiesta multipart fornendo il file e i restanti parametri di input:
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);
}
Infine, eseguiamo la richiesta.
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;
}
}
Definiamo ora un metodo readTask
che, utilizzando il task_id
, otterrà lo stato corrente dell’attività e lo stamperà a video.
private static void readTask(String taskId) throws IOException {
...
}
Analogamente al metodo precedente, instanziamo un client HTTP che, questa volta, effettua una richiesta utilizzando il metodo GET
, fornendo il task_id
e il access-token
per l’identificazione dell’utente.
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);
}
Eseguiamo la richiesta, analizziamo la risposta JSON e stampiamo a schermo il task_id
.
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();
}
}
Infine definiamo il metodo main
, che sarà il punto di ingresso del nostro script. I comandi disponibili saranno:
Ecco la conversione in Markdown:
-
create
to create the taskjava -jar your-jar-file.jar create filename.csv
-
read
to read the taskjava -jar your-jar-file.jar read task-id
Verifichiamo quindi che siano stati forniti i parametri di input, altrimenti mostriamo un messaggio a schermo.
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;
}
}
Verifichiamo quindi se il primo parametro passato è create
oppure read
e chiamiamo i rispettivi metodi, passando il nome del file CSV oppure l’ID dell’attività.
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();
}
}
Eseguiamo ora lo script avviando la creazione dell’attività con il comando:
java -jar target/my-java-project-1.0-SNAPSHOT-jar-with-dependencies.jar
create employees2-1.csv
In risposta, otteniamo l’ID dell’attività creata:
Created Task ID: be9eec92-621c-31b1-a525-7df391f599bf
Dopo qualche secondo, eseguiamo il comando per leggere lo stato dell’attività creata:
java -jar target/my-java-project-1.0-SNAPSHOT-jar-with-dependencies.jar
read be9eec92-621c-31b1-a525-7df391f599bf
In risposta, otteniamo il report completo delle azioni eseguite dal server:
task_id
: ID dell’attivitàentity_id
: ID del progettouser_id
: l’utente che ha creato l’attivitàstatus
: indica se l’attività è stata eseguita con successo o menocreated_at
,updated_at
: forniscono riferimenti temporali alla creazione e all’aggiornamento dell’attivitàresponse.result
: è una cronologia delle operazioni eseguite per ogni record nel nostro file CSV. In questo caso, essendo nuovi dipendenti, ciascun dipendente, identificato dal propriotax_code
, è stato aggiunto al progetto.response.changes
: è un riepilogo di tutte le operazioni eseguite. In questo caso, l’attività ha effettuato un totale di 7 modifiche, tutte “aggiunte.”log
: il registro del server registrato durante l’esecuzione dell’attività. È utile per identificare problemi in caso di errori.
{
"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
}
}
}
A questo punto cambiamo il parametro di emulazione in false
e rieseguiamo il comando di creazione.
private final static boolean EMULATION = false;
Riceveremo la stessa risposta di prima, ma in questo caso, le modifiche sono state effettivamente eseguite e, di conseguenza, avremo 7 dipendenti nel progetto “La mia azienda”.
Supponiamo ora che in azienda 2 persone siano andate in pensione. 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.
code | first_name | last_name | street | locality | postal_code | region | sex | country | birth_date | birth_place | tax_code | note | is_employee |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | John | Doe | 123 Main St | Anytown | 12345 | CA | M | USA | 1980-05-15 | Anytown | ABC123456 | Note for John Doe | 1 |
2 | Jane | Smith | 456 Oak St | Someville | 67890 | NY | F | USA | 1985-08-21 | Someville | DEF789012 | Note for Jane Smith | 1 |
3 | Robert | Johnson | 789 Pine St | Another City | 54321 | TX | M | USA | 1972-11-03 | Another City | GHI345678 | Note for Robert Johnson | 1 |
4 | Emily | Davis | 101 Elm St | Yourtown | 98765 | FL | F | USA | 1992-04-18 | Yourtown | JKL901234 | Note for Emily Davis | 1 |
5 | Michael | Miller | 202 Cedar St | Cityville | 13579 | IL | M | USA | 1988-07-27 | Cityville | MNO567890 | Note for Michael Miller | 1 |
Modifichiamo lo script, in particolare il parametro MAX_CHANGES
, riducendolo a 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
Leggiamo lo stato dell’attività appena creata e riscontriamo un errore: il numero effettivo di modifiche (3) supera il massimo consentito dalla richiesta (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
}
]
}
}
Modifichiamo di nuovo il valore di MAX_CHANGES
, impostandolo a 150
.
private final static int MAX_CHANGES = 150;
Rieseguiamo il comando per la creazione dell’attività e controlliamo il suo stato:
{
"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
}
}
}
La risposta mostra le tre modifiche attese. I due dipendenti pensionati sono stati storicizzati e la nota del dipendente è stata aggiornata.
Conclusioni
Abbiamo visto quanto sia semplice sincronizzare il database HR con 4hse, mantenendo entrambi i sistemi perfettamente allineati. Nel prossimo tutorial estenderemo questo esempio per sfruttare meccanismi di sincronizzazione più approfonditi.