<?php
//----------------------------------------------------------------------
// src/Services/AccessClient/AccessClientTools.php
//----------------------------------------------------------------------
namespace App\Services\AccessClient;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use App\Entity\Access;
use App\Entity\AccessClient\AccessClientAddress;
use App\Entity\AccessClient\AccessClient;
use App\Entity\AccessClient\AccessClientRecord;
use App\Entity\Client\Client;
use App\Entity\Client\Individual;
use App\Entity\Client\Title;
use App\Entity\Common\Attachment;
use App\Entity\Ding\Notification;
use App\Entity\Ding\NotificationType;
use App\Entity\Mission\Mission;
use App\Entity\Planning\Task;
use App\Entity\Platform\Devis\Devis;
use App\Entity\Platform\Invoice\Invoice;
use App\Entity\Platform\Note;
use App\Entity\Platform\Phone;
use App\Entity\Platform\PhoneLabel;
use App\Entity\Platform\Society;
use App\Entity\ProjectManager\ProjectNotebook;
use App\Entity\SocietyGroup;
use App\Entity\Webapp\Document;
use App\Services\Config\OptionConfigTools;
use App\Services\LogTools;
use App\Services\Security\PasswordTools;
class AccessClientTools
{
public function __construct(ManagerRegistry $doctrine, LogTools $logTools, TranslatorInterface $translator,
UserPasswordHasherInterface $passwordHasher, PasswordTools $passwordTools,
OptionConfigTools $optionConfigTools)
{
$this->em = $doctrine->getManager();
$this->logTools = $logTools;
$this->translator = $translator;
$this->optionConfigTools = $optionConfigTools;
$this->passwordHasher = $passwordHasher;
$this->passwordTools = $passwordTools;
}
/**
* Returns TRUE if an AccessClientRecord exists for
* the given Client AND the Client's SocietyGroup
* and the corresponding AccessClient is active
*
* If an AccessClientRecord exists for another Client
* having the same email, this method returns FALSE
*/
public function accessClientIsActiveForClient(Client $client)
{
$accessClientRecord = $this->em->getRepository(AccessClientRecord::class)->findOneByClient($client);
if ($accessClientRecord === null)
{
return false;
}
$accessClient = $accessClientRecord->getAccessClient();
if ($accessClient === null)
{
return false;
}
if ($accessClient->isActive())
{
return true;
}
return false;
}
/**
* Returns TRUE if an AccessClientRecord exists for
* the given Client AND the Client's SocietyGroup
*
* The corresponding AccessClient can be active or not,
* the method will still return true.
*
* If an AccessClientRecord exists for another Client
* having the same email, this method returns FALSE
*/
public function clientHasAccessClient(Client $client)
{
$accessClientRecord = $this->em->getRepository(AccessClientRecord::class)->findOneByClient($client);
if ($accessClientRecord !== null)
{
if ($accessClientRecord->getAccessClient() !== null)
{
return true;
}
}
return false;
}
/**
* Returns TRUE if an AccessClientRecord exists for
* the given Mission's Receiver (Client) AND the Receiver's SocietyGroup
*
* The corresponding AccessClient can be active or not,
* the method will still return true.
*
* If an AccessClientRecord exists for another Receiver
* having the same email, this method returns FALSE
*/
public function missionHasAccessClient(Mission $mission)
{
$receiver = $mission->getReceiver();
if ($receiver === null)
{
return false;
}
return $this->clientHasAccessClient($receiver);
}
/**
* Returns the corresponding AccessClientRecord and AccessClient
* for the given Client
*
* This method will ALWAYS return an array
* having two keys :
* accessClient
* accessClientRecord
*
* If no AccessClientRecord has been found,
* both array entries are null (but keys still exist)
*/
public function getDataForClient(Client $client)
{
// This is specific to a certain Client for a certain SocietyGroup
// This can be null,
// but that does not mean that there is not some other AccessClientRecord for the same email address
$accessClientRecord = $this->em->getRepository(AccessClientRecord::class)->findOneByClient($client);
if ($accessClientRecord !== null)
{
$accessClient = $accessClientRecord->getAccessClient();
// No need to go any further
return array(
'accessClient' => $accessClient,
'accessClientRecord' => $accessClientRecord,
);
}
// No record found for this particular client
// ... so look for an AccessClient with this email
if (!empty($client->getEmail()))
{
$accessClient = $this->em->getRepository(AccessClient::class)->findOneByEmail($client->getEmail());
if ($accessClient !== null)
{
// No need to go any further
return array(
'accessClient' => $accessClient,
'accessClientRecord' => null,
);
}
}
// All hope is lost
return array(
'accessClient' => null,
'accessClientRecord' => null,
);
}
/**
* Returns the corresponding AccessClientRecord and AccessClient for the given Client
*
* Find only from record, find by email can cause issue during merge process
*
* Plan.io Task #4392
*/
public function getDataForMergeIndividual(Client $client)
{
$output = array(
'accessClient' => null,
'accessClientRecord' => null,
);
// This is specific to a certain Client for a certain SocietyGroup
// This can be null,
// but that does not mean that there is not some other AccessClientRecord for the same email address
$accessClientRecord = $this->em->getRepository(AccessClientRecord::class)->findOneByClient($client);
if ($accessClientRecord === null)
{
return $output;
}
$accessClient = $accessClientRecord->getAccessClient();
$output['accessClient'] = $accessClient;
$output['accessClientRecord'] = $accessClientRecord;
return $output;
}
/**
* Returns the corresponding clients and individuals
* for the given AccessClient
*
* If data is found, the method will return an array having two keys :
* clients
* individuals
*
* Returns NULL in case of error,
* which should never occur, since an AccessClient should always have
* at least one AccessClientRecord corresponding to a Client
*/
public function getDataForAccessClient(AccessClient $accessClient)
{
if ($accessClient->getAccessClientRecords()->count() < 1)
{
// This should not happen
$this->logTools->errorlog("No AccessClientRecord found for AccessClient ".$accessClient->displayForLog());
return null;
}
$clients = array();
$individuals = array();
foreach ($accessClient->getAccessClientRecords() as $record)
{
$societyGroup = $record->getSocietyGroup();
if ($societyGroup === null)
{
// This should not happen
$this->logTools->errorlog("SocietyGroup is null for AccessClientRecord ".$record->displayForLog());
return null;
}
$client = $record->getClient();
if ($client === null)
{
// This should not happen
$this->logTools->errorlog("Client is null for AccessClientRecord ".$record->displayForLog());
return null;
}
// Only Individuals for now
$individual = $client->getIndividual();
if ($individual === null)
{
// This should not happen
$this->logTools->errorlog("Individual is null for Client ".$client->displayForLog());
return null;
}
$clients[] = $client;
$individuals[] = $individual;
}
return array(
'clients' => $clients,
'individuals' => $individuals,
);
}
/**
* Returns all the missions having as Receivers
* the Clients associated with the given AccessClient
*/
public function getMissionsForAccessClient(AccessClient $accessClient)
{
$missions = array();
foreach ($accessClient->getAccessClientRecords() as $record)
{
$client = $record->getClient();
if ($client === null) continue;
$clientMissions = $this->em->getRepository(Mission::class)->findByReceiver($client);
if (!empty($clientMissions))
{
$missions = array_merge($missions, $clientMissions);
}
}
return $missions;
}
public function areLinked(Client $client, AccessClient $accessClient)
{
foreach ($accessClient->getAccessClientRecords() as $record)
{
if ($client->equals($record->getClient()))
{
return true;
}
}
return false;
}
// We need to be able to decide if a user belonging to a given SocietyGroup
// can make an object visble to the Client (via the Client Platform) or not.
// => User trying to share and Author of object belongs to the same SocietyGroup
// => SocietyGroup = User.SocietyGroup
// Make visble condition : societyGroup == object.author.societyGroup
public function canBeMadeVisible($object, SocietyGroup $societyGroup)
{
if (!($object instanceof Attachment) &&
!($object instanceof Devis) &&
!($object instanceof Document) &&
!($object instanceof Invoice) &&
!($object instanceof Task) &&
!($object instanceof ProjectNotebook))
{
return false;
}
// Get the SocietyGroup that has created this object
$author = $object->getAuthor();
if ($author === null)
{
// All "error" cases should return true
// it means that nothing can be decided, so take a permissive approach
return true;
}
$objectSocietyGroupAuthor = $author->getSocietyGroup();
if ($objectSocietyGroupAuthor === null)
{
// All "error" cases should return true
// it means that nothing can be decided, so take a permissive approach
return true;
}
if ($societyGroup->equals($objectSocietyGroupAuthor))
{
return true;
}
return false;
}
// Return values
// accessClient => Created or existing AccessClient
// accessClientRecord => The AccessClientRecord we created for the Client $client
// newAccessClient => We need to know if the AccessClient already existed (send activation email)
public function createAccessClientData(Client $client)
{
$badOutput = array(
'accessClient' => null,
'accessClientRecord' => null,
'newAccessClient' => null,
);
// $badOutput['error'] = "access_client_record_already_exists";
// return $badOutput;
// Individuals only for now at least
if ($client->getIndividual() === null)
{
$badOutput['error'] = "client_is_not_an_individual";
return $badOutput;
}
// Just in case
$accessClientRecord = $this->em->getRepository(AccessClientRecord::class)->findOneByClient($client);
if ($accessClientRecord !== null)
{
$badOutput['error'] = "access_client_record_already_exists";
return $badOutput;
}
// No email, no game
$email = $client->getEmail();
if (empty($email))
{
$badOutput['error'] = "client_email_is_empty";
return $badOutput;
}
// All looks good
$societyGroup = $client->getSocietyGroup();
$individual = $client->getIndividual();
// Do we already have an AccessClient ?
$newAccessClient = false;
$accessClient = $this->em->getRepository(AccessClient::class)->findOneByEmail($email);
if ($accessClient === null)
{
// Hold that thought ;)
$newAccessClient = true;
// Nope => Create it
$accessClient = new AccessClient();
$accessClient->setEmail($email);
$accessClient->addRole('ROLE_CLIENT');
$accessClient->setName($client->getName());
// Generate password
$pass = $this->passwordTools->generatePassword(10);
// Encode the password
$encoded_password = $this->passwordHasher->hashPassword($accessClient, $pass);
$accessClient->setPassword($encoded_password);
// Activation code for access
$code = $this->passwordTools->generateUniqueActivationCode();
$accessClient->setActivationCode($code);
// Create Addresses
$this->createAddresses($individual, $accessClient);
}
// Now create the AccessClientRecord
$accessClientRecord = new AccessClientRecord();
$accessClientRecord->setClient($client);
$accessClientRecord->setSocietyGroup($societyGroup);
// $accessClientRecord->setAccessClient($accessClient);
// Respect Bidirectional Owing side in order to have logging
$accessClient->addAccessClientRecord($accessClientRecord);
$this->em->persist($accessClientRecord);
$this->em->persist($accessClient);
return array(
'accessClient' => $accessClient,
'accessClientRecord' => $accessClientRecord,
'newAccessClient' => $newAccessClient,
'error' => null,
);
}
public function createAccessClientDataForTask(Task $task)
{
$badOutput = array(
'accessClient' => null,
'accessClientRecord' => null,
'newAccessClient' => null,
);
if ($task->getMission() === null)
{
// Nothing to do here
// This is not logged and not displayed (for now)
$badOutput['error'] = "unknown_error";
return $badOutput;
}
$mission = $task->getMission();
if ($mission->getReceiver() === null)
{
// Nothing to do here
// This is not logged and not displayed (for now)
$badOutput['error'] = "unknown_error";
return $badOutput;
}
$client = $mission->getReceiver();
// Handle Sharing correctly
// Each SocietyGroup decides for its own Clients
// => SocietyGroup is the one of the Client, not the Mission
$societyGroup = $client->getSocietyGroup();
if ($societyGroup === null)
{
// Nothing to do here
// This is not logged and not displayed (for now)
$badOutput['error'] = "unknown_error";
return $badOutput;
}
// Auto Option Activated ?
if (!$this->optionConfigTools->isActive_ClientAccountAuto($societyGroup))
{
// Nope => Nothing to do here
// This is not logged and not displayed (for now)
$badOutput['error'] = "client_account_auto_option_not_active";
return $badOutput;
}
// Does the AccessClientRecord already exist ?
$accessClientRecord = $this->em->getRepository(AccessClientRecord::class)->findOneByClient($client);
if ($accessClientRecord !== null)
{
// All is good => Bye ;)
return array(
'accessClient' => $accessClientRecord->getAccessClient(),
'accessClientRecord' => $accessClientRecord,
'newAccessClient' => false,
'error' => null,
);
}
// Create the AccessClientRecord and, if needed, the AccessClient
// The method checks
// - client is individual
// - client has a non empty email address
// - no AccessClientRecord already exsist
$data = $this->createAccessClientData($client);
if ($data['error'] === null)
{
// All went well
return $data;
}
// If we are here it means that something went wrong
// Update output error
$badOutput['error'] = $data['error'];
// Output info in various forms to users
$error = $this->translator->trans($data['error']);
$this->logTools->errorLog($error);
// Create a note
$body = $this->translator->trans('note_body_create_access_client_from_task_failed');
$body .= " : ";
$body .= $error;
$author = $this->em->getRepository(Access::class)->findOneByEmail(Access::REKAPP);
$note = new Note();
$note->setClient($client);
$note->setMission($mission);
$note->setAuthor($author);
$note->setVisibleToClient(false);
$note->setReadonly(false);
$note->setBody($body);
$this->em->persist($note);
// Create a notification for the Client's Manager
$title = $this->translator->trans('note_title_create_access_client_from_task_failed');
$body = $this->translator->trans('note_body_create_access_client_from_task_failed');
$body .= " : ";
$body .= $error;
$body .= " : ";
$body .= $client->getName();
$type = $this->em->getRepository(NotificationType::class)
->findOneBy(array(
'dev' => 0,
'warning' => 1,
));
$notification = new Notification();
$notification->setType($type);
$notification->setSocietyGroup($societyGroup);
$notification->setSociety($client->getSociety());
$notification->setAccess($client->getManager());
$notification->setTitle($title);
$notification->setBody($body);
$this->em->persist($notification);
// TODO #4327
// Move this to the NotificationTools service
return $badOutput;
}
private function createAddresses(Individual $individual, AccessClient $accessClient)
{
// Create its two main addresses
$interventionAddress = $individual->getAddress();
$billingAddress = $individual->getBillingAddress();
$addresses = [];
if ($interventionAddress !== null)
{
// Create the first one
$address = new AccessClientAddress();
$address->initFrom($interventionAddress);
$addresses[] = $address;
if ($billingAddress !== null)
{
if ($billingAddress->differsInValue($interventionAddress))
{
// Create the second one
$secondAddress = new AccessClientAddress();
$secondAddress->initFrom($billingAddress);
$addresses[] = $secondAddress;
}
}
}
else
{
// Intervention Address is null
if ($billingAddress !== null)
{
// Create the only one
$address = new AccessClientAddress();
$address->initFrom($billingAddress);
$address->setAccessClient($accessClient);
$addresses[] = $address;
}
}
if (count($addresses))
{
$addresses[0]->setIntervention(1);
if (count($addresses) == 2)
{
$addresses[1]->setBilling(1);
}
else
{
$addresses[0]->setBilling(1);
}
}
foreach ($addresses as $key => $address)
{
$address->setAccessClient($accessClient);
$this->em->persist($address);
}
}
// This is called when an Individual is modified on the Platform OR the ClientPlatform
// Individual $individual is the object holding the source data
public function syncIndividualData(Individual $individual)
{
// Get the corresponding Client
$client = $individual->getClient();
if ($client === null)
{
return false;
}
// Does it have an AccessClient ?
$accessClientData = $this->getDataForClient($client);
if ($accessClientData['accessClient'] === null)
{
// Nothing do do here
return true;
}
$accessClient = $accessClientData['accessClient'];
$accessClientRecords = $accessClient->getAccessClientRecords();
// Is this Client the only AccessClientRecord ?
if (count($accessClientRecords) == 1)
{
// Nothing do do here
return true;
}
// More than one AccessClientRecord => Do some update
// Get all related Individuals
$accessClientData = $this->getDataForAccessClient($accessClient);
if ($accessClientData === null)
{
return false;
}
$individuals = $accessClientData['individuals'];
if (empty($individuals))
{
return false;
}
// Check all fields that may have been changed by the AccessClient
$firstname = $individual->getFirstname();
$lastname = $individual->getLastname();
$company = $individual->getCompany();
$phone = $individual->getPhone();
$email = $individual->getEmail();
$title = $individual->getTitle();
$titleCode = null;
// Title is tricky :)
// Title is Something that belongs to each SocietyGroup
// So we need to look for an equivalent
// Use codes
if ($title !== null)
{
$titleCode = $title->getCode();
}
foreach ($individuals as $item)
{
if ($individual->equals($item))
{
// Do not self update :)
continue;
}
if ($item->getFirstname() != $firstname) $item->setFirstname($firstname);
if ($item->getLastname() != $lastname) $item->setLastname($lastname);
if ($item->getCompany() != $company) $item->setCompany($company);
if ($item->getEmail() != $email) $item->setEmail($email);
// Update phone field
// Also update the main phoneline of the client
if ($item->getPhone() != $phone)
{
$item->setPhone($phone);
$this->syncIndividualPhoneObject($item);
}
// Apply update on title if needed
if ($item->getTitle() === null || $titleCode === null || $item->getTitle()->getCode() != $titleCode)
{
$newTitle = null;
if ($titleCode !== null)
{
$newTitle = $this->em->getRepository(Title::class)->findOneBy(array(
'societyGroup' => $item->getSocietyGroup(),
'code' => $titleCode,
));
}
// If not found, get n/c one
if ($newTitle === null)
{
$newTitle = $this->em->getRepository(Title::class)->findOneBy(array(
'societyGroup' => $item->getSocietyGroup(),
'code' => Title::CODE_NC,
));
}
if ($newTitle !== null)
{
$item->setTitle($newTitle);
}
}
// Client prePersist / preUpdate event is not called
// So trigger updateName manualy
if ($item->getClient() !== null)
{
$item->getClient()->updateName();
}
}
return true;
}
public function syncIndividualPhoneObject(Individual $individual)
{
$phoneDefaultLabel = $this->em->getRepository(PhoneLabel::class)->findOneBy(array(
'societyGroup' => $individual->getSocietyGroup(),
'defaultValue' => 1,
));
$phone = $this->em->getRepository(Phone::class)->findOneBy(array(
'individual' => $individual,
'label' => $phoneDefaultLabel,
));
if ($phone !== null)
{
$phone->setNumber($individual->getPhone());
}
}
/**
* Plan.io Task #4408
* @usedBy \App\Controller\Security\SecurityController\activation
*/
public function activateCommercialConsentForAccessClient(AccessClient $accessClient)
{
$records = $accessClient->getAccessClientRecords();
foreach($records as $rec)
{
$client = $rec->getClient();
if ($client === null || $client->getIndividual() === null)
{
continue;
}
$individual = $client->getIndividual();
$individual->setCommercialConsent(true);
$individual->setCommercialConsentDeactivationDate(null);
$individual->setLoggingData([
"info" => $this->translator->trans('activation_source'),
]);
}
}
}