add tests
This commit is contained in:
1 parent
aa23eec392
commit
0bbb4571bd
5 files changed
+300
No files matched your search
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<\App\Models\Company>
|
||||||
|
*/
|
||||||
|
class CompanyFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Company::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->faker->unique()->company() . ' ' . Str::upper(Str::random(3)),
|
||||||
|
'legal_name' => $this->faker->company() . ' AG',
|
||||||
|
'ticker' => Str::upper($this->faker->lexify('???')),
|
||||||
|
'sector' => $this->faker->randomElement(['Finance', 'Technology', 'Energy']),
|
||||||
|
'country' => $this->faker->randomElement(['DE', 'AT', 'CH']),
|
||||||
|
'headquarters' => $this->faker->city(),
|
||||||
|
'kyc_risk_level' => $this->faker->randomElement(['low', 'medium', 'high']),
|
||||||
|
'summary' => $this->faker->sentence(8),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function highRisk(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn () => ['kyc_risk_level' => 'high']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\Transaction;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<\App\Models\Transaction>
|
||||||
|
*/
|
||||||
|
class TransactionFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Transaction::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'company_id' => Company::factory(),
|
||||||
|
'reference' => Str::upper($this->faker->unique()->bothify('TRX########')),
|
||||||
|
'amount' => $this->faker->randomFloat(2, 1000, 500000),
|
||||||
|
'currency' => 'EUR',
|
||||||
|
'counterparty' => $this->faker->company(),
|
||||||
|
'counterparty_country' => $this->faker->countryCode(),
|
||||||
|
'channel' => $this->faker->randomElement(['SWIFT', 'SEPA', 'ACH']),
|
||||||
|
'executed_at' => $this->faker->dateTimeBetween('-7 days', 'now'),
|
||||||
|
'risk_score' => $this->faker->numberBetween(10, 99),
|
||||||
|
'status' => $this->faker->randomElement([
|
||||||
|
Transaction::STATUS_TRUE_POSITIVE,
|
||||||
|
Transaction::STATUS_FALSE_POSITIVE,
|
||||||
|
Transaction::STATUS_CLEARED,
|
||||||
|
]),
|
||||||
|
'requires_review' => $this->faker->boolean(70),
|
||||||
|
'flagged_by' => $this->faker->randomElement(['Rule Engine', 'Sanctions Monitor', 'Analyst']),
|
||||||
|
'flagged_reason' => $this->faker->sentence(6),
|
||||||
|
'signals' => [$this->faker->word()],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requiresReview(bool $value = true): static
|
||||||
|
{
|
||||||
|
return $this->state(fn () => ['requires_review' => $value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status(string $status): static
|
||||||
|
{
|
||||||
|
return $this->state(fn () => ['status' => $status]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\Transaction;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Livewire\Volt\Volt;
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
test('guests are redirected to the login page', function () {
|
test('guests are redirected to the login page', function () {
|
||||||
@@ -31,3 +34,67 @@ test('search input is wired to component', function () {
|
|||||||
->set('search', 'Test Firma')
|
->set('search', 'Test Firma')
|
||||||
->assertSet('search', 'Test Firma');
|
->assertSet('search', 'Test Firma');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('overview aggregates open alert metrics', function () {
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
$acme = Company::factory()->create(['legal_name' => 'Acme Investigations AG']);
|
||||||
|
$globex = Company::factory()->create(['legal_name' => 'Globex Risk GmbH']);
|
||||||
|
|
||||||
|
Transaction::factory()->for($acme)->requiresReview()->status(Transaction::STATUS_TRUE_POSITIVE)->create([
|
||||||
|
'amount' => 120000,
|
||||||
|
'risk_score' => 90,
|
||||||
|
'executed_at' => Carbon::parse('2024-01-10 09:00:00'),
|
||||||
|
'signals' => ['Sanktionsliste'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Transaction::factory()->for($globex)->requiresReview()->status(Transaction::STATUS_FALSE_POSITIVE)->create([
|
||||||
|
'amount' => 45000,
|
||||||
|
'risk_score' => 60,
|
||||||
|
'executed_at' => Carbon::parse('2024-01-11 12:30:00'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Transaction::factory()->for($acme)->requiresReview(false)->status(Transaction::STATUS_FALSE_POSITIVE)->create([
|
||||||
|
'amount' => 8000,
|
||||||
|
'risk_score' => 20,
|
||||||
|
'executed_at' => Carbon::parse('2024-01-09 08:15:00'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Volt::test('company-search');
|
||||||
|
|
||||||
|
$overview = $component->get('overview');
|
||||||
|
|
||||||
|
expect($overview['companies'])->toBe(2)
|
||||||
|
->and($overview['open_alerts'])->toBe(2)
|
||||||
|
->and($overview['open_volume'])->toEqualWithDelta(165000, 0.001)
|
||||||
|
->and($overview['avg_risk'])->toEqualWithDelta(75, 0.001)
|
||||||
|
->and($overview['watchlist_hits'])->toBe(1)
|
||||||
|
->and($overview['automation_share'])->toBe(68);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search filters companies by ticker and name', function () {
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
$match = Company::factory()->create([
|
||||||
|
'name' => 'Fintrac Labs',
|
||||||
|
'legal_name' => 'Fintrac Labs AG',
|
||||||
|
'ticker' => 'FTR',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$other = Company::factory()->create([
|
||||||
|
'name' => 'Velocity Partners',
|
||||||
|
'ticker' => 'VLP',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Transaction::factory()->for($match)->requiresReview()->create(['amount' => 250000]);
|
||||||
|
Transaction::factory()->for($other)->requiresReview()->create(['amount' => 125000]);
|
||||||
|
|
||||||
|
$component = Volt::test('company-search');
|
||||||
|
|
||||||
|
$component->set('search', 'FTR');
|
||||||
|
|
||||||
|
$companies = $component->get('companies');
|
||||||
|
|
||||||
|
expect($companies)->toHaveCount(1)
|
||||||
|
->and($companies->first()->id)->toBe($match->id);
|
||||||
|
});
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\Transaction;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
test('guests are redirected to the login page', function () {
|
test('guests are redirected to the login page', function () {
|
||||||
$response = $this->get(route('dashboard'));
|
$response = $this->get(route('dashboard'));
|
||||||
@@ -14,3 +17,37 @@ test('authenticated users can visit the dashboard', function () {
|
|||||||
$response = $this->get(route('dashboard'));
|
$response = $this->get(route('dashboard'));
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('dashboard displays aggregated transaction insights', function () {
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
$acme = Company::factory()->create(['legal_name' => 'Acme Investigations AG']);
|
||||||
|
$globex = Company::factory()->create(['legal_name' => 'Globex Surveillance GmbH']);
|
||||||
|
|
||||||
|
Transaction::factory()->for($acme)->requiresReview()->status(Transaction::STATUS_TRUE_POSITIVE)->create([
|
||||||
|
'amount' => 123456.78,
|
||||||
|
'risk_score' => 92,
|
||||||
|
'executed_at' => Carbon::now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Transaction::factory()->for($acme)->requiresReview(false)->status(Transaction::STATUS_CLEARED)->create([
|
||||||
|
'amount' => 6543.21,
|
||||||
|
'risk_score' => 30,
|
||||||
|
'executed_at' => Carbon::now()->subDay(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Transaction::factory()->for($globex)->requiresReview()->status(Transaction::STATUS_FALSE_POSITIVE)->create([
|
||||||
|
'amount' => 25000.00,
|
||||||
|
'risk_score' => 70,
|
||||||
|
'executed_at' => Carbon::now()->subHours(2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get(route('dashboard'));
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertSee('Acme Investigations AG')
|
||||||
|
->assertSee('Globex Surveillance GmbH')
|
||||||
|
->assertSee('154.999,99 €')
|
||||||
|
->assertSee('123.456,78 €')
|
||||||
|
->assertSee('Ø Risikoscore 92');
|
||||||
|
});
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\Transaction;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
|
test('guests are redirected to the login page', function () {
|
||||||
|
$response = $this->get(route('transaction-review'));
|
||||||
|
$response->assertRedirect(route('login'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('authenticated users can view transaction review', function () {
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
$response = $this->get(route('transaction-review'));
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transaction review filters by search and updates selection', function () {
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
$acme = Company::factory()->create(['name' => 'Acme', 'legal_name' => 'Acme Investigations AG']);
|
||||||
|
$globex = Company::factory()->create(['name' => 'Globex', 'legal_name' => 'Globex Risk GmbH']);
|
||||||
|
|
||||||
|
$priority = Transaction::factory()->for($acme)->requiresReview()->status(Transaction::STATUS_TRUE_POSITIVE)->create([
|
||||||
|
'reference' => 'REF-ACME-001',
|
||||||
|
'counterparty' => 'Alpha Logistics',
|
||||||
|
'amount' => 125000.75,
|
||||||
|
'risk_score' => 95,
|
||||||
|
'executed_at' => Carbon::parse('2024-01-11 12:00:00'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$match = Transaction::factory()->for($globex)->requiresReview()->status(Transaction::STATUS_FALSE_POSITIVE)->create([
|
||||||
|
'reference' => 'REF-GLOBEX-001',
|
||||||
|
'counterparty' => 'Beta Holdings',
|
||||||
|
'amount' => 45000.50,
|
||||||
|
'risk_score' => 72,
|
||||||
|
'executed_at' => Carbon::parse('2024-01-10 09:00:00'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Transaction::factory()->for($globex)->requiresReview(false)->status(Transaction::STATUS_CLEARED)->create([
|
||||||
|
'reference' => 'REF-GLOBEX-002',
|
||||||
|
'counterparty' => 'Gamma Partners',
|
||||||
|
'amount' => 32000.25,
|
||||||
|
'risk_score' => 40,
|
||||||
|
'executed_at' => Carbon::parse('2024-01-09 08:15:00'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Volt::test('transaction-review');
|
||||||
|
|
||||||
|
expect($component->get('selectedTransactionId'))->toBe($priority->id);
|
||||||
|
|
||||||
|
$component->set('search', 'Beta');
|
||||||
|
|
||||||
|
/** @var LengthAwarePaginator $transactions */
|
||||||
|
$transactions = $component->get('transactions');
|
||||||
|
|
||||||
|
expect($transactions->total())->toBe(1)
|
||||||
|
->and($transactions->items()[0]->id)->toBe($match->id)
|
||||||
|
->and($component->get('selectedTransactionId'))->toBe($match->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transaction review metrics aggregate totals by status', function () {
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
$acme = Company::factory()->create();
|
||||||
|
$globex = Company::factory()->create();
|
||||||
|
|
||||||
|
$truePositive = Transaction::factory()->for($acme)->requiresReview()->status(Transaction::STATUS_TRUE_POSITIVE)->create([
|
||||||
|
'amount' => 90000.00,
|
||||||
|
'risk_score' => 88,
|
||||||
|
'executed_at' => Carbon::parse('2024-01-08 10:00:00'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$falsePositive = Transaction::factory()->for($globex)->requiresReview()->status(Transaction::STATUS_FALSE_POSITIVE)->create([
|
||||||
|
'amount' => 15000.00,
|
||||||
|
'risk_score' => 65,
|
||||||
|
'executed_at' => Carbon::parse('2024-01-08 09:00:00'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Transaction::factory()->for($globex)->requiresReview(false)->status(Transaction::STATUS_CLEARED)->create([
|
||||||
|
'amount' => 6000.00,
|
||||||
|
'risk_score' => 35,
|
||||||
|
'executed_at' => Carbon::parse('2024-01-07 08:00:00'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Volt::test('transaction-review');
|
||||||
|
|
||||||
|
$metrics = $component->get('metrics');
|
||||||
|
|
||||||
|
expect($metrics['total']['count'])->toBe(3)
|
||||||
|
->and($metrics['by_status'][Transaction::STATUS_TRUE_POSITIVE]['count'])->toBe(1)
|
||||||
|
->and($metrics['by_status'][Transaction::STATUS_TRUE_POSITIVE]['amount'])->toEqualWithDelta(90000, 0.001)
|
||||||
|
->and($metrics['by_status'][Transaction::STATUS_FALSE_POSITIVE]['count'])->toBe(1)
|
||||||
|
->and($metrics['by_status'][Transaction::STATUS_FALSE_POSITIVE]['amount'])->toEqualWithDelta(15000, 0.001)
|
||||||
|
->and($metrics['by_status'][Transaction::STATUS_CLEARED]['count'])->toBe(1);
|
||||||
|
|
||||||
|
$component->set('status', Transaction::STATUS_FALSE_POSITIVE);
|
||||||
|
|
||||||
|
$filteredMetrics = $component->get('metrics');
|
||||||
|
|
||||||
|
expect($filteredMetrics['total']['count'])->toBe(1)
|
||||||
|
->and($filteredMetrics['total']['amount'])->toEqualWithDelta(15000, 0.001)
|
||||||
|
->and($component->get('selectedTransactionId'))->toBe($falsePositive->id);
|
||||||
|
});
|
||||||
|
|
||||||
Reference in new issue
Block a user