diff --git a/.idea/php.xml b/.idea/php.xml
index bfa7ae1eff..f3e0ae429f 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -23,4 +23,4 @@
Hello,
+ +You've been added to the Vehicle Operator Licensing service for escapeHtml($this->orgName); ?>.
+ +Your username is: escapeHtml($this->loginId); ?>
+ +You'll receive a second email with a temporary password in the next two hours. If it doesn't arrive email notification@vehicle-operator-licensing.service.gov.uk
+ ++ Sign in +
diff --git a/app/api/module/Email/view/email/en_GB/plain/user-registered-tc.phtml b/app/api/module/Email/view/email/en_GB/plain/user-registered-tc.phtml new file mode 100644 index 0000000000..b10b7d164c --- /dev/null +++ b/app/api/module/Email/view/email/en_GB/plain/user-registered-tc.phtml @@ -0,0 +1,9 @@ +Hello, + +You've been added to the Vehicle Operator Licensing service for orgName; ?>. + +Your username is: loginId; ?> + +You'll receive a second email with a temporary password in the next two hours. If it doesn't arrive email notification@vehicle-operator-licensing.service.gov.uk + +Sign in at url; ?> diff --git a/app/api/test/module/Api/src/Domain/CommandHandler/Email/SendUserRegisteredTest.php b/app/api/test/module/Api/src/Domain/CommandHandler/Email/SendUserRegisteredTest.php index 432a444593..8b63b74b43 100644 --- a/app/api/test/module/Api/src/Domain/CommandHandler/Email/SendUserRegisteredTest.php +++ b/app/api/test/module/Api/src/Domain/CommandHandler/Email/SendUserRegisteredTest.php @@ -14,6 +14,7 @@ use Dvsa\Olcs\Api\Entity\Organisation\Organisation as OrganisationEntity; use Dvsa\Olcs\Api\Entity\Organisation\OrganisationUser as OrganisationUserEntity; use Dvsa\Olcs\Api\Entity\User\User; +use Dvsa\Olcs\Api\Service\Toggle\ToggleService; use Dvsa\Olcs\Email\Data\Message; use Dvsa\Olcs\Email\Domain\Command\SendEmail; use Dvsa\Olcs\Email\Service\TemplateRenderer; @@ -32,6 +33,7 @@ public function setUp(): void $this->mockedSmServices = [ TemplateRenderer::class => m::mock(TemplateRenderer::class), + ToggleService::class => m::mock(ToggleService::class) ]; parent::setUp(); @@ -87,6 +89,10 @@ public function testHandleCommand() 'default' ); + $this->mockedSmServices[ToggleService::class]->shouldReceive('isEnabled') + ->with(\Dvsa\Olcs\Api\Entity\System\FeatureToggle::TRANSPORT_CONSULTANT_ROLE) + ->andReturn(false); + $this->expectedSideEffect( SendEmail::class, [ diff --git a/app/api/test/module/Api/src/Domain/CommandHandler/User/RegisterConsultantAndOperatorTest.php b/app/api/test/module/Api/src/Domain/CommandHandler/User/RegisterConsultantAndOperatorTest.php new file mode 100644 index 0000000000..307820442a --- /dev/null +++ b/app/api/test/module/Api/src/Domain/CommandHandler/User/RegisterConsultantAndOperatorTest.php @@ -0,0 +1,87 @@ +sut = new RegisterConsultantAndOperator(); + $this->mockRepo('User', UserRepo::class); + $this->mockRepo('Role', \Dvsa\Olcs\Api\Domain\Repository\Role::class); + + $mockAuthService = m::mock(\LmcRbacMvc\Service\AuthorizationService::class); + $this->mockedSmServices['LmcRbacMvc\Service\AuthorizationService'] = $mockAuthService; + + parent::setUp(); + } + + public function testHandleCommand() + { + $operatorDetails = ['organisationName' => 'Operator Org',]; + + $command = RegisterConsultantAndOperatorCommand::create( + [ + 'operatorDetails' => $operatorDetails, + 'consultantDetails' => [] + ]); + + $operatorResult = new Result(); + $operatorResult->addId('user', 100)->addMessage('User created successfully'); + + $this->expectedSideEffect( + RegisterUserSelfServeCommand::class, + $operatorDetails, + $operatorResult + ); + + $organisationId = 200; + $organisation = m::mock(); + $organisation->shouldReceive('getId')->andReturn($organisationId); + + $organisationUser = m::mock(); + $organisationUser->shouldReceive('getOrganisation')->andReturn($organisation); + + $user = m::mock(UserEntity::class)->makePartial(); + $user->shouldReceive('getOrganisationUsers->first')->andReturn($organisationUser); + + $this->repoMap['User']->shouldReceive('fetchById')->with(100)->andReturn($user); + + $consultantDetails['organisation'] = $organisationId; + + $consultantResult = new Result(); + $consultantResult->addId('user', 101)->addMessage('User created successfully'); + + $consultant = m::mock(UserEntity::class)->makePartial(); + $this->repoMap['User']->shouldReceive('fetchById')->with(101)->andReturn($consultant); + + $consultant->shouldReceive('setRoles') + ->with(m::type(\Doctrine\Common\Collections\ArrayCollection::class)) + ->once() + ->andReturnSelf(); + + $this->repoMap['User']->shouldReceive('save')->with($consultant); + + $mockRole = m::mock(Role::class); + $this->repoMap['Role']->shouldReceive('fetchByRole')->with(Role::ROLE_OPERATOR_TC)->andReturn($mockRole); + + $this->expectedSideEffect( + RegisterUserSelfServeCommand::class, + $consultantDetails, + $consultantResult + ); + + $result = $this->sut->handleCommand($command); + $this->assertEquals(['User created successfully', 'User created successfully'], $result->getMessages()); + } +} diff --git a/app/selfserve/module/Olcs/config/module.config.php b/app/selfserve/module/Olcs/config/module.config.php index 1c71aaea86..d86900881c 100644 --- a/app/selfserve/module/Olcs/config/module.config.php +++ b/app/selfserve/module/Olcs/config/module.config.php @@ -8,6 +8,7 @@ use Olcs\Auth\Adapter\SelfserveCommandAdapterFactory; use Olcs\Auth\Service\AuthenticationServiceFactory; use Olcs\Auth\Service\AuthenticationServiceInterface; +use Olcs\Controller\ConsultantRegistrationController; use Olcs\Controller\Cookie\DetailsController as CookieDetailsController; use Olcs\Controller\Cookie\DetailsControllerFactory; use Olcs\Controller\Cookie\SettingsController as CookieSettingsController; @@ -502,7 +503,50 @@ 'route' => '/register[/]', 'defaults' => [ 'controller' => UserRegistrationController::class, - 'action' => 'add' + 'action' => 'start' + ] + ], + 'may_terminate' => true, + 'child_routes' => [ + 'operator' => [ + 'type' => 'segment', + 'options' => [ + 'route' => 'operator[/]', + 'defaults' => [ + 'controller' => UserRegistrationController::class, + 'action' => 'add' + ] + ] + ], + 'operator-representation' => [ + 'type' => 'segment', + 'options' => [ + 'route' => 'operator-representation[/]', + 'defaults' => [ + 'controller' => ConsultantRegistrationController::class, + 'action' => 'operatorRepresentation' + ] + ] + ], + 'register-for-operator' => [ + 'type' => 'segment', + 'options' => [ + 'route' => 'register-for-operator[/]', + 'defaults' => [ + 'controller' => ConsultantRegistrationController::class, + 'action' => 'registerForOperator' + ] + ] + ], + 'register-consultant-account' => [ + 'type' => 'segment', + 'options' => [ + 'route' => 'register-consultant-account[/]', + 'defaults' => [ + 'controller' => \Olcs\Controller\ConsultantRegistrationController::class, + 'action' => 'registerConsultantAccount' + ] + ] ] ] ], @@ -1326,6 +1370,7 @@ Olcs\Controller\UserController::class => \Olcs\Controller\Factory\UserControllerFactory::class, UserForgotUsernameController::class => \Olcs\Controller\Factory\UserForgotUsernameControllerFactory::class, UserRegistrationController::class => \Olcs\Controller\Factory\UserRegistrationControllerFactory::class, + ConsultantRegistrationController::class => \Olcs\Controller\Factory\ConsultantRegistrationControllerFactory::class, Olcs\Controller\Entity\ViewController::class => \Olcs\Controller\Factory\Entity\ViewControllerFactory::class, @@ -1434,7 +1479,10 @@ 'CookieSettingsCookieNamesProvider' => CookieService\SettingsCookieNamesProvider::class, 'QaIrhpApplicationViewGenerator' => QaService\ViewGenerator\IrhpApplicationViewGenerator::class, 'QaIrhpPermitApplicationViewGenerator' => QaService\ViewGenerator\IrhpPermitApplicationViewGenerator::class, - LicenceVehicleManagement::class => LicenceVehicleManagement::class + LicenceVehicleManagement::class => LicenceVehicleManagement::class, + \Olcs\Session\ConsultantRegistration::class => \Olcs\Session\ConsultantRegistration::class, + \Olcs\Controller\Mapper\CreateAccountMapper::class => \Olcs\Controller\Mapper\CreateAccountMapper::class, + ], 'abstract_factories' => [ \Laminas\Cache\Service\StorageCacheAbstractServiceFactory::class, @@ -1668,7 +1716,7 @@ 'verify/process-response' => ['*'], 'search*' => ['*'], 'index' => ['*'], - 'user-registration' => ['*'], + 'user-registration*' => ['*'], 'user-forgot-username' => ['*'], 'cookies*' => ['*'], 'privacy-notice' => ['*'], @@ -1701,5 +1749,10 @@ 'options_default_plus_cancel' => \Permits\Form\Model\Fieldset\SubmitOrCancelApplication::class, 'options_bilateral' => \Permits\Form\Model\Fieldset\SubmitOnly::class, ] - ] + ], + 'validators' => [ + 'factories' => [ + \Olcs\Form\Validator\UniqueConsultantDetails::class => \Olcs\Form\Validator\Factory\UniqueConsultantDetailsFactory::class, + ], + ], ]; diff --git a/app/selfserve/module/Olcs/src/Controller/ConsultantRegistrationController.php b/app/selfserve/module/Olcs/src/Controller/ConsultantRegistrationController.php new file mode 100644 index 0000000000..6c8e3e3c6e --- /dev/null +++ b/app/selfserve/module/Olcs/src/Controller/ConsultantRegistrationController.php @@ -0,0 +1,218 @@ +formHelper->createFormWithRequest(ExistingOperatorLicence::class, $this->getRequest()); + + if ($this->getRequest()->isPost()) { + if ($this->isButtonPressed('cancel')) { + return $this->redirectToHome(); + } + + $postData = $this->formatDataMapper->formatPostData($this->params()->fromPost()); + $form->setData($postData); + + if ($form->isValid()) { + $formData = $form->getData(); + if(($formData['fields']['existingOperatorLicence'] ?? null) === 'Y') { + // Add "speak to your adminstrator page + } elseif (($formData['fields']['existingOperatorLicence'] ?? null) === 'N') { + $this->redirect()->toRoute('user-registration/operator-representation'); + } + } + } + + return $this->prepareView('olcs/user-registration/operator-registration', [ + 'form' => $form, + 'pageTitle' => 'user-registration.page.title' + ]); + } + + /** + * @return Response|ViewModel + */ + public function operatorRepresentationAction() + { + $form = $this->formHelper->createFormWithRequest(OperatorRepresentation::class, $this->getRequest()); + + if ($this->getRequest()->isPost()) { + if ($this->isButtonPressed('cancel')) { + return $this->redirectToHome(); + } + + $postData = $this->formatDataMapper->formatPostData($this->params()->fromPost()); + $form->setData($postData); + + if ($form->isValid()) { + if ($postData['fields']['actingOnOperatorsBehalf'] == 'Y') { + // Move on to capture details for an operator, then consultant account + return $this->redirect()->toRoute('user-registration/register-for-operator'); + } else { + // Show the original operator registration form + return $this->redirect()->toRoute('user-registration/operator'); + } + } + } + + return $this->prepareView('olcs/user-registration/index', [ + 'form' => $form, + ]); + } + + + /** + * @return Response|ViewModel + */ + public function registerForOperatorAction() + { + $form = $this->formHelper->createFormWithRequest(RegisterForOperator::class, $this->getRequest()); + + if ($this->getRequest()->isPost()) { + if ($this->isButtonPressed('cancel')) { + return $this->redirectToHome(); + } + + $postData = $this->formatDataMapper->formatPostData($this->params()->fromPost()); + $form->setData($postData); + + if ($form->isValid()) { + // Save the operator details in session container and move on to consultant account registration + $this->consultantRegistrationSession->setOperatorDetails($form->getData()); + return $this->redirect()->toRoute('user-registration/register-consultant-account'); + } + } + + return $this->prepareView('olcs/user-registration/index', [ + 'form' => $form, + 'pageTitle' => 'register-for-operator.form.label' + ]); + } + + /** + * @return Response|ViewModel + */ + public function registerConsultantAccountAction() + { + $form = $this->formHelper->createFormWithRequest(RegisterConsultantAccount::class, $this->getRequest()); + + if ($this->getRequest()->isPost()) { + if ($this->isButtonPressed('cancel')) { + return $this->redirectToHome(); + } + + $form->setData($this->formatDataMapper->formatPostData($this->params()->fromPost())); + + if ($form->isValid()) { + $result = $this->registerConsultantAndOperator($form->getData()); + if ($result === null) { + return $this->prepareView('olcs/user-registration/check-email-consultant', [ + 'consultantEmailAddress' => $form->get('fields')->get('emailAddress')->getValue(), + 'operatorEmailAddress' => $this->consultantRegistrationSession->getOperatorDetails()['fields']['emailAddress'], + 'pageTitle' => 'user-registration.page.check-email.title' + ]); + } + return $result; + } + } + + return $this->prepareView('olcs/user-registration/index', [ + 'form' => $form, + 'pageTitle' => 'register-consultant-account.form.label' + ]); + } + + private function registerConsultantAndOperator($consultantFormData) + { + $operatorData = $this->consultantRegistrationSession->getOperatorDetails(); + $formattedOperatorData = $this->formatDataMapper->formatSaveData($operatorData); + $formattedConsultantData = $this->formatDataMapper->formatSaveData($consultantFormData); + + $response = $this->handleCommand( + RegisterConsultantAndOperator::create( + [ + 'operatorDetails' => $formattedOperatorData, + 'consultantDetails' => $formattedConsultantData, + ] + ) + ); + + if ($response->isOk()) { + return null; + } + + $this->flashMessengerHelper->addErrorMessage('unknown-error'); + + $this->redirect()->toRoute('user-registration'); + } + + /** + * @param string $template + * @param array $variables + * @return ViewModel + */ + private function prepareView(string $template, array $variables = []): ViewModel + { + $view = new ViewModel($variables); + $view->setTemplate($template); + + if (isset($variables['pageTitle'])) { + $this->placeholder()->setPlaceholder('pageTitle', $variables['pageTitle']); + } + + return $view; + } + + /** + * Redirects to home + * + * @return \Laminas\Http\Response + */ + private function redirectToHome() + { + return $this->redirect()->toRoute('index'); + } +} diff --git a/app/selfserve/module/Olcs/src/Controller/Factory/ConsultantRegistrationControllerFactory.php b/app/selfserve/module/Olcs/src/Controller/Factory/ConsultantRegistrationControllerFactory.php new file mode 100644 index 0000000000..f1a84f12e2 --- /dev/null +++ b/app/selfserve/module/Olcs/src/Controller/Factory/ConsultantRegistrationControllerFactory.php @@ -0,0 +1,51 @@ +get(NiTextTranslation::class); + $authService = $container->get(AuthorizationService::class); + $formHelper = $container->get(FormHelperService::class); + $scriptFactory = $container->get(ScriptFactory::class); + $translationHelper = $container->get(TranslationHelperService::class); + $urlHelper = $container->get(UrlHelperService::class); + $flashMessengerHelper = $container->get(FlashMessengerHelperService::class); + $consultantRegistrationSession = $container->get(ConsultantRegistration::class); + $formatDataMapper = $container->get(CreateAccountMapper::class); + + return new ConsultantRegistrationController( + $niTextTranslationUtil, + $authService, + $formHelper, + $scriptFactory, + $translationHelper, + $urlHelper, + $flashMessengerHelper, + $consultantRegistrationSession, + $formatDataMapper + ); + } +} diff --git a/app/selfserve/module/Olcs/src/Controller/Factory/UserRegistrationControllerFactory.php b/app/selfserve/module/Olcs/src/Controller/Factory/UserRegistrationControllerFactory.php index c465e8bd24..d3d459e478 100644 --- a/app/selfserve/module/Olcs/src/Controller/Factory/UserRegistrationControllerFactory.php +++ b/app/selfserve/module/Olcs/src/Controller/Factory/UserRegistrationControllerFactory.php @@ -8,6 +8,8 @@ use Common\Service\Helper\UrlHelperService; use Common\Service\Script\ScriptFactory; use Dvsa\Olcs\Utils\Translation\NiTextTranslation; +use Olcs\Controller\Mapper\CreateAccountMapper; +use Olcs\Session\ConsultantRegistration; use Psr\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; use Olcs\Controller\UserRegistrationController; @@ -30,6 +32,7 @@ public function __invoke(ContainerInterface $container, $requestedName, array $o $translationHelper = $container->get(TranslationHelperService::class); $urlHelper = $container->get(UrlHelperService::class); $flashMessengerHelper = $container->get(FlashMessengerHelperService::class); + $formatDataMapper = $container->get(CreateAccountMapper::class); return new UserRegistrationController( $niTextTranslationUtil, @@ -38,7 +41,8 @@ public function __invoke(ContainerInterface $container, $requestedName, array $o $scriptFactory, $translationHelper, $urlHelper, - $flashMessengerHelper + $flashMessengerHelper, + $formatDataMapper ); } } diff --git a/app/selfserve/module/Olcs/src/Controller/Mapper/CreateAccountMapper.php b/app/selfserve/module/Olcs/src/Controller/Mapper/CreateAccountMapper.php new file mode 100644 index 0000000000..43f6f56787 --- /dev/null +++ b/app/selfserve/module/Olcs/src/Controller/Mapper/CreateAccountMapper.php @@ -0,0 +1,47 @@ +handleQuery( + IsEnabledQry::create(['ids' => [FeatureToggle::TRANSPORT_CONSULTANT_ROLE]]) + )->getResult()['isEnabled']) { + // If the feature toggle is enabled, start the TC journey in new controller + return $this->forward()->dispatch(ConsultantRegistrationController::class, ['action' => 'add']); + } else { + // If disabled, start the normal add journey in this controller + return $this->forward()->dispatch(static::class, ['action' => 'add']); + } + } + /** * Method used for the registration form page * @@ -49,7 +71,7 @@ public function addAction() return $this->redirectToHome(); } - $postData = $this->formatPostData( + $postData = $this->formatDataMapper->formatPostData( $this->params()->fromPost() ); @@ -67,6 +89,7 @@ public function addAction() ] ); $view->setTemplate('olcs/user-registration/index'); + $this->placeholder()->setPlaceholder('pageTitle', 'page.title.user-registration.add'); $this->scriptFactory->loadFile('user-registration'); @@ -275,7 +298,7 @@ private function createUserWithOrg($formData) */ private function createUser($formData) { - $data = $this->formatSaveData($formData); + $data = $this->formatDataMapper->formatSaveData($formData); $response = $this->handleCommand( RegisterDto::create($data) @@ -299,51 +322,6 @@ private function createUser($formData) return $this->generateContentForUserRegistration($formData, $errors); } - /** - * Formats the data from what's in the form to what the service needs. - * This is mapping, not business logic. - * - * @param array $data Posted form data - * - * @return array - */ - private function formatSaveData($data) - { - $output = []; - $output['loginId'] = $data['fields']['loginId']; - $output['translateToWelsh'] = $data['fields']['translateToWelsh']; - $output['contactDetails']['emailAddress'] = $data['fields']['emailAddress']; - $output['contactDetails']['person']['familyName'] = $data['fields']['familyName']; - $output['contactDetails']['person']['forename'] = $data['fields']['forename']; - - if ('Y' === $data['fields']['isLicenceHolder']) { - $output['licenceNumber'] = $data['fields']['licenceNumber']; - } else { - $output['organisationName'] = $data['fields']['organisationName']; - $output['businessType'] = $data['fields']['businessType']; - } - - return $output; - } - - /** - * A radio button is used and validated only if a checkbox is selected. - * As browsers by default do not post the value or default value of a radio - * button. We specify an empty input for this field. - * - * @param array $postData Data from posted form - * - * @return array - */ - private function formatPostData(array $postData) - { - if (empty($postData['fields']['businessType'])) { - $postData['fields']['businessType'] = null; - } - - return $postData; - } - /** * Redirects to home * diff --git a/app/selfserve/module/Olcs/src/Form/Model/Fieldset/ContinueButton.php b/app/selfserve/module/Olcs/src/Form/Model/Fieldset/ContinueButton.php new file mode 100644 index 0000000000..a461a78175 --- /dev/null +++ b/app/selfserve/module/Olcs/src/Form/Model/Fieldset/ContinueButton.php @@ -0,0 +1,24 @@ +get(ConsultantRegistration::class); + return new UniqueConsultantDetails($session, $options); + } +} diff --git a/app/selfserve/module/Olcs/src/Form/Validator/UniqueConsultantDetails.php b/app/selfserve/module/Olcs/src/Form/Validator/UniqueConsultantDetails.php new file mode 100644 index 0000000000..4911abdcab --- /dev/null +++ b/app/selfserve/module/Olcs/src/Form/Validator/UniqueConsultantDetails.php @@ -0,0 +1,39 @@ + '%value% was used for the operator administrator account. You must use a different username and email for your consultant account.' + ]; + + protected $session; + + public function __construct(ConsultantRegistration $session) + { + $this->session = $session; + parent::__construct(); + } + + public function isValid($value, $context = null) + { + $this->setValue($value); + + $operatorDetails = $this->session->getOperatorDetails(); + + if ( + ($value === $operatorDetails['fields']['loginId']) + || ($value === $operatorDetails['fields']['emailAddress']) + ) { + $this->error(self::NOT_UNIQUE); + return false; + } + return true; + } +} diff --git a/app/selfserve/module/Olcs/src/Session/ConsultantRegistration.php b/app/selfserve/module/Olcs/src/Session/ConsultantRegistration.php new file mode 100644 index 0000000000..d36047d132 --- /dev/null +++ b/app/selfserve/module/Olcs/src/Session/ConsultantRegistration.php @@ -0,0 +1,47 @@ + + */ +class ConsultantRegistration extends \Laminas\Session\Container +{ + public const SESSION_NAME = 'ConsultantRegistration'; + protected const OPERATOR_DETAILS = 'operatorDetails'; + protected const CONSULTANT_DETAILS = 'consultantDetails'; + + public function __construct() + { + parent::__construct(self::SESSION_NAME); + } + + public function setOperatorDetails(array $details): self + { + $this->offsetSet(self::OPERATOR_DETAILS, $details); + return $this; + } + + public function getOperatorDetails(): ?array + { + return $this->offsetGet(self::OPERATOR_DETAILS); + } + + public function setConsultantDetails(array $details): self + { + $this->offsetSet(self::CONSULTANT_DETAILS, $details); + return $this; + } + + public function getConsultantDetails(): ?array + { + return $this->offsetGet(self::CONSULTANT_DETAILS); + } + + public function clear(): void + { + $this->getManager()->getStorage()->clear(self::SESSION_NAME); + } +} diff --git a/app/selfserve/module/Olcs/view/olcs/user-registration/check-email-consultant.phtml b/app/selfserve/module/Olcs/view/olcs/user-registration/check-email-consultant.phtml new file mode 100644 index 0000000000..8a5fa7d23a --- /dev/null +++ b/app/selfserve/module/Olcs/view/olcs/user-registration/check-email-consultant.phtml @@ -0,0 +1,38 @@ +partial( + 'partials/page-header-simple', + [ + 'pageTitle' => $this->pageTitle(), + 'pageHeaderText' => + $this->translateReplace( + 'user-registration.page.check-email-consultant-inset.content', + [ + $this->consultantEmailAddress, + $this->operatorEmailAddress + ] + ), + 'pageHeaderTextEscape' => false, + ] +); +?> + ++ translateReplace( + 'user-registration.page.check-email-consultant.content', + [ + $this->url('auth/login/GET') + ] + ); ?> +
+Sign in +
+ translate('markup-problems-signing-in');?> +