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 @@ use Illuminate\Foundation\Configuration\Middleware; | ||||
| 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) { | ||||
|         //
 | ||||
|     }) | ||||
|  | ||||
							
								
								
									
										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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user