feat(controllers): add api endpoints
This commit is contained in:
parent
1a66420eb1
commit
1e8f4983be
136
app/Http/Controllers/Api/V1/TaskEstimation.php
Normal file
136
app/Http/Controllers/Api/V1/TaskEstimation.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Rules\CountryCodeExists;
|
||||||
|
use App\Rules\DateHI;
|
||||||
|
use App\Rules\DateYMDHI;
|
||||||
|
use App\Services\Utils\DurationConvertor;
|
||||||
|
use App\Services\WorkingDays\DurationSolverFactory;
|
||||||
|
use Brick\DateTime\Duration;
|
||||||
|
use Brick\DateTime\LocalTime;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
final class TaskEstimation
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DurationConvertor $durationConvertor,
|
||||||
|
private readonly DurationSolverFactory $durationSolverFactory,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/task-estimation/{countryCode}/{startDate}/{minutesDuration}/{calculateWithWorkingDays}/{startWorkday}/{endWorkday}",
|
||||||
|
* summary="Get estimation date for a given task",
|
||||||
|
* description="Returns estimation date for a given task based on the given start date, duration, working day calculation",
|
||||||
|
* @OA\Parameter(name="countryCode", in="path", description="The ISO 3166-1 alpha-2 country code"),
|
||||||
|
* @OA\Parameter(name="startDate", in="path", description="Date in YYYY-MM-DD HH:MM format"),
|
||||||
|
* @OA\Parameter(name="minutesDuration", in="path", description="Duration of task in minutes"),
|
||||||
|
* @OA\Parameter(name="calculateWithWorkingDays", in="path", description="Include working days calculation (0 for no, 1 for yes)"),
|
||||||
|
* @OA\Parameter(name="startWorkday", in="path", description="Start of workday in HH:MM format"),
|
||||||
|
* @OA\Parameter(name="endWorkday", in="path", description="End of workday in HH:MM format"),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="200",
|
||||||
|
* description="Returns the working day status for the given date and country code",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* properties={
|
||||||
|
* @OA\Property(property="estimation_date", type="string", format="date"),
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="400",
|
||||||
|
* description="Invalid request parameters",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* type="object",
|
||||||
|
* properties={
|
||||||
|
* @OA\Property (
|
||||||
|
* type="object",
|
||||||
|
* property="errors",
|
||||||
|
* anyOf={
|
||||||
|
* @OA\Property(type="object", description="Error", properties={@OA\Property (property="countryCode", type="string", example="Invalid date reason")}),
|
||||||
|
* @OA\Property(type="object", description="Error", properties={@OA\Property (property="startDate", type="string", example="Invalid startDate reason")}),
|
||||||
|
* @OA\Property(type="object", description="Error", properties={@OA\Property (property="minutesDuration", type="string", example="Invalid minutesDuration reason")}),
|
||||||
|
* @OA\Property(type="object", description="Error", properties={@OA\Property (property="calculateWithWorkingDays", type="string", example="Invalid workingDays reason")}),
|
||||||
|
* @OA\Property(type="object", description="Error", properties={@OA\Property (property="startWorkday", type="string", example="Invalid startWorkday reason")}),
|
||||||
|
* @OA\Property(type="object", description="Error", properties={@OA\Property (property="endWorkday", type="string", example="Invalid endWorkday reason")}),
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* })
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function estimate(string $countryCode, string $startDate, int $minutesDuration, bool $calculateWithWorkingDays, string $startWorkday, string $endWorkday): JsonResponse
|
||||||
|
{
|
||||||
|
|
||||||
|
$validator = Validator::make(
|
||||||
|
[
|
||||||
|
'countryCode' => $countryCode,
|
||||||
|
'startDate' => $startDate,
|
||||||
|
'minutesDuration' => $minutesDuration,
|
||||||
|
'calculateWithWorkingDays' => $calculateWithWorkingDays,
|
||||||
|
'startWorkday' => $startWorkday,
|
||||||
|
'endWorkday' => $endWorkday,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'countryCode' => [new CountryCodeExists()],
|
||||||
|
'startDate' => ['required', new DateYMDHI()],
|
||||||
|
'minutesDuration' => 'integer|min:1',
|
||||||
|
'calculateWithWorkingDays' => 'boolean',
|
||||||
|
'startWorkday' => ['required', new DateHI()],
|
||||||
|
'endWorkday' => ['required', new DateHI()],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['errors' => $validator->errors()], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$duration = Duration::parse(sprintf('P0DT%dM', $minutesDuration));
|
||||||
|
$startWorkday = LocalTime::parse($startWorkday);
|
||||||
|
$startDate = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $startDate . ":00");
|
||||||
|
$endWorkday = LocalTime::parse($endWorkday);
|
||||||
|
|
||||||
|
if ($startDate === false) {
|
||||||
|
return response()->json(['errors' => ['startDate' => ['validation.date_ymdhi']]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startWorkday->isAfter($endWorkday)) {
|
||||||
|
return response()->json(['errors' => ['startWorkday' => ['validation.start_workday_must_be_before_end_workday']]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startWorkday->isEqualTo($endWorkday)) {
|
||||||
|
return response()->json(['errors' => ['startWorkday' => ['validation.start_workday_must_not_be_equal_to_end_workday']]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($startWorkday->isAfter(LocalTime::fromNativeDateTime($startDate))) {
|
||||||
|
return response()->json(['errors' => ['startDate' => ['validation.start_workday_must_be_before_start_date']]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endWorkday->isBefore(LocalTime::fromNativeDateTime($startDate))) {
|
||||||
|
return response()->json(['errors' => ['endWorkday' => ['validation.end_workday_must_be_after_start_date']]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$workMinutes = $this->durationConvertor->convertIntoWorkDuration($duration, $startWorkday, $endWorkday, $startDate);
|
||||||
|
if ($workMinutes->overlaps === true) {
|
||||||
|
$dueDate = $startDate->setTime($startWorkday->getHour(), $startWorkday->getMinute(), $startWorkday->getSecond())
|
||||||
|
->add(new \DateInterval(sprintf('PT%dM', $workMinutes->duration->toMinutes())));
|
||||||
|
} else {
|
||||||
|
$dueDate = $startDate->add(new \DateInterval(sprintf('PT%dM', $workMinutes->duration->toMinutes())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if false startDate + startDate must be after startWorkday and before endWorkday
|
||||||
|
if ($calculateWithWorkingDays) {
|
||||||
|
$durationSolver = $this->durationSolverFactory->createDurationSolverForCountry($countryCode);
|
||||||
|
$dueDate = $durationSolver->calculateDuration($startDate, $dueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'estimation_date' => $dueDate->format('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
95
app/Http/Controllers/Api/V1/WorkingDay.php
Normal file
95
app/Http/Controllers/Api/V1/WorkingDay.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Rules\CountryCodeExists;
|
||||||
|
use App\Rules\DateYMD;
|
||||||
|
use App\Services\WorkingDays\WorkingDayDeterminerFactory;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
final class WorkingDay
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly WorkingDayDeterminerFactory $workingDayDeterminerFactory
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/working-days/{countryCode}/{date}",
|
||||||
|
* summary="Get the working day status for a given date and country code",
|
||||||
|
* description="Returns whether the given date is a working day for the specified country code",
|
||||||
|
* @OA\Parameter(name="countryCode", in="path", description="The ISO 3166-1 alpha-2 country code"),
|
||||||
|
* @OA\Parameter(name="date", in="path", description="Date in YYYY-MM-DD format"),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="200",
|
||||||
|
* description="Returns the working day status for the given date and country code",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* properties={
|
||||||
|
* @OA\Property(property="date", type="string", format="date"),
|
||||||
|
* @OA\Property(property="isWorkingDay", type="boolean"),
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response="400",
|
||||||
|
* description="Invalid request parameters",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* type="object",
|
||||||
|
* properties={
|
||||||
|
* @OA\Property (
|
||||||
|
* type="object",
|
||||||
|
* property="errors",
|
||||||
|
* anyOf={
|
||||||
|
* @OA\Property(type="object", description="Error", properties={@OA\Property (property="date", type="string", example="Invalid date reason")}),
|
||||||
|
* @OA\Property(type="object", description="Error", properties={@OA\Property (property="countryCode", type="string", example="Invalid countryCode reason")}),
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* })
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param string $countryCode
|
||||||
|
* @param string $date
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function getWorkingDays(Request $request, string $countryCode, string $date): \Illuminate\Http\JsonResponse
|
||||||
|
{
|
||||||
|
$validator = Validator::make(
|
||||||
|
[
|
||||||
|
'date' => $date,
|
||||||
|
'countryCode' => $countryCode,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'date' => ['required', new DateYMD()],
|
||||||
|
'countryCode' => ['required', 'string', 'min:2', 'max:2', new CountryCodeExists()],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(['errors' => $validator->errors()], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$workingDayDeterminer = $this->workingDayDeterminerFactory->createForCountry($countryCode);
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
return response()->json(['errors' => ['countryCode' => $e->getMessage()]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = \DateTimeImmutable::createFromFormat("Y-m-d", $date);
|
||||||
|
|
||||||
|
if ($date === false) {
|
||||||
|
return response()->json(['errors' => ['date' => ['validation.dateymdhis']]], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'date' => $date->format('Y-m-d'),
|
||||||
|
'isWorkingDay' => $workingDayDeterminer->isWorkingDay($date)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -7,9 +7,11 @@
|
|||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
|
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
//
|
//
|
||||||
})
|
})
|
||||||
|
6
routes/api.php
Normal file
6
routes/api.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get('v1/working-days/{countryCode}/{date}', [\App\Http\Controllers\Api\V1\WorkingDay::class, 'getWorkingDays']);
|
||||||
|
Route::get('v1/task-estimation/{countryCode}/{startDate}/{minutesDuration}/{calculateWithWorkingDays}/{startWorkday}/{endWorkday}', [\App\Http\Controllers\Api\V1\TaskEstimation::class, 'estimate']);
|
88
tests/Feature/TaskEstimationTest.php
Normal file
88
tests/Feature/TaskEstimationTest.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class TaskEstimationTest extends TestCase
|
||||||
|
{
|
||||||
|
#[DataProvider('estimationDataProvider')]
|
||||||
|
public function testEstimationTasks(bool $calculateWithWorkingDays, string $startDate, int $minutesDuration, string $result): void
|
||||||
|
{
|
||||||
|
$response = $this->get($this->buildUrl('CZ', $startDate, $minutesDuration, $calculateWithWorkingDays, '08:00', '16:00'));
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJson([
|
||||||
|
'estimation_date' => $result,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<array<bool|string|int>>
|
||||||
|
*/
|
||||||
|
public static function estimationDataProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[false, '2024-01-03 08:00', 60, '2024-01-03 09:00:00'], // 1H
|
||||||
|
[false, '2024-01-03 08:00', 60 * 9 + 1, '2024-01-04 09:01:00'], // 1D, 1H, 1M
|
||||||
|
[false, '2024-01-01 08:00', 1, '2024-01-01 08:01:00'], // 1M
|
||||||
|
[false, '2024-01-01 08:00', 60 * 8, '2024-01-01 16:00:00'], // 1 working day
|
||||||
|
[true, '2024-01-05 08:00', 60 * 8 * 2, '2024-01-08 16:00:00'], // 1 working day, 6,7 is weekend
|
||||||
|
[true, '2024-01-05 08:00', 60 * 8, '2024-01-05 16:00:00'], // 1 working day without weekend*/
|
||||||
|
[false, '2024-01-05 15:59', 1, '2024-01-05 16:00:00'], // 1 minute before ending workday
|
||||||
|
[false, '2024-01-05 15:59', 2, '2024-01-06 08:01:00'], // 2 minutes before ending workday
|
||||||
|
[true, '2024-01-05 15:59', 2, '2024-01-08 08:01:00'], // 1 minute before ending workday, with weekend
|
||||||
|
[true, '2024-01-03 08:00', 60 * 9 + 1, '2024-01-04 09:01:00'], // 1D, 1H, 1M
|
||||||
|
[true, '2024-01-06 08:00', 60, '2024-01-08 09:00:00'] // 1 H, over weekend
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('invalidArgumentsDataProvider')]
|
||||||
|
public function testInvalidArguments(string $countryCode, string $startDate, int $minutesDuration, bool $calculateWithWorkingDays, string $startWork, string $endWork): void
|
||||||
|
{
|
||||||
|
$url = $this->buildUrl($countryCode, $startDate, $minutesDuration, $calculateWithWorkingDays, $startWork, $endWork);
|
||||||
|
$response = $this->get($url);
|
||||||
|
$response->assertStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<array<bool|string|int>>
|
||||||
|
*/
|
||||||
|
public static function invalidArgumentsDataProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['ZZ', '2024-01-03 08:00', 30, false, '08:00', '16:00'], // bad country code
|
||||||
|
['FOO', '2024-01-03 08:00', 30, false, '08:00', '16:00'], // bad country code
|
||||||
|
['CZ', '2024-01-03 00:00', 30, false, '08:00', '16:00'], // missing seconds
|
||||||
|
['CZ', '2024-01-03 08:00', -1000, false, '08:00', '16:00'], // negative duration
|
||||||
|
['CZ', '2024-01-03 08:00', 30, false, '08:0?', '16:00'], // ? in start work time
|
||||||
|
['CZ', '2024-01-03 08:00', 30, false, '08:00', '16:X0'], // X in end work time
|
||||||
|
['CZ', '2024-01-03 08:00', 30, false, '08:00', 'FOO:00'], // FOO in end work time
|
||||||
|
['CZ', '2024-01-03 08:00', 30, false, '09:00', '16:00'], // 08:00 is before workday
|
||||||
|
['CZ', '2024-01-03 11:00', 30, false, '09:00', '10:00'], // 11 is after workday
|
||||||
|
['CZ', '2024-01-03 09:00', 30, false, '09:00', '08:00'], // 09:00 > 08:00
|
||||||
|
['CZ', '2024-01-03 08:00', 30, false, '09:00', '09:00'], // equal startDate and endDate
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function buildUrl(string $countryCode, string $startDate, int $minutesDuration, bool $calculateWithWorkingDays, string $startWork, string $endWork): string
|
||||||
|
{
|
||||||
|
$uri = [];
|
||||||
|
$uri[] = "api";
|
||||||
|
$uri[] = "v1";
|
||||||
|
$uri[] = "task-estimation";
|
||||||
|
$uri[] = $countryCode;
|
||||||
|
$uri[] = $startDate;
|
||||||
|
$uri[] = $minutesDuration;
|
||||||
|
$uri[] = (($calculateWithWorkingDays === false) ? '0' : '1');
|
||||||
|
$uri[] = $startWork;
|
||||||
|
$uri[] = $endWork;
|
||||||
|
|
||||||
|
return implode('/', array_map(function ($part) {
|
||||||
|
return rawurlencode((string) $part);
|
||||||
|
}, $uri));
|
||||||
|
}
|
||||||
|
}
|
57
tests/Feature/WorkingDayTest.php
Normal file
57
tests/Feature/WorkingDayTest.php
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class WorkingDayTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testWorkingDayWillReturnSuccessResponseAndTrueIfWorkingDayGiven(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/api/v1/working-days/CZ/2024-01-15'); // Mon
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJson(
|
||||||
|
[
|
||||||
|
'date' => '2024-01-15',
|
||||||
|
'isWorkingDay' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWorkingDayWillReturnSuccessResponseAndFalseIfWeekendDayGiven(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/api/v1/working-days/CZ/2024-01-07'); // San
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJson(
|
||||||
|
[
|
||||||
|
'date' => '2024-01-07',
|
||||||
|
'isWorkingDay' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWorkingDayWillReturnErrorResponseIfInvalidCountryCodeGiven(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/api/v1/working-days/XX/2024-01-01'); // Invalid country code
|
||||||
|
$response->assertStatus(400);
|
||||||
|
$response->assertJsonStructure(
|
||||||
|
[
|
||||||
|
'errors' => ['countryCode']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWorkingDayWillReturnErrorResponseIfInvalidDateFormatGiven(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/api/v1/working-days/CZ/invalid-date'); // Invalid date format
|
||||||
|
$response->assertStatus(400);
|
||||||
|
$response->assertJsonStructure(
|
||||||
|
[
|
||||||
|
'errors' => ['date']
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user