Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
InviteMemberHandler
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
2 / 2
7
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handle
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace App\Trip\Application\Command;
6
7use App\Trip\Application\Port\MemberDirectory;
8use App\Trip\Application\TripAccess;
9use App\Trip\Domain\Exceptions\AlreadyMemberOrInvited;
10use App\Trip\Domain\Exceptions\InviteeNotFound;
11use App\Trip\Domain\TripInvitation;
12use App\Trip\Domain\TripInvitationRepository;
13use App\Trip\Domain\TripMemberRepository;
14use App\Trip\Domain\ValueObject\InvitationId;
15use App\Trip\Domain\ValueObject\MemberId;
16use App\Trip\Domain\ValueObject\OwnerId;
17use App\Trip\Domain\ValueObject\TripId;
18use App\Trip\Domain\ValueObject\TripRole;
19use InvalidArgumentException;
20
21final readonly class InviteMemberHandler
22{
23    public function __construct(
24        private TripAccess $access,
25        private MemberDirectory $directory,
26        private TripMemberRepository $members,
27        private TripInvitationRepository $invitations,
28    ) {
29    }
30
31    public function handle(InviteMemberCommand $command): InvitationId
32    {
33        // Owner-only: stranger -> TripNotFound (404), non-owner member -> TripAccessDenied (403).
34        $trip = $this->access->getForManage(new TripId($command->tripId), $command->inviterUserId);
35
36        $role = TripRole::tryFrom($command->role);
37        if ($role === null || !$role->isStorableMembership()) {
38            throw new InvalidArgumentException('Role must be editor or viewer.');
39        }
40
41        $inviteeId = $this->directory->findIdByEmail($command->email)
42            ?? throw InviteeNotFound::withEmail($command->email);
43
44        $alreadyOwner = $trip->isOwnedBy(new OwnerId($inviteeId->value()));
45        $alreadyMember = $this->members->roleOf($trip->id(), $inviteeId) !== null;
46        $alreadyInvited = $this->invitations->pendingFor($trip->id(), $inviteeId) !== null;
47
48        if ($alreadyOwner || $alreadyMember || $alreadyInvited) {
49            throw AlreadyMemberOrInvited::create();
50        }
51
52        $invitation = TripInvitation::invite(
53            $trip->id(),
54            $inviteeId,
55            new MemberId($command->inviterUserId),
56            $role,
57        );
58        $this->invitations->save($invitation);
59
60        return $invitation->id();
61    }
62}