Skip to content
78 changes: 25 additions & 53 deletions src/Application/Verification/VerificationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
use Maatify\Verification\Domain\Contracts\VerificationCodeValidatorInterface;
use Maatify\Verification\Domain\Enum\IdentityTypeEnum;
use Maatify\Verification\Domain\Enum\VerificationPurposeEnum;
use RuntimeException;
use Maatify\Verification\Domain\Exception\InvalidVerificationCodeException;
use Maatify\Verification\Domain\Exception\VerificationAttemptsExceededException as DomainAttemptsExceededException;
use Maatify\Verification\Domain\Exception\VerificationCodeExpiredException as DomainCodeExpiredException;
use Maatify\Verification\Domain\Exception\VerificationDomainException;
use Maatify\Verification\Domain\Exception\VerificationGenerationBlockedException as DomainGenerationBlockedException;
use Maatify\Verification\Domain\Exception\VerificationRateLimitExceededException as DomainRateLimitExceededException;

readonly class VerificationService implements VerificationServiceInterface
{
Expand All @@ -32,8 +37,12 @@ public function startVerification(
try {
$generated = $this->generator->generate($identityType, $identity, $purpose);
return $generated->plainCode;
} catch (RuntimeException $e) {
$this->mapGenerationException($e);
} catch (DomainRateLimitExceededException $e) {
throw new VerificationRateLimitException($e->getMessage());
} catch (DomainGenerationBlockedException $e) {
throw new VerificationGenerationBlockedException($e->getMessage());
} catch (VerificationDomainException $e) {
throw new VerificationInternalException($e->getMessage());
}
}

Expand All @@ -44,12 +53,15 @@ public function verifyCode(
string $code
): void {
try {
$result = $this->validator->validate($identityType, $identity, $purpose, $code);
if (!$result->success) {
throw new RuntimeException($result->reason);
}
} catch (RuntimeException $e) {
$this->mapValidationException($e);
$this->validator->validate($identityType, $identity, $purpose, $code);
} catch (DomainCodeExpiredException $e) {
throw new VerificationCodeExpiredException($e->getMessage());
} catch (DomainAttemptsExceededException $e) {
throw new VerificationAttemptsExceededException($e->getMessage());
} catch (InvalidVerificationCodeException $e) {
throw new VerificationInvalidCodeException($e->getMessage());
} catch (VerificationDomainException $e) {
throw new VerificationInternalException($e->getMessage());
}
}

Expand All @@ -61,52 +73,12 @@ public function resendVerification(
try {
$generated = $this->generator->generate($identityType, $identity, $purpose);
return $generated->plainCode;
} catch (RuntimeException $e) {
$this->mapGenerationException($e);
}
}

/**
* @return never
*/
private function mapGenerationException(RuntimeException $e): void
{
$message = strtolower($e->getMessage());

if (str_contains($message, 'rate limit exceeded')) {
} catch (DomainRateLimitExceededException $e) {
throw new VerificationRateLimitException($e->getMessage());
}

if (str_contains($message, 'too many codes')) {
throw new VerificationRateLimitException($e->getMessage());
}

if (str_contains($message, 'please wait')) {
} catch (DomainGenerationBlockedException $e) {
throw new VerificationGenerationBlockedException($e->getMessage());
} catch (VerificationDomainException $e) {
throw new VerificationInternalException($e->getMessage());
}

throw new VerificationInternalException($e->getMessage());
}

/**
* @return never
*/
private function mapValidationException(RuntimeException $e): void
{
$message = strtolower($e->getMessage());

if (str_contains($message, 'expired')) {
throw new VerificationCodeExpiredException($e->getMessage());
}

if (str_contains($message, 'attempts')) {
throw new VerificationAttemptsExceededException($e->getMessage());
}

if (str_contains($message, 'invalid')) {
throw new VerificationInvalidCodeException($e->getMessage());
}

throw new VerificationInternalException($e->getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function markUsed(
VerificationPurposeEnum $purpose,
string $codeHash,
?string $usedIp = null
): bool;
): \Maatify\Verification\Domain\DTO\VerificationUseResult;

public function expire(int $codeId): void;

Expand Down
7 changes: 4 additions & 3 deletions src/Domain/DTO/VerificationResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public function __construct(
public string $reason = '',
public ?IdentityTypeEnum $identityType = null,
public ?string $identityId = null,
public ?VerificationPurposeEnum $purpose = null
public ?VerificationPurposeEnum $purpose = null,
public ?\Throwable $exception = null
) {
}

Expand All @@ -23,8 +24,8 @@ public static function success(?IdentityTypeEnum $identityType = null, ?string $
return new self(true, '', $identityType, $identityId, $purpose);
}

public static function failure(string $reason): self
public static function failure(string $reason, ?\Throwable $exception = null): self
{
return new self(false, $reason);
return new self(false, $reason, null, null, null, $exception);
}
}
16 changes: 16 additions & 0 deletions src/Domain/DTO/VerificationUseResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Maatify\Verification\Domain\DTO;

use Maatify\Verification\Domain\Enum\VerificationUseStatus;

readonly class VerificationUseResult
{
public function __construct(
public VerificationUseStatus $status,
public ?string $message = null
) {
}
}
13 changes: 13 additions & 0 deletions src/Domain/Enum/VerificationUseStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Maatify\Verification\Domain\Enum;

enum VerificationUseStatus: string
{
case SUCCESS = 'success';
case INVALID_CODE = 'invalid_code';
case EXPIRED = 'expired';
case ATTEMPTS_EXCEEDED = 'attempts_exceeded';
}
9 changes: 9 additions & 0 deletions src/Domain/Exception/InvalidVerificationCodeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Maatify\Verification\Domain\Exception;

class InvalidVerificationCodeException extends VerificationDomainException
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Maatify\Verification\Domain\Exception;

class VerificationAttemptsExceededException extends VerificationDomainException
{
}
9 changes: 9 additions & 0 deletions src/Domain/Exception/VerificationCodeExpiredException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Maatify\Verification\Domain\Exception;

class VerificationCodeExpiredException extends VerificationDomainException
{
}
11 changes: 11 additions & 0 deletions src/Domain/Exception/VerificationDomainException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Maatify\Verification\Domain\Exception;

use RuntimeException;

class VerificationDomainException extends RuntimeException
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Maatify\Verification\Domain\Exception;

class VerificationGenerationBlockedException extends VerificationDomainException
{
}
9 changes: 9 additions & 0 deletions src/Domain/Exception/VerificationInternalDomainException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Maatify\Verification\Domain\Exception;

class VerificationInternalDomainException extends VerificationDomainException
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Maatify\Verification\Domain\Exception;

class VerificationRateLimitExceededException extends VerificationDomainException
{
}
10 changes: 6 additions & 4 deletions src/Domain/Service/VerificationCodeGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
use Maatify\Verification\Domain\Enum\IdentityTypeEnum;
use Maatify\Verification\Domain\Enum\VerificationCodeStatus;
use Maatify\Verification\Domain\Enum\VerificationPurposeEnum;
use RuntimeException;
use Maatify\Verification\Domain\Exception\VerificationGenerationBlockedException;
use Maatify\Verification\Domain\Exception\VerificationInternalDomainException;
use Maatify\Verification\Domain\Exception\VerificationRateLimitExceededException;

readonly class VerificationCodeGenerator implements VerificationCodeGeneratorInterface
{
Expand Down Expand Up @@ -46,7 +48,7 @@ public function generate(IdentityTypeEnum $identityType, string $identityId, Ver
$countInWindow = $this->repository->countActiveInWindow($identityType, $identityId, $purpose, $since);

if ($countInWindow >= $policy->maxCodesPerWindow) {
throw new RuntimeException('Too many codes generated in the current window.');
throw new VerificationRateLimitExceededException('Too many codes generated in the current window.');
}

// 3. Generation Cooldown & Multi-Code Window
Expand All @@ -61,7 +63,7 @@ public function generate(IdentityTypeEnum $identityType, string $identityId, Ver
$secondsSinceLastCode = $now->getTimestamp() - $latestCode->createdAt->getTimestamp();

if ($secondsSinceLastCode < $policy->resendCooldownSeconds) {
throw new RuntimeException('Please wait before requesting a new code.');
throw new VerificationGenerationBlockedException('Please wait before requesting a new code.');
}
}

Expand All @@ -85,7 +87,7 @@ public function generate(IdentityTypeEnum $identityType, string $identityId, Ver
try {
$plainCode = (string)random_int(100000, 999999);
} catch (Exception $e) {
throw new RuntimeException('Failed to generate secure random code.', 0, $e);
throw new VerificationInternalDomainException('Failed to generate secure random code.', 0, $e);
}

// 5. Hash
Expand Down
13 changes: 10 additions & 3 deletions src/Domain/Service/VerificationCodeValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Maatify\Verification\Domain\DTO\VerificationResult;
use Maatify\Verification\Domain\Enum\IdentityTypeEnum;
use Maatify\Verification\Domain\Enum\VerificationPurposeEnum;
use Maatify\Verification\Domain\Exception\InvalidVerificationCodeException;

readonly class VerificationCodeValidator implements VerificationCodeValidatorInterface
{
Expand All @@ -24,19 +25,25 @@ public function validate(IdentityTypeEnum $identityType, string $identityId, Ver

// Atomic Database-Enforced Validation
// This query evaluates status, expiry, attempts, and hash in a single atomic operation.
$success = $this->repository->markUsed(
// It now returns a VerificationUseResult with an explicit status enum.
$useResult = $this->repository->markUsed(
$identityType,
$identityId,
$purpose,
$inputHash,
$usedIp
);

if (!$success) {
if ($useResult->status !== \Maatify\Verification\Domain\Enum\VerificationUseStatus::SUCCESS) {
// If validation failed (wrong guess, expired, or locked out), increment attempts
// strictly on the latest active challenge for this identity scope.
$this->repository->incrementAttempts($identityType, $identityId, $purpose);
return VerificationResult::failure('Invalid code.');

throw match ($useResult->status) {
\Maatify\Verification\Domain\Enum\VerificationUseStatus::EXPIRED => new \Maatify\Verification\Domain\Exception\VerificationCodeExpiredException('Verification code has expired.'),
\Maatify\Verification\Domain\Enum\VerificationUseStatus::ATTEMPTS_EXCEEDED => new \Maatify\Verification\Domain\Exception\VerificationAttemptsExceededException('Maximum attempts exceeded.'),
default => new InvalidVerificationCodeException('Invalid code.'),
};
}

// Revoke all other active codes for this scope upon success
Expand Down
4 changes: 2 additions & 2 deletions src/Infrastructure/RateLimiter/RedisRateLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
use Maatify\Verification\Domain\Contracts\VerificationRateLimiterInterface;
use Maatify\Verification\Domain\Enum\IdentityTypeEnum;
use Maatify\Verification\Domain\Enum\VerificationPurposeEnum;
use Maatify\Verification\Domain\Exception\VerificationRateLimitExceededException;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use RuntimeException;
use Redis;

class RedisRateLimiter implements VerificationRateLimiterInterface
Expand Down Expand Up @@ -82,7 +82,7 @@ public function hit(IdentityTypeEnum $identityType, string $identityId, Verifica

// Check if limits exceeded
if (isset($this->limits[$field]) && $currentHits > $this->limits[$field]) {
throw new RuntimeException(sprintf('Rate limit exceeded for window %s', $field));
throw new VerificationRateLimitExceededException(sprintf('Rate limit exceeded for window %s', $field));
}
$i++;
}
Expand Down
Loading
Loading