feat: subscriptions
CI / 🧪 Tests Laravel (push) Successful in 2m48s
CI / 🐳 Build & Push Image (push) Successful in 3m0s

This commit is contained in:
2026-08-07 11:53:46 +02:00
parent 99cb0e63e6
commit 2ece4b6e3e
51 changed files with 1489 additions and 254 deletions
@@ -0,0 +1,84 @@
<?php
namespace App\Actions;
use App\Enums\IdentityVerificationStatus;
use App\Models\IdentityVerificationRequest;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class ReviewIdentityVerification
{
public function execute(
IdentityVerificationRequest $verificationRequest,
IdentityVerificationStatus $status,
User $reviewer,
?string $rejectionReason = null,
): IdentityVerificationRequest {
abort_unless($reviewer->isAdmin(), 403);
abort_unless(
in_array($status, [IdentityVerificationStatus::APPROVED, IdentityVerificationStatus::REJECTED], true),
422,
);
abort_if(
$status === IdentityVerificationStatus::REJECTED && blank($rejectionReason),
422,
__('api.certification.rejection_reason_required'),
);
$reviewedRequest = DB::transaction(function () use (
$rejectionReason,
$reviewer,
$status,
$verificationRequest,
): IdentityVerificationRequest {
$lockedRequest = IdentityVerificationRequest::query()
->whereKey($verificationRequest->getKey())
->lockForUpdate()
->firstOrFail();
abort_unless($lockedRequest->status === IdentityVerificationStatus::PENDING, 409);
$lockedRequest->update([
'status' => $status,
'reviewed_at' => now(),
'reviewed_by' => $reviewer->getKey(),
'rejection_reason' => $status === IdentityVerificationStatus::REJECTED
? trim((string) $rejectionReason)
: null,
]);
if ($status === IdentityVerificationStatus::APPROVED) {
$lockedRequest->user()->update([
'account_verified_at' => now(),
]);
}
return $lockedRequest->fresh(['user', 'reviewer']);
});
$this->deleteReviewedDocument($reviewedRequest);
return $reviewedRequest->fresh(['user', 'reviewer']);
}
private function deleteReviewedDocument(IdentityVerificationRequest $verificationRequest): void
{
if (! $verificationRequest->hasDocument()) {
return;
}
try {
Storage::disk($verificationRequest->document_disk)
->delete($verificationRequest->document_path);
$verificationRequest->update([
'document_path' => null,
'document_deleted_at' => now(),
]);
} catch (\Throwable $exception) {
report($exception);
}
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Actions;
use App\Enums\IdentityVerificationStatus;
use App\Models\IdentityVerificationRequest;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Throwable;
class SubmitIdentityVerification
{
public function execute(User $user, UploadedFile $document): IdentityVerificationRequest
{
return Cache::lock('identity-verification-submit:'.$user->getKey(), 30)
->block(5, fn (): IdentityVerificationRequest => $this->store($user, $document));
}
private function store(User $user, UploadedFile $document): IdentityVerificationRequest
{
$existingRequest = $user->identityVerificationRequest()->first();
if ($existingRequest?->status === IdentityVerificationStatus::PENDING) {
throw new ConflictHttpException(__('api.certification.already_pending'));
}
$disk = (string) config('billing.identity_verification.disk', 'identity_documents');
$path = null;
try {
$path = Storage::disk($disk)->putFileAs(
'submissions',
$document,
Str::ulid().'.'.$this->extensionFor($document),
['visibility' => 'private'],
);
if (! is_string($path) || $path === '') {
throw new \RuntimeException('The identity document could not be stored.');
}
$verificationRequest = IdentityVerificationRequest::query()->updateOrCreate(
['user_id' => $user->getKey()],
[
'status' => IdentityVerificationStatus::PENDING,
'document_disk' => $disk,
'document_path' => $path,
'document_mime_type' => $document->getMimeType(),
'document_size' => $document->getSize(),
'submitted_at' => now(),
'reviewed_at' => null,
'reviewed_by' => null,
'rejection_reason' => null,
'document_deleted_at' => null,
],
);
} catch (Throwable $exception) {
if ($path) {
Storage::disk($disk)->delete($path);
}
throw $exception;
}
if ($existingRequest?->hasDocument() && $existingRequest->document_path !== $path) {
Storage::disk($existingRequest->document_disk)->delete($existingRequest->document_path);
}
return $verificationRequest;
}
private function extensionFor(UploadedFile $document): string
{
return match ($document->getMimeType()) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'application/pdf' => 'pdf',
default => throw new \InvalidArgumentException('Unsupported identity document type.'),
};
}
}