From 8ac62dd1ca2f1b4da4c7d57a6b224285103a4dc5 Mon Sep 17 00:00:00 2001 From: GantasG Date: Mon, 31 Aug 2026 14:19:58 +0300 Subject: [PATCH 1/4] DGS-444, DGS-446: align courier request pickup time with DPD's slot contract (#181) DPD's live pickupOrderSave_ endpoint now rejects any pickup time that is not one of its fixed pickupTimeFrom/pickupTimeTo slots, and answers with a JSON object instead of the legacy plain-text response. Request side: - The 'Desired pick-up time' / 'Last pick-up time' datetime inputs are replaced with a pick-up date field and two slot dropdowns constrained to DPD's whitelists, so an invalid value can no longer be submitted. - Defaults are slot-aligned (next available slot today, or tomorrow 08:00) instead of the raw 'now + 30 min' value DPD always rejected. - CourierRequestValidator rejects non-whitelisted slots server-side. - The API request now carries pickupDate/pickupTimeFrom/pickupTimeTo alongside the legacy pickupTime/senderWorkUntil datetimes, which are kept for DPD endpoints still running the old contract and for the module's own entity. Response side: - CourierRequestResponseParser accepts both response shapes: the legacy '

DONE' / '402 Error:' strings and the new JSON object, whose body->message (or errlog) is surfaced to the merchant. Previously the JSON object reached strpos() and fatally crashed the admin page on PHP 8. The unit-test bootstrap now defines _PS_VERSION_, without which every test touching module classes silently exits on the file guards. Requires invertus/dpdbaltics-api with CourierRequestRequest slot setters (companion PR on that repo); composer.lock to be bumped once it lands on the developer branch. --- config/service.yml | 4 + ...dminDPDBalticsCourierRequestController.php | 63 +++++++++-- src/Config/Config.php | 7 ++ src/DTO/CourierRequestData.php | 63 +++++++++++ src/Service/API/CourierRequestService.php | 45 ++++---- .../Parser/CourierRequestResponseParser.php | 71 ++++++++++++ src/Util/PickupTimeSlotUtility.php | 79 +++++++++++++ src/Util/TimeZoneUtility.php | 29 +++++ .../CourierRequestValidator.php | 20 ++++ .../CourierRequestResponseParserTest.php | 104 ++++++++++++++++++ tests/Unit/Util/PickupTimeSlotUtilityTest.php | 63 +++++++++++ .../CourierRequestValidatorTest.php | 92 ++++++++++++++++ tests/Unit/bootstrap.php | 4 + 13 files changed, 610 insertions(+), 34 deletions(-) create mode 100644 src/Service/API/Parser/CourierRequestResponseParser.php create mode 100644 src/Util/PickupTimeSlotUtility.php create mode 100644 tests/Unit/Service/API/Parser/CourierRequestResponseParserTest.php create mode 100644 tests/Unit/Util/PickupTimeSlotUtilityTest.php create mode 100644 tests/Unit/Validate/CourierRequest/CourierRequestValidatorTest.php diff --git a/config/service.yml b/config/service.yml index 7cfbbfea..5ee6c37e 100644 --- a/config/service.yml +++ b/config/service.yml @@ -213,11 +213,15 @@ services: - '@invertus.dpdbaltics_api.factory.apirequest.collection_request_factory' - '@dpdbaltics' + invertus.dpdbaltics.service.api.parser.courier_request_response_parser: + class: 'Invertus\dpdBaltics\Service\API\Parser\CourierRequestResponseParser' + invertus.dpdbaltics.service.api.courier_request_service: class: 'Invertus\dpdBaltics\Service\API\CourierRequestService' arguments: - '@invertus.dpdbaltics_api.factory.apirequest.courier_request_factory' - '@dpdbaltics' + - '@invertus.dpdbaltics.service.api.parser.courier_request_response_parser' invertus.dpdbaltics.service.parcel.parcel_update_service: class: 'Invertus\dpdBaltics\Service\Parcel\ParcelUpdateService' diff --git a/controllers/admin/AdminDPDBalticsCourierRequestController.php b/controllers/admin/AdminDPDBalticsCourierRequestController.php index c8a696c1..361e04f8 100644 --- a/controllers/admin/AdminDPDBalticsCourierRequestController.php +++ b/controllers/admin/AdminDPDBalticsCourierRequestController.php @@ -30,6 +30,7 @@ use Invertus\dpdBaltics\Repository\PhonePrefixRepository; use Invertus\dpdBaltics\Service\API\CourierRequestService; use Invertus\dpdBaltics\Service\Exception\ExceptionService; +use Invertus\dpdBaltics\Util\PickupTimeSlotUtility; use Invertus\dpdBaltics\Util\TimeZoneUtility; use Invertus\dpdBaltics\Validate\CourierRequest\CourierRequestValidator; use Invertus\dpdBalticsApi\Exception\DPDBalticsAPIException; @@ -160,16 +161,35 @@ private function initForm() 'required' => true, ], [ - 'label' => $this->module->l('Desired pick-up time'), - 'name' => 'pick_up_time', - 'type' => 'datetime', + 'label' => $this->module->l('Pick-up date'), + 'name' => 'pick_up_date', + 'type' => 'date', 'required' => true, ], [ - 'label' => $this->module->l('Last pick-up time'), - 'name' => 'sender_work_until', - 'type' => 'datetime', + 'label' => $this->module->l('Pick-up time from'), + 'name' => 'pick_up_time_from', + 'type' => 'select', + 'class' => 'fixed-width-xxl', 'required' => true, + 'desc' => $this->module->l('Only these time slots are accepted by DPD.'), + 'options' => [ + 'id' => 'id', + 'name' => 'name', + 'query' => $this->getPickupTimeSlotOptions(PickupTimeSlotUtility::getPickupTimeFromSlots()), + ], + ], + [ + 'label' => $this->module->l('Pick-up time until'), + 'name' => 'pick_up_time_to', + 'type' => 'select', + 'class' => 'fixed-width-xxl', + 'required' => true, + 'options' => [ + 'id' => 'id', + 'name' => 'name', + 'query' => $this->getPickupTimeSlotOptions(PickupTimeSlotUtility::getPickupTimeToSlots()), + ], ], [ 'label' => $this->module->l('Weight'), @@ -251,9 +271,11 @@ private function initForm() $phoneData['sender_phone_code_list'] ); - if (!Tools::getValue('pick_up_time') && !Tools::getValue('sender_work_until')) { - $this->fields_value['pick_up_time'] = TimeZoneUtility::getCourierDefaultPickUpTime(); - $this->fields_value['sender_work_until'] = TimeZoneUtility::getCourierDefaultWorkUntil(); + if (!Tools::getValue('pick_up_date')) { + $defaultPickUpSlot = TimeZoneUtility::getCourierDefaultPickUpSlot(); + $this->fields_value['pick_up_date'] = $defaultPickUpSlot['date']; + $this->fields_value['pick_up_time_from'] = $defaultPickUpSlot['from']; + $this->fields_value['pick_up_time_to'] = $defaultPickUpSlot['to']; } if (!Tools::getValue('order_nr')) { $this->fields_value['order_nr'] = (new DateTime())->getTimestamp(); @@ -315,6 +337,16 @@ public function setMedia($isNewTheme = false) $this->addCSS($this->module->getPathUri() . 'views/css/admin/courier_request.css'); } + private function getPickupTimeSlotOptions(array $slots) + { + $options = []; + foreach ($slots as $slot) { + $options[] = ['id' => $slot, 'name' => $slot]; + } + + return $options; + } + private function renderPrefillSelect($prefix) { /** @var AddressRepository $addressRepository */ @@ -337,11 +369,24 @@ public function postProcess() /** @var CourierRequestValidator $courierRequestValidator */ $formDataConverter = $this->module->getModuleContainer('invertus.dpdbaltics.converter.form_data_converter'); $courierRequestValidator = $this->module->getModuleContainer('invertus.dpdbaltics.validate.courier_request.courier_request_validator'); + + $_POST['pick_up_time'] = Tools::getValue('pick_up_date') . ' ' . Tools::getValue('pick_up_time_from') . ':00'; + $_POST['sender_work_until'] = Tools::getValue('pick_up_date') . ' ' . Tools::getValue('pick_up_time_to') . ':00'; + $data = Tools::getAllValues(); /** @var CourierRequestData $courierRequestObj */ $courierRequestObj = $formDataConverter->convertCourierRequestFormDataToCourierRequestObj($data); + if (!$courierRequestValidator->validatePickupTimeSlots($courierRequestObj)) { + $this->errors[] = sprintf( + $this->module->l('Pick-up time must use the time slots DPD accepts. "From" slots: %s. "Until" slots: %s.'), + implode(', ', PickupTimeSlotUtility::getPickupTimeFromSlots()), + implode(', ', PickupTimeSlotUtility::getPickupTimeToSlots()) + ); + return parent::postProcess(); + } + $countryIso = Configuration::get(Config::WEB_SERVICE_COUNTRY); if (!$courierRequestValidator->validate($courierRequestObj, $countryIso)) { $this->errors[] = sprintf( diff --git a/src/Config/Config.php b/src/Config/Config.php index 67023e47..9bbb8db6 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -250,6 +250,13 @@ class Config const COURIER_SAME_DAY_TIME_LIMITATION = '15:00'; const COURIER_SAME_DAY_TIME_ADDITIONAL_MINUTES = '30'; + const COURIER_PICKUP_TIME_FROM_SLOTS = [ + '08:00', '09:00', '10:00', '11:00', '12:00', '12:30', '13:00', '13:30', '14:00', '14:30', '15:00', + ]; + const COURIER_PICKUP_TIME_TO_SLOTS = ['15:00', '16:00', '17:00', '18:00']; + const COURIER_DEFAULT_PICKUP_TIME_FROM_SLOT = '08:00'; + const COURIER_DEFAULT_PICKUP_TIME_TO_SLOT = '17:00'; + const SAME_DAY_DELIVERY_CITY = 'Rīga'; const DOCUMENT_RETURN_CODE = '-DOCRET'; diff --git a/src/DTO/CourierRequestData.php b/src/DTO/CourierRequestData.php index 773c1763..c733b1ac 100644 --- a/src/DTO/CourierRequestData.php +++ b/src/DTO/CourierRequestData.php @@ -96,6 +96,21 @@ class CourierRequestData */ private $pickupTime; + /** + * @var string + */ + private $pickupDate; + + /** + * @var string + */ + private $pickupTimeFrom; + + /** + * @var string + */ + private $pickupTimeTo; + /** * @var string */ @@ -340,6 +355,54 @@ public function setPickupTime($pickupTime) $this->pickupTime = $pickupTime; } + /** + * @return string + */ + public function getPickupDate() + { + return $this->pickupDate; + } + + /** + * @param string $pickupDate + */ + public function setPickupDate($pickupDate) + { + $this->pickupDate = $pickupDate; + } + + /** + * @return string + */ + public function getPickupTimeFrom() + { + return $this->pickupTimeFrom; + } + + /** + * @param string $pickupTimeFrom + */ + public function setPickupTimeFrom($pickupTimeFrom) + { + $this->pickupTimeFrom = $pickupTimeFrom; + } + + /** + * @return string + */ + public function getPickupTimeTo() + { + return $this->pickupTimeTo; + } + + /** + * @param string $pickupTimeTo + */ + public function setPickupTimeTo($pickupTimeTo) + { + $this->pickupTimeTo = $pickupTimeTo; + } + /** * @return string */ diff --git a/src/Service/API/CourierRequestService.php b/src/Service/API/CourierRequestService.php index 4da67a9c..4ccf262f 100644 --- a/src/Service/API/CourierRequestService.php +++ b/src/Service/API/CourierRequestService.php @@ -23,8 +23,8 @@ use Country; use DPDBaltics; -use Invertus\dpdBaltics\Config\Config; use Invertus\dpdBaltics\DTO\courierRequestData; +use Invertus\dpdBaltics\Service\API\Parser\CourierRequestResponseParser; use Invertus\dpdBalticsApi\Api\DTO\Request\CourierRequestRequest; use Invertus\dpdBalticsApi\Api\DTO\Response\courierRequestResponse; use Invertus\dpdBalticsApi\Factory\APIRequest\courierRequestFactory; @@ -44,11 +44,19 @@ class CourierRequestService * @var DPDBaltics */ private $module; + /** + * @var CourierRequestResponseParser + */ + private $responseParser; - public function __construct(CourierRequestFactory $courierRequestFactory, DPDBaltics $module) - { + public function __construct( + CourierRequestFactory $courierRequestFactory, + DPDBaltics $module, + CourierRequestResponseParser $responseParser + ) { $this->courierRequestFactory = $courierRequestFactory; $this->module = $module; + $this->responseParser = $responseParser; } public function createCourierRequest(CourierRequestData $courierRequestData) @@ -74,16 +82,22 @@ public function createCourierRequest(CourierRequestData $courierRequestData) $courierRequestData->getWeight(), $courierRequestData->getParcelsCount() ); - + + if ($courierRequestData->getPickupTimeFrom() && $courierRequestData->getPickupTimeTo()) { + $request->setPickupDate($courierRequestData->getPickupDate()); + $request->setPickupTimeFrom($courierRequestData->getPickupTimeFrom()); + $request->setPickupTimeTo($courierRequestData->getPickupTimeTo()); + } + $courierRequest = $this->courierRequestFactory->makecourierRequest(); /** @var courierRequestResponse $response */ $response = $courierRequest->courierRequest($request); - if (!$this->checkIfCourierRequestIsSuccess($response)) { + if (!$this->responseParser->isSuccess($response)) { return [ 'status' => false, - 'message' => $this->getCourierRequestError($response) + 'message' => $this->responseParser->getError($response) ]; } @@ -92,23 +106,4 @@ public function createCourierRequest(CourierRequestData $courierRequestData) 'message' => $this->module->l('courier request was successfully created!') ]; } - - private function checkIfCourierRequestIsSuccess($courierRequestResponse) - { - if ($courierRequestResponse === Config::API_COURIER_REQUEST_SUCCESS_STATUS) { - return true; - } - - return false; - } - - private function getCourierRequestError($courierRequestResponse) - { - $errorPosition = strpos( - $courierRequestResponse, - Config::API_COURIER_REQUEST_ERROR_STATUS - ); - - return substr($courierRequestResponse, $errorPosition); - } } diff --git a/src/Service/API/Parser/CourierRequestResponseParser.php b/src/Service/API/Parser/CourierRequestResponseParser.php new file mode 100644 index 00000000..cbf2caa3 --- /dev/null +++ b/src/Service/API/Parser/CourierRequestResponseParser.php @@ -0,0 +1,71 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + + +namespace Invertus\dpdBaltics\Service\API\Parser; + +use Invertus\dpdBaltics\Config\Config; + +class CourierRequestResponseParser +{ + /** + * @param mixed $response + * + * @return bool + */ + public function isSuccess($response) + { + if (is_string($response)) { + return strpos($response, Config::API_COURIER_REQUEST_SUCCESS_STATUS) !== false; + } + + return false; + } + + /** + * @param mixed $response + * + * @return string + */ + public function getError($response) + { + if (is_string($response)) { + $errorPosition = strpos($response, Config::API_COURIER_REQUEST_ERROR_STATUS); + + if ($errorPosition === false) { + return $response; + } + + return substr($response, $errorPosition); + } + + if (is_object($response)) { + if (isset($response->body->message)) { + return (string) $response->body->message; + } + + if (isset($response->errlog)) { + return (string) $response->errlog; + } + } + + return json_encode($response); + } +} diff --git a/src/Util/PickupTimeSlotUtility.php b/src/Util/PickupTimeSlotUtility.php new file mode 100644 index 00000000..b2407a4a --- /dev/null +++ b/src/Util/PickupTimeSlotUtility.php @@ -0,0 +1,79 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + + +namespace Invertus\dpdBaltics\Util; + +use Invertus\dpdBaltics\Config\Config; + +class PickupTimeSlotUtility +{ + /** + * @return string[] + */ + public static function getPickupTimeFromSlots() + { + return Config::COURIER_PICKUP_TIME_FROM_SLOTS; + } + + /** + * @return string[] + */ + public static function getPickupTimeToSlots() + { + return Config::COURIER_PICKUP_TIME_TO_SLOTS; + } + + /** + * @param string $time + * + * @return bool + */ + public static function isValidPickupTimeFromSlot($time) + { + return in_array($time, Config::COURIER_PICKUP_TIME_FROM_SLOTS, true); + } + + /** + * @param string $time + * + * @return bool + */ + public static function isValidPickupTimeToSlot($time) + { + return in_array($time, Config::COURIER_PICKUP_TIME_TO_SLOTS, true); + } + + /** + * @param string $time + * + * @return string|null + */ + public static function findNextPickupTimeFromSlot($time) + { + foreach (Config::COURIER_PICKUP_TIME_FROM_SLOTS as $slot) { + if ($slot >= $time) { + return $slot; + } + } + + return null; + } +} diff --git a/src/Util/TimeZoneUtility.php b/src/Util/TimeZoneUtility.php index f174fc2f..f22ec96c 100644 --- a/src/Util/TimeZoneUtility.php +++ b/src/Util/TimeZoneUtility.php @@ -76,6 +76,35 @@ public static function getCourierDefaultWorkUntil() } } + /** + * @return array ['date' => 'Y-m-d', 'from' => 'H:i', 'to' => 'H:i'] + */ + public static function getCourierDefaultPickUpSlot() + { + $now = self::getBalticTimeZoneAsDateTime(); + $earliest = clone $now; + $earliest->modify('+' . Config::COURIER_SAME_DAY_TIME_ADDITIONAL_MINUTES . ' minutes'); + + $fromSlot = PickupTimeSlotUtility::findNextPickupTimeFromSlot($earliest->format('H:i')); + + if ($fromSlot !== null && $earliest->format('Y-m-d') === $now->format('Y-m-d')) { + return [ + 'date' => $now->format('Y-m-d'), + 'from' => $fromSlot, + 'to' => Config::COURIER_DEFAULT_PICKUP_TIME_TO_SLOT, + ]; + } + + $tomorrow = clone $now; + $tomorrow->modify('+1 day'); + + return [ + 'date' => $tomorrow->format('Y-m-d'), + 'from' => Config::COURIER_DEFAULT_PICKUP_TIME_FROM_SLOT, + 'to' => Config::COURIER_DEFAULT_PICKUP_TIME_TO_SLOT, + ]; + } + public static function getBalticTimeZoneAsDateTime() { $tz = 'Europe/Vilnius'; diff --git a/src/Validate/CourierRequest/CourierRequestValidator.php b/src/Validate/CourierRequest/CourierRequestValidator.php index bd79b895..eed36b8d 100644 --- a/src/Validate/CourierRequest/CourierRequestValidator.php +++ b/src/Validate/CourierRequest/CourierRequestValidator.php @@ -24,6 +24,7 @@ use DateTime; use Invertus\dpdBaltics\Config\Config; use Invertus\dpdBaltics\DTO\CourierRequestData; +use Invertus\dpdBaltics\Util\PickupTimeSlotUtility; if (!defined('_PS_VERSION_')) { exit; @@ -31,6 +32,25 @@ class CourierRequestValidator { + /** + * @return bool + */ + public function validatePickupTimeSlots(CourierRequestData $courierRequestData) + { + $from = $courierRequestData->getPickupTimeFrom(); + $to = $courierRequestData->getPickupTimeTo(); + + if (!PickupTimeSlotUtility::isValidPickupTimeFromSlot($from)) { + return false; + } + + if (!PickupTimeSlotUtility::isValidPickupTimeToSlot($to)) { + return false; + } + + return $from < $to; + } + public function validate(CourierRequestData $courierRequestData, $countryIso) { $dateFrom = new DateTime($courierRequestData->getPickupTime()); diff --git a/tests/Unit/Service/API/Parser/CourierRequestResponseParserTest.php b/tests/Unit/Service/API/Parser/CourierRequestResponseParserTest.php new file mode 100644 index 00000000..bbff34c9 --- /dev/null +++ b/tests/Unit/Service/API/Parser/CourierRequestResponseParserTest.php @@ -0,0 +1,104 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +use Invertus\dpdBaltics\Service\API\Parser\CourierRequestResponseParser; +use PHPUnit\Framework\TestCase; + +class CourierRequestResponseParserTest extends TestCase +{ + /** + * @var CourierRequestResponseParser + */ + private $parser; + + protected function setUp(): void + { + $this->parser = new CourierRequestResponseParser(); + } + + public function testLegacyDoneStringIsSuccess() + { + $this->assertTrue($this->parser->isSuccess('

DONE')); + } + + public function testLegacyDoneHtmlDocumentIsSuccess() + { + $html = "\n</head>\n<body>\n<p>DONE</p>\n</body>\n</html>"; + + $this->assertTrue($this->parser->isSuccess($html)); + } + + public function testLegacyErrorStringIsNotSuccessAndErrorIsExtracted() + { + $response = 'HTTP 402 Error: Pickup order to time wrong for Product 1'; + + $this->assertFalse($this->parser->isSuccess($response)); + $this->assertSame('402 Error: Pickup order to time wrong for Product 1', $this->parser->getError($response)); + } + + public function testLegacyStringWithoutErrorMarkerFallsBackToWholeString() + { + $response = 'Some unexpected plain-text failure'; + + $this->assertFalse($this->parser->isSuccess($response)); + $this->assertSame($response, $this->parser->getError($response)); + } + + public function testJsonErrorObjectIsNotSuccessAndMessageIsExtracted() + { + $message = 'Available "pickupTimeFrom" values: [08:00, 09:00, 10:00, 11:00, 12:00, 12:30, 13:00, ' + . '13:30, 14:00, 14:30, 15:00]. Available "pickupTimeTo" values: [15:00, 16:00, 17:00, 18:00].'; + $response = json_decode(json_encode([ + 'level' => 'error', + 'body' => ['message' => $message], + ])); + + $this->assertFalse($this->parser->isSuccess($response)); + $this->assertSame($message, $this->parser->getError($response)); + } + + public function testJsonErrlogObjectIsNotSuccessAndErrlogIsExtracted() + { + $response = json_decode(json_encode([ + 'status' => 'err', + 'errlog' => 'Failed to Authenticate User. Check your user name and password!', + ])); + + $this->assertFalse($this->parser->isSuccess($response)); + $this->assertSame( + 'Failed to Authenticate User. Check your user name and password!', + $this->parser->getError($response) + ); + } + + public function testUnknownObjectShapeIsNotSuccessAndIsSurfacedAsJson() + { + $response = json_decode(json_encode(['foo' => 'bar'])); + + $this->assertFalse($this->parser->isSuccess($response)); + $this->assertSame('{"foo":"bar"}', $this->parser->getError($response)); + } + + public function testEmptyArrayResponseIsNotSuccessAndDoesNotCrash() + { + $this->assertFalse($this->parser->isSuccess([])); + $this->assertSame('[]', $this->parser->getError([])); + } +} diff --git a/tests/Unit/Util/PickupTimeSlotUtilityTest.php b/tests/Unit/Util/PickupTimeSlotUtilityTest.php new file mode 100644 index 00000000..fec63353 --- /dev/null +++ b/tests/Unit/Util/PickupTimeSlotUtilityTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * NOTICE OF LICENSE + * + * This source file is subject to the Academic Free License version 3.0 + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/AFL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * @author PrestaShop SA and Contributors <contact@prestashop.com> + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +use Invertus\dpdBaltics\Util\PickupTimeSlotUtility; +use PHPUnit\Framework\TestCase; + +class PickupTimeSlotUtilityTest extends TestCase +{ + public function testValidPickupTimeFromSlots() + { + $this->assertTrue(PickupTimeSlotUtility::isValidPickupTimeFromSlot('08:00')); + $this->assertTrue(PickupTimeSlotUtility::isValidPickupTimeFromSlot('12:30')); + $this->assertTrue(PickupTimeSlotUtility::isValidPickupTimeFromSlot('15:00')); + + $this->assertFalse(PickupTimeSlotUtility::isValidPickupTimeFromSlot('08:15')); + $this->assertFalse(PickupTimeSlotUtility::isValidPickupTimeFromSlot('16:00')); + $this->assertFalse(PickupTimeSlotUtility::isValidPickupTimeFromSlot('11:35:26')); + $this->assertFalse(PickupTimeSlotUtility::isValidPickupTimeFromSlot('')); + } + + public function testValidPickupTimeToSlots() + { + $this->assertTrue(PickupTimeSlotUtility::isValidPickupTimeToSlot('15:00')); + $this->assertTrue(PickupTimeSlotUtility::isValidPickupTimeToSlot('17:00')); + $this->assertTrue(PickupTimeSlotUtility::isValidPickupTimeToSlot('18:00')); + + $this->assertFalse(PickupTimeSlotUtility::isValidPickupTimeToSlot('08:00')); + $this->assertFalse(PickupTimeSlotUtility::isValidPickupTimeToSlot('17:30')); + $this->assertFalse(PickupTimeSlotUtility::isValidPickupTimeToSlot('')); + } + + public function testFindNextPickupTimeFromSlotSnapsForward() + { + $this->assertSame('08:00', PickupTimeSlotUtility::findNextPickupTimeFromSlot('06:45')); + $this->assertSame('08:00', PickupTimeSlotUtility::findNextPickupTimeFromSlot('08:00')); + $this->assertSame('12:00', PickupTimeSlotUtility::findNextPickupTimeFromSlot('11:35')); + $this->assertSame('12:30', PickupTimeSlotUtility::findNextPickupTimeFromSlot('12:05')); + $this->assertSame('15:00', PickupTimeSlotUtility::findNextPickupTimeFromSlot('14:31')); + } + + public function testFindNextPickupTimeFromSlotReturnsNullAfterLastSlot() + { + $this->assertNull(PickupTimeSlotUtility::findNextPickupTimeFromSlot('15:01')); + $this->assertNull(PickupTimeSlotUtility::findNextPickupTimeFromSlot('23:59')); + } +} diff --git a/tests/Unit/Validate/CourierRequest/CourierRequestValidatorTest.php b/tests/Unit/Validate/CourierRequest/CourierRequestValidatorTest.php new file mode 100644 index 00000000..098a277b --- /dev/null +++ b/tests/Unit/Validate/CourierRequest/CourierRequestValidatorTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * NOTICE OF LICENSE + * + * This source file is subject to the Academic Free License version 3.0 + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/AFL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * @author PrestaShop SA and Contributors <contact@prestashop.com> + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +use Invertus\dpdBaltics\DTO\CourierRequestData; +use Invertus\dpdBaltics\Validate\CourierRequest\CourierRequestValidator; +use PHPUnit\Framework\TestCase; + +class CourierRequestValidatorTest extends TestCase +{ + /** + * @var CourierRequestValidator + */ + private $validator; + + protected function setUp(): void + { + $this->validator = new CourierRequestValidator(); + } + + public function testValidSlotPairPassesSlotValidation() + { + $data = $this->createCourierRequestData('08:00', '17:00'); + + $this->assertTrue($this->validator->validatePickupTimeSlots($data)); + } + + public function testNonWhitelistedFromSlotFailsSlotValidation() + { + $data = $this->createCourierRequestData('11:35', '17:00'); + + $this->assertFalse($this->validator->validatePickupTimeSlots($data)); + } + + public function testNonWhitelistedToSlotFailsSlotValidation() + { + $data = $this->createCourierRequestData('08:00', '12:00'); + + $this->assertFalse($this->validator->validatePickupTimeSlots($data)); + } + + public function testFromSlotEqualOrLaterThanToSlotFailsSlotValidation() + { + $data = $this->createCourierRequestData('15:00', '15:00'); + + $this->assertFalse($this->validator->validatePickupTimeSlots($data)); + } + + public function testMinimalIntervalValidationStillPassesForComposedDatetimes() + { + $data = $this->createCourierRequestData('08:00', '17:00'); + $data->setPickupTime('2030-02-20 08:00:00'); + $data->setSenderWorkUntil('2030-02-20 17:00:00'); + + $this->assertTrue($this->validator->validate($data, 'LT')); + } + + public function testMinimalIntervalValidationStillFailsWhenIntervalTooShort() + { + $data = $this->createCourierRequestData('15:00', '16:00'); + $data->setPickupTime('2030-02-20 15:00:00'); + $data->setSenderWorkUntil('2030-02-20 16:00:00'); + + $this->assertFalse($this->validator->validate($data, 'LT')); + } + + private function createCourierRequestData($from, $to) + { + $data = new CourierRequestData(); + $data->setPickupDate('2030-02-20'); + $data->setPickupTimeFrom($from); + $data->setPickupTimeTo($to); + + return $data; + } +} diff --git a/tests/Unit/bootstrap.php b/tests/Unit/bootstrap.php index 2ce5e077..60a94304 100644 --- a/tests/Unit/bootstrap.php +++ b/tests/Unit/bootstrap.php @@ -22,3 +22,7 @@ $projectDir = __DIR__ . '/../../'; require_once $projectDir . 'vendor/autoload.php'; + +if (!defined('_PS_VERSION_')) { + define('_PS_VERSION_', '8.2.7'); +} From f5a798e2499429b4676a4b32d71c4b16741edbce Mon Sep 17 00:00:00 2001 From: GytisZum <96050852+GytisZum@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:20:26 +0000 Subject: [PATCH 2/4] Adding auto indexes --- src/Service/API/Parser/index.php | 11 +++++++++++ tests/Unit/Service/API/Parser/index.php | 11 +++++++++++ tests/Unit/Service/API/index.php | 11 +++++++++++ tests/Unit/Util/index.php | 11 +++++++++++ tests/Unit/Validate/CourierRequest/index.php | 11 +++++++++++ 5 files changed, 55 insertions(+) create mode 100644 src/Service/API/Parser/index.php create mode 100644 tests/Unit/Service/API/Parser/index.php create mode 100644 tests/Unit/Service/API/index.php create mode 100644 tests/Unit/Util/index.php create mode 100644 tests/Unit/Validate/CourierRequest/index.php diff --git a/src/Service/API/Parser/index.php b/src/Service/API/Parser/index.php new file mode 100644 index 00000000..15aba820 --- /dev/null +++ b/src/Service/API/Parser/index.php @@ -0,0 +1,11 @@ +<?php + +header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); +header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); + +header("Cache-Control: no-store, no-cache, must-revalidate"); +header("Cache-Control: post-check=0, pre-check=0", false); +header("Pragma: no-cache"); + +header("Location: ../"); +exit; diff --git a/tests/Unit/Service/API/Parser/index.php b/tests/Unit/Service/API/Parser/index.php new file mode 100644 index 00000000..15aba820 --- /dev/null +++ b/tests/Unit/Service/API/Parser/index.php @@ -0,0 +1,11 @@ +<?php + +header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); +header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); + +header("Cache-Control: no-store, no-cache, must-revalidate"); +header("Cache-Control: post-check=0, pre-check=0", false); +header("Pragma: no-cache"); + +header("Location: ../"); +exit; diff --git a/tests/Unit/Service/API/index.php b/tests/Unit/Service/API/index.php new file mode 100644 index 00000000..15aba820 --- /dev/null +++ b/tests/Unit/Service/API/index.php @@ -0,0 +1,11 @@ +<?php + +header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); +header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); + +header("Cache-Control: no-store, no-cache, must-revalidate"); +header("Cache-Control: post-check=0, pre-check=0", false); +header("Pragma: no-cache"); + +header("Location: ../"); +exit; diff --git a/tests/Unit/Util/index.php b/tests/Unit/Util/index.php new file mode 100644 index 00000000..15aba820 --- /dev/null +++ b/tests/Unit/Util/index.php @@ -0,0 +1,11 @@ +<?php + +header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); +header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); + +header("Cache-Control: no-store, no-cache, must-revalidate"); +header("Cache-Control: post-check=0, pre-check=0", false); +header("Pragma: no-cache"); + +header("Location: ../"); +exit; diff --git a/tests/Unit/Validate/CourierRequest/index.php b/tests/Unit/Validate/CourierRequest/index.php new file mode 100644 index 00000000..15aba820 --- /dev/null +++ b/tests/Unit/Validate/CourierRequest/index.php @@ -0,0 +1,11 @@ +<?php + +header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); +header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); + +header("Cache-Control: no-store, no-cache, must-revalidate"); +header("Cache-Control: post-check=0, pre-check=0", false); +header("Pragma: no-cache"); + +header("Location: ../"); +exit; From eacc3b1633f80f959662e24406e40bdc40032c33 Mon Sep 17 00:00:00 2001 From: GantasG <gantas.grigalionis@invertus.eu> Date: Mon, 31 Aug 2026 15:49:15 +0300 Subject: [PATCH 3/4] DGS-444, DGS-446: bump invertus/dpdbaltics-api for pickup time slot fields (#183) --- composer.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index 4bb346d7..a9c1f4d0 100644 --- a/composer.lock +++ b/composer.lock @@ -174,7 +174,7 @@ "source": { "type": "git", "url": "https://github.com/Invertus/dpdbaltics-api.git", - "reference": "665c9e8ca25afce5cf9ac1a0a62000826353975d" + "reference": "e372e0b66bdf71010a6a6b7368aefd5c231ec6a1" }, "require": { "apimatic/jsonmapper": "^2.0", @@ -206,7 +206,7 @@ "email": "developers@invertus.eu" } ], - "time": "2022-09-20T11:10:13+00:00" + "time": "2026-08-31T12:04:50+00:00" }, { "name": "invertus/psModuleTabs", From 5ba913c9a1c524f98d4a8fee7777c82724f1ece1 Mon Sep 17 00:00:00 2001 From: GytisZum <96050852+GytisZum@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:50:05 +0000 Subject: [PATCH 4/4] Automatic license addition applying --- src/Service/API/Parser/index.php | 19 +++++++++++++++++++ tests/Unit/Service/API/Parser/index.php | 19 +++++++++++++++++++ tests/Unit/Service/API/index.php | 19 +++++++++++++++++++ tests/Unit/Util/index.php | 19 +++++++++++++++++++ tests/Unit/Validate/CourierRequest/index.php | 19 +++++++++++++++++++ 5 files changed, 95 insertions(+) diff --git a/src/Service/API/Parser/index.php b/src/Service/API/Parser/index.php index 15aba820..74851d77 100644 --- a/src/Service/API/Parser/index.php +++ b/src/Service/API/Parser/index.php @@ -1,4 +1,23 @@ <?php +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * NOTICE OF LICENSE + * + * This source file is subject to the Academic Free License version 3.0 + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/AFL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * @author PrestaShop SA and Contributors <contact@prestashop.com> + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); diff --git a/tests/Unit/Service/API/Parser/index.php b/tests/Unit/Service/API/Parser/index.php index 15aba820..74851d77 100644 --- a/tests/Unit/Service/API/Parser/index.php +++ b/tests/Unit/Service/API/Parser/index.php @@ -1,4 +1,23 @@ <?php +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * NOTICE OF LICENSE + * + * This source file is subject to the Academic Free License version 3.0 + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/AFL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * @author PrestaShop SA and Contributors <contact@prestashop.com> + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); diff --git a/tests/Unit/Service/API/index.php b/tests/Unit/Service/API/index.php index 15aba820..74851d77 100644 --- a/tests/Unit/Service/API/index.php +++ b/tests/Unit/Service/API/index.php @@ -1,4 +1,23 @@ <?php +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * NOTICE OF LICENSE + * + * This source file is subject to the Academic Free License version 3.0 + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/AFL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * @author PrestaShop SA and Contributors <contact@prestashop.com> + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); diff --git a/tests/Unit/Util/index.php b/tests/Unit/Util/index.php index 15aba820..74851d77 100644 --- a/tests/Unit/Util/index.php +++ b/tests/Unit/Util/index.php @@ -1,4 +1,23 @@ <?php +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * NOTICE OF LICENSE + * + * This source file is subject to the Academic Free License version 3.0 + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/AFL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * @author PrestaShop SA and Contributors <contact@prestashop.com> + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); diff --git a/tests/Unit/Validate/CourierRequest/index.php b/tests/Unit/Validate/CourierRequest/index.php index 15aba820..74851d77 100644 --- a/tests/Unit/Validate/CourierRequest/index.php +++ b/tests/Unit/Validate/CourierRequest/index.php @@ -1,4 +1,23 @@ <?php +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * NOTICE OF LICENSE + * + * This source file is subject to the Academic Free License version 3.0 + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/AFL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * @author PrestaShop SA and Contributors <contact@prestashop.com> + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");