create()); $this->post('/api/certification/identity-document', [ 'document' => UploadedFile::fake()->image('identity.jpg'), ])->assertForbidden(); expect(IdentityVerificationRequest::query()->count())->toBe(0); }); it('stores identity documents privately without exposing their path', function () { Storage::fake('identity_documents'); $user = User::factory()->create([ 'certification_purchased_at' => now(), ]); Sanctum::actingAs($user); $response = $this->post('/api/certification/identity-document', [ 'document' => UploadedFile::fake()->image('my-passport.jpg'), ]); $response ->assertOk() ->assertJsonPath('data.entitlementPurchased', true) ->assertJsonPath('data.canSubmitIdentity', false) ->assertJsonPath('data.verification.status', IdentityVerificationStatus::PENDING->value) ->assertJsonMissingPath('data.verification.documentPath'); $verificationRequest = IdentityVerificationRequest::query()->sole(); Storage::disk('identity_documents')->assertExists($verificationRequest->document_path); expect($verificationRequest->document_path)->toStartWith('submissions/') ->not->toContain('my-passport') ->and(Storage::disk('identity_documents')->getVisibility($verificationRequest->document_path)) ->toBe('private'); }); it('prevents replacing a document while it is under review', function () { Storage::fake('identity_documents'); $user = User::factory()->create([ 'certification_purchased_at' => now(), ]); IdentityVerificationRequest::factory()->for($user)->create(); Sanctum::actingAs($user); $this ->withHeader('Accept', 'application/json') ->post('/api/certification/identity-document', [ 'document' => UploadedFile::fake()->image('identity.jpg'), ]) ->assertConflict() ->assertJsonPath('message', __('api.certification.already_pending')); }); it('approves a certification and removes the reviewed identity document', function () { Storage::fake('identity_documents'); $user = User::factory()->create([ 'certification_purchased_at' => now(), ]); $admin = User::factory()->create([ 'role' => UserRole::ADMIN, ]); $verificationRequest = IdentityVerificationRequest::factory()->for($user)->create(); Storage::disk('identity_documents')->put($verificationRequest->document_path, 'identity'); app(ReviewIdentityVerification::class)->execute( $verificationRequest, IdentityVerificationStatus::APPROVED, $admin, ); Storage::disk('identity_documents')->assertMissing($verificationRequest->document_path); expect($user->fresh()->account_verified_at)->not->toBeNull() ->and($verificationRequest->fresh()->status)->toBe(IdentityVerificationStatus::APPROVED) ->and($verificationRequest->fresh()->document_path)->toBeNull() ->and($verificationRequest->fresh()->document_deleted_at)->not->toBeNull(); }); it('allows a new submission after rejection without another purchase', function () { Storage::fake('identity_documents'); $user = User::factory()->create([ 'certification_purchased_at' => now(), ]); $admin = User::factory()->create([ 'role' => UserRole::ADMIN, ]); $verificationRequest = IdentityVerificationRequest::factory()->for($user)->create(); Storage::disk('identity_documents')->put($verificationRequest->document_path, 'identity'); app(ReviewIdentityVerification::class)->execute( $verificationRequest, IdentityVerificationStatus::REJECTED, $admin, 'Le document est illisible.', ); Sanctum::actingAs($user); $this->post('/api/certification/identity-document', [ 'document' => UploadedFile::fake()->image('identity-new.jpg'), ]) ->assertOk() ->assertJsonPath('data.verification.status', IdentityVerificationStatus::PENDING->value); expect($verificationRequest->fresh()->rejection_reason)->toBeNull() ->and($verificationRequest->fresh()->document_path)->not->toBeNull(); }); it('restricts identity review records to administrators', function () { $policy = new IdentityVerificationRequestPolicy; $request = IdentityVerificationRequest::factory()->make(); $admin = User::factory()->make(['role' => UserRole::ADMIN]); $moderator = User::factory()->make(['role' => UserRole::MODERATOR]); expect($policy->viewAny($admin))->toBeTrue() ->and($policy->view($admin, $request))->toBeTrue() ->and($policy->viewAny($moderator))->toBeFalse() ->and($policy->view($moderator, $request))->toBeFalse(); }); it('lets an administrator approve the identity request from filament', function () { app()->setLocale('fr'); Filament::setCurrentPanel(Filament::getPanel('dashboard')); Storage::fake('identity_documents'); $admin = User::factory()->create([ 'role' => UserRole::ADMIN, ]); $user = User::factory()->create([ 'certification_purchased_at' => now(), ]); $verificationRequest = IdentityVerificationRequest::factory()->for($user)->create(); Storage::disk('identity_documents')->put($verificationRequest->document_path, 'identity'); $this->actingAs($admin); Livewire::test(ListIdentityVerificationRequests::class) ->assertCanSeeTableRecords([$verificationRequest]) ->callTableAction('approve', $verificationRequest); expect($verificationRequest->fresh()->status)->toBe(IdentityVerificationStatus::APPROVED) ->and($user->fresh()->account_verified_at)->not->toBeNull(); }); it('grants lifetime certification entitlement from the paid Stripe webhook once', function () { $user = User::factory()->create([ 'stripe_id' => 'cus_certification', ]); $product = BillingProduct::create([ 'name' => 'Certification', 'purpose' => BillingProductPurpose::CERTIFICATION, 'amount' => 1999, 'currency' => 'eur', 'stripe_price_id' => 'price_certification', ]); $event = (object) [ 'payload' => [ 'id' => 'evt_certification', 'type' => 'checkout.session.completed', 'data' => [ 'object' => [ 'id' => 'cs_certification', 'mode' => 'payment', 'payment_status' => 'paid', 'customer' => $user->stripe_id, 'payment_intent' => 'pi_certification', 'amount_total' => 1999, 'currency' => 'eur', 'metadata' => [ 'billing_product_id' => $product->getKey(), 'billing_product_purpose' => BillingProductPurpose::CERTIFICATION->value, ], ], ], ], ]; $listener = app(HandleStripeWebhook::class); $listener->handle($event); $firstPurchaseDate = $user->fresh()->certification_purchased_at; $listener->handle($event); expect($firstPurchaseDate)->not->toBeNull() ->and($user->fresh()->certification_purchased_at?->equalTo($firstPurchaseDate))->toBeTrue() ->and(PaymentTransaction::query()->count())->toBe(1) ->and(PaymentTransaction::query()->sole()->status)->toBe(PaymentTransactionStatus::PAID); });