Skip to content
This repository was archived by the owner on Oct 23, 2024. It is now read-only.

Commit 4dc146e

Browse files
committed
money validator
1 parent 0c03f57 commit 4dc146e

File tree

7 files changed

+243
-0
lines changed

7 files changed

+243
-0
lines changed

code/app/DTO/Request.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\DTO;
6+
7+
use Money\Currency;
8+
use Money\Money;
9+
10+
final readonly class Request
11+
{
12+
private function __construct(private Money $amount)
13+
{
14+
}
15+
16+
public static function create(int $amountInCents, string $currency): self
17+
{
18+
return new self(new Money($amountInCents, new Currency(strtoupper($currency))));
19+
}
20+
21+
public function getAmount(): Money
22+
{
23+
return $this->amount;
24+
}
25+
26+
public function getCurrency(): string
27+
{
28+
return $this->amount->getCurrency()->getCode();
29+
}
30+
}

code/app/DTO/Transaction.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\DTO;
6+
7+
use Money\Currency;
8+
use Money\Money;
9+
10+
final readonly class Transaction
11+
{
12+
private function __construct(private Money $amount)
13+
{
14+
}
15+
16+
public static function create(int $amountInCents, string $currency): self
17+
{
18+
return new self(new Money($amountInCents, new Currency(strtoupper($currency))));
19+
}
20+
21+
public function getAmount(): Money
22+
{
23+
return $this->amount;
24+
}
25+
26+
public function getCurrency(): string
27+
{
28+
return $this->amount->getCurrency()->getCode();
29+
}
30+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Controllers;
6+
7+
use App\DTO\Request;
8+
use App\DTO\Transaction;
9+
use App\Services\RequestMoneyValidator;
10+
use Illuminate\Http\JsonResponse;
11+
use Illuminate\Routing\Controller;
12+
use InvalidArgumentException;
13+
14+
final class TransactionController extends Controller
15+
{
16+
public function __construct(private readonly RequestMoneyValidator $validator)
17+
{
18+
}
19+
20+
public function validateTransaction(): JsonResponse
21+
{
22+
try {
23+
$userRequest = Request::create(10000, 'USD');
24+
$transaction = Transaction::create(9000, 'USD');
25+
26+
$isValid = $this->validator->validate($userRequest, $transaction);
27+
28+
return response()->json(['isValid' => $isValid]);
29+
} catch (InvalidArgumentException $e) {
30+
return response()->json([
31+
'error' => 'Validation failed',
32+
'message' => $e->getMessage()
33+
], 400);
34+
}
35+
}
36+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Providers;
6+
7+
use App\Services\RequestMoneyValidator;
8+
use Illuminate\Support\ServiceProvider;
9+
10+
class AppServiceProvider extends ServiceProvider
11+
{
12+
/**
13+
* Register any application services.
14+
*/
15+
public function register(): void
16+
{
17+
$this->app->singleton(RequestMoneyValidator::class, function ($app) {
18+
return new RequestMoneyValidator(
19+
deviation: config('app.transaction_deviation')
20+
);
21+
});
22+
}
23+
24+
/**
25+
* Bootstrap any application services.
26+
*/
27+
public function boot(): void
28+
{
29+
//
30+
}
31+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Services;
6+
7+
use App\DTO\Request;
8+
use App\DTO\Transaction;
9+
use InvalidArgumentException;
10+
11+
final readonly class RequestMoneyValidator
12+
{
13+
public function __construct(private int $deviation)
14+
{
15+
if ($deviation < 0 || $deviation > 100) {
16+
throw new InvalidArgumentException('Deviation must be between 0 and 100.');
17+
}
18+
}
19+
20+
public function validate(Request $request, Transaction $transaction): bool
21+
{
22+
if ($request->getCurrency() !== $transaction->getCurrency()) {
23+
return false;
24+
}
25+
26+
$deviationPercentage = (string) ($this->deviation / 100);
27+
$deviationAmount = $transaction->getAmount()->multiply($deviationPercentage);
28+
29+
$minAmount = $transaction->getAmount()->subtract($deviationAmount);
30+
$maxAmount = $transaction->getAmount()->add($deviationAmount);
31+
32+
return $request->getAmount()->greaterThanOrEqual($minAmount)
33+
&& $request->getAmount()->lessThanOrEqual($maxAmount);
34+
}
35+
}

code/routes/web.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
use App\Http\Controllers\TransactionController;
4+
use Illuminate\Support\Facades\Route;
5+
6+
Route::get('/', function () {
7+
return view('welcome');
8+
});
9+
10+
Route::get('/validate-transaction', [TransactionController::class, 'validateTransaction']);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit;
6+
7+
use App\DTO\Request;
8+
use App\DTO\Transaction;
9+
use App\Services\RequestMoneyValidator;
10+
use InvalidArgumentException;
11+
use Tests\TestCase;
12+
13+
class RequestMoneyValidatorTest extends TestCase
14+
{
15+
public function test_validate_currency_mismatch(): void
16+
{
17+
$request = Request::create(10000, 'USD');
18+
$transaction = Transaction::create(10000, 'EUR');
19+
$validator = new RequestMoneyValidator(10);
20+
21+
$this->assertFalse($validator->validate($request, $transaction));
22+
}
23+
24+
public function test_validate_within_deviation(): void
25+
{
26+
$request = Request::create(9500, 'USD');
27+
$transaction = Transaction::create(9000, 'USD');
28+
29+
$validator = new RequestMoneyValidator(10);
30+
31+
$this->assertTrue($validator->validate($request, $transaction));
32+
}
33+
34+
public function test_validate_outside_deviation(): void
35+
{
36+
$request = Request::create(10000, 'USD');
37+
$transaction = Transaction::create(9000, 'USD');
38+
39+
$validator = new RequestMoneyValidator(5);
40+
41+
$this->assertFalse($validator->validate($request, $transaction));
42+
}
43+
44+
public function test_validate_exact_amount(): void
45+
{
46+
$request = Request::create(10000, 'USD');
47+
$transaction = Transaction::create(10000, 'USD');
48+
49+
$validator = new RequestMoneyValidator(0);
50+
51+
$this->assertTrue($validator->validate($request, $transaction));
52+
}
53+
54+
public function test_validate_small_deviation(): void
55+
{
56+
$request = Request::create(9950, 'USD');
57+
$transaction = Transaction::create(10000, 'USD');
58+
59+
$validator = new RequestMoneyValidator(1);
60+
61+
$this->assertTrue($validator->validate($request, $transaction));
62+
}
63+
64+
public function test_negative_deviation_throws_exception(): void
65+
{
66+
$this->expectException(InvalidArgumentException::class);
67+
$this->expectExceptionMessage('Deviation must be between 0 and 100.');
68+
69+
$validator = new RequestMoneyValidator(-5);
70+
}
71+
}

0 commit comments

Comments
 (0)