diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index c51474e..2393b5a 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -20,7 +20,7 @@ 'strict_param' => false, 'array_syntax' => ['syntax' => 'short'], 'concat_space' => ['spacing' => 'one'], - 'phpdoc_align' => [], + 'phpdoc_align' => ['align' => 'left'], 'phpdoc_summary' => false, 'void_return' => false, 'phpdoc_var_without_name' => false, diff --git a/src/Client/Client.php b/src/Client/Client.php index ea1cac1..55bdc03 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -2,10 +2,13 @@ namespace Answear\FanCourierBundle\Client; +use Answear\FanCourierBundle\ConfigProvider; use Answear\FanCourierBundle\Exception\RequestException; use Answear\FanCourierBundle\Exception\ResponseException; use Answear\FanCourierBundle\Logger\FanCourierLogger; +use Answear\FanCourierBundle\Request\LoginRequest; use Answear\FanCourierBundle\Request\RequestInterface; +use Answear\FanCourierBundle\Response\LoginResponse; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\GuzzleException; @@ -15,21 +18,45 @@ class Client { private const CONNECTION_TIMEOUT = 10; private const TIMEOUT = 30; - + + private ?string $token = null; + public function __construct( private readonly RequestTransformerInterface $requestTransformer, private readonly FanCourierLogger $logger, + private readonly ConfigProvider $configProvider, private ?ClientInterface $client = null, ) { $this->client ??= new GuzzleClient(['timeout' => self::TIMEOUT, 'connect_timeout' => self::CONNECTION_TIMEOUT]); } + public function login(): void + { + $loginRequest = new LoginRequest( + $this->configProvider->username, + $this->configProvider->password, + ); + + $response = $this->request($loginRequest); + + $loginResponse = LoginResponse::fromArray( + \json_decode($response->getBody()->getContents(), true), + ); + + $this->token = $loginResponse->token; + } + public function request(RequestInterface $request): ResponseInterface { $this->logger->setRequestId(uniqid('FANCOURIER-', more_entropy: true)); try { $psrRequest = $this->requestTransformer->transform($request); + + if ($this->token) { + $psrRequest = $psrRequest->withHeader('Authorization', 'Bearer ' . $this->token); + } + $this->logger->logRequest($request->getEndpoint(), $psrRequest); $response = $this->client->send($psrRequest); diff --git a/src/Client/RequestTransformer.php b/src/Client/RequestTransformer.php index 7cfe355..4d38025 100644 --- a/src/Client/RequestTransformer.php +++ b/src/Client/RequestTransformer.php @@ -21,17 +21,11 @@ public function transform(RequestInterface $request): PsrRequestInterface { $url = $this->configProvider->apiUrl . $request->getEndpoint(); - $formParams = [ - 'username' => $this->configProvider->username, - 'client_id' => $this->configProvider->clientId, - 'user_pass' => $this->configProvider->password, - ]; - return new HttpRequest( $request->getMethod(), new Uri($url), - ['Content-Type' => 'application/x-www-form-urlencoded'], - http_build_query(array_merge($formParams, $request->getOptions())) + ['Content-Type' => 'application/json'], + http_build_query($request->getOptions()), ); } } diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 7690a60..277b71f 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -7,7 +7,6 @@ class ConfigProvider { public function __construct( - public readonly string $clientId, public readonly string $username, public readonly string $password, public readonly string $apiUrl, diff --git a/src/DTO/AddressDTO.php b/src/DTO/AddressDTO.php new file mode 100644 index 0000000..686fe7a --- /dev/null +++ b/src/DTO/AddressDTO.php @@ -0,0 +1,43 @@ + $drawerArray + * @param array $drawerArray */ public static function fromArray(array $drawerArray): self { diff --git a/src/DTO/DrawerDTO.php b/src/DTO/DrawerDTO.php index a426aa7..5b7195d 100644 --- a/src/DTO/DrawerDTO.php +++ b/src/DTO/DrawerDTO.php @@ -8,7 +8,7 @@ class DrawerDTO { public function __construct( public readonly int $number, - public readonly string $type + public readonly string $type, ) { } } diff --git a/src/DTO/PickupPointDTO.php b/src/DTO/PickupPointDTO.php index dbef1ba..5e93835 100644 --- a/src/DTO/PickupPointDTO.php +++ b/src/DTO/PickupPointDTO.php @@ -10,52 +10,42 @@ class PickupPointDTO { public function __construct( public readonly string $id, + public readonly string $code, public readonly string $name, public readonly string $routingLocation, public readonly string $description, - public readonly string $county, - public readonly string $locality, - public readonly string $address, - public readonly string $zipCode, - public readonly string $locationReference, - public readonly float $latitude, - public readonly float $longitude, + public readonly AddressDTO $address, + public readonly string $latitude, + public readonly string $longitude, public readonly ScheduleCollection $schedule, - public readonly DrawerCollection $drawer + public readonly DrawerCollection $drawer, ) { } public static function fromArray(array $pickupPoint): self { - Assert::stringNotEmpty($pickupPoint['Id']); - Assert::stringNotEmpty($pickupPoint['Name']); - Assert::stringNotEmpty($pickupPoint['RoutingLocation']); - Assert::stringNotEmpty($pickupPoint['Description']); - Assert::stringNotEmpty($pickupPoint['County']); - Assert::stringNotEmpty($pickupPoint['Locality']); - Assert::stringNotEmpty($pickupPoint['Address']); - Assert::stringNotEmpty($pickupPoint['ZipCode']); - Assert::stringNotEmpty($pickupPoint['LocationReference']); - Assert::float($pickupPoint['Latitude']); - Assert::float($pickupPoint['Longitude']); - Assert::range($pickupPoint['Latitude'], -90, 90); - Assert::range($pickupPoint['Longitude'], -180, 180); - Assert::count($pickupPoint['Schedule'], 7); + Assert::stringNotEmpty($pickupPoint['id']); + Assert::stringNotEmpty($pickupPoint['name']); + Assert::stringNotEmpty($pickupPoint['routingLocation']); + Assert::stringNotEmpty($pickupPoint['description']); + Assert::notEmpty($pickupPoint['address']); + Assert::stringNotEmpty($pickupPoint['latitude']); + Assert::stringNotEmpty($pickupPoint['longitude']); + Assert::range((float) $pickupPoint['latitude'], -90, 90); + Assert::range((float) $pickupPoint['longitude'], -180, 180); + Assert::count($pickupPoint['schedule'], 7); return new self( - $pickupPoint['Id'], - $pickupPoint['Name'], - $pickupPoint['RoutingLocation'], - $pickupPoint['Description'], - $pickupPoint['County'], - $pickupPoint['Locality'], - $pickupPoint['Address'], - $pickupPoint['ZipCode'], - $pickupPoint['LocationReference'], - $pickupPoint['Latitude'], - $pickupPoint['Longitude'], - ScheduleCollection::fromArray($pickupPoint['Schedule']), - DrawerCollection::fromArray($pickupPoint['Drawer']) + $pickupPoint['id'], + $pickupPoint['code'], + $pickupPoint['name'], + $pickupPoint['routingLocation'], + $pickupPoint['description'], + AddressDTO::fromArray($pickupPoint['address']), + $pickupPoint['latitude'], + $pickupPoint['longitude'], + ScheduleCollection::fromArray($pickupPoint['schedule']), + DrawerCollection::fromArray($pickupPoint['drawer']), ); } } diff --git a/src/DTO/ScheduleCollection.php b/src/DTO/ScheduleCollection.php index 24e9d90..2d43f14 100644 --- a/src/DTO/ScheduleCollection.php +++ b/src/DTO/ScheduleCollection.php @@ -12,20 +12,20 @@ class ScheduleCollection * @param ScheduleDTO[] $schedules */ public function __construct( - public readonly array $schedules = [] + public readonly array $schedules = [], ) { } /** - * @param array $scheduleArray + * @param array $scheduleArray */ public static function fromArray(array $scheduleArray): self { $schedules = []; foreach ($scheduleArray as $schedule) { - Assert::stringNotEmpty($schedule['startHour']); - Assert::stringNotEmpty($schedule['stopHour']); - $schedules[] = new ScheduleDTO($schedule['startHour'], $schedule['stopHour']); + Assert::stringNotEmpty($schedule['firstHour']); + Assert::stringNotEmpty($schedule['secondHour']); + $schedules[] = new ScheduleDTO($schedule['firstHour'], $schedule['secondHour']); } return new self($schedules); diff --git a/src/DTO/ScheduleDTO.php b/src/DTO/ScheduleDTO.php index 7a19e2f..2c9fbd3 100644 --- a/src/DTO/ScheduleDTO.php +++ b/src/DTO/ScheduleDTO.php @@ -7,8 +7,8 @@ class ScheduleDTO { public function __construct( - public readonly string $startHour, - public readonly string $stopHour + public readonly string $firstHour, + public readonly string $secondHour, ) { } } diff --git a/src/DependencyInjection/AnswearFanCourierExtension.php b/src/DependencyInjection/AnswearFanCourierExtension.php index f8dd5fb..0ea192a 100755 --- a/src/DependencyInjection/AnswearFanCourierExtension.php +++ b/src/DependencyInjection/AnswearFanCourierExtension.php @@ -38,7 +38,6 @@ public function load(array $configs, ContainerBuilder $container): void $definition = $container->getDefinition(ConfigProvider::class); $definition->setArguments([ - $this->config['clientId'], $this->config['username'], $this->config['password'], $this->config['apiUrl'], diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 599ec19..ce7b56f 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -15,8 +15,7 @@ public function getConfigTreeBuilder(): TreeBuilder $treeBuilder->getRootNode() ->children() - ->scalarNode('clientId')->isRequired()->cannotBeEmpty()->end() - ?->scalarNode('username')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('username')->isRequired()->cannotBeEmpty()->end() ?->scalarNode('password')->isRequired()->cannotBeEmpty()->end() ?->scalarNode('apiUrl')->isRequired()->cannotBeEmpty()->end() ?->scalarNode('logger')->defaultNull()->end() diff --git a/src/Request/GetPickupPointsRequest.php b/src/Request/GetPickupPointsRequest.php index b80def3..fd074de 100644 --- a/src/Request/GetPickupPointsRequest.php +++ b/src/Request/GetPickupPointsRequest.php @@ -6,7 +6,7 @@ class GetPickupPointsRequest implements RequestInterface { - private const ENDPOINT = '/pickup-points.php'; + private const ENDPOINT = '/reports/pickup-points'; private const HTTP_METHOD = 'POST'; public function getEndpoint(): string @@ -23,4 +23,9 @@ public function getOptions(): array { return ['type' => 'fanbox']; } + + public function getBody(): array + { + return []; + } } diff --git a/src/Request/LoginRequest.php b/src/Request/LoginRequest.php new file mode 100644 index 0000000..4cf2cba --- /dev/null +++ b/src/Request/LoginRequest.php @@ -0,0 +1,40 @@ + $this->username, + 'password' => $this->password, + ]; + } +} diff --git a/src/Request/RequestInterface.php b/src/Request/RequestInterface.php index fdf7d0e..d8510fe 100644 --- a/src/Request/RequestInterface.php +++ b/src/Request/RequestInterface.php @@ -11,4 +11,6 @@ public function getEndpoint(): string; public function getMethod(): string; public function getOptions(): array; + + public function getBody(): array; } diff --git a/src/Response/GetPickupPointsResponse.php b/src/Response/GetPickupPointsResponse.php index 11272dc..c15172e 100644 --- a/src/Response/GetPickupPointsResponse.php +++ b/src/Response/GetPickupPointsResponse.php @@ -5,6 +5,7 @@ namespace Answear\FanCourierBundle\Response; use Answear\FanCourierBundle\DTO\PickupPointDTO; +use Webmozart\Assert\Assert; class GetPickupPointsResponse implements ResponseInterface { @@ -12,17 +13,23 @@ class GetPickupPointsResponse implements ResponseInterface * @param PickupPointDTO[] $pickupPoints */ public function __construct( - public readonly array $pickupPoints + public readonly string $status, + public readonly array $pickupPoints, ) { } public static function fromArray(array $data): self { - $pickupPoints = []; - foreach ($data as $pickupPoint) { - $pickupPoints[] = PickupPointDTO::fromArray($pickupPoint); - } + Assert::same($data['status'], 'success', 'Login failed'); - return new self($pickupPoints); + $pickupPoints = array_map( + static fn(array $pickupPoint): PickupPointDTO => PickupPointDTO::fromArray($pickupPoint), + $data['data'], + ); + + return new self( + $data['status'], + $pickupPoints, + ); } } diff --git a/src/Response/LoginResponse.php b/src/Response/LoginResponse.php new file mode 100644 index 0000000..fcf2507 --- /dev/null +++ b/src/Response/LoginResponse.php @@ -0,0 +1,27 @@ +client->login(); + $response = $this->client->request(new GetPickupPointsRequest()); $pickupPointsResponse = GetPickupPointsResponse::fromArray($this->decodeResponse($response)); diff --git a/tests/Acceptance/DependencyInjection/ConfigurationTest.php b/tests/Acceptance/DependencyInjection/ConfigurationTest.php index 481e41b..dc5030d 100644 --- a/tests/Acceptance/DependencyInjection/ConfigurationTest.php +++ b/tests/Acceptance/DependencyInjection/ConfigurationTest.php @@ -31,15 +31,14 @@ public function validTest(array $configs): void $configProviderDefinition = $builder->getDefinition(ConfigProvider::class); - self::assertSame($configs[0]['clientId'], $configProviderDefinition->getArgument(0)); - self::assertSame($configs[0]['username'], $configProviderDefinition->getArgument(1)); - self::assertSame($configs[0]['password'], $configProviderDefinition->getArgument(2)); - self::assertSame($configs[0]['apiUrl'], $configProviderDefinition->getArgument(3)); + self::assertSame($configs[0]['username'], $configProviderDefinition->getArgument(0)); + self::assertSame($configs[0]['password'], $configProviderDefinition->getArgument(1)); + self::assertSame($configs[0]['apiUrl'], $configProviderDefinition->getArgument(2)); } #[Test] #[DataProvider('provideInvalidConfig')] - public function invalidConfig(array $config, string $expectedMessage = null): void + public function invalidConfig(array $config, ?string $expectedMessage = null): void { $this->assertConfigurationIsInvalid( $config, @@ -71,15 +70,6 @@ public static function provideInvalidConfig(): iterable '"answear_fan_courier" must be configured.', ]; - yield [ - [ - [ - 'clientId' => 'test', - ], - ], - '"answear_fan_courier" must be configured.', - ]; - yield [ [ [ @@ -113,7 +103,6 @@ public static function provideInvalidLogger(): iterable yield [ [ [ - 'clientId' => 'clientId', 'username' => 'username', 'password' => 'password', 'apiUrl' => 'apiUrl', @@ -129,7 +118,6 @@ public static function provideValidConfig(): iterable yield [ [ [ - 'clientId' => 'clientId', 'username' => 'username', 'password' => 'password', 'apiUrl' => 'apiUrl', @@ -140,10 +128,9 @@ public static function provideValidConfig(): iterable yield [ [ [ - 'clientId' => 123, 'username' => 'kimi', 'password' => 'password', - 'apiUrl' => 'www.softwear.co', + 'apiUrl' => 'api.fancourier.ro', ], ], ]; diff --git a/tests/Integration/Service/PickupPointServiceTest.php b/tests/Integration/Service/PickupPointServiceTest.php index b71b883..6e180ac 100644 --- a/tests/Integration/Service/PickupPointServiceTest.php +++ b/tests/Integration/Service/PickupPointServiceTest.php @@ -8,29 +8,35 @@ use Answear\FanCourierBundle\Client\RequestTransformer; use Answear\FanCourierBundle\ConfigProvider; use Answear\FanCourierBundle\DTO\PickupPointDTO; +use Answear\FanCourierBundle\Exception\RequestException; use Answear\FanCourierBundle\Logger\FanCourierLogger; use Answear\FanCourierBundle\Service\PickupPointService; use Answear\FanCourierBundle\Tests\MockGuzzleTrait; use GuzzleHttp\Psr7\Response; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Log\NullLogger; +use Psr\Log\LoggerInterface; class PickupPointServiceTest extends TestCase { use MockGuzzleTrait; private Client $client; + private LoggerInterface|MockObject $logger; public function setUp(): void { parent::setUp(); - $configProvider = new ConfigProvider('clientId', 'username', 'password', 'www.softwear.co'); + $this->logger = $this->createMock(LoggerInterface::class); + + $configProvider = new ConfigProvider('username', 'password', 'https://api.fancourier.ro'); $this->client = new Client( new RequestTransformer($configProvider), - new FanCourierLogger(new NullLogger()), - $this->setupGuzzleClient() + new FanCourierLogger($this->logger), + $configProvider, + $this->setupGuzzleClient(), ); } @@ -38,7 +44,9 @@ public function setUp(): void public function successfulFindPoints(): void { $service = $this->getService(); - $this->mockGuzzleResponse(new Response(200, [], $this->getSuccessfulBody())); + + $this->mockGuzzleResponse(new Response(200, [], $this->getSuccessfulLoginBody())); + $this->mockGuzzleResponse(new Response(200, [], $this->getSuccessfulPickupPointsBody())); $pickupPoints = $service->getAll(); @@ -46,22 +54,46 @@ public function successfulFindPoints(): void $this->assertPoint($pickupPoints[0]); } + #[Test] + public function failedLogin(): void + { + $service = $this->getService(); + + $this->mockGuzzleResponse(new Response(401, [])); + + $this->logger->expects(self::once()) + ->method('error') + ->with('[FANCOURIER] Exception - /login'); + + $this->expectException(RequestException::class); + + $service->getAll(); + } + private function assertPoint(PickupPointDTO $pickupPoint): void { $this->assertNotNull($pickupPoint); - $this->assertSame($pickupPoint->id, 'FAN0039'); + $this->assertSame($pickupPoint->id, 'F1000005'); + $this->assertSame($pickupPoint->code, 'FAN0039'); $this->assertSame($pickupPoint->name, 'FANbox Kaufland Theodor Pallady'); $this->assertSame($pickupPoint->routingLocation, 'FANbox Kaufland Theodor Pallady (Locker)'); $this->assertSame($pickupPoint->description, 'In dreapta intrarii principale'); - $this->assertSame($pickupPoint->county, 'Bucuresti'); - $this->assertSame($pickupPoint->locality, 'Bucuresti'); - $this->assertSame($pickupPoint->address, 'Bd. Theodor Pallady, Nr. 51'); - $this->assertSame($pickupPoint->zipCode, '32258'); - $this->assertSame($pickupPoint->locationReference, 'In dreapta intrarii principale'); - $this->assertSame($pickupPoint->latitude, 44.40874); - $this->assertSame($pickupPoint->longitude, 26.19726); + $this->assertSame($pickupPoint->address->county, 'Bucuresti'); + $this->assertSame($pickupPoint->address->locality, 'Bucuresti'); + $this->assertSame($pickupPoint->address->street, 'Bd. Theodor Pallady'); + $this->assertSame($pickupPoint->address->streetNo, '51'); + $this->assertSame($pickupPoint->address->zipCode, '032258'); + $this->assertSame($pickupPoint->address->reference, 'In dreapta intrarii principale'); + $this->assertSame($pickupPoint->latitude, '44.40874'); + $this->assertSame($pickupPoint->longitude, '26.19726'); $this->assertCount(7, $pickupPoint->schedule->schedules); $this->assertCount(3, $pickupPoint->drawer->drawers); + + $this->assertSame($pickupPoint->schedule->schedules[0]->firstHour, '00:00'); + $this->assertSame($pickupPoint->schedule->schedules[0]->secondHour, '23:59'); + + $this->assertSame($pickupPoint->drawer->drawers[0]->type, 'L'); + $this->assertSame($pickupPoint->drawer->drawers[0]->number, 4); } private function getService(): PickupPointService @@ -69,8 +101,13 @@ private function getService(): PickupPointService return new PickupPointService($this->client); } - private function getSuccessfulBody(): string + private function getSuccessfulLoginBody(): string + { + return file_get_contents(__DIR__ . '/data/exampleLoginResponse.json'); + } + + private function getSuccessfulPickupPointsBody(): string { - return file_get_contents(__DIR__ . '/data/exampleResponse.json'); + return file_get_contents(__DIR__ . '/data/examplePickupPointsResponse.json'); } } diff --git a/tests/Integration/Service/data/exampleLoginResponse.json b/tests/Integration/Service/data/exampleLoginResponse.json new file mode 100644 index 0000000..a6c6d6c --- /dev/null +++ b/tests/Integration/Service/data/exampleLoginResponse.json @@ -0,0 +1,7 @@ +{ + "status": "success", + "data": { + "token": "10000000|yaaTDcsMwFfdlrAnD0MtOkEnprn1NMqgS30RLH4C5gg", + "expiresAt": "2024-09-18 16:35:24" + } +} \ No newline at end of file diff --git a/tests/Integration/Service/data/examplePickupPointsResponse.json b/tests/Integration/Service/data/examplePickupPointsResponse.json new file mode 100644 index 0000000..172a565 --- /dev/null +++ b/tests/Integration/Service/data/examplePickupPointsResponse.json @@ -0,0 +1,69 @@ +{ + "status": "success", + "data": [ + { + "id": "F1000005", + "code": "FAN0039", + "name": "FANbox Kaufland Theodor Pallady", + "routingLocation": "FANbox Kaufland Theodor Pallady (Locker)", + "description": "In dreapta intrarii principale", + "address": { + "locality": "Bucuresti", + "county": "Bucuresti", + "street": "Bd. Theodor Pallady", + "streetNo": "51", + "zipCode": "032258", + "floor": "", + "reference": "In dreapta intrarii principale" + }, + "latitude": "44.40874", + "longitude": "26.19726", + "schedule": [ + { + "firstHour": "00:00", + "secondHour": "23:59" + }, + { + "firstHour": "00:00", + "secondHour": "23:59" + }, + { + "firstHour": "00:00", + "secondHour": "23:59" + }, + { + "firstHour": "00:00", + "secondHour": "23:59" + }, + { + "firstHour": "00:00", + "secondHour": "23:59" + }, + { + "firstHour": "00:00", + "secondHour": "23:59" + }, + { + "firstHour": "00:00", + "secondHour": "23:59" + } + ], + "drawer": [ + { + "type": "L", + "number": 4 + }, + { + "type": "M", + "number": 2 + }, + { + "type": "S", + "number": 9 + } + ], + "phones": null, + "email": null + } + ] +} \ No newline at end of file diff --git a/tests/Integration/Service/data/exampleResponse.json b/tests/Integration/Service/data/exampleResponse.json deleted file mode 100644 index 6aba817..0000000 --- a/tests/Integration/Service/data/exampleResponse.json +++ /dev/null @@ -1,59 +0,0 @@ -[ - { - "Id": "FAN0039", - "Name": "FANbox Kaufland Theodor Pallady", - "RoutingLocation": "FANbox Kaufland Theodor Pallady (Locker)", - "Description": "In dreapta intrarii principale", - "County": "Bucuresti", - "Locality": "Bucuresti", - "Address": "Bd. Theodor Pallady, Nr. 51", - "ZipCode": "32258", - "LocationReference": "In dreapta intrarii principale", - "Latitude": 44.40874, - "Longitude": 26.19726, - "Schedule": [ - { - "startHour": "00: 00", - "stopHour": "23:59" - }, - { - "startHour": "00: 00", - "stopHour": "23: 59" - }, - { - "startHour": "00: 00", - "stopHour": "23: 59" - }, - { - "startHour": "00:00", - "stopHour": "23: 59" - }, - { - "startHour": "00: 00", - "stopHour": "23: 59" - }, - { - "startHour": "00: 00", - "stopHour": "23:59" - }, - { - "startHour": "00: 00", - "stopHour": "23: 59" - } - ], - "Drawer": [ - { - "number": 6, - "type": "L" - }, - { - "number": 3, - "type": "M" - }, - { - "number": 17, - "type": "S" - } - ] - } -]