Integrazione HR

Esploreremo come sviluppare un software che, sfruttando le API fornite da 4hse, consenta a qualsiasi software HR di integrarsi con 4HSE mantenendo i dati anagrafici del personale costantemente 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.

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:

  1. Il file segue una struttura di colonne predefinita da 4hse.
  2. Il file contiene sempre l’intero dataset aggiornato dei dipendenti.
  3. I valori dei campi per ciascun dipendente nel dataset sovrascrivono i valori dello stesso dipendente in 4hse.
  4. I dipendenti presenti in 4hse ma non nella sorgente verranno storicizzati.
  5. I dipendenti presenti nella sorgente ma non in 4hse verranno aggiunti in 4hse.

The CSV file consists of the following columns:

fieldrequired
codeno
first_nameyes
last_nameyes
streetno
localityno
postal_codeno
regionno
sexno
countryno
birth_dateno
birth_placeno
tax_codeno
noteno
is_employeeyes

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.

codefirst_namelast_namestreetlocalitypostal_coderegionsexcountrybirth_datebirth_placetax_codenoteis_employee
1JohnDoe123 Main StAnytown12345CAMUSA1980-05-15AnytownABC123456Note for John Doe1
2JaneSmith456 Oak StSomeville67890NYFUSA1985-08-21SomevilleDEF789012Note for Jane Smith1
3RobertJohnson789 Pine StAnother City54321TXMUSA1972-11-03Another CityGHI345678Note for Robert Johnson1
4EmilyDavis101 Elm StYourtown98765FLFUSA1992-04-18YourtownJKL901234Note for Emily Davis1
5MichaelMiller202 Cedar StCityville13579ILMUSA1988-07-27CityvilleMNO567890Note for Michael Miller1
6SophiaBrown303 Maple StSmalltown24680GAFUSA1995-02-09SmalltownPQR123456Note for Sophia Brown1
7DanielWilson404 Birch StNewtown86420WAMUSA1983-09-12NewtownSTU789012Note for Daniel Wilson1
  • 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 su TRUE.
  • 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 a 150.

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 task

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

    java -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 progetto
  • user_id: l’utente che ha creato l’attività
  • status: indica se l’attività è stata eseguita con successo o meno
  • created_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 proprio tax_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.

codefirst_namelast_namestreetlocalitypostal_coderegionsexcountrybirth_datebirth_placetax_codenoteis_employee
1JohnDoe123 Main StAnytown12345CAMUSA1980-05-15AnytownABC123456Note for John Doe1
2JaneSmith456 Oak StSomeville67890NYFUSA1985-08-21SomevilleDEF789012Note for Jane Smith1
3RobertJohnson789 Pine StAnother City54321TXMUSA1972-11-03Another CityGHI345678Note for Robert Johnson1
4EmilyDavis101 Elm StYourtown98765FLFUSA1992-04-18YourtownJKL901234Note for Emily Davis1
5MichaelMiller202 Cedar StCityville13579ILMUSA1988-07-27CityvilleMNO567890Note for Michael Miller1

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.

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