214 lines
8.1 KiB
PHP
214 lines
8.1 KiB
PHP
<?php
|
|
|
|
use App\Actions\ReviewIdentityVerification;
|
|
use App\Enums\BillingProductPurpose;
|
|
use App\Enums\IdentityVerificationStatus;
|
|
use App\Enums\PaymentTransactionStatus;
|
|
use App\Enums\UserRole;
|
|
use App\Filament\Resources\IdentityVerificationRequests\Pages\ListIdentityVerificationRequests;
|
|
use App\Listeners\HandleStripeWebhook;
|
|
use App\Models\BillingProduct;
|
|
use App\Models\IdentityVerificationRequest;
|
|
use App\Models\PaymentTransaction;
|
|
use App\Models\User;
|
|
use App\Policies\IdentityVerificationRequestPolicy;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Laravel\Sanctum\Sanctum;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('requires the lifetime purchase before accepting an identity document', function () {
|
|
Storage::fake('identity_documents');
|
|
Sanctum::actingAs(User::factory()->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);
|
|
});
|