From 1f2cd295f9c51b1d90c5f0440232c76ea214efb0 Mon Sep 17 00:00:00 2001 From: Bjarn Bronsveld Date: Wed, 12 Aug 2026 23:12:39 +0200 Subject: [PATCH 1/2] Sync SDK with current OpenAPI specifications --- src/Endpoints/ProjectsEndpoint.php | 27 -------- src/Endpoints/TeamEndpoint.php | 22 +++++++ src/Objects/DomainData.php | 6 +- src/Objects/DomainDnsRecordData.php | 3 + src/Objects/DomainListData.php | 1 + src/Objects/MessageListData.php | 8 ++- src/Objects/ProjectData.php | 13 ++-- src/Objects/ProjectListData.php | 5 +- src/Objects/RouteData.php | 15 +++-- src/Objects/RouteStatisticData.php | 3 + src/Objects/StatsDailyData.php | 11 ++-- src/Objects/StatsTotalsData.php | 11 ++-- src/Objects/StoreWebhookData.php | 2 +- src/Objects/SuppressedRecipientData.php | 5 +- src/Objects/SuppressionSourceMessageData.php | 16 +++++ src/Objects/TeamData.php | 7 +- src/Objects/TeamMemberData.php | 8 ++- src/Objects/TeamMemberProjectAccessData.php | 14 ++++ src/Objects/TeamRoleData.php | 17 +++++ src/Objects/UpdateProjectMembersData.php | 13 ---- src/Objects/UpdateRouteSettingsData.php | 4 +- .../UpdateTeamMemberAssignmentData.php | 14 ++++ src/Objects/UpdateWebhookData.php | 2 +- src/Objects/UserData.php | 16 ----- src/Objects/WebhookDeliveryData.php | 2 +- src/Objects/WebhookDeliveryListData.php | 2 +- src/Objects/WebhookListData.php | 2 +- src/Responses/DeleteSuppressionResponse.php | 3 + src/Responses/DomainResponse.php | 5 +- src/Responses/ProjectMemberResponse.php | 13 ---- src/Responses/ProjectResponse.php | 11 ++-- src/Responses/RouteResponse.php | 15 +++-- .../TeamMembersAssignmentUpdateResponse.php | 21 ++++++ src/Responses/TeamMembersShowResponse.php | 21 ++++++ src/Responses/TeamResponse.php | 6 +- src/Responses/TeamRolesResponse.php | 16 +++++ .../UpdateProjectMembersResponse.php | 16 ----- src/Responses/VerifyDnsRecordsResponse.php | 1 + src/Responses/WebhookDeliveryResponse.php | 2 +- src/Types/ApiTypes.php | 65 +++++++++++-------- tests/Endpoints/ProjectsEndpointTest.php | 19 ------ tests/Endpoints/TeamEndpointTest.php | 22 +++++++ tests/SdkCoverageTest.php | 24 ++++--- 43 files changed, 311 insertions(+), 198 deletions(-) create mode 100644 src/Objects/SuppressionSourceMessageData.php create mode 100644 src/Objects/TeamMemberProjectAccessData.php create mode 100644 src/Objects/TeamRoleData.php delete mode 100644 src/Objects/UpdateProjectMembersData.php create mode 100644 src/Objects/UpdateTeamMemberAssignmentData.php delete mode 100644 src/Objects/UserData.php delete mode 100644 src/Responses/ProjectMemberResponse.php create mode 100644 src/Responses/TeamMembersAssignmentUpdateResponse.php create mode 100644 src/Responses/TeamMembersShowResponse.php create mode 100644 src/Responses/TeamRolesResponse.php delete mode 100644 src/Responses/UpdateProjectMembersResponse.php diff --git a/src/Endpoints/ProjectsEndpoint.php b/src/Endpoints/ProjectsEndpoint.php index 4d1f09a..795986e 100644 --- a/src/Endpoints/ProjectsEndpoint.php +++ b/src/Endpoints/ProjectsEndpoint.php @@ -6,17 +6,14 @@ use Lettermint\Responses\CreateRouteResponse; use Lettermint\Responses\DeleteProjectResponse; use Lettermint\Responses\ProjectListResponse; -use Lettermint\Responses\ProjectMemberResponse; use Lettermint\Responses\ProjectResponse; use Lettermint\Responses\ProjectRoutesResponse; use Lettermint\Responses\RotateProjectTokenResponse; -use Lettermint\Responses\UpdateProjectMembersResponse; use Lettermint\Responses\UpdateProjectResponse; /** * @phpstan-import-type StoreProjectData from \Lettermint\Types\ApiTypes * @phpstan-import-type UpdateProjectData from \Lettermint\Types\ApiTypes - * @phpstan-import-type UpdateProjectMembersData from \Lettermint\Types\ApiTypes * @phpstan-import-type StoreRouteData from \Lettermint\Types\ApiTypes */ class ProjectsEndpoint extends Endpoint @@ -57,30 +54,6 @@ public function rotateToken(string $projectId): RotateProjectTokenResponse return $this->hydrate(RotateProjectTokenResponse::class, $this->postArray($this->path('/projects/{projectId}/rotate-token', ['projectId' => $projectId]), [], [])); } - /** - * @phpstan-param UpdateProjectMembersData $data - */ - public function updateMembers(string $projectId, array $data): UpdateProjectMembersResponse - { - return $this->hydrate(UpdateProjectMembersResponse::class, $this->putArray($this->path('/projects/{projectId}/members', ['projectId' => $projectId]), $data, [])); - } - - public function addMember(string $projectId, string $teamMemberId): ProjectMemberResponse - { - return $this->hydrate(ProjectMemberResponse::class, $this->postArray($this->path('/projects/{projectId}/members/{teamMemberId}', [ - 'projectId' => $projectId, - 'teamMemberId' => $teamMemberId, - ]), [], [])); - } - - public function removeMember(string $projectId, string $teamMemberId): ProjectMemberResponse - { - return $this->hydrate(ProjectMemberResponse::class, $this->deleteArray($this->path('/projects/{projectId}/members/{teamMemberId}', [ - 'projectId' => $projectId, - 'teamMemberId' => $teamMemberId, - ]), [])); - } - public function routes(string $projectId, array $query = []): ProjectRoutesResponse { return $this->hydrate(ProjectRoutesResponse::class, $this->getArray($this->path('/projects/{projectId}/routes', ['projectId' => $projectId]), $query)); diff --git a/src/Endpoints/TeamEndpoint.php b/src/Endpoints/TeamEndpoint.php index 454e5f4..5f1d4bf 100644 --- a/src/Endpoints/TeamEndpoint.php +++ b/src/Endpoints/TeamEndpoint.php @@ -2,13 +2,17 @@ namespace Lettermint\Endpoints; +use Lettermint\Responses\TeamMembersAssignmentUpdateResponse; use Lettermint\Responses\TeamMembersResponse; +use Lettermint\Responses\TeamMembersShowResponse; use Lettermint\Responses\TeamResponse; +use Lettermint\Responses\TeamRolesResponse; use Lettermint\Responses\TeamUsageResponse; use Lettermint\Responses\UpdateTeamResponse; /** * @phpstan-import-type UpdateTeamData from \Lettermint\Types\ApiTypes + * @phpstan-import-type UpdateTeamMemberAssignmentData from \Lettermint\Types\ApiTypes */ class TeamEndpoint extends Endpoint { @@ -30,8 +34,26 @@ public function usage(): TeamUsageResponse return $this->hydrate(TeamUsageResponse::class, $this->getArray($this->path('/team/usage'), [])); } + public function roles(): TeamRolesResponse + { + return $this->hydrate(TeamRolesResponse::class, $this->getArray($this->path('/team/roles'), [])); + } + public function members(array $query = []): TeamMembersResponse { return $this->hydrate(TeamMembersResponse::class, $this->getArray($this->path('/team/members'), $query)); } + + public function member(string $userId): TeamMembersShowResponse + { + return $this->hydrate(TeamMembersShowResponse::class, $this->getArray($this->path('/team/members/{userId}', ['userId' => $userId]), [])); + } + + /** + * @phpstan-param UpdateTeamMemberAssignmentData $data + */ + public function updateMemberAssignment(string $userId, array $data): TeamMembersAssignmentUpdateResponse + { + return $this->hydrate(TeamMembersAssignmentUpdateResponse::class, $this->putArray($this->path('/team/members/{userId}/assignment', ['userId' => $userId]), $data, [])); + } } diff --git a/src/Objects/DomainData.php b/src/Objects/DomainData.php index 48900d8..288e687 100644 --- a/src/Objects/DomainData.php +++ b/src/Objects/DomainData.php @@ -7,14 +7,16 @@ /** * @property string $id * @property string $domain + * @property 'legacy_txt'|'managed_cname' $dkim_mode + * @property bool $rotation_ready * @property string|null $status_changed_at - * @property list<\Lettermint\Objects\DomainDnsRecordData> $dns_records + * @property list $dns_records * @property list> $projects * @property string $created_at */ final class DomainData extends Resource { protected static array $casts = [ - 'dns_records' => [\Lettermint\Objects\DomainDnsRecordData::class], + 'dns_records' => [DomainDnsRecordData::class], ]; } diff --git a/src/Objects/DomainDnsRecordData.php b/src/Objects/DomainDnsRecordData.php index e5ee22f..1bde524 100644 --- a/src/Objects/DomainDnsRecordData.php +++ b/src/Objects/DomainDnsRecordData.php @@ -11,6 +11,9 @@ * @property string $fqdn * @property string $content * @property 'active'|'failed'|'pending' $status + * @property 'return_path'|'dmarc'|'dkim_legacy'|'dkim_primary'|'dkim_secondary' $purpose + * @property 'required'|'recommended'|'migration'|'deprecated' $verification_scope + * @property bool $required_for_verification * @property string|null $verified_at * @property string|null $last_checked_at */ diff --git a/src/Objects/DomainListData.php b/src/Objects/DomainListData.php index b829d6a..c94a698 100644 --- a/src/Objects/DomainListData.php +++ b/src/Objects/DomainListData.php @@ -8,6 +8,7 @@ * @property string $id * @property string $domain * @property 'verified'|'partially_verified'|'pending_verification'|'failed_verification' $status + * @property 'legacy_txt'|'managed_cname' $dkim_mode * @property string|null $status_changed_at * @property string $created_at */ diff --git a/src/Objects/MessageListData.php b/src/Objects/MessageListData.php index 21b1d30..248f281 100644 --- a/src/Objects/MessageListData.php +++ b/src/Objects/MessageListData.php @@ -8,14 +8,16 @@ * @property string $id * @property 'inbound'|'outbound' $type * @property 'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed' $status + * @property float|int|null $spam_score * @property string $from_email * @property string|null $from_name * @property string|null $subject - * @property list<\Lettermint\Objects\MessageRecipientData>|null $to - * @property list<\Lettermint\Objects\MessageRecipientData>|null $cc - * @property list<\Lettermint\Objects\MessageRecipientData>|null $bcc + * @property list|null $to + * @property list|null $cc + * @property list|null $bcc * @property list|null $reply_to * @property string|null $tag + * @property string|null $status_changed_at * @property string $created_at */ final class MessageListData extends Resource diff --git a/src/Objects/ProjectData.php b/src/Objects/ProjectData.php index 059783c..d7bec33 100644 --- a/src/Objects/ProjectData.php +++ b/src/Objects/ProjectData.php @@ -13,21 +13,18 @@ * @property string|null $token_generated_at * @property string|null $token_last_used_at * @property string|null $token_last_used_ip - * @property list<\Lettermint\Objects\RouteListData> $routes + * @property list $routes * @property int $routes_count - * @property list<\Lettermint\Objects\DomainData> $domains + * @property list $domains * @property int $domains_count - * @property list<\Lettermint\Objects\TeamMemberData> $team_members - * @property int $team_members_count - * @property \Lettermint\Objects\MessageStatsData|mixed $last_28_days + * @property MessageStatsData|mixed $last_28_days * @property string $created_at * @property string $updated_at */ final class ProjectData extends Resource { protected static array $casts = [ - 'routes' => [\Lettermint\Objects\RouteData::class], - 'domains' => [\Lettermint\Objects\DomainData::class], - 'team_members' => [\Lettermint\Objects\TeamMemberData::class], + 'routes' => [RouteData::class], + 'domains' => [DomainData::class], ]; } diff --git a/src/Objects/ProjectListData.php b/src/Objects/ProjectListData.php index ae33d5d..4ed7af1 100644 --- a/src/Objects/ProjectListData.php +++ b/src/Objects/ProjectListData.php @@ -10,14 +10,13 @@ * @property bool $smtp_enabled * @property int $routes_count * @property int $domains_count - * @property int $team_members_count - * @property \Lettermint\Objects\MessageStatsData $last_28_days + * @property MessageStatsData $last_28_days * @property string $created_at * @property string $updated_at */ final class ProjectListData extends Resource { protected static array $casts = [ - 'last_28_days' => \Lettermint\Objects\MessageStatsData::class, + 'last_28_days' => MessageStatsData::class, ]; } diff --git a/src/Objects/RouteData.php b/src/Objects/RouteData.php index 94a0523..0280455 100644 --- a/src/Objects/RouteData.php +++ b/src/Objects/RouteData.php @@ -11,21 +11,22 @@ * @property string $name * @property 'transactional'|'broadcast'|'inbound' $route_type * @property bool $is_default - * @property string $inbound_address - * @property string $inbound_domain - * @property string $inbound_domain_verified_at - * @property float|int $inbound_spam_threshold + * @property string|null $inbound_address + * @property string|null $inbound_domain + * @property string|null $inbound_domain_verified_at + * @property float|int|null $inbound_spam_threshold * @property 'inline'|'url' $attachment_delivery - * @property \Lettermint\Objects\ProjectListData $project + * @property array|mixed $settings + * @property ProjectListData $project * @property int $webhooks_count * @property int $suppressed_recipients_count - * @property array|list<\Lettermint\Objects\RouteStatisticData> $statistics + * @property array|list $statistics * @property string $created_at * @property string $updated_at */ final class RouteData extends Resource { protected static array $casts = [ - 'project' => \Lettermint\Objects\ProjectData::class, + 'project' => ProjectData::class, ]; } diff --git a/src/Objects/RouteStatisticData.php b/src/Objects/RouteStatisticData.php index 05fb961..5ddc42e 100644 --- a/src/Objects/RouteStatisticData.php +++ b/src/Objects/RouteStatisticData.php @@ -13,6 +13,9 @@ * @property int $hard_bounce_count * @property int $spam_complaint_count * @property int $inbound_received_count + * @property int|null $observed_opened_count + * @property int|null $human_opened_count + * @property int|null $privacy_opened_count * @property int|null $effective_opened_count * @property int|null $machine_opened_count * @property int|null $machine_clicked_count diff --git a/src/Objects/StatsDailyData.php b/src/Objects/StatsDailyData.php index eed5f81..f44765e 100644 --- a/src/Objects/StatsDailyData.php +++ b/src/Objects/StatsDailyData.php @@ -12,9 +12,12 @@ * @property int $spam_complaints * @property int|null $opened * @property int|null $clicked - * @property \Lettermint\Objects\StatsInboundData $inbound - * @property \Lettermint\Objects\StatsTypeData|mixed $transactional - * @property \Lettermint\Objects\StatsTypeData|mixed $broadcast + * @property StatsInboundData $inbound + * @property StatsTypeData|mixed $transactional + * @property StatsTypeData|mixed $broadcast + * @property int|null $observed_opened + * @property int|null $human_opened + * @property int|null $privacy_opened * @property int|null $effective_opened * @property int|null $machine_opened * @property int|null $machine_clicked @@ -22,6 +25,6 @@ final class StatsDailyData extends Resource { protected static array $casts = [ - 'inbound' => \Lettermint\Objects\StatsInboundData::class, + 'inbound' => StatsInboundData::class, ]; } diff --git a/src/Objects/StatsTotalsData.php b/src/Objects/StatsTotalsData.php index 872b180..de85481 100644 --- a/src/Objects/StatsTotalsData.php +++ b/src/Objects/StatsTotalsData.php @@ -11,9 +11,12 @@ * @property int $spam_complaints * @property int|null $opened * @property int|null $clicked - * @property \Lettermint\Objects\StatsInboundData $inbound - * @property \Lettermint\Objects\StatsTypeData|mixed $transactional - * @property \Lettermint\Objects\StatsTypeData|mixed $broadcast + * @property StatsInboundData $inbound + * @property StatsTypeData|mixed $transactional + * @property StatsTypeData|mixed $broadcast + * @property int|null $observed_opened + * @property int|null $human_opened + * @property int|null $privacy_opened * @property int|null $effective_opened * @property int|null $machine_opened * @property int|null $machine_clicked @@ -21,6 +24,6 @@ final class StatsTotalsData extends Resource { protected static array $casts = [ - 'inbound' => \Lettermint\Objects\StatsInboundData::class, + 'inbound' => StatsInboundData::class, ]; } diff --git a/src/Objects/StoreWebhookData.php b/src/Objects/StoreWebhookData.php index e25046b..b0eed6a 100644 --- a/src/Objects/StoreWebhookData.php +++ b/src/Objects/StoreWebhookData.php @@ -10,7 +10,7 @@ * @property string $url * @property bool|null $enabled * @property bool|null $include_machine_events - * @property list<'message.created'|'message.sent'|'message.delivered'|'message.auto_replied'|'message.hard_bounced'|'message.soft_bounced'|'message.spam_complaint'|'message.failed'|'message.suppressed'|'message.unsubscribed'|'message.opened'|'message.clicked'|'message.inbound'|'message.policy_rejected'|'webhook.test'> $events + * @property list<'message.created'|'message.sent'|'message.delivered'|'message.auto_replied'|'message.hard_bounced'|'message.soft_bounced'|'message.spam_complaint'|'message.failed'|'message.suppressed'|'message.unsubscribed'|'message.opened'|'message.clicked'|'message.inbound'|'message.policy_rejected'|'suppression.added'|'suppression.removed'|'webhook.test'> $events */ final class StoreWebhookData extends Resource { diff --git a/src/Objects/SuppressedRecipientData.php b/src/Objects/SuppressedRecipientData.php index c0d970d..40f0949 100644 --- a/src/Objects/SuppressedRecipientData.php +++ b/src/Objects/SuppressedRecipientData.php @@ -12,10 +12,13 @@ * @property 'global'|'team'|'project'|'route' $scope * @property string|null $project_id * @property string|null $route_id + * @property SuppressionSourceMessageData|null $source_message * @property string $created_at * @property string $updated_at */ final class SuppressedRecipientData extends Resource { - // + protected static array $casts = [ + 'source_message' => SuppressionSourceMessageData::class, + ]; } diff --git a/src/Objects/SuppressionSourceMessageData.php b/src/Objects/SuppressionSourceMessageData.php new file mode 100644 index 0000000..4f793f3 --- /dev/null +++ b/src/Objects/SuppressionSourceMessageData.php @@ -0,0 +1,16 @@ + $features - * @property list<\Lettermint\Objects\TeamAddonData> $addons + * @property list $addons * @property string $created_at * @property int $domains_count * @property int $projects_count @@ -21,6 +22,6 @@ final class TeamData extends Resource { protected static array $casts = [ - 'addons' => [\Lettermint\Objects\TeamAddonData::class], + 'addons' => [TeamAddonData::class], ]; } diff --git a/src/Objects/TeamMemberData.php b/src/Objects/TeamMemberData.php index e47a6b2..640f0b4 100644 --- a/src/Objects/TeamMemberData.php +++ b/src/Objects/TeamMemberData.php @@ -6,13 +6,15 @@ /** * @property string $id - * @property \Lettermint\Objects\UserData $user - * @property string|null $role + * @property string $name + * @property string $email + * @property array $role + * @property TeamMemberProjectAccessData $project_access * @property string|null $joined_at */ final class TeamMemberData extends Resource { protected static array $casts = [ - 'user' => \Lettermint\Objects\UserData::class, + 'project_access' => TeamMemberProjectAccessData::class, ]; } diff --git a/src/Objects/TeamMemberProjectAccessData.php b/src/Objects/TeamMemberProjectAccessData.php new file mode 100644 index 0000000..5297f29 --- /dev/null +++ b/src/Objects/TeamMemberProjectAccessData.php @@ -0,0 +1,14 @@ +> $projects + */ +final class TeamMemberProjectAccessData extends Resource +{ + // +} diff --git a/src/Objects/TeamRoleData.php b/src/Objects/TeamRoleData.php new file mode 100644 index 0000000..7eecb14 --- /dev/null +++ b/src/Objects/TeamRoleData.php @@ -0,0 +1,17 @@ + $permissions + * @property bool $assignable + */ +final class TeamRoleData extends Resource +{ + // +} diff --git a/src/Objects/UpdateProjectMembersData.php b/src/Objects/UpdateProjectMembersData.php deleted file mode 100644 index e3b3a84..0000000 --- a/src/Objects/UpdateProjectMembersData.php +++ /dev/null @@ -1,13 +0,0 @@ - $team_member_ids - */ -final class UpdateProjectMembersData extends Resource -{ - // -} diff --git a/src/Objects/UpdateRouteSettingsData.php b/src/Objects/UpdateRouteSettingsData.php index 18df499..30b2c07 100644 --- a/src/Objects/UpdateRouteSettingsData.php +++ b/src/Objects/UpdateRouteSettingsData.php @@ -7,7 +7,9 @@ /** * @property bool|null $track_opens * @property bool|null $track_clicks - * @property bool|null $disable_plaintext_generation + * @property bool|null $generate_plaintext_fallback + * @property bool|null $suppress_auto_responders + * @property 'opportunistic'|'enforced'|null $tls * @property bool|null $disable_hosted_unsubscribe * @property bool|null $redact_email_content */ diff --git a/src/Objects/UpdateTeamMemberAssignmentData.php b/src/Objects/UpdateTeamMemberAssignmentData.php new file mode 100644 index 0000000..bb0faa5 --- /dev/null +++ b/src/Objects/UpdateTeamMemberAssignmentData.php @@ -0,0 +1,14 @@ + $project_access + */ +final class UpdateTeamMemberAssignmentData extends Resource +{ + // +} diff --git a/src/Objects/UpdateWebhookData.php b/src/Objects/UpdateWebhookData.php index aee4cd6..2fb1a7f 100644 --- a/src/Objects/UpdateWebhookData.php +++ b/src/Objects/UpdateWebhookData.php @@ -9,7 +9,7 @@ * @property string $url * @property bool $enabled * @property bool $include_machine_events - * @property list<'message.created'|'message.sent'|'message.delivered'|'message.auto_replied'|'message.hard_bounced'|'message.soft_bounced'|'message.spam_complaint'|'message.failed'|'message.suppressed'|'message.unsubscribed'|'message.opened'|'message.clicked'|'message.inbound'|'message.policy_rejected'|'webhook.test'> $events + * @property list<'message.created'|'message.sent'|'message.delivered'|'message.auto_replied'|'message.hard_bounced'|'message.soft_bounced'|'message.spam_complaint'|'message.failed'|'message.suppressed'|'message.unsubscribed'|'message.opened'|'message.clicked'|'message.inbound'|'message.policy_rejected'|'suppression.added'|'suppression.removed'|'webhook.test'> $events */ final class UpdateWebhookData extends Resource { diff --git a/src/Objects/UserData.php b/src/Objects/UserData.php deleted file mode 100644 index a9bf9c4..0000000 --- a/src/Objects/UserData.php +++ /dev/null @@ -1,16 +0,0 @@ - $events + * @property list<'message.created'|'message.sent'|'message.delivered'|'message.auto_replied'|'message.hard_bounced'|'message.soft_bounced'|'message.spam_complaint'|'message.failed'|'message.suppressed'|'message.unsubscribed'|'message.opened'|'message.clicked'|'message.inbound'|'message.policy_rejected'|'suppression.added'|'suppression.removed'|'webhook.test'> $events * @property bool $enabled * @property string|null $last_called_at * @property string $created_at diff --git a/src/Responses/DeleteSuppressionResponse.php b/src/Responses/DeleteSuppressionResponse.php index 2e8d5c0..2da6206 100644 --- a/src/Responses/DeleteSuppressionResponse.php +++ b/src/Responses/DeleteSuppressionResponse.php @@ -5,7 +5,10 @@ use Lettermint\Resource; /** + * @property bool $success + * @property string $status * @property string $message + * @property float|int $confidence */ final class DeleteSuppressionResponse extends Resource { diff --git a/src/Responses/DomainResponse.php b/src/Responses/DomainResponse.php index d216fb2..f01a742 100644 --- a/src/Responses/DomainResponse.php +++ b/src/Responses/DomainResponse.php @@ -2,13 +2,16 @@ namespace Lettermint\Responses; +use Lettermint\Objects\DomainDnsRecordData; use Lettermint\Resource; /** * @property string $id * @property string $domain + * @property 'legacy_txt'|'managed_cname' $dkim_mode + * @property bool $rotation_ready * @property string|null $status_changed_at - * @property list<\Lettermint\Objects\DomainDnsRecordData> $dns_records + * @property list $dns_records * @property list> $projects * @property string $created_at */ diff --git a/src/Responses/ProjectMemberResponse.php b/src/Responses/ProjectMemberResponse.php deleted file mode 100644 index 549061b..0000000 --- a/src/Responses/ProjectMemberResponse.php +++ /dev/null @@ -1,13 +0,0 @@ - $routes + * @property list $routes * @property int $routes_count - * @property list<\Lettermint\Objects\DomainData> $domains + * @property list $domains * @property int $domains_count - * @property list<\Lettermint\Objects\TeamMemberData> $team_members - * @property int $team_members_count - * @property \Lettermint\Objects\MessageStatsData|mixed $last_28_days + * @property MessageStatsData|mixed $last_28_days * @property string $created_at * @property string $updated_at */ diff --git a/src/Responses/RouteResponse.php b/src/Responses/RouteResponse.php index 630878b..fef6dc8 100644 --- a/src/Responses/RouteResponse.php +++ b/src/Responses/RouteResponse.php @@ -2,6 +2,8 @@ namespace Lettermint\Responses; +use Lettermint\Objects\ProjectData; +use Lettermint\Objects\RouteStatisticData; use Lettermint\Resource; /** @@ -11,15 +13,16 @@ * @property string $name * @property 'transactional'|'broadcast'|'inbound' $route_type * @property bool $is_default - * @property string $inbound_address - * @property string $inbound_domain - * @property string $inbound_domain_verified_at - * @property float|int $inbound_spam_threshold + * @property string|null $inbound_address + * @property string|null $inbound_domain + * @property string|null $inbound_domain_verified_at + * @property float|int|null $inbound_spam_threshold * @property 'inline'|'url' $attachment_delivery - * @property \Lettermint\Objects\ProjectData $project + * @property array|mixed $settings + * @property ProjectData $project * @property int $webhooks_count * @property int $suppressed_recipients_count - * @property array|list<\Lettermint\Objects\RouteStatisticData> $statistics + * @property array|list $statistics * @property string $created_at * @property string $updated_at */ diff --git a/src/Responses/TeamMembersAssignmentUpdateResponse.php b/src/Responses/TeamMembersAssignmentUpdateResponse.php new file mode 100644 index 0000000..30fd634 --- /dev/null +++ b/src/Responses/TeamMembersAssignmentUpdateResponse.php @@ -0,0 +1,21 @@ + $role + * @property TeamMemberProjectAccessData $project_access + * @property string|null $joined_at + */ +final class TeamMembersAssignmentUpdateResponse extends Resource +{ + protected static array $casts = [ + 'project_access' => TeamMemberProjectAccessData::class, + ]; +} diff --git a/src/Responses/TeamMembersShowResponse.php b/src/Responses/TeamMembersShowResponse.php new file mode 100644 index 0000000..e180ffb --- /dev/null +++ b/src/Responses/TeamMembersShowResponse.php @@ -0,0 +1,21 @@ + $role + * @property TeamMemberProjectAccessData $project_access + * @property string|null $joined_at + */ +final class TeamMembersShowResponse extends Resource +{ + protected static array $casts = [ + 'project_access' => TeamMemberProjectAccessData::class, + ]; +} diff --git a/src/Responses/TeamResponse.php b/src/Responses/TeamResponse.php index 1ebc825..404000f 100644 --- a/src/Responses/TeamResponse.php +++ b/src/Responses/TeamResponse.php @@ -2,6 +2,7 @@ namespace Lettermint\Responses; +use Lettermint\Objects\TeamAddonData; use Lettermint\Resource; /** @@ -9,10 +10,11 @@ * @property string $name * @property 'personal'|'business' $type * @property 'free'|'starter'|'growth'|'pro' $plan - * @property 300|10000|50000|125000|300000|500000|750000|1000000|1500000 $tier + * @property int $included_volume + * @property int $tier * @property string|null $verified_at * @property list $features - * @property list<\Lettermint\Objects\TeamAddonData> $addons + * @property list $addons * @property string $created_at * @property int $domains_count * @property int $projects_count diff --git a/src/Responses/TeamRolesResponse.php b/src/Responses/TeamRolesResponse.php new file mode 100644 index 0000000..6074d8a --- /dev/null +++ b/src/Responses/TeamRolesResponse.php @@ -0,0 +1,16 @@ + $data + */ +final class TeamRolesResponse extends Resource +{ + protected static array $casts = [ + 'data' => [TeamRoleData::class], + ]; +} diff --git a/src/Responses/UpdateProjectMembersResponse.php b/src/Responses/UpdateProjectMembersResponse.php deleted file mode 100644 index 5ae58a6..0000000 --- a/src/Responses/UpdateProjectMembersResponse.php +++ /dev/null @@ -1,16 +0,0 @@ - \Lettermint\Objects\ProjectData::class, - ]; -} diff --git a/src/Responses/VerifyDnsRecordsResponse.php b/src/Responses/VerifyDnsRecordsResponse.php index 7c86382..0f68a11 100644 --- a/src/Responses/VerifyDnsRecordsResponse.php +++ b/src/Responses/VerifyDnsRecordsResponse.php @@ -6,6 +6,7 @@ /** * @property string $message + * @property list> $recommended_failed_records */ final class VerifyDnsRecordsResponse extends Resource { diff --git a/src/Responses/WebhookDeliveryResponse.php b/src/Responses/WebhookDeliveryResponse.php index 43081ac..a69d6f2 100644 --- a/src/Responses/WebhookDeliveryResponse.php +++ b/src/Responses/WebhookDeliveryResponse.php @@ -7,7 +7,7 @@ /** * @property string $id * @property string $webhook_id - * @property 'message.created'|'message.sent'|'message.delivered'|'message.auto_replied'|'message.hard_bounced'|'message.soft_bounced'|'message.spam_complaint'|'message.failed'|'message.suppressed'|'message.unsubscribed'|'message.opened'|'message.clicked'|'message.inbound'|'message.policy_rejected'|'webhook.test' $event_type + * @property 'message.created'|'message.sent'|'message.delivered'|'message.auto_replied'|'message.hard_bounced'|'message.soft_bounced'|'message.spam_complaint'|'message.failed'|'message.suppressed'|'message.unsubscribed'|'message.opened'|'message.clicked'|'message.inbound'|'message.policy_rejected'|'suppression.added'|'suppression.removed'|'webhook.test' $event_type * @property 'pending'|'success'|'failed'|'client_error'|'server_error'|'timeout' $status * @property int $attempt_number * @property int|null $http_status_code diff --git a/src/Types/ApiTypes.php b/src/Types/ApiTypes.php index 7097ef6..250b426 100644 --- a/src/Types/ApiTypes.php +++ b/src/Types/ApiTypes.php @@ -8,68 +8,78 @@ * @phpstan-type ApiObject array * @phpstan-type CursorPage array{data: list, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null} * @phpstan-type MessageStatus 'pending'|'queued'|'suppressed'|'processed'|'delivered'|'opened'|'clicked'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed' - * @phpstan-type SendMailRequest array{route?: string, from: string, subject: string, tag?: string|null, html?: string|null, text?: string|null, to: non-empty-list, cc?: list, bcc?: list, reply_to?: list, headers?: array, metadata?: array, settings?: array{track_opens?: bool, track_clicks?: bool}|null, attachments?: list} + * @phpstan-type SendMailRequest array{route?: string, from: string, to: non-empty-list, cc?: list, bcc?: list, reply_to?: list, subject: string, headers?: array, metadata?: array, tag?: string|null, settings?: array{track_opens?: bool, track_clicks?: bool, tls?: TlsPolicy}|null, html?: string|null, text?: string|null, attachments?: list} * @phpstan-type SendBatchMailRequest list + * @phpstan-type TlsPolicy 'opportunistic'|'enforced' * @phpstan-type AttachmentDelivery 'inline'|'url' + * @phpstan-type BuiltInTeamRole 'owner'|'admin'|'member' + * @phpstan-type CursorPaginator array{data: list, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null} + * @phpstan-type DkimMode 'legacy_txt'|'managed_cname' + * @phpstan-type DnsRecordPurpose 'return_path'|'dmarc'|'dkim_legacy'|'dkim_primary'|'dkim_secondary' * @phpstan-type DnsRecordStatus 'active'|'failed'|'pending' - * @phpstan-type DomainData array{id: string, domain: string, status_changed_at: string|null, dns_records?: list, projects?: list, created_at: string} - * @phpstan-type DomainDnsRecordData array{id: string, type: RecordType, hostname: string, fqdn: string, content: string, status: DnsRecordStatus, verified_at: string|null, last_checked_at: string|null} - * @phpstan-type DomainListData array{id: string, domain: string, status: DomainStatus, status_changed_at: string|null, created_at: string} + * @phpstan-type DnsVerificationScope 'required'|'recommended'|'migration'|'deprecated' + * @phpstan-type DomainData array{id: string, domain: string, dkim_mode: DkimMode, rotation_ready: bool, status_changed_at: string|null, dns_records?: list, projects?: list, created_at: string} + * @phpstan-type DomainDnsRecordData array{id: string, type: RecordType, hostname: string, fqdn: string, content: string, status: DnsRecordStatus, purpose: DnsRecordPurpose, verification_scope: DnsVerificationScope, required_for_verification: bool, verified_at: string|null, last_checked_at: string|null} + * @phpstan-type DomainListData array{id: string, domain: string, status: DomainStatus, dkim_mode: DkimMode, status_changed_at: string|null, created_at: string} * @phpstan-type DomainStatus 'verified'|'partially_verified'|'pending_verification'|'failed_verification' * @phpstan-type InitialRoutes 'both'|'transactional'|'broadcast' * @phpstan-type MessageAttachmentData array{size: int, filename: string, content_id: string|null, content_type: string} * @phpstan-type MessageData array{id: string, type: MessageType, status: MessageStatus, status_changed_at: string|null, tag: string|null, from_email: string, from_name: string|null, reply_to: list|null, subject: string|null, to: list|null, cc: list|null, bcc: list|null, attachments: list|null, metadata: array|null, spam_score?: float|int|null, spam_symbols?: list, route_id: string, created_at: string} * @phpstan-type MessageEventData array{message_id: string, event: MessageEventType, metadata: array|null, timestamp: string} * @phpstan-type MessageEventType 'queued'|'processed'|'suppressed'|'delivered'|'auto_replied'|'soft_bounced'|'hard_bounced'|'spam_complaint'|'failed'|'blocked'|'policy_rejected'|'unsubscribed'|'opened'|'clicked'|'inbound_received'|'inbound_queued'|'inbound_spam_blocked'|'inbound_processed'|'inbound_retry' - * @phpstan-type MessageListData array{id: string, type: MessageType, status: MessageStatus, from_email: string, from_name: string|null, subject: string|null, to: list|null, cc: list|null, bcc: list|null, reply_to: list|null, tag: string|null, created_at: string} + * @phpstan-type MessageListData array{id: string, type: MessageType, status: MessageStatus, spam_score?: float|int|null, from_email: string, from_name: string|null, subject: string|null, to: list|null, cc: list|null, bcc: list|null, reply_to: list|null, tag: string|null, status_changed_at: string|null, created_at: string} * @phpstan-type MessageRecipientData array{email: string, name: string|null} * @phpstan-type MessageStatsData array{messages_transactional: int, messages_broadcast: int, messages_inbound: int, deliverability: float|int} * @phpstan-type MessageType 'inbound'|'outbound' * @phpstan-type Plan 'free'|'starter'|'growth'|'pro' - * @phpstan-type ProjectData array{id: string, name: string, smtp_enabled: bool, redact_email_content: bool, default_route_id: string|null, token_generated_at: string|null, token_last_used_at: string|null, token_last_used_ip: string|null, routes?: list, routes_count?: int, domains?: list, domains_count?: int, team_members?: list, team_members_count?: int, last_28_days?: MessageStatsData|null, created_at: string, updated_at: string} - * @phpstan-type ProjectListData array{id: string, name: string, smtp_enabled: bool, routes_count: int, domains_count: int, team_members_count: int, last_28_days: MessageStatsData, created_at: string, updated_at: string} + * @phpstan-type ProjectAccessScope 'all'|'selected' + * @phpstan-type ProjectData array{id: string, name: string, smtp_enabled: bool, redact_email_content: bool, default_route_id: string|null, token_generated_at: string|null, token_last_used_at: string|null, token_last_used_ip: string|null, routes?: list, routes_count?: int, domains?: list, domains_count?: int, last_28_days?: MessageStatsData|null, created_at: string, updated_at: string} + * @phpstan-type ProjectListData array{id: string, name: string, smtp_enabled: bool, routes_count: int, domains_count: int, last_28_days: MessageStatsData, created_at: string, updated_at: string} + * @phpstan-type RbacConflictCode 'stale_resource'|'owner_protected'|'last_owner'|'built_in_role_immutable'|'custom_role_requires_pro' + * @phpstan-type RbacPermission 'team:manage'|'billing:manage'|'security:manage'|'audit:read'|'support:manage'|'members:read'|'members:manage'|'roles:manage'|'team_tokens:read'|'team_tokens:manage'|'team_tokens:rotate'|'team_tokens:revoke'|'projects:create'|'team_suppressions:read'|'team_suppressions:add'|'team_suppressions:remove'|'projects:read'|'projects:manage'|'projects:delete'|'routes:read'|'routes:manage'|'routes:delete'|'domains:read'|'domains:manage'|'domains:delete'|'project_tokens:read'|'project_tokens:manage'|'project_tokens:rotate'|'project_tokens:revoke'|'webhooks:read'|'webhooks:manage'|'webhooks:delete'|'webhooks:rotate_secret'|'stats:read'|'messages:read'|'messages:read_content'|'messages:send'|'suppressions:read'|'suppressions:add'|'suppressions:remove' * @phpstan-type RecordType 'TXT'|'CNAME'|'MX' - * @phpstan-type RouteData array{id: string, project_id: string, slug: string, name: string, route_type: RouteType, is_default: bool, inbound_address?: string, inbound_domain?: string, inbound_domain_verified_at?: string, inbound_spam_threshold?: float|int, attachment_delivery?: AttachmentDelivery, project?: ProjectListData, webhooks_count?: int, suppressed_recipients_count?: int, statistics?: array|list, created_at: string, updated_at: string} + * @phpstan-type RouteData array{id: string, project_id: string, slug: string, name: string, route_type: RouteType, is_default: bool, inbound_address?: string|null, inbound_domain?: string|null, inbound_domain_verified_at?: string|null, inbound_spam_threshold?: float|int|null, attachment_delivery?: AttachmentDelivery, settings?: array{disable_hosted_unsubscribe?: bool, track_opens?: bool, track_clicks?: bool, generate_plaintext_fallback?: bool, suppress_auto_responders?: bool, tls?: TlsPolicy, redact_email_content?: bool, attachment_delivery?: array}|null, project?: ProjectListData, webhooks_count?: int, suppressed_recipients_count?: int, statistics?: list, created_at: string, updated_at: string} * @phpstan-type RouteListData array{id: string, slug: string, name: string, route_type: RouteType, is_default: bool, webhooks_count: int, suppressed_recipients_count: int, created_at: string, updated_at: string} - * @phpstan-type RouteStatisticData array{date: string, sent_count: int, delivered_count: int, opened_count: int, clicked_count: int, hard_bounce_count: int, spam_complaint_count: int, inbound_received_count: int, effective_opened_count: int|null, machine_opened_count: int|null, machine_clicked_count: int|null} + * @phpstan-type RouteStatisticData array{date: string, sent_count: int, delivered_count: int, opened_count: int, clicked_count: int, hard_bounce_count: int, spam_complaint_count: int, inbound_received_count: int, observed_opened_count?: int|null, human_opened_count?: int|null, privacy_opened_count?: int|null, effective_opened_count: int|null, machine_opened_count: int|null, machine_clicked_count: int|null} * @phpstan-type RouteType 'transactional'|'broadcast'|'inbound' * @phpstan-type SpamSymbol array{name: string, score: float|int, options: non-empty-list, description: string|null} - * @phpstan-type StatsDailyData array{date: string, sent: int, delivered: int, hard_bounced: int, spam_complaints: int, opened: int|null, clicked: int|null, inbound: StatsInboundData, transactional: StatsTypeData|null, broadcast: StatsTypeData|null, effective_opened: int|null, machine_opened: int|null, machine_clicked: int|null} + * @phpstan-type StatsDailyData array{date: string, sent: int, delivered: int, hard_bounced: int, spam_complaints: int, opened: int|null, clicked: int|null, inbound: StatsInboundData, transactional: StatsTypeData|null, broadcast: StatsTypeData|null, observed_opened?: int|null, human_opened?: int|null, privacy_opened?: int|null, effective_opened: int|null, machine_opened: int|null, machine_clicked: int|null} * @phpstan-type StatsData array{from: string, to: string, totals: StatsTotalsData, daily: list} * @phpstan-type StatsInboundData array{received: int} * @phpstan-type StatsRequestData array{from: string, to: string, project_id?: string|null, include_machine?: bool} - * @phpstan-type StatsTotalsData array{sent: int, delivered: int, hard_bounced: int, spam_complaints: int, opened: int|null, clicked: int|null, inbound: StatsInboundData, transactional: StatsTypeData|null, broadcast: StatsTypeData|null, effective_opened: int|null, machine_opened: int|null, machine_clicked: int|null} + * @phpstan-type StatsTotalsData array{sent: int, delivered: int, hard_bounced: int, spam_complaints: int, opened: int|null, clicked: int|null, inbound: StatsInboundData, transactional: StatsTypeData|null, broadcast: StatsTypeData|null, observed_opened?: int|null, human_opened?: int|null, privacy_opened?: int|null, effective_opened: int|null, machine_opened: int|null, machine_clicked: int|null} * @phpstan-type StatsTypeData array{sent: int, hard_bounced: int, spam_complaints: int} * @phpstan-type StoreDomainData array{domain: string} * @phpstan-type StoreProjectData array{name: string, smtp_enabled?: bool, initial_routes?: InitialRoutes, short_token?: bool} * @phpstan-type StoreRouteData array{name: string, route_type: RouteType, slug?: string|null} - * @phpstan-type StoreSuppressionData array{email?: string|null, reason: SuppressionReason, scope: SuppressionScope, route_id?: string|null, project_id?: string|null, emails?: non-empty-list|null} - * @phpstan-type StoreWebhookData array{route_id: string, name: string, url: string, enabled?: bool|null, include_machine_events?: bool|null, events: non-empty-list} - * @phpstan-type SuppressedRecipientData array{id: string, type: SuppressionType, value: string, reason: SuppressionReason, scope: SuppressionScope, project_id: string|null, route_id: string|null, created_at: string, updated_at: string} + * @phpstan-type StoreSuppressionData array{email?: string|null, emails?: non-empty-list|null, reason: SuppressionReason, scope: SuppressionScope, route_id?: string|null, project_id?: string|null} + * @phpstan-type StoreWebhookData array{route_id: string, name: string, url: string, events: non-empty-list, enabled?: bool|null, include_machine_events?: bool|null} + * @phpstan-type SuppressedRecipientData array{id: string, type: SuppressionType, value: string, reason: SuppressionReason, scope: SuppressionScope, project_id: string|null, route_id: string|null, source_message?: SuppressionSourceMessageData|null, created_at: string, updated_at: string} * @phpstan-type SuppressionReason 'spam_complaint'|'hard_bounce'|'unsubscribe'|'manual' * @phpstan-type SuppressionScope 'global'|'team'|'project'|'route' + * @phpstan-type SuppressionSourceMessageData array{id: string, available: bool, subject: string|null, created_at: string|null} * @phpstan-type SuppressionType 'email'|'domain'|'extension' * @phpstan-type TeamAddonData array{type: string|null, expires_at: string|null} - * @phpstan-type TeamData array{id: string, name: string, type: TeamType, plan: Plan, tier: VolumeTier, verified_at: string|null, features?: list, addons?: list, created_at: string, domains_count?: int, projects_count?: int, members_count?: int} - * @phpstan-type TeamMemberData array{id: string, user?: UserData, role: string|null, joined_at: string|null} + * @phpstan-type TeamData array{id: string, name: string, type: TeamType, plan: Plan, included_volume: int, tier: int, verified_at: string|null, features?: list, addons?: list, created_at: string, domains_count?: int, projects_count?: int, members_count?: int} + * @phpstan-type TeamMemberData array{id: string, name: string, email: string, role: array{id: string, name: string}, project_access: TeamMemberProjectAccessData, joined_at: string|null} + * @phpstan-type TeamMemberProjectAccessData array{scope: ProjectAccessScope, projects: list} + * @phpstan-type TeamRoleData array{id: string, name: string, system_key: BuiltInTeamRole|null, permissions: list, assignable: bool} * @phpstan-type TeamType 'personal'|'business' * @phpstan-type TeamUsageDetailData array{current_period: TeamUsagePeriodData, historical_usage: list} * @phpstan-type TeamUsagePeriodData array{usage: int, last_incremented_at: string|null, period_start: string, period_end: string} * @phpstan-type UpdateDomainProjectsData array{project_ids: non-empty-list} * @phpstan-type UpdateProjectData array{name?: string|null, smtp_enabled?: bool|null, redact_email_content?: bool|null, default_route_id?: string|null} - * @phpstan-type UpdateProjectMembersData array{team_member_ids: non-empty-list} * @phpstan-type UpdateRouteData array{name?: string|null, settings?: UpdateRouteSettingsData|null, inbound_settings?: UpdateRouteInboundSettingsData|null} * @phpstan-type UpdateRouteInboundSettingsData array{inbound_domain?: string|null, inbound_spam_threshold?: float|int|null, attachment_delivery?: AttachmentDelivery|null} - * @phpstan-type UpdateRouteSettingsData array{track_opens?: bool|null, track_clicks?: bool|null, disable_plaintext_generation?: bool|null, disable_hosted_unsubscribe?: bool|null, redact_email_content?: bool|null} + * @phpstan-type UpdateRouteSettingsData array{track_opens?: bool|null, track_clicks?: bool|null, generate_plaintext_fallback?: bool|null, suppress_auto_responders?: bool|null, tls?: TlsPolicy|null, disable_hosted_unsubscribe?: bool|null, redact_email_content?: bool|null} * @phpstan-type UpdateTeamData array{name?: string|null} - * @phpstan-type UpdateWebhookData array{name?: string, url?: string, enabled?: bool, include_machine_events?: bool, events?: non-empty-list} - * @phpstan-type UserData array{id: string, name: string, email: string, avatar: string|null} - * @phpstan-type VolumeTier 300|10000|50000|125000|300000|500000|750000|1000000|1500000 + * @phpstan-type UpdateTeamMemberAssignmentData array{role_id: string, project_access: array{scope: ProjectAccessScope, project_ids?: list}} + * @phpstan-type UpdateWebhookData array{name?: string, url?: string, events?: non-empty-list, enabled?: bool, include_machine_events?: bool} * @phpstan-type WebhookData array{id: string, route_id: string, name: string, url: string, events: non-empty-list, enabled: bool, include_machine_events: bool, secret?: string, last_called_at: string|null, created_at: string, updated_at: string} * @phpstan-type WebhookDeliveryData array{id: string, webhook_id: string, event_type: WebhookEvent, status: WebhookDeliveryStatus, attempt_number: int, http_status_code: int|null, duration_ms: int|null, payload: non-empty-list, response_body: string|null, response_headers: list|null, error_message: string|null, delivered_at: string|null, timestamp: string} * @phpstan-type WebhookDeliveryListData array{id: string, webhook_id: string, event_type: WebhookEvent, status: WebhookDeliveryStatus, attempt_number: int, http_status_code: int|null, duration_ms: int|null, delivered_at: string|null, created_at: string} * @phpstan-type WebhookDeliveryStatus 'pending'|'success'|'failed'|'client_error'|'server_error'|'timeout' - * @phpstan-type WebhookEvent 'message.created'|'message.sent'|'message.delivered'|'message.auto_replied'|'message.hard_bounced'|'message.soft_bounced'|'message.spam_complaint'|'message.failed'|'message.suppressed'|'message.unsubscribed'|'message.opened'|'message.clicked'|'message.inbound'|'message.policy_rejected'|'webhook.test' + * @phpstan-type WebhookEvent 'message.created'|'message.sent'|'message.delivered'|'message.auto_replied'|'message.hard_bounced'|'message.soft_bounced'|'message.spam_complaint'|'message.failed'|'message.suppressed'|'message.unsubscribed'|'message.opened'|'message.clicked'|'message.inbound'|'message.policy_rejected'|'suppression.added'|'suppression.removed'|'webhook.test' * @phpstan-type WebhookListData array{id: string, route_id: string, name: string, url: string, events: list, enabled: bool, last_called_at: string|null, created_at: string, updated_at: string} * @phpstan-type StatsQuery StatsRequestData * @phpstan-type SendMailResponse array{message_id: string, status: MessageStatus} @@ -77,11 +87,11 @@ * @phpstan-type DomainListResponse array{data: list, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null} * @phpstan-type DomainResponse DomainData * @phpstan-type DeleteDomainResponse array{message: string} - * @phpstan-type VerifyDnsRecordsResponse array{message: string} + * @phpstan-type VerifyDnsRecordsResponse array{message: string, recommended_failed_records: list} * @phpstan-type VerifyDnsRecordResponse array{message: string} * @phpstan-type UpdateDomainProjectsResponse array{data: DomainData, message: string} * @phpstan-type BlockedFileTypesResponse array{extensions: list, mime_types: list} - * @phpstan-type MessageListResponse array{data: list, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null}|list + * @phpstan-type MessageListResponse array{data: list, links: list, meta: array{path: string|null, per_page: int, next_cursor: string|null, next_cursor_url: string|null, prev_cursor: string|null, prev_cursor_url: string|null}} * @phpstan-type MessageResponse MessageData * @phpstan-type MessageEventsResponse array{data: list, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null} * @phpstan-type ProjectListResponse array{data: list, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null} @@ -90,8 +100,6 @@ * @phpstan-type UpdateProjectResponse array{data: ProjectData, message: string} * @phpstan-type DeleteProjectResponse array{message: string} * @phpstan-type RotateProjectTokenResponse array{data: ProjectData, new_token: string, message: string} - * @phpstan-type UpdateProjectMembersResponse array{data: ProjectData, message: string} - * @phpstan-type ProjectMemberResponse array{message: string} * @phpstan-type ProjectRoutesResponse array{data: list, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null} * @phpstan-type CreateRouteResponse array{data: RouteData, message: string} * @phpstan-type RouteResponse RouteData @@ -101,11 +109,14 @@ * @phpstan-type StatsResponse StatsData * @phpstan-type SuppressionListResponse array{data: list, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null} * @phpstan-type CreateSuppressionResponse array{message: string|'No emails were added.', data: array{created: list, skipped: list}} - * @phpstan-type DeleteSuppressionResponse array{message: string} + * @phpstan-type DeleteSuppressionResponse array{success: bool, status: string, message: string, confidence?: float|int} * @phpstan-type TeamResponse TeamData * @phpstan-type UpdateTeamResponse array{data: TeamData, message: string} * @phpstan-type TeamUsageResponse TeamUsageDetailData + * @phpstan-type TeamRolesResponse array{data: list} * @phpstan-type TeamMembersResponse array{data: list, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null} + * @phpstan-type TeamMembersShowResponse TeamMemberData + * @phpstan-type TeamMembersAssignmentUpdateResponse TeamMemberData * @phpstan-type WebhookListResponse array{data: list, path: string|null, per_page: int, next_cursor: string|null, next_page_url: string|null, prev_cursor: string|null, prev_page_url: string|null} * @phpstan-type CreateWebhookResponse array{data: WebhookData, message: string} * @phpstan-type WebhookResponse WebhookData diff --git a/tests/Endpoints/ProjectsEndpointTest.php b/tests/Endpoints/ProjectsEndpointTest.php index ef63655..46f1b2d 100644 --- a/tests/Endpoints/ProjectsEndpointTest.php +++ b/tests/Endpoints/ProjectsEndpointTest.php @@ -52,25 +52,6 @@ expect($this->endpoint->rotateToken('project-id')->toArray())->toBe(['token' => 'new-token']); }); -test('it updates project members', function () { - $data = ['members' => ['member-id']]; - $this->httpClient->shouldReceive('put')->once()->with('/v1/projects/project-id/members', $data, [])->andReturn(['data' => []]); - - expect($this->endpoint->updateMembers('project-id', $data)->toArray())->toBe(['data' => []]); -}); - -test('it adds project members', function () { - $this->httpClient->shouldReceive('post')->once()->with('/v1/projects/project-id/members/member-id', [], [])->andReturn(['data' => []]); - - expect($this->endpoint->addMember('project-id', 'member-id')->toArray())->toBe(['data' => []]); -}); - -test('it removes project members', function () { - $this->httpClient->shouldReceive('delete')->once()->with('/v1/projects/project-id/members/member-id', [])->andReturn(['data' => []]); - - expect($this->endpoint->removeMember('project-id', 'member-id')->toArray())->toBe(['data' => []]); -}); - test('it lists project routes', function () { $query = ['filter[route_type]' => 'outbound']; $this->httpClient->shouldReceive('get')->once()->with('/v1/projects/project-id/routes', $query)->andReturn(['data' => []]); diff --git a/tests/Endpoints/TeamEndpointTest.php b/tests/Endpoints/TeamEndpointTest.php index 8703679..3ef35b3 100644 --- a/tests/Endpoints/TeamEndpointTest.php +++ b/tests/Endpoints/TeamEndpointTest.php @@ -32,9 +32,31 @@ expect($this->endpoint->usage()->toArray())->toBe(['data' => []]); }); +test('it retrieves reusable team roles', function () { + $this->httpClient->shouldReceive('get')->once()->with('/v1/team/roles', [])->andReturn(['data' => []]); + + expect($this->endpoint->roles()->toArray())->toBe(['data' => []]); +}); + test('it retrieves team members', function () { $query = ['page[size]' => 10]; $this->httpClient->shouldReceive('get')->once()->with('/v1/team/members', $query)->andReturn(['data' => []]); expect($this->endpoint->members($query)->toArray())->toBe(['data' => []]); }); + +test('it retrieves a team member', function () { + $this->httpClient->shouldReceive('get')->once()->with('/v1/team/members/user%2Fid', [])->andReturn(['id' => 'user/id']); + + expect($this->endpoint->member('user/id')->toArray())->toBe(['id' => 'user/id']); +}); + +test('it updates a team member assignment', function () { + $data = [ + 'role_id' => 'role-id', + 'project_access' => ['scope' => 'all'], + ]; + $this->httpClient->shouldReceive('put')->once()->with('/v1/team/members/user%2Fid/assignment', $data, [])->andReturn(['id' => 'user/id']); + + expect($this->endpoint->updateMemberAssignment('user/id', $data)->toArray())->toBe(['id' => 'user/id']); +}); diff --git a/tests/SdkCoverageTest.php b/tests/SdkCoverageTest.php index dab6c24..ecf10e7 100644 --- a/tests/SdkCoverageTest.php +++ b/tests/SdkCoverageTest.php @@ -37,9 +37,6 @@ ['team', 'project.update', ProjectsEndpoint::class, 'update'], ['team', 'project.destroy', ProjectsEndpoint::class, 'delete'], ['team', 'project.rotateToken', ProjectsEndpoint::class, 'rotateToken'], - ['team', 'project.updateMembers', ProjectsEndpoint::class, 'updateMembers'], - ['team', 'project.addMember', ProjectsEndpoint::class, 'addMember'], - ['team', 'project.removeMember', ProjectsEndpoint::class, 'removeMember'], ['team', 'route.index', ProjectsEndpoint::class, 'routes'], ['team', 'route.store', ProjectsEndpoint::class, 'createRoute'], ['team', 'route.show', RoutesEndpoint::class, 'retrieve'], @@ -53,7 +50,10 @@ ['team', 'team.show', TeamEndpoint::class, 'retrieve'], ['team', 'team.update', TeamEndpoint::class, 'update'], ['team', 'team.usage', TeamEndpoint::class, 'usage'], + ['team', 'team.roles', TeamEndpoint::class, 'roles'], ['team', 'team.members', TeamEndpoint::class, 'members'], + ['team', 'team.members.show', TeamEndpoint::class, 'member'], + ['team', 'team.members.assignment.update', TeamEndpoint::class, 'updateMemberAssignment'], ['team', 'webhook.index', WebhooksEndpoint::class, 'list'], ['team', 'webhook.store', WebhooksEndpoint::class, 'create'], ['team', 'webhook.show', WebhooksEndpoint::class, 'retrieve'], @@ -115,8 +115,6 @@ 'UpdateProjectResponse', 'DeleteProjectResponse', 'RotateProjectTokenResponse', - 'UpdateProjectMembersResponse', - 'ProjectMemberResponse', 'ProjectRoutesResponse', 'CreateRouteResponse', 'RouteResponse', @@ -130,7 +128,10 @@ 'TeamResponse', 'UpdateTeamResponse', 'TeamUsageResponse', + 'TeamRolesResponse', 'TeamMembersResponse', + 'TeamMembersShowResponse', + 'TeamMembersAssignmentUpdateResponse', 'WebhookListResponse', 'CreateWebhookResponse', 'WebhookResponse', @@ -189,9 +190,16 @@ ->toContain('redact_email_content?: bool|null') ->toContain('@phpstan-type UpdateRouteSettingsData array{') ->toContain('@phpstan-type UpdateRouteInboundSettingsData array{') - ->toContain('disable_plaintext_generation?: bool|null') + ->toContain('generate_plaintext_fallback?: bool|null') + ->toContain('tls?: TlsPolicy|null') ->toContain('short_token?: bool') ->toContain('@phpstan-type BlockedFileTypesResponse array{extensions: list, mime_types: list}') - ->toContain('300000') - ->toContain("scope: SuppressionScope"); + ->toContain('@phpstan-type BuiltInTeamRole') + ->toContain('@phpstan-type TeamRoleData array{') + ->toContain('included_volume: int') + ->toContain('@phpstan-type UpdateTeamMemberAssignmentData array{') + ->toContain('dkim_mode: DkimMode') + ->toContain('source_message?: SuppressionSourceMessageData|null') + ->toContain('spam_score?: float|int|null') + ->toContain('scope: SuppressionScope'); }); From 30a4c9ec48db6895681f7f91f4a05379cc24252e Mon Sep 17 00:00:00 2001 From: Bjarn Bronsveld Date: Thu, 13 Aug 2026 11:39:42 +0200 Subject: [PATCH 2/2] Add TLS settings and batch idempotency --- src/Endpoints/EmailEndpoint.php | 16 +++++++++-- tests/Endpoints/EmailEndpointTest.php | 39 ++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/Endpoints/EmailEndpoint.php b/src/Endpoints/EmailEndpoint.php index 183d4a9..476db65 100644 --- a/src/Endpoints/EmailEndpoint.php +++ b/src/Endpoints/EmailEndpoint.php @@ -17,7 +17,8 @@ * } * @phpstan-type EmailSettings array{ * track_opens?: bool, - * track_clicks?: bool + * track_clicks?: bool, + * tls?: 'opportunistic'|'enforced' * } * @phpstan-type EmailPayload array{ * from?: string, @@ -273,7 +274,18 @@ public function ping(): string */ public function sendBatch(array $messages): SendBatchMailResponse { - return $this->hydrateList(SendBatchMailResponse::class, $this->postArray('/v1/send/batch', $messages, [])); + $headers = []; + + if ($this->idempotencyKey !== null) { + $headers['Idempotency-Key'] = $this->idempotencyKey; + } + + try { + return $this->hydrateList(SendBatchMailResponse::class, $this->postArray('/v1/send/batch', $messages, $headers)); + } finally { + $this->payload = []; + $this->idempotencyKey = null; + } } /** diff --git a/tests/Endpoints/EmailEndpointTest.php b/tests/Endpoints/EmailEndpointTest.php index 7f3a120..43122f2 100644 --- a/tests/Endpoints/EmailEndpointTest.php +++ b/tests/Endpoints/EmailEndpointTest.php @@ -260,7 +260,7 @@ 'from' => 'sender@example.com', 'to' => ['recipient@example.com'], 'subject' => 'Test Subject', - 'settings' => ['track_opens' => false, 'track_clicks' => true], + 'settings' => ['track_opens' => false, 'track_clicks' => true, 'tls' => 'enforced'], ], []) ->andReturn(['message_id' => '123', 'status' => 'pending']); @@ -268,7 +268,7 @@ ->from('sender@example.com') ->to('recipient@example.com') ->subject('Test Subject') - ->settings(['track_opens' => false, 'track_clicks' => true]) + ->settings(['track_opens' => false, 'track_clicks' => true, 'tls' => 'enforced']) ->send(); }); @@ -299,6 +299,11 @@ }); test('it handles custom headers', function () { + $headers = [ + 'Message-ID' => '', + 'X-LM-Preserve-Message-ID' => 'true', + ]; + $this->httpClient ->shouldReceive('post') ->once() @@ -306,7 +311,7 @@ 'from' => 'sender@example.com', 'to' => ['recipient@example.com'], 'subject' => 'Test Subject', - 'headers' => ['X-Custom' => 'Value'], + 'headers' => $headers, ], []) ->andReturn(['message_id' => '123', 'status' => 'pending']); @@ -314,10 +319,36 @@ ->from('sender@example.com') ->to('recipient@example.com') ->subject('Test Subject') - ->headers(['X-Custom' => 'Value']) + ->headers($headers) ->send(); }); +test('it sends and resets the batch idempotency key', function () { + $messages = [[ + 'from' => 'sender@example.com', + 'to' => ['recipient@example.com'], + 'subject' => 'Test Subject', + ]]; + + $this->httpClient + ->shouldReceive('post') + ->once() + ->with('/v1/send/batch', $messages, ['Idempotency-Key' => 'batch-key']) + ->andReturn([['message_id' => '123', 'status' => 'pending']]); + + $this->endpoint + ->idempotencyKey('batch-key') + ->sendBatch($messages); + + $this->httpClient + ->shouldReceive('post') + ->once() + ->with('/v1/send/batch', $messages, []) + ->andReturn([['message_id' => '456', 'status' => 'pending']]); + + $this->endpoint->sendBatch($messages); +}); + test('it supports RFC 5322 email addresses', function () { $this->httpClient ->shouldReceive('post')