Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
TripInvitation
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
12 / 12
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 invite
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 accept
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 decline
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tripId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 inviteeId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 inviterId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 role
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 status
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isPending
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createdAt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace App\Trip\Domain;
6
7use App\Trip\Domain\ValueObject\InvitationId;
8use App\Trip\Domain\ValueObject\InvitationStatus;
9use App\Trip\Domain\ValueObject\MemberId;
10use App\Trip\Domain\ValueObject\TripId;
11use App\Trip\Domain\ValueObject\TripRole;
12use DateTimeImmutable;
13use Doctrine\ORM\Mapping as ORM;
14use InvalidArgumentException;
15
16/**
17 * An owner's invitation for an existing user to join a trip as editor/viewer.
18 * Accepting mints the TripMember row; the invitation itself never grants access,
19 * it only records intent + outcome. Surrogate id (InvitationId) because the
20 * inbox/accept/decline endpoints reference an invitation directly.
21 */
22#[ORM\Entity]
23#[ORM\Table(name: 'trip_invitations')]
24final class TripInvitation
25{
26    #[ORM\Column(name: 'status', type: 'invitation_status', length: 20)]
27    private InvitationStatus $status;
28
29    public function __construct(
30        #[ORM\Id]
31        #[ORM\Column(type: 'invitation_id')]
32        private readonly InvitationId $id,
33        #[ORM\Column(name: 'trip_id', type: 'trip_id')]
34        private readonly TripId $tripId,
35        #[ORM\Column(name: 'invitee_id', type: 'member_id')]
36        private readonly MemberId $inviteeId,
37        #[ORM\Column(name: 'inviter_id', type: 'member_id')]
38        private readonly MemberId $inviterId,
39        #[ORM\Column(type: 'trip_role', length: 20)]
40        private readonly TripRole $role,
41        #[ORM\Column(name: 'created_at', type: 'datetime_immutable')]
42        private readonly DateTimeImmutable $createdAt = new DateTimeImmutable(),
43    ) {
44        if (!$role->isStorableMembership()) {
45            throw new InvalidArgumentException('A trip invitation can only offer editor or viewer.');
46        }
47        $this->status = InvitationStatus::PENDING;
48    }
49
50    public static function invite(
51        TripId $tripId,
52        MemberId $inviteeId,
53        MemberId $inviterId,
54        TripRole $role,
55    ): self {
56        return new self(InvitationId::generate(), $tripId, $inviteeId, $inviterId, $role);
57    }
58
59    // ponytail: no state guard here — the accept/decline handlers only ever load
60    // PENDING invitations (a resolved one reads as "not found"), so these can't be
61    // reached on an already-resolved row. Add a guard if a non-pending path appears.
62    public function accept(): void
63    {
64        $this->status = InvitationStatus::ACCEPTED;
65    }
66
67    public function decline(): void
68    {
69        $this->status = InvitationStatus::DECLINED;
70    }
71
72    public function id(): InvitationId
73    {
74        return $this->id;
75    }
76
77    public function tripId(): TripId
78    {
79        return $this->tripId;
80    }
81
82    public function inviteeId(): MemberId
83    {
84        return $this->inviteeId;
85    }
86
87    public function inviterId(): MemberId
88    {
89        return $this->inviterId;
90    }
91
92    public function role(): TripRole
93    {
94        return $this->role;
95    }
96
97    public function status(): InvitationStatus
98    {
99        return $this->status;
100    }
101
102    public function isPending(): bool
103    {
104        return $this->status === InvitationStatus::PENDING;
105    }
106
107    public function createdAt(): DateTimeImmutable
108    {
109        return $this->createdAt;
110    }
111}