Skip to content

Commit

Permalink
Create invitation
Browse files Browse the repository at this point in the history
  • Loading branch information
mmarchois committed Feb 10, 2025
1 parent 2d61ac2 commit 035606b
Show file tree
Hide file tree
Showing 37 changed files with 1,319 additions and 92 deletions.
23 changes: 23 additions & 0 deletions config/validator/Application/User/CreateInvitationCommand.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
<class name="App\Application\User\Command\CreateInvitationCommand">
<property name="fullName">
<constraint name="NotBlank"/>
<constraint name="Length">
<option name="max">255</option>
</constraint>
</property>
<property name="email">
<constraint name="NotBlank"/>
<constraint name="Email"/>
<constraint name="Length">
<option name="max">255</option>
</constraint>
</property>
<property name="role">
<constraint name="NotBlank"/>
</property>
</class>
</constraint-mapping>
22 changes: 22 additions & 0 deletions src/Application/User/Command/CreateInvitationCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command;

use App\Application\CommandInterface;
use App\Domain\User\Organization;
use App\Domain\User\User;

final class CreateInvitationCommand implements CommandInterface
{
public function __construct(
public Organization $organization,
public User $owner,
) {
}

public ?string $fullName = null;
public ?string $email = null;
public ?string $role = null;
}
51 changes: 51 additions & 0 deletions src/Application/User/Command/CreateInvitationCommandHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command;

use App\Application\DateUtilsInterface;
use App\Application\IdFactoryInterface;
use App\Application\StringUtilsInterface;
use App\Domain\User\Exception\InvitationAlreadyExistsException;
use App\Domain\User\Exception\OrganizationUserAlreadyExistException;
use App\Domain\User\Invitation;
use App\Domain\User\Repository\InvitationRepositoryInterface;
use App\Infrastructure\Persistence\Doctrine\Repository\User\OrganizationUserRepository;

final readonly class CreateInvitationCommandHandler
{
public function __construct(
private InvitationRepositoryInterface $invitationRepository,
private OrganizationUserRepository $organizationUserRepository,
private IdFactoryInterface $idFactory,
private DateUtilsInterface $dateUtils,
private StringUtilsInterface $stringUtils,
) {
}

public function __invoke(CreateInvitationCommand $command): Invitation
{
$email = $this->stringUtils->normalizeEmail($command->email);

if ($this->invitationRepository->findOneByEmailAndOrganization($email, $command->organization)) {
throw new InvitationAlreadyExistsException();
}

if ($this->organizationUserRepository->findByEmailAndOrganization($email, $command->organization->getUuid())) {
throw new OrganizationUserAlreadyExistException();
}

return $this->invitationRepository->add(
new Invitation(
uuid: $this->idFactory->make(),
email: $email,
role: $command->role,
fullName: $command->fullName,
createdAt: $this->dateUtils->getNow(),
owner: $command->owner,
organization: $command->organization,
),
);
}
}
17 changes: 17 additions & 0 deletions src/Application/User/Command/JoinOrganizationCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command;

use App\Application\CommandInterface;
use App\Domain\User\User;

final readonly class JoinOrganizationCommand implements CommandInterface
{
public function __construct(
public string $invitationUuid,
public User $user,
) {
}
}
50 changes: 50 additions & 0 deletions src/Application/User/Command/JoinOrganizationCommandHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command;

use App\Application\IdFactoryInterface;
use App\Domain\User\Exception\InvitationNotFoundException;
use App\Domain\User\Exception\OrganizationUserAlreadyExistException;
use App\Domain\User\Invitation;
use App\Domain\User\OrganizationUser;
use App\Domain\User\Repository\InvitationRepositoryInterface;
use App\Domain\User\Repository\OrganizationUserRepositoryInterface;

final readonly class JoinOrganizationCommandHandler
{
public function __construct(
private InvitationRepositoryInterface $invitationRepository,
private OrganizationUserRepositoryInterface $organizationUserRepository,
private IdFactoryInterface $idFactory,
) {
}

public function __invoke(JoinOrganizationCommand $command): void
{
$invitation = $this->invitationRepository->findOneByUuid($command->invitationUuid);
if (!$invitation instanceof Invitation) {
throw new InvitationNotFoundException();
}

$user = $command->user;
$organization = $invitation->getOrganization();

$userOrganization = $this->organizationUserRepository
->findOrganizationUser($organization->getUuid(), $user->getUuid());

if ($userOrganization instanceof OrganizationUser) {
throw new OrganizationUserAlreadyExistException();
}

$this->organizationUserRepository->add(
(new OrganizationUser($this->idFactory->make()))
->setUser($user)
->setOrganization($organization)
->setRoles($invitation->getRole()),
);

$this->invitationRepository->delete($invitation);
}
}
16 changes: 16 additions & 0 deletions src/Application/User/Command/Mail/SendInvitationMailCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command\Mail;

use App\Application\AsyncCommandInterface;
use App\Domain\User\Invitation;

final class SendInvitationMailCommand implements AsyncCommandInterface
{
public function __construct(
public readonly Invitation $invitation,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command\Mail;

use App\Application\MailerInterface;
use App\Domain\Mail;

final readonly class SendInvitationMailCommandHandler
{
public function __construct(
private MailerInterface $mailer,
) {
}

public function __invoke(SendInvitationMailCommand $command): void
{
$invitation = $command->invitation;

$this->mailer->send(
new Mail(
address: $invitation->getEmail(),
subject: 'organization_invitation.subject',
template: 'email/user/organization_invitation.html.twig',
payload: [
'fullName' => $invitation->getFullName(),
'invitedBy' => $invitation->getOwner()->getFullName(),
'organizationName' => $invitation->getOrganization()->getName(),
'invitationUuid' => $invitation->getUuid(),
],
),
);
}
}
15 changes: 15 additions & 0 deletions src/Application/User/Query/GetInvitationsQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Query;

use App\Application\QueryInterface;

final class GetInvitationsQuery implements QueryInterface
{
public function __construct(
public readonly string $organizationUuid,
) {
}
}
20 changes: 20 additions & 0 deletions src/Application/User/Query/GetInvitationsQueryHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Query;

use App\Domain\User\Repository\InvitationRepositoryInterface;

final class GetInvitationsQueryHandler
{
public function __construct(
private InvitationRepositoryInterface $invitationRepository,
) {
}

public function __invoke(GetInvitationsQuery $query): array
{
return $this->invitationRepository->findByOrganizationUuid($query->organizationUuid);
}
}
16 changes: 16 additions & 0 deletions src/Application/User/View/InvitationView.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace App\Application\User\View;

final readonly class InvitationView
{
public function __construct(
public string $uuid,
public string $fullName,
public string $email,
public string $role,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace App\Domain\User\Exception;

final class InvitationAlreadyExistsException extends \Exception
{
}
9 changes: 9 additions & 0 deletions src/Domain/User/Exception/InvitationNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace App\Domain\User\Exception;

final class InvitationNotFoundException extends \Exception
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace App\Domain\User\Exception;

final class OrganizationUserAlreadyExistException extends \Exception
{
}
21 changes: 21 additions & 0 deletions src/Domain/User/Repository/InvitationRepositoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Domain\User\Repository;

use App\Domain\User\Invitation;
use App\Domain\User\Organization;

interface InvitationRepositoryInterface
{
public function add(Invitation $invitation): Invitation;

public function delete(Invitation $invitation): void;

public function findOneByEmailAndOrganization(string $email, Organization $organization): ?Invitation;

public function findByOrganizationUuid(string $organizationUuid): array;

public function findOneByUuid(string $uuid): ?Invitation;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace App\Infrastructure\Controller\MyArea\Organization\User;

use App\Application\CommandBusInterface;
use App\Application\User\Command\JoinOrganizationCommand;
use App\Domain\User\Exception\InvitationNotFoundException;
use App\Domain\User\Exception\OrganizationUserAlreadyExistException;
use App\Infrastructure\Security\AuthenticatedUser;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

final class AcceptInvitationController
{
public function __construct(
private CommandBusInterface $commandBus,
private RouterInterface $router,
private AuthenticatedUser $authenticatedUser,
private TranslatorInterface $translator,
) {
}

#[Route(
'/invitations/{uuid}/accept',
name: 'app_invitation_accept',
requirements: ['uuid' => Requirement::UUID],
methods: ['GET'],
)]
public function __invoke(Request $request, string $uuid): Response
{
/** @var FlashBagAwareSessionInterface */
$session = $request->getSession();
$user = $this->authenticatedUser->getUser();

try {
$this->commandBus->handle(new JoinOrganizationCommand($uuid, $user));
$session->getFlashBag()->add('success', $this->translator->trans('invitation.accept.success'));

return new RedirectResponse(
url: $this->router->generate('app_my_area'),
status: Response::HTTP_SEE_OTHER,
);
} catch (InvitationNotFoundException) {
throw new NotFoundHttpException();
} catch (OrganizationUserAlreadyExistException) {
$session->getFlashBag()->add('error', $this->translator->trans('invitation.accept.error'));

return new RedirectResponse(
url: $this->router->generate('app_my_area'),
status: Response::HTTP_SEE_OTHER,
);
}
}
}
Loading

0 comments on commit 035606b

Please sign in to comment.