فهرست منبع

refactor dolibarr sync service and implement i18n

Olivier Massot 3 سال پیش
والد
کامیت
e89ba44949

+ 2 - 0
composer.json

@@ -34,10 +34,12 @@
         "symfony/intl": "5.3.*",
         "symfony/lock": "5.3.*",
         "symfony/monolog-bundle": "^3.0",
+        "symfony/polyfill-intl-messageformatter": "^1.24",
         "symfony/property-access": "5.3.*",
         "symfony/property-info": "5.3.*",
         "symfony/security-bundle": "5.3.*",
         "symfony/serializer": "5.3.*",
+        "symfony/translation": "5.3.*",
         "symfony/twig-bundle": "^5.3",
         "symfony/validator": "5.3.*",
         "symfony/yaml": "5.3.*",

+ 180 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "374a0d1bd7348ba6d31cf0679c9603b6",
+    "content-hash": "05e37f86dd41e7725be5e8e42cf13eb7",
     "packages": [
         {
             "name": "api-platform/core",
@@ -5630,6 +5630,90 @@
             ],
             "time": "2021-05-27T09:27:20+00:00"
         },
+        {
+            "name": "symfony/polyfill-intl-messageformatter",
+            "version": "v1.24.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-messageformatter.git",
+                "reference": "22c4bba53bfadde90a4c1b32088e720638a42a83"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-messageformatter/zipball/22c4bba53bfadde90a4c1b32088e720638a42a83",
+                "reference": "22c4bba53bfadde90a4c1b32088e720638a42a83",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\MessageFormatter\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's MessageFormatter class and related functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "intl",
+                "messageformatter",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-messageformatter/tree/v1.24.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-02-19T12:13:01+00:00"
+        },
         {
             "name": "symfony/polyfill-intl-normalizer",
             "version": "v1.23.0",
@@ -7048,6 +7132,101 @@
             ],
             "time": "2021-11-24T10:02:00+00:00"
         },
+        {
+            "name": "symfony/translation",
+            "version": "v5.3.14",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/translation.git",
+                "reference": "945066809dc18f6e26123098e1b6e1d7a948660b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/945066809dc18f6e26123098e1b6e1d7a948660b",
+                "reference": "945066809dc18f6e26123098e1b6e1d7a948660b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2.5",
+                "symfony/deprecation-contracts": "^2.1",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php80": "^1.16",
+                "symfony/translation-contracts": "^2.3"
+            },
+            "conflict": {
+                "symfony/config": "<4.4",
+                "symfony/dependency-injection": "<5.0",
+                "symfony/http-kernel": "<5.0",
+                "symfony/twig-bundle": "<5.0",
+                "symfony/yaml": "<4.4"
+            },
+            "provide": {
+                "symfony/translation-implementation": "2.3"
+            },
+            "require-dev": {
+                "psr/log": "^1|^2|^3",
+                "symfony/config": "^4.4|^5.0",
+                "symfony/console": "^4.4|^5.0",
+                "symfony/dependency-injection": "^5.0",
+                "symfony/finder": "^4.4|^5.0",
+                "symfony/http-kernel": "^5.0",
+                "symfony/intl": "^4.4|^5.0",
+                "symfony/polyfill-intl-icu": "^1.21",
+                "symfony/service-contracts": "^1.1.2|^2",
+                "symfony/yaml": "^4.4|^5.0"
+            },
+            "suggest": {
+                "psr/log-implementation": "To use logging capability in translator",
+                "symfony/config": "",
+                "symfony/yaml": ""
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "Resources/functions.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Component\\Translation\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides tools to internationalize your application",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/translation/tree/v5.3.14"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-01-03T19:49:08+00:00"
+        },
         {
             "name": "symfony/translation-contracts",
             "version": "v2.5.0",

+ 13 - 0
config/packages/translation.yaml

@@ -0,0 +1,13 @@
+framework:
+    default_locale: en
+    translator:
+        default_path: '%kernel.project_dir%/translations'
+        fallbacks:
+            - fr
+#        providers:
+#            crowdin:
+#                dsn: '%env(CROWDIN_DSN)%'
+#            loco:
+#                dsn: '%env(LOCO_DSN)%'
+#            lokalise:
+#                dsn: '%env(LOKALISE_DSN)%'

+ 45 - 0
messages.fr.yaml.off

@@ -0,0 +1,45 @@
+#@see https://symfony.com/doc/5.4/translation.html
+
+# Activities functions
+STUDENT: Elève
+TEACHER: Professeur
+DIRECTOR: Directeur(ice) pédagogique
+DIRECTOR_ASSISTANT: Directeur(ice) pédagogique adjoint(e)
+INITIATOR: Initiateur(ice)
+MONITOR: Moniteur(ice)
+MUSIC_DIRECTOR_AND_HEAD: Directeur(ice) musical(e) ou Chef
+MUSIC_DIRECTOR_AND_HEAD_ASSISTANT: Directeur(ice) musical(e) ou Chef adjoint(e)
+DESK_OFFICER: Chef(fe) de pupitre
+ADMINISTRATIVE_OFFICER: Responsable administratif(ve)
+ADMINISTRATIVE_SECRETARY: Secrétaire administratif(ve)
+ADMINISTRATIVE_DIRECTOR: Directeur(ice) administratif(ve)
+ADMINISTRATIVE_DIRECTOR_ASSISTANT: Directeur(ice) administratif(ve) adjoint(e)
+ARCHIVIST: Archiviste
+PRESENTER: Présentateur(ice)
+ADMINISTRATIVE_STAFF: Personnel administratif
+NETWORK_ANIMATOR: Animateur(rice) réseau
+CORRESPONDING: Correspondant(e)
+COORDINATOR: Coordinateur(ice)
+TECHNICAL_STAFF: Personnel technique
+ACCOUNTANT: Comptable
+ACTIVE_MEMBER_OF_THE_CA: Membre actif du CA
+HONORARY_PRESIDENT: Président(e) d'honneur
+PRESIDENT: Président(e)
+YOUTH_REPRESENTATIVE: Représentant(e) des jeunes
+SECRETARY: Secrétaire
+ASSISTANT_SECRETARY: Secrétaire adjoint(e)
+TREASURER: Trésorier(e)
+TREASURER_ASSISTANT: Trésorier(e) adjoint(e)
+VICE_PRESIDENT: Vice président(e)
+ADHERENT: Adhérent(e)
+NO_MEMBER: Non membre
+VICE_PRESIDENT_OF_HONOR: Vice-président(e) d'honneur
+HOUR_PRESIDENT: Président(e) honoraire
+PRESIDENT_ASSISTANT: Président(e) adjoint(e)
+ACTIVE_COOPTED_MEMBER_OF_THE_CA: Membre actif du CA coopté(e)
+ACTIVE_SUBSTITUTE_MEMBER_OF_THE_CA: Membre actif du CA suppléant(e)
+MEMBER_OF_THE_BOARD: Membre du bureau
+MEMBER_OF_BOARD_OF_HONOR: Membre d'Honneur du CA
+HONORARY_MEMBER: Membre d'honneur
+BENEFACTOR_MEMBER: Membre bienfaiteur
+HOUR_MEMBER: Membre honoraire

+ 1 - 0
src/Commands/DolibarrSyncCommand.php

@@ -58,6 +58,7 @@ class DolibarrSyncCommand extends Command
         $output->writeln(count($operations) . " operations to be executed");
 
         if ($input->getOption('preview')) {
+            $output->writeln("-- Preview --");
             for ($i = 0; $i < count($operations); $i++) {
                 $output->writeln($i . '. ' . $operations[$i]->getLabel());
                 foreach ($operations[$i]->getChangeLog() as $message) {

+ 18 - 0
src/Enum/Access/FunctionEnum.php

@@ -98,6 +98,24 @@ class FunctionEnum extends Enum
             self::MUSIC_DIRECTOR_AND_HEAD
         ];
     }
+
+    /**
+     * Office missions
+     *
+     * Used by the DolibarrSyncService
+     */
+    public static function getOfficeMissions(): array
+    {
+        return [
+            self::PRESIDENT,
+            self::SECRETARY,
+            self::TREASURER,
+            self::ADMINISTRATIVE_OFFICER,
+            self::ADMINISTRATIVE_SECRETARY,
+            self::ADMINISTRATIVE_DIRECTOR,
+            self::ADMINISTRATIVE_STAFF
+        ];
+    }
 }
 
 

+ 12 - 0
src/Enum/Organization/SettingsProductEnum.php

@@ -16,4 +16,16 @@ class SettingsProductEnum extends Enum
     private const SCHOOL_PREMIUM = 'school-premium';
     private const MANAGER = 'manager';
     private const MANAGER_PREMIUM = 'manager-premium';
+
+    public static function isArtist(string $product): bool {
+        return $product === self::ARTIST || $product === self::ARTIST_PREMIUM;
+    }
+
+    public static function isSchool(string $product): bool {
+        return $product === self::SCHOOL || $product === self::SCHOOL_PREMIUM;
+    }
+
+    public static function isManager(string $product): bool {
+        return $product === self::MANAGER || $product === self::MANAGER_PREMIUM;
+    }
 }

+ 6 - 6
src/Repository/Access/FunctionTypeRepository.php

@@ -3,20 +3,20 @@ declare(strict_types=1);
 
 namespace App\Repository\Access;
 
-use App\Entity\Access\OrganizationFunction;
+use App\Entity\Access\FunctionType;
 use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 use Doctrine\Persistence\ManagerRegistry;
 
 /**
- * @method OrganizationFunction|null find($id, $lockMode = null, $lockVersion = null)
- * @method OrganizationFunction|null findOneBy(array $criteria, array $orderBy = null)
- * @method OrganizationFunction[]    findAll()
- * @method OrganizationFunction[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ * @method FunctionType|null find($id, $lockMode = null, $lockVersion = null)
+ * @method FunctionType|null findOneBy(array $criteria, array $orderBy = null)
+ * @method FunctionType[]    findAll()
+ * @method FunctionType[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
  */
 final class FunctionTypeRepository extends ServiceEntityRepository
 {
     public function __construct(ManagerRegistry $registry)
     {
-        parent::__construct($registry, OrganizationFunction::class);
+        parent::__construct($registry, FunctionType::class);
     }
 }

+ 120 - 153
src/Service/Dolibarr/DolibarrSync/DolibarrSyncService.php

@@ -1,25 +1,33 @@
 <?php
+declare(strict_types=1);
 
 namespace App\Service\Dolibarr\DolibarrSync;
 
 use App\Entity\Core\AddressPostal;
 use App\Entity\Organization\Organization;
+use App\Entity\Person\Person;
 use App\Enum\Access\FunctionEnum;
+use App\Enum\Access\RoleEnum;
 use App\Enum\Core\ContactPointTypeEnum;
 use App\Enum\Network\NetworkEnum;
 use App\Enum\Organization\AddressPostalOrganizationTypeEnum;
 use App\Enum\Organization\OrganizationIdsEnum;
 use App\Enum\Organization\SettingsProductEnum;
 use App\Repository\Access\AccessRepository;
+use App\Repository\Access\FunctionTypeRepository;
 use App\Repository\Core\ContactPointRepository;
 use App\Repository\Organization\OrganizationRepository;
 use App\Service\Core\AddressPostalUtils;
 use App\Service\Dolibarr\DolibarrApiService;
+use App\Service\Dolibarr\DolibarrSync\SyncOperation\DolibarrCreateOperation;
+use App\Service\Dolibarr\DolibarrSync\SyncOperation\DolibarrSyncOperation;
+use App\Service\Dolibarr\DolibarrSync\SyncOperation\DolibarrUpdateOperation;
 use Exception;
 use HttpException;
 use libphonenumber\PhoneNumber;
 use libphonenumber\PhoneNumberFormat;
 use libphonenumber\PhoneNumberUtil;
+use Symfony\Contracts\Translation\TranslatorInterface;
 
 /**
  * Push the data from the Opentalent DB into the Dolibarr DB, trough both applications
@@ -29,12 +37,13 @@ use libphonenumber\PhoneNumberUtil;
  */
 class DolibarrSyncService
 {
-
     public function __construct(
         private OrganizationRepository $organizationRepository,
         private AccessRepository $accessRepository,
         private ContactPointRepository $contactPointRepository,
+        private FunctionTypeRepository $functionTypeRepository,
         private DolibarrApiService $dolibarrApiService,
+        private TranslatorInterface $translator
     ) {}
 
     /**
@@ -50,12 +59,20 @@ class DolibarrSyncService
      */
     public function scan(): array {
 
+        var_dump($this->translator->trans('HOUR_PRESIDENT', ['gender' => 'F'] )); die;
+
         // Index the dolibarr clients by organization ids
         $dolibarrClientsIndex = $this->getDolibarrSocietiesIndex();
 
         // Get all active accesses
         $membersIndex = $this->getActiveMembersIndex();
 
+        // Get all the missions with an admin default role
+        $adminMissions = [];
+        foreach ($this->functionTypeRepository->findBy(['roleByDefault' => RoleEnum::ROLE_ADMIN()]) as $functionType) {
+            $adminMissions[] = $functionType->getMission();
+        }
+
         // Loop over the Opentalent organizations, and fill up the operations list
         $operations = [];
         foreach ($this->organizationRepository->findAll() as $organization) {
@@ -86,12 +103,15 @@ class DolibarrSyncService
             $mainAddress = $this->getOrganizationPostalAddress($organization);
 
             if ($mainAddress !== null) {
-                $streetAddress = AddressPostalUtils::getFullStreetAddress($mainAddress);
+                $streetAddress = AddressPostalUtils::getFullStreetAddress($mainAddress, '\n');
                 if (trim($mainAddress->getAddressOwner() ?? '') !== '') {
-                    $streetAddress = 'Chez ' . $mainAddress->getAddressOwner() . '\n' . $streetAddress;
+                    $streetAddress = $mainAddress->getAddressOwner() . '\n' . $streetAddress;
                 }
 
-                if ($streetAddress !== $dolibarrSociety['address']) {
+                if (
+                    preg_replace('/\s/', ' ', $streetAddress) !==
+                    preg_replace('/\s/', ' ', $dolibarrSociety['address'])
+                ) {
                     $putSocietyData['address'] = $streetAddress;
                 }
 
@@ -128,18 +148,16 @@ class DolibarrSyncService
             }
 
             $parent = $this->dolibarrApiService->getSociety($parentOrganizationId);
-            if ($parent['id'] !== $dolibarrSociety['parent']) {
-                $putSocietyData['parent'] = $parent['id'];
+            if ((int)$parent['id'] !== (int)$dolibarrSociety['parent']) {
+                $putSocietyData['parent'] = (int)$parent['id'];
             }
 
             // More infos
             $infosArray = [];
 
             $product = $organization->getSettings()->getProduct();
-            if (
-                $product == SettingsProductEnum::SCHOOL()->getValue() ||
-                $product == SettingsProductEnum::SCHOOL_PREMIUM()->getValue()
-            ) {
+
+            if (SettingsProductEnum::isSchool($product)) {
                 $studentsCount = count(array_filter(
                     $organizationMembers,
                     function ($missions) { return in_array(FunctionEnum::STUDENT()->getValue(), $missions); }
@@ -148,10 +166,8 @@ class DolibarrSyncService
             }
 
             if (
-                $product == SettingsProductEnum::SCHOOL()->getValue() ||
-                $product == SettingsProductEnum::SCHOOL_PREMIUM()->getValue() ||
-                $product == SettingsProductEnum::ARTIST()->getValue() ||
-                $product == SettingsProductEnum::ARTIST_PREMIUM()->getValue()
+                SettingsProductEnum::isSchool($product) ||
+                SettingsProductEnum::isArtist($product)
             ) {
                 $membersCount = count(array_filter(
                     $organizationMembers,
@@ -160,11 +176,9 @@ class DolibarrSyncService
                 $infosArray[] = "Nombre d'adhérents : " . $membersCount;
             }
 
-            $adminsCount = count($this->accessRepository->findBy(
-                [
-                    'organization' => $organization,
-                    'adminAccess' => true
-                ]
+            $adminsCount = count(array_filter(
+                $organizationMembers,
+                function ($missions) use ($adminMissions) { return !empty(array_intersect($adminMissions, $missions)); }
             ));
             $infosArray[] = "Nombre d'accès admin : " . $adminsCount;
 
@@ -177,47 +191,16 @@ class DolibarrSyncService
                 $putSocietyData['array_options'] = $arrayOptions;
             }
 
-            $operations[] = new DolibarrSyncOperation(
-                'Update organization ' . $organization->getId() . ' - ' . $organization->getName(),
-                'PUT',
-                'thirdparties/' . $dolibarrSociety['id'],
-                $putSocietyData,
-                $dolibarrSociety
+            $operations[] = new DolibarrUpdateOperation(
+                'Update society : ' . $organization->getName() . ' (' . $organization->getId() . ')',
+                'thirdparties',
+                $dolibarrSociety,
+                $putSocietyData
             );
 
             // ** Contacts
-            $officeMissions = [
-                FunctionEnum::PRESIDENT()->getValue(),
-                FunctionEnum::SECRETARY()->getValue(),
-                FunctionEnum::TREASURER()->getValue(),
-                FunctionEnum::ADMINISTRATIVE_OFFICER()->getValue(),
-                FunctionEnum::ADMINISTRATIVE_SECRETARY()->getValue(),
-                FunctionEnum::ADMINISTRATIVE_DIRECTOR()->getValue(),
-                FunctionEnum::ADMINISTRATIVE_STAFF()->getValue()
-            ];
-
-            $rolesLabels =  [
-                'MISS' => [
-                    FunctionEnum::PRESIDENT()->getValue() => 'Présidente',
-                    FunctionEnum::SECRETARY()->getValue() => 'Secrétaire',
-                    FunctionEnum::TREASURER()->getValue() => 'Trésorière',
-                    FunctionEnum::ADMINISTRATIVE_OFFICER()->getValue() => 'Responsable admin.',
-                    FunctionEnum::ADMINISTRATIVE_SECRETARY()->getValue() => 'Secrétaire admin.',
-                    FunctionEnum::ADMINISTRATIVE_DIRECTOR()->getValue() => 'Directrice',
-                    FunctionEnum::ADMINISTRATIVE_STAFF()->getValue() => 'Personnel administratif'
-                ],
-                'MISTER' => [
-                    FunctionEnum::PRESIDENT()->getValue() => 'Président',
-                    FunctionEnum::SECRETARY()->getValue() => 'Secrétaire',
-                    FunctionEnum::TREASURER()->getValue() => 'Trésorier',
-                    FunctionEnum::ADMINISTRATIVE_OFFICER()->getValue() => 'Responsable admin.',
-                    FunctionEnum::ADMINISTRATIVE_SECRETARY()->getValue() => 'Secrétaire admin.',
-                    FunctionEnum::ADMINISTRATIVE_DIRECTOR()->getValue() => 'Directeur',
-                    FunctionEnum::ADMINISTRATIVE_STAFF()->getValue() => 'Personnel administratif'
-                ]
-            ];
-
-            $dolibarrContactsIndex = $this->getDolibarrContactsIndex($dolibarrSociety['id']);
+            $officeMissions = FunctionEnum::getOfficeMissions();
+            $dolibarrContactsIndex = $this->getDolibarrContactsIndex((int)$dolibarrSociety['id']);
             $contactsProcessed = [];
 
             foreach ($organizationMembers as $accessId => $missions) {
@@ -226,105 +209,73 @@ class DolibarrSyncService
                         $access = $this->accessRepository->find($accessId);
                         $person = $access->getPerson();
 
+                        // Keep track of the contacts seen
                         if (in_array($person->getId(), $contactsProcessed)) {
                             // already updated from another mission
                             continue;
                         }
                         $contactsProcessed[] = $person->getId();
 
-                        if (array_key_exists($person->getId(), $dolibarrContactsIndex)) {
-                            $dolibarrContact = $dolibarrContactsIndex[$person->getId()];
-                        } else {
-                            // new contact
-                            $dolibarrContact = null;
-                        }
-
-                        $putContactData = [];
+                        // Build parameters for the query (if a query is needed
+                        $dolibarrContact = $dolibarrContactsIndex[$person->getId()] ?? null;
 
-                        $contactRes = $this->contactPointRepository->getByTypeAndPerson(
-                            ContactPointTypeEnum::PRINCIPAL()->getValue(), $person
-                        );
-                        if (empty($contactRes)) {
-                            $contactRes = $this->contactPointRepository->getByTypeAndPerson(
-                                ContactPointTypeEnum::OTHER()->getValue(), $person
-                            );
-                        }
-                        $contact = empty($contactRes) ? null : $contactRes[0];
-
-                        $lastname = $person->getName();
-                        $firstname = $person->getGivenName();
-                        $email = $contact?->getEmail();
-                        $phone = null;
-                        if ($contact !== null && $contact->getTelphone() !== null) {
-                            $phone = $this->formatPhoneNumber($contact->getTelphone());
-                        }
-                        $mobilePhone = null;
-                        if ($contact !== null && $contact->getMobilPhone() !== null) {
-                            $mobilePhone = $this->formatPhoneNumber($contact->getMobilPhone());
+                        // <-- special case: for phone numbers, dolibarr api returns empty strings even when field is null in DB
+                        // if left like this, it would leads to unnecessary updates
+                        if ($dolibarrContact !== null) {
+                            if ($dolibarrContact['phone_pro'] === '')
+                                $dolibarrContact['phone_pro'] = null;
+                            if ($dolibarrContact['phone_mobile'] === '')
+                                $dolibarrContact['phone_mobile'] = null;
                         }
-                        $civility = $person->getGender() === 'MISS' ? 'Mrs.' : 'Mr.';
-                        $poste = implode(
-                            ', ',
-                            array_map(
-                                function($m) use ($rolesLabels, $person) {
-                                    return $rolesLabels[$person->getGender() ?? 'MISTER'][$m];
-                                    },
-                                array_filter(
-                                    $missions,
-                                    function($m) use ($officeMissions){ return in_array($m, $officeMissions); }
+                        // -->
+
+                        $contact = $this->getPersonContact($person);
+
+                        $newData = [
+                            'civility_code' => ['MISS' => 'MME', 'MISTER' => 'MR'][$person->getGender()] ?? null,
+                            'lastname' => $person->getName(),
+                            'firstname' => $person->getGivenName(),
+                            'email' => $contact?->getEmail(),
+                            'phone_pro' => $contact?->getTelphone() ? $this->formatPhoneNumber($contact?->getTelphone()) : null,
+                            'phone_mobile' => $contact?->getMobilPhone() ? $this->formatPhoneNumber($contact?->getMobilPhone()): null,
+                            'poste' => implode(
+                                ', ',
+                                array_map(
+                                    function($m) use ($person) {
+                                        return $this->translator->trans($m, ['gender' => ['MISS' => 'F', 'MISTER' => 'M'][$person->getGender()] ?? '?'] );
+                                        },
+                                    array_filter(
+                                        $missions,
+                                        function ($m) { return $m !== FunctionEnum::ADHERENT()->getValue() && $m !== FunctionEnum::STUDENT()->getValue(); }
+                                    )
                                 )
                             )
-                        );
+                        ];
 
                         if ($dolibarrContact === null) {
-                            $postContactData = [
-                                'civility' => $civility,
-                                'lastname' => $lastname,
-                                'firstname' => $firstname,
-                                'email' => $email,
-                                'phone_pro' => $phone,
-                                'phone_mobile' => $mobilePhone,
-                                'poste' => $poste
-                            ];
-
-                            $operations[] = new DolibarrSyncOperation(
-                                'Create person ' . $person->getId() . ' - ' . $person->getName() . ' ' . $person->getGivenName(),
-                                'POST',
+                            $operations[] = new DolibarrCreateOperation(
+                                'New contact: ' . $person->getName() . ' ' . $person->getGivenName() . ' (' . $person->getId() . ')',
                                 'contacts',
-                                $postContactData
+                                $newData
                             );
                         } else {
-                            if ($civility !== $dolibarrContact['civility']) {
-                                $putContactData['civility'] = $civility;
-                            }
-                            if ($lastname !== $dolibarrContact['lastname']) {
-                                $putContactData['lastname'] = $lastname;
-                            }
-                            if ($firstname !== $dolibarrContact['firstname']) {
-                                $putContactData['firstname'] = $firstname;
-                            }
-                            if ($email !== $dolibarrContact['email']) {
-                                $putContactData['email'] = $contact->getEmail();
-                            }
-                            // the dolibarr api return an empty string even if the field is null
-                            if ($phone !== $dolibarrContact['phone_pro'] && $dolibarrContact['phone_pro'] !== '') {
-                                $putContactData['phone_pro'] = $phone;
-                            }
-                            // the dolibarr api return an empty string even if the field is null
-                            if ($mobilePhone !== $dolibarrContact['phone_mobile'] && $dolibarrContact['phone_mobile'] !== '') {
-                                $putContactData['phone_mobile'] = $mobilePhone;
-                            }
-                            if ($poste !== $dolibarrContact['poste']) {
-                                $putContactData['poste'] = $poste;
-                            }
-
-                            $operations[] = new DolibarrSyncOperation(
-                                'Update person ' . $person->getId() . ' - ' . $person->getName() . ' ' . $person->getGivenName(),
-                                'PUT',
-                                'contacts/' . $dolibarrContact['id'],
-                                $putContactData,
-                                $dolibarrContact
+                            // Only update the fields that are different
+                            $newData = array_filter(
+                                $newData,
+                                function($v, $k) use ($dolibarrContact) { return $v !== $dolibarrContact[$k]; },
+                                ARRAY_FILTER_USE_BOTH
                             );
+
+                            // add an update operation if some data has to be updated
+                            if (!empty($newData)) {
+                                $operations[] = new DolibarrUpdateOperation(
+                                    'Update contact: ' . $person->getName() . ' ' . $person->getGivenName() . ' (' . $person->getId() . ')' .
+                                    ' in ' . $organization->getName() . ' (' . $organization->getId() . ')',
+                                    'contacts',
+                                    $dolibarrContact,
+                                    $newData,
+                                );
+                            }
                         }
 
                         // No need to test the other missions of this access
@@ -334,13 +285,18 @@ class DolibarrSyncService
             }
 
             foreach ($dolibarrContactsIndex as $personId => $contactData) {
+                if ($contactData['statut'] === 0) {
+                    // contact is already disabled
+                    continue;
+                }
                 if (!in_array($personId, $contactsProcessed)) {
                     // Ce personId n'existe plus dans les membres Opentalent de cette société, on delete
-                    $operations[] = new DolibarrSyncOperation(
-                        'Delete person ' . $personId . ' - ' . $person->getName() . ' ' . $person->getGivenName() .
-                        ' from the contacts of organization ' . $organization->getId() . ' - ' . $organization->getName(),
-                        'DELETE',
-                        'contacts/' . $contactData['id']
+                    $operations[] = new DolibarrUpdateOperation(
+                        'Disable contact: ' . $contactData['lastname'] . ' ' . $contactData['firstname'] . ' (' . $personId . ')' .
+                        ' from  ' . $organization->getName() . ' (' . $organization->getId() . ')',
+                        'contacts',
+                        $contactData,
+                        ['statut' => 0]
                     );
                 }
             }
@@ -537,20 +493,31 @@ class DolibarrSyncService
         return null;
     }
 
+    private function getPersonContact(Person $person) {
+        $contactPriorities = [
+            ContactPointTypeEnum::PRINCIPAL()->getValue(),
+            ContactPointTypeEnum::OTHER()->getValue()
+        ];
+
+        foreach ($contactPriorities as $contactType) {
+            $result = $this->contactPointRepository->getByTypeAndPerson(
+                ContactPointTypeEnum::PRINCIPAL()->getValue(), $person
+            );
+            if (!empty($result)) {
+                return $result[0];
+            }
+        }
+        return null;
+    }
+
     /**
-     * Formatte un numéro de téléphone, au format français si l'indicatif national
-     * est l'indicatif français ou s'il est manquant,
-     * au format international sinon.
+     * Format a phone number into international format
      *
      * @param PhoneNumber $phoneNumber
      * @return mixed
      */
     private function formatPhoneNumber(PhoneNumber $phoneNumber): string {
         $phoneUtil = PhoneNumberUtil::getInstance();
-        if (!$phoneNumber->hasCountryCode() || $phoneNumber->getCountryCode() == 33) {
-            return $phoneUtil->format($phoneNumber, PhoneNumberFormat::NATIONAL);
-        } else {
-            return $phoneUtil->format($phoneNumber, PhoneNumberFormat::INTERNATIONAL);
-        }
+        return $phoneUtil->format($phoneNumber, PhoneNumberFormat::INTERNATIONAL);
     }
 }

+ 37 - 0
src/Service/Dolibarr/DolibarrSync/SyncOperation/DolibarrCreateOperation.php

@@ -0,0 +1,37 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Dolibarr\DolibarrSync\SyncOperation;
+
+use JetBrains\PhpStorm\Pure;
+
+class DolibarrCreateOperation extends DolibarrSyncOperation
+{
+    protected string $entity;
+
+    #[Pure]
+    public function __construct(string $label, string $entity, array $parameters) {
+        parent::__construct(
+            $label,
+            'POST',
+            $entity,
+            $parameters
+        );
+        $this->entity = $entity;
+    }
+
+    /**
+     * Return an array of messages describing the change that this operation will bring
+     *
+     * @return array
+     */
+    public function getChangeLog(): array {
+        $messages = [
+            '> POST ' . $this->entity
+        ];
+        foreach ($this->parameters as $field => $newValue) {
+            $messages[] = $field . ' : ' . $newValue;
+        }
+        return $messages;
+    }
+}

+ 37 - 0
src/Service/Dolibarr/DolibarrSync/SyncOperation/DolibarrDeleteOperation.php

@@ -0,0 +1,37 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Dolibarr\DolibarrSync\SyncOperation;
+
+use JetBrains\PhpStorm\Pure;
+
+class DolibarrDeleteOperation extends DolibarrSyncOperation
+{
+    protected string $entity;
+    protected int $id;
+
+    #[Pure]
+    public function __construct(string $label, string $entity, array $current) {
+        $id = (int)$current['id'];
+
+        parent::__construct(
+            $label,
+            'DELETE',
+            $entity . '/' . $id
+        );
+
+        $this->entity = $entity;
+        $this->id = $id;
+    }
+
+    /**
+     * Return an array of messages describing the change that this operation will bring
+     *
+     * @return array
+     */
+    public function getChangeLog(): array {
+        return [
+            '> DELETE ' . $this->entity . '/' . $this->id
+        ];
+    }
+}

+ 13 - 42
src/Service/Dolibarr/DolibarrSync/DolibarrSyncOperation.php → src/Service/Dolibarr/DolibarrSync/SyncOperation/DolibarrSyncOperation.php

@@ -1,6 +1,7 @@
 <?php
+declare(strict_types=1);
 
-namespace App\Service\Dolibarr\DolibarrSync;
+namespace App\Service\Dolibarr\DolibarrSync\SyncOperation;
 
 use App\Service\Dolibarr\DolibarrApiService;
 use HttpException;
@@ -15,21 +16,21 @@ use Symfony\Contracts\Service\Attribute\Required;
  * Single synchronization operation, corresponding to a single request
  * to the Dolibarr API
  */
-class DolibarrSyncOperation
+abstract class DolibarrSyncOperation
 {
     const STATUS_READY = 0;
     const STATUS_PENDING = 1;
     const STATUS_DONE = 2;
     const STATUS_ERROR = 3;
 
-    private DolibarrApiService $dolibarrApiService;
-    private int $status = self::STATUS_READY;
-    private string $label;
-    private string $method;
-    private string $path;
-    private array $parameters;
-    private array $currentData;
-    private string $errorMessage = "";
+    protected DolibarrApiService $dolibarrApiService;
+    protected int $status = self::STATUS_READY;
+    protected string $label;
+    protected string $method;
+    protected string $path;
+    protected array $parameters;
+    protected array $currentData;
+    protected string $errorMessage = "";
 
     #[Required]
     public function setDolibarrApiService(DolibarrApiService $dolibarrApiService) {
@@ -46,8 +47,6 @@ class DolibarrSyncOperation
 
     /**
      * Execute the operation and update its status according to the result
-     *
-     * @throws HttpException
      */
     public function execute() {
         $this->status = self::STATUS_PENDING;
@@ -122,37 +121,9 @@ class DolibarrSyncOperation
      * @return array
      * @throws \Exception
      */
-    public function getChangeLog(): array {
-        $messages = [];
-        if ($this->getMethod() == 'PUT') {
-            foreach ($this->parameters as $field => $newValue) {
-                if (!array_key_exists($field, $this->currentData)) {
-                    throw new \Exception('Field does not exists in the current object data : ' . $field);
-                }
-                if (is_array($newValue)) {
-                    foreach ($newValue as $subField => $newSubValue) {
-                        if (!array_key_exists($subField, $this->currentData[$field])) {
-                            throw new \Exception('Field does not exists in the current object data : ' . $field . '.' . $subField);
-                        }
-                        if ($newSubValue !== $this->currentData[$field][$subField]) {
-                            $messages[] = $field . '.' . $subField . ' : `'. $this->currentData[$field][$subField] . '` => `' . $newSubValue . '`';
-                        }
-                    }
-                } else {
-                    if ($newValue !== $this->currentData[$field]) {
-                        $messages[] = $field . ' : `' . $this->currentData[$field] . '` => `' . $newValue . '`';
-                    }
-                }
-            }
-        } elseif ($this->getMethod() == 'POST') {
-            foreach ($this->parameters as $field => $newValue) {
-                $messages[] = $field . ' : ' . $newValue;
-            }
-        }
-        return $messages;
-    }
+    abstract public function getChangeLog(): array;
 
     public function __toString(): string {
-        return $this->getLabel() . " > " . $this->getMethod() . " " . $this->getPath() . " " . json_encode($this->getParameters());
+        return $this->getMethod() . " " . $this->getPath() . " " . json_encode($this->getParameters());
     }
 }

+ 62 - 0
src/Service/Dolibarr/DolibarrSync/SyncOperation/DolibarrUpdateOperation.php

@@ -0,0 +1,62 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Dolibarr\DolibarrSync\SyncOperation;
+
+use JetBrains\PhpStorm\Pure;
+
+class DolibarrUpdateOperation extends DolibarrSyncOperation
+{
+    protected string $entity;
+    protected int $id;
+
+    #[Pure]
+    public function __construct(string $label, string $entity, array $current, array $parameters) {
+        $id = (int)$current['id'];
+
+        parent::__construct(
+            $label,
+            'PUT',
+            $entity . '/' . $id,
+            $parameters,
+            $current
+        );
+
+        $this->entity = $entity;
+        $this->id = $id;
+    }
+
+
+    /**
+     * Return an array of messages describing the change that this operation will bring
+     *
+     * @return array
+     * @throws \Exception
+     */
+    public function getChangeLog(): array {
+        $messages = [
+            '> PUT ' . $this->entity . '/' . $this->id
+        ];
+        foreach ($this->parameters as $field => $newValue) {
+            if (!array_key_exists($field, $this->currentData)) {
+                throw new \Exception('Field does not exists in the current object data : ' . $field);
+            }
+            if (is_array($newValue)) {
+                foreach ($newValue as $subField => $newSubValue) {
+                    if (!array_key_exists($subField, $this->currentData[$field])) {
+                        throw new \Exception('Field does not exists in the current object data : ' . $field . '.' . $subField);
+                    }
+                    if ($newSubValue !== $this->currentData[$field][$subField]) {
+                        $messages[] = $field . '.' . $subField . ' : `'. $this->currentData[$field][$subField] . '` => `' . $newSubValue . '`';
+                    }
+                }
+            } else {
+                if ($newValue !== $this->currentData[$field]) {
+                    $messages[] = $field . ' : `' . $this->currentData[$field] . '` => `' . $newValue . '`';
+                }
+            }
+        }
+        return $messages;
+    }
+
+}

+ 16 - 0
symfony.lock

@@ -412,6 +412,9 @@
     "symfony/polyfill-intl-idn": {
         "version": "v1.22.1"
     },
+    "symfony/polyfill-intl-messageformatter": {
+        "version": "v1.24.0"
+    },
     "symfony/polyfill-intl-normalizer": {
         "version": "v1.18.1"
     },
@@ -486,6 +489,19 @@
     "symfony/string": {
         "version": "v5.1.7"
     },
+    "symfony/translation": {
+        "version": "5.3",
+        "recipe": {
+            "repo": "github.com/symfony/recipes",
+            "branch": "master",
+            "version": "5.3",
+            "ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43"
+        },
+        "files": [
+            "config/packages/translation.yaml",
+            "translations/.gitignore"
+        ]
+    },
     "symfony/translation-contracts": {
         "version": "v2.3.0"
     },

+ 187 - 0
translations/enum/missions/messages+intl-icu.fr.yaml

@@ -0,0 +1,187 @@
+STUDENT: Elève
+TEACHER: >-
+  {gender, select,
+  M      {Professeur}
+  F      {Professeure}
+  other  {Professeur(e)}
+  }
+DIRECTOR: >-
+  {gender, select,
+  M      {Directeur pédagogique}
+  F      {Directrice pédagogique}
+  other  {Directeur(ice) pédagogique}
+  }
+DIRECTOR_ASSISTANT: >-
+  {gender, select,
+  M      {Directeur pédagogique adjoint}
+  F      {Directrice pédagogique adjointe}
+  other  {Directeur(ice) pédagogique adjoint(e)}
+  }
+INITIATOR: >-
+  {gender, select,
+  M      {Initiateur}
+  F      {Initiatrice}
+  other  {Initiateur(ice)}
+  }
+MONITOR: >-
+  {gender, select,
+  M      {Moniteur}
+  F      {Monitrice}
+  other  {Moniteur(ice)}
+  }
+MUSIC_DIRECTOR_AND_HEAD: >-
+  {gender, select,
+  M      {Directeur musical ou Chef}
+  F      {Directrice musicale ou Cheffe}
+  other  {Directeur(ice) musical(e) ou Chef(fe)}
+  }
+MUSIC_DIRECTOR_AND_HEAD_ASSISTANT: >-
+  {gender, select,
+  M      {Directeur musical ou Chef adjoint}
+  F      {Directrice musicale ou Cheffe adjointe}
+  other  {Directeur(ice) musical(e) ou Chef(fe) adjoint(e)}
+  }
+DESK_OFFICER: >-
+  {gender, select,
+  M      {Chef de pupitre}
+  F      {Cheffe de pupitre}
+  other  {Chef(fe) de pupitre}
+  }
+ADMINISTRATIVE_OFFICER:  >-
+  {gender, select,
+  M      {Responsable administratif}
+  F      {Responsable administrative}
+  other  {Responsable administratif(ve)}
+  }
+ADMINISTRATIVE_SECRETARY: >-
+  {gender, select,
+  M      {Secrétaire administratif}
+  F      {Secrétaire administrative}
+  other  {Secrétaire administratif(ve)}
+  }
+ADMINISTRATIVE_DIRECTOR: >-
+  {gender, select,
+  M      {Directeur administratif}
+  F      {Directrice administrative}
+  other  {Directeur(ice) administratif(ve)}
+  }
+ADMINISTRATIVE_DIRECTOR_ASSISTANT: >-
+  {gender, select,
+  M      {Directeur administratif adjoint}
+  F      {Directrice administrative adjointe}
+  other  {Directeur(ice) administratif(ve) adjoint(e)}
+  }
+ARCHIVIST: Archiviste
+PRESENTER:  >-
+  {gender, select,
+  M      {Présentateur}
+  F      {Présentatrice}
+  other  {Présentateur(ice)}
+  }
+ADMINISTRATIVE_STAFF: Personnel administratif
+NETWORK_ANIMATOR: >-
+  {gender, select,
+  M      {Animateur réseau}
+  F      {Animatrice réseau}
+  other  {Animateur(ice) réseau}
+  }
+CORRESPONDING: >-
+  {gender, select,
+  M      {Correspondant}
+  F      {Correspondante}
+  other  {Correspondant(e)}
+  }
+COORDINATOR: >-
+  {gender, select,
+  M      {Coordinateur}
+  F      {Coordinatrice}
+  other  {Coordinateur(ice)}
+  }
+TECHNICAL_STAFF: Personnel technique
+ACCOUNTANT: Comptable
+ACTIVE_MEMBER_OF_THE_CA: Membre actif du CA
+HONORARY_PRESIDENT: >-
+  {gender, select,
+  M      {Président d'honneur}
+  F      {Présidente d'honneur}
+  other  {Président(e) d'honneur}
+  }
+PRESIDENT: >-
+  {gender, select,
+  M {Président}
+  F {Présidente}
+  other {Président(e)}
+  }
+YOUTH_REPRESENTATIVE: >-
+  {gender, select,
+  M      {Représentant des jeunes}
+  F      {Représentante des jeunes}
+  other  {Représentant(e) des jeunes}
+  }
+SECRETARY: Secrétaire
+ASSISTANT_SECRETARY: >-
+  {gender, select,
+  M      {Secrétaire adjoint}
+  F      {Secrétaire adjointe}
+  other  {Secrétaire adjoint(e)}
+  }
+TREASURER: >-
+  {gender, select,
+  M      {Trésorier}
+  F      {Trésorière}
+  other  {Trésorier(e)}
+  }
+TREASURER_ASSISTANT: >-
+  {gender, select,
+  M      {Trésorier adjoint}
+  F      {Trésorière adjointe}
+  other  {Trésorier(e) adjoint(e)}
+  }
+VICE_PRESIDENT: >-
+  {gender, select,
+  M      {Vice président}
+  F      {Vice présidente}
+  other  {Vice président(e)}
+  }
+ADHERENT: >-
+  {gender, select,
+  M      {Adhérent}
+  F      {Adhérente}
+  other  {Adhérent(e)}
+  }
+NO_MEMBER: Non membre
+VICE_PRESIDENT_OF_HONOR: >-
+  {gender, select,
+  M      {Vice-président d'honneur}
+  F      {Vice-présidente d'honneur}
+  other  {Vice-président(e) d'honneur}
+  }
+HOUR_PRESIDENT: >-
+  {gender, select,
+  M      {Président honoraire}
+  F      {Présidente honoraire}
+  other  {Président(e) honoraire}
+  }
+PRESIDENT_ASSISTANT: >-
+  {gender, select,
+  M      {Président adjoint}
+  F      {Présidente adjointe}
+  other  {Président(e) adjoint(e)}
+  }
+ACTIVE_COOPTED_MEMBER_OF_THE_CA: >-
+  {gender, select,
+  M      {Membre actif du CA coopté}
+  F      {Membre actif du CA cooptée}
+  other  {Membre actif du CA coopté(e)}
+  }
+ACTIVE_SUBSTITUTE_MEMBER_OF_THE_CA: >-
+  {gender, select,
+  M      {Membre actif du CA suppléant}
+  F      {Membre actif du CA suppléante}
+  other  {Membre actif du CA suppléant(e)}
+  }
+MEMBER_OF_THE_BOARD: Membre du bureau
+MEMBER_OF_BOARD_OF_HONOR: Membre d'Honneur du CA
+HONORARY_MEMBER: Membre d'honneur
+BENEFACTOR_MEMBER: Membre bienfaiteur
+HOUR_MEMBER: Membre honoraire

+ 2 - 0
translations/messages+intl-icu.fr.yaml

@@ -0,0 +1,2 @@
+# @see https://symfony.com/doc/5.4/translation/message_format.html
+# @see https://symfony.com/doc/5.4/translation.html