From 1e8f4983be1b9c47e5ea0b06738f9fd0ea3b8bba Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Wed, 7 Aug 2024 13:01:34 +0200 Subject: [PATCH] feat(controllers): add api endpoints --- .../Controllers/Api/V1/TaskEstimation.php | 136 ++++++++++++++++++ app/Http/Controllers/Api/V1/WorkingDay.php | 95 ++++++++++++ bootstrap/app.php | 2 + routes/api.php | 6 + tests/Feature/TaskEstimationTest.php | 88 ++++++++++++ tests/Feature/WorkingDayTest.php | 57 ++++++++ 6 files changed, 384 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/TaskEstimation.php create mode 100644 app/Http/Controllers/Api/V1/WorkingDay.php create mode 100644 routes/api.php create mode 100644 tests/Feature/TaskEstimationTest.php create mode 100644 tests/Feature/WorkingDayTest.php diff --git a/app/Http/Controllers/Api/V1/TaskEstimation.php b/app/Http/Controllers/Api/V1/TaskEstimation.php new file mode 100644 index 0000000..b445e25 --- /dev/null +++ b/app/Http/Controllers/Api/V1/TaskEstimation.php @@ -0,0 +1,136 @@ + $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'), + ]); + } +} diff --git a/app/Http/Controllers/Api/V1/WorkingDay.php b/app/Http/Controllers/Api/V1/WorkingDay.php new file mode 100644 index 0000000..885df0e --- /dev/null +++ b/app/Http/Controllers/Api/V1/WorkingDay.php @@ -0,0 +1,95 @@ + $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) + ]); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 7b162da..b4b52b4 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,9 +7,11 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) + ->withMiddleware(function (Middleware $middleware) { // }) diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..af9fd5d --- /dev/null +++ b/routes/api.php @@ -0,0 +1,6 @@ +get($this->buildUrl('CZ', $startDate, $minutesDuration, $calculateWithWorkingDays, '08:00', '16:00')); + $response->assertStatus(200); + $response->assertJson([ + 'estimation_date' => $result, + ]); + } + + /** + * @return array> + */ + 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> + */ + 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)); + } +} diff --git a/tests/Feature/WorkingDayTest.php b/tests/Feature/WorkingDayTest.php new file mode 100644 index 0000000..9512323 --- /dev/null +++ b/tests/Feature/WorkingDayTest.php @@ -0,0 +1,57 @@ +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'] + ] + ); + } +}