From c16b3c6c590bf39c8292663dbf8162cf39944d42 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:42:21 -0700 Subject: [PATCH 01/34] bump database version --- app/config/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/config/config.yml b/app/config/config.yml index eed0e48a3b..c6552c53fe 100755 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -304,13 +304,15 @@ doctrine: numrange: string tsrange: string + # Tells Doctrine what database features are supported + # Make sure that your production database uses this version or higher. # If you don't define this option and you haven't created your database yet, # you may get PDOException errors because Doctrine will try to guess # the database server version automatically and none is available. # http://symfony.com/doc/current/reference/configuration/doctrine.html # Should be fixed in next Doctrine version # https://github.com/doctrine/dbal/pull/2671 - server_version: 9.4 + server_version: '13' # Add a schema filter to avoid having PostGIS tables tiger.* & topology.* in migrations diff # https://symfony.com/doc/master/bundles/DoctrineMigrationsBundle/index.html#manual-tables From 0f571e4a904718923332c126c5ac7bf2b7956831 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:47:17 -0700 Subject: [PATCH 02/34] WIP; return DTOs from `/api/me/tasks/*` endpoint instead of entities --- app/config/services.yml | 2 + app/config/services/serializer.yml | 5 + src/Action/MyTasks.php | 52 +++----- src/Api/Dto/MyTask.php | 35 +++++ src/Api/Dto/MyTaskList.php | 34 +++++ src/Entity/TaskListRepository.php | 66 ++++++++++ .../config/doctrine/TaskList.orm.xml | 2 +- src/Serializer/MyTaskListNormalizer.php | 123 ++++++++++++++++++ 8 files changed, 281 insertions(+), 38 deletions(-) create mode 100644 src/Api/Dto/MyTask.php create mode 100644 src/Api/Dto/MyTaskList.php create mode 100644 src/Entity/TaskListRepository.php create mode 100644 src/Serializer/MyTaskListNormalizer.php diff --git a/app/config/services.yml b/app/config/services.yml index 6566da3de5..ce697a0d6a 100755 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -625,6 +625,8 @@ services: sylius.repository.order: alias: AppBundle\Entity\Sylius\OrderRepository + AppBundle\Entity\TaskListRepository: ~ + AppBundle\Entity\Task\RecurrenceRuleRepository: ~ AppBundle\Service\Routing\Osrm: ~ diff --git a/app/config/services/serializer.yml b/app/config/services/serializer.yml index afa57d8128..3f386f9d5e 100644 --- a/app/config/services/serializer.yml +++ b/app/config/services/serializer.yml @@ -30,6 +30,11 @@ services: - "@api_platform.jsonld.normalizer.item" tags: [ { name: serializer.normalizer, priority: 128 } ] + AppBundle\Serializer\MyTaskListNormalizer: + arguments: + - "@api_platform.jsonld.normalizer.object" + tags: [ { name: serializer.normalizer, priority: 128 } ] + AppBundle\Serializer\TaskImageNormalizer: arguments: $normalizer: '@api_platform.jsonld.normalizer.item' diff --git a/src/Action/MyTasks.php b/src/Action/MyTasks.php index 7d8fd775a8..5f75ac260d 100644 --- a/src/Action/MyTasks.php +++ b/src/Action/MyTasks.php @@ -2,11 +2,12 @@ namespace AppBundle\Action; +use AppBundle\Entity\TaskListRepository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use AppBundle\Action\Utils\TokenStorageTrait; -use AppBundle\Entity\Task; use AppBundle\Entity\TaskList; +use Doctrine\ORM\EntityRepository; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -14,23 +15,30 @@ final class MyTasks { use TokenStorageTrait; + /** + * @var TaskListRepository + */ + private readonly EntityRepository $taskListRepository; + public function __construct( TokenStorageInterface $tokenStorage, - protected EntityManagerInterface $entityManager) + private readonly EntityManagerInterface $entityManager) { $this->tokenStorage = $tokenStorage; + $this->taskListRepository = $this->entityManager->getRepository(TaskList::class); } public function __invoke(Request $request) { + $user = $this->getUser(); $date = new \DateTime($request->get('date')); - $taskList = $this->loadExisting($date); + $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); - if (null === $taskList) { + if (null === $taskListDto) { $taskList = new TaskList(); - $taskList->setCourier($this->getUser()); + $taskList->setCourier($user); $taskList->setDate($date); try { @@ -40,40 +48,10 @@ public function __invoke(Request $request) // If 2 requests are received at the very same time, // we can have a race condition // @see https://github.com/coopcycle/coopcycle-app/issues/1265 - $taskList = $this->loadExisting($date); + $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); } } - return $taskList; - } - - /** - * @param \DateTime $date - * @return TaskList|null - */ - private function loadExisting(\DateTime $date): ?TaskList - { - $taskList = $this->entityManager->getRepository(TaskList::class) - ->findOneBy([ - 'courier' => $this->getUser(), - 'date' => $date, - ]); - - if ($taskList) { - - // reset array index to 0 with array_values, otherwise you might get weird stuff in the serializer - $notCancelled = array_values( - array_filter(array_filter($taskList->getTasks(), function (Task $task) { - return !$task->isCancelled(); - })) - ); - - // supports the legacy display of TaskList as tasks for the app courier part - $taskList->setTempLegacyTaskStorage($notCancelled); - - return $taskList; - } - - return null; + return $taskListDto; } } diff --git a/src/Api/Dto/MyTask.php b/src/Api/Dto/MyTask.php new file mode 100644 index 0000000000..82ad13f421 --- /dev/null +++ b/src/Api/Dto/MyTask.php @@ -0,0 +1,35 @@ +id = $id; + $this->type = $type; + $this->status = $status; + $this->orderId = $orderId; + } +} diff --git a/src/Api/Dto/MyTaskList.php b/src/Api/Dto/MyTaskList.php new file mode 100644 index 0000000000..a8e63c57f3 --- /dev/null +++ b/src/Api/Dto/MyTaskList.php @@ -0,0 +1,34 @@ +id = $id; + $this->items = $items; + + $this->isTempLegacyTaskStorage = true; + } +} diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php new file mode 100644 index 0000000000..712289085d --- /dev/null +++ b/src/Entity/TaskListRepository.php @@ -0,0 +1,66 @@ +findOneBy([ + 'courier' => $user, + 'date' => $date, + ]); + + if (null === $taskList) { + return null; + } + + $taskIds = $taskList->getItems()->map(function (TaskList\Item $item) { + return $item->getTask()->getId(); + }); + + $queryResult = $this->entityManager->createQueryBuilder() + ->select([ + 't.id', + 't.type', + 't.status', + 'o.id AS orderId', + ]) + ->from(Task::class, 't') + ->leftJoin('t.delivery', 'd') + ->leftJoin('d.order', 'o') + ->where('t.id IN (:taskIds)') + ->setParameter('taskIds', $taskIds) + ->getQuery() + ->getArrayResult(); +// ->getResult(\AppBundle\Api\Dto\Task::class); + + //TODO; sort by position + + $tasks = array_map(function ($row) { + $taskDto = new MyTask( + $row['id'], + $row['type'], + $row['status'], + $row['orderId'] + ); + return $taskDto; + }, $queryResult); + + $taskListDto = new MyTaskList($taskList->getId(), $tasks); + return $taskListDto; + } +} diff --git a/src/Resources/config/doctrine/TaskList.orm.xml b/src/Resources/config/doctrine/TaskList.orm.xml index 5c72ec9afb..be8bcc0725 100644 --- a/src/Resources/config/doctrine/TaskList.orm.xml +++ b/src/Resources/config/doctrine/TaskList.orm.xml @@ -1,6 +1,6 @@ - + diff --git a/src/Serializer/MyTaskListNormalizer.php b/src/Serializer/MyTaskListNormalizer.php new file mode 100644 index 0000000000..58555eae14 --- /dev/null +++ b/src/Serializer/MyTaskListNormalizer.php @@ -0,0 +1,123 @@ +normalizer = $normalizer; + } + +// private function flattenItemsUris(array $items) +// { +// $itemsUris = []; +// foreach($items as $item) { +// if (isset($item['task'])) { +// array_push( +// $itemsUris, +// $item['task'] +// ); +// } else { +// array_push( +// $itemsUris, +// $item['tour']['@id'], // to the best of my knowledge, tour is eagerly fetch because we use the table inheritance for the tour table +// ); +// } +// } +// +// return $itemsUris; +// } + + public function normalize($object, $format = null, array $context = array()) + { + $this->checkoutLogger->info('MyTaskListNormalizer::normalize'); + + // legacy serialization for API endpoints that output TaskList.items as a list of tasks + // look for "setTempLegacyTaskStorage" usage in the code. + // known usage at the time of the writing : + // - used for the rider/dispatcher smartphone app (does not display or handle tours) + // - used for stores to access /api/task_lists/ and display only tasks linked to the org (it is only used by Tricargo coop and should be considered legacy) + if ($object->isTempLegacyTaskStorage) { + $context[AbstractNormalizer::IGNORED_ATTRIBUTES] = ['items']; + $data = $this->normalizer->normalize($object, $format, $context); + + // override json-ld to match the existing API + $data['@context'] = '/api/contexts/TaskList'; + $data['@type'] = 'TaskList'; + $data['@id'] = "/api/task_lists/" . $object->id; + + $data['items'] = array_map(function($task) { + $taskData = $this->normalizer->normalize( + $task, + 'jsonld', + ['groups' => ["task_list", "task_collection", "task", "delivery", "address"]] + ); + + // override json-ld to match the existing API + $taskData['@context'] = '/api/contexts/Task'; + $taskData['@type'] = 'Task'; + $taskData['@id'] = "/api/tasks/" . $task->id; + + return $taskData; + }, $object->items + ); + } + // legacy serialization for app and events + // see https://github.com/coopcycle/coopcycle-app/issues/1803 + else if (in_array('task', $context['groups'])) { + $context[AbstractNormalizer::IGNORED_ATTRIBUTES] = ['items']; + $data = $this->normalizer->normalize($object, $format, $context); + $data['items'] = array_map(function($task) { + return $this->normalizer->normalize( + $task, + 'jsonld', + ['groups' => ["task_list", "task_collection", "task", "delivery", "address"]] + ); + }, $object->getTasks() + ); + } else { + $data = $this->normalizer->normalize($object, $format, $context); + + if (isset($data['items'])) { + $data['items'] = $this->flattenItemsUris($data['items']); + } + } + + // Legacy + if (isset($context['item_operation_name']) && $context['item_operation_name'] === 'my_tasks') { + $data['hydra:member'] = $data['items']; + $data['hydra:totalItems'] = count($data['items']); + } + + return $data; + } + + public function supportsNormalization($data, $format = null) + { + $result = $this->normalizer->supportsNormalization($data, $format) && $data instanceof MyTaskList; + + $this->checkoutLogger->info('MyTaskListNormalizer::supportsNormalization', [ + 'result' => $result, + 'data' => $data, + 'format' => $format, + ]); + + return $result; + } +} From 73726822e58c46434021088b6903fe404a428c91 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:44:23 -0700 Subject: [PATCH 03/34] WIP; map more fields --- src/Api/Dto/MyTask.php | 107 ++++++++++++++++++++++++++++-- src/Api/Dto/MyTaskList.php | 53 ++++++++++++++- src/Entity/TaskListRepository.php | 47 ++++++++++++- 3 files changed, 197 insertions(+), 10 deletions(-) diff --git a/src/Api/Dto/MyTask.php b/src/Api/Dto/MyTask.php index 82ad13f421..041239ea6d 100644 --- a/src/Api/Dto/MyTask.php +++ b/src/Api/Dto/MyTask.php @@ -2,6 +2,9 @@ namespace AppBundle\Api\Dto; +use AppBundle\Entity\Address; +use AppBundle\Entity\Task; +use DateTime; use Symfony\Component\Serializer\Annotation\Groups; final class MyTask @@ -9,27 +12,121 @@ final class MyTask #[Groups(["task"])] public readonly int $id; + #[Groups(["task"])] + public readonly DateTime $createdAt; + + #[Groups(["task"])] + public readonly DateTime $updatedAt; + #[Groups(["task"])] public readonly string $type; #[Groups(["task"])] public readonly string $status; - //todo; move into metadata + //TODO; make non-nullable + #[Groups(["task"])] + public readonly ?Address $address; + + #[Groups(["task"])] + public readonly DateTime $doneAfter; + + #[Groups(["task"])] + public readonly DateTime $doneBefore; + + /** + * @var DateTime + * @deprecated + */ + #[Groups(["task"])] + public readonly DateTime $after; + + /** + * @var DateTime + * @deprecated + */ + #[Groups(["task"])] + public readonly DateTime $before; + + #[Groups(["task"])] + public readonly ?Task $previous; + + #[Groups(["task"])] + public readonly ?Task $next; + + #[Groups(["task"])] + public readonly ?string $comment; + #[Groups(["task"])] - public readonly ?int $orderId; + public readonly bool $hasIncidents; + + #[Groups(["task"])] + public readonly string $orgName; + + #[Groups(["task"])] + public readonly Metadata $metadata; /** * @param int $id + * @param DateTime $createdAt + * @param DateTime $updatedAt * @param string $type * @param string $status - * @param int|null $orderId + * @param Address $address + * @param DateTime $doneAfter + * @param DateTime $doneBefore + * @param Task|null $previous + * @param Task|null $next + * @param string|null $comment + * @param bool $hasIncidents + * @param string $orgName + * @param Metadata $metadata */ - public function __construct(int $id, string $type, string $status, ?int $orderId) + public function __construct(int $id, DateTime $createdAt, DateTime $updatedAt, string $type, string $status, ?Address $address, DateTime $doneAfter, DateTime $doneBefore, ?Task $previous, ?Task $next, ?string $comment, bool $hasIncidents, string $orgName, Metadata $metadata) { $this->id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; $this->type = $type; $this->status = $status; - $this->orderId = $orderId; + $this->address = $address; + $this->doneAfter = $doneAfter; + $this->doneBefore = $doneBefore; + $this->after = $doneAfter; + $this->before = $doneBefore; + $this->previous = $previous; + $this->next = $next; + $this->comment = $comment; + $this->hasIncidents = $hasIncidents; + $this->orgName = $orgName; + $this->metadata = $metadata; + } +} + +final class Metadata { + #[Groups(["task"])] + public readonly int $delivery_position; + + #[Groups(["task"])] + public readonly ?string $order_number; + + #[Groups(["task"])] + public readonly ?string $payment_method; + + #[Groups(["task"])] + public readonly ?int $order_total; + + /** + * @param int $delivery_position + * @param string|null $order_number + * @param string|null $payment_method + * @param int|null $order_total + */ + public function __construct(int $delivery_position, ?string $order_number, ?string $payment_method, ?int $order_total) + { + $this->delivery_position = $delivery_position; + $this->order_number = $order_number; + $this->payment_method = $payment_method; + $this->order_total = $order_total; } } diff --git a/src/Api/Dto/MyTaskList.php b/src/Api/Dto/MyTaskList.php index a8e63c57f3..6b94cc1256 100644 --- a/src/Api/Dto/MyTaskList.php +++ b/src/Api/Dto/MyTaskList.php @@ -2,6 +2,9 @@ namespace AppBundle\Api\Dto; +use AppBundle\Entity\Trailer; +use AppBundle\Entity\Vehicle; +use DateTime; use Symfony\Component\Serializer\Annotation\Groups; class MyTaskList @@ -9,12 +12,41 @@ class MyTaskList #[Groups(["task_list"])] public readonly int $id; + #[Groups(["task_list"])] + public readonly DateTime $createdAt; + + #[Groups(["task_list"])] + public readonly DateTime $updatedAt; + + #[Groups(["task_list"])] + public readonly DateTime $date; + + #[Groups(["task_list"])] + public readonly string $username; + /** * @var MyTask[] */ #[Groups(["task_list"])] public readonly array $items; + #[Groups(["task_list"])] + public readonly int $distance; + + #[Groups(["task_list"])] + public readonly int $duration; + + #[Groups(["task_list"])] + public readonly string $polyline; + + //TODO + #[Groups(["task_list"])] + public readonly ?Vehicle $vehicle; + + //TODO + #[Groups(["task_list"])] + public readonly ?Trailer $trailer; + /** * @deprecated */ @@ -22,13 +54,30 @@ class MyTaskList /** * @param int $id + * @param DateTime $createdAt + * @param DateTime $updatedAt + * @param DateTime $date + * @param string $username * @param MyTask[] $items + * @param int $distance + * @param int $duration + * @param string $polyline + * @param Vehicle|null $vehicle + * @param Trailer|null $trailer */ - public function __construct(int $id, array $items) + public function __construct(int $id, DateTime $createdAt, DateTime $updatedAt, DateTime $date, string $username, array $items, int $distance, int $duration, string $polyline, ?Vehicle $vehicle, ?Trailer $trailer) { $this->id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + $this->date = $date; + $this->username = $username; $this->items = $items; - + $this->distance = $distance; + $this->duration = $duration; + $this->polyline = $polyline; + $this->vehicle = $vehicle; + $this->trailer = $trailer; $this->isTempLegacyTaskStorage = true; } } diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 712289085d..c78a63e978 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -2,6 +2,7 @@ namespace AppBundle\Entity; +use AppBundle\Api\Dto\Metadata; use AppBundle\Api\Dto\MyTask; use AppBundle\Api\Dto\MyTaskList; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; @@ -35,13 +36,25 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $queryResult = $this->entityManager->createQueryBuilder() ->select([ 't.id', + 't.createdAt', + 't.updatedAt', 't.type', 't.status', - 'o.id AS orderId', +// 'a', //TODO + 't.doneAfter', + 't.doneBefore', +// 't.previous', //TODO +// 't.next', //TODO + 't.comments', + 't.metadata', + 'o.total AS orderTotal', + 'org.name AS orgName', ]) ->from(Task::class, 't') + ->leftJoin('t.address', 'a') ->leftJoin('t.delivery', 'd') ->leftJoin('d.order', 'o') + ->leftJoin('t.organization', 'org') ->where('t.id IN (:taskIds)') ->setParameter('taskIds', $taskIds) ->getQuery() @@ -53,14 +66,42 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $tasks = array_map(function ($row) { $taskDto = new MyTask( $row['id'], + $row['createdAt'], + $row['updatedAt'], $row['type'], $row['status'], - $row['orderId'] + null, //TODO: $row['address'], + $row['doneAfter'], + $row['doneBefore'], + null, //TODO: $row['previous'], + null, //TODO: $row['next'], + $row['comments'], + false, //TODO + $row['orgName'], + new Metadata( + $row['metadata']['delivery_position'], + $row['metadata']['order_number'], + $row['metadata']['payment_method'], + $row['orderTotal'] + ), ); + return $taskDto; }, $queryResult); - $taskListDto = new MyTaskList($taskList->getId(), $tasks); + $taskListDto = new MyTaskList( + $taskList->getId(), + $taskList->getCreatedAt(), + $taskList->getUpdatedAt(), + $taskList->getDate(), + $user->getUsername(), + $tasks, + $taskList->getDistance(), + $taskList->getDuration(), + $taskList->getPolyline(), + $taskList->getVehicle(), + $taskList->getTrailer(), + ); return $taskListDto; } } From a02f22eb42c1d6a4ebcf0097626da0a54f45707e Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:00:35 -0700 Subject: [PATCH 04/34] WIP: read more fields from the task object --- src/Api/Dto/MyTaskList.php | 15 +++- src/Api/Dto/{MyTask.php => TaskDto.php} | 93 +++++++++++-------------- src/Api/Dto/TaskMetadataDto.php | 34 +++++++++ src/Api/Dto/TaskPackageDto.php | 22 ++++++ src/Entity/TaskListRepository.php | 76 ++++++++++---------- 5 files changed, 151 insertions(+), 89 deletions(-) rename src/Api/Dto/{MyTask.php => TaskDto.php} (54%) create mode 100644 src/Api/Dto/TaskMetadataDto.php create mode 100644 src/Api/Dto/TaskPackageDto.php diff --git a/src/Api/Dto/MyTaskList.php b/src/Api/Dto/MyTaskList.php index 6b94cc1256..39e034d845 100644 --- a/src/Api/Dto/MyTaskList.php +++ b/src/Api/Dto/MyTaskList.php @@ -58,14 +58,25 @@ class MyTaskList * @param DateTime $updatedAt * @param DateTime $date * @param string $username - * @param MyTask[] $items + * @param TaskDto[] $items * @param int $distance * @param int $duration * @param string $polyline * @param Vehicle|null $vehicle * @param Trailer|null $trailer */ - public function __construct(int $id, DateTime $createdAt, DateTime $updatedAt, DateTime $date, string $username, array $items, int $distance, int $duration, string $polyline, ?Vehicle $vehicle, ?Trailer $trailer) + public function __construct( + int $id, + DateTime $createdAt, + DateTime $updatedAt, + DateTime $date, + string $username, + array $items, + int $distance, + int $duration, + string $polyline, + ?Vehicle $vehicle, + ?Trailer $trailer) { $this->id = $id; $this->createdAt = $createdAt; diff --git a/src/Api/Dto/MyTask.php b/src/Api/Dto/TaskDto.php similarity index 54% rename from src/Api/Dto/MyTask.php rename to src/Api/Dto/TaskDto.php index 041239ea6d..83697b8c58 100644 --- a/src/Api/Dto/MyTask.php +++ b/src/Api/Dto/TaskDto.php @@ -3,11 +3,10 @@ namespace AppBundle\Api\Dto; use AppBundle\Entity\Address; -use AppBundle\Entity\Task; use DateTime; use Symfony\Component\Serializer\Annotation\Groups; -final class MyTask +final class TaskDto { #[Groups(["task"])] public readonly int $id; @@ -26,37 +25,40 @@ final class MyTask //TODO; make non-nullable #[Groups(["task"])] - public readonly ?Address $address; - - #[Groups(["task"])] - public readonly DateTime $doneAfter; - - #[Groups(["task"])] - public readonly DateTime $doneBefore; + public readonly Address $address; /** * @var DateTime * @deprecated */ #[Groups(["task"])] - public readonly DateTime $after; + public readonly DateTime $doneAfter; /** * @var DateTime * @deprecated */ + #[Groups(["task"])] + public readonly DateTime $doneBefore; + + #[Groups(["task"])] + public readonly DateTime $after; + #[Groups(["task"])] public readonly DateTime $before; #[Groups(["task"])] - public readonly ?Task $previous; + public readonly ?int $previous; #[Groups(["task"])] - public readonly ?Task $next; + public readonly ?int $next; #[Groups(["task"])] public readonly ?string $comment; + #[Groups(["task"])] + public readonly array $packages; + #[Groups(["task"])] public readonly bool $hasIncidents; @@ -64,7 +66,7 @@ final class MyTask public readonly string $orgName; #[Groups(["task"])] - public readonly Metadata $metadata; + public readonly TaskMetadataDto $metadata; /** * @param int $id @@ -73,16 +75,32 @@ final class MyTask * @param string $type * @param string $status * @param Address $address - * @param DateTime $doneAfter - * @param DateTime $doneBefore - * @param Task|null $previous - * @param Task|null $next + * @param DateTime $after + * @param DateTime $before + * @param int|null $previous + * @param int|null $next * @param string|null $comment + * @param array $packages * @param bool $hasIncidents * @param string $orgName - * @param Metadata $metadata + * @param TaskMetadataDto $metadata */ - public function __construct(int $id, DateTime $createdAt, DateTime $updatedAt, string $type, string $status, ?Address $address, DateTime $doneAfter, DateTime $doneBefore, ?Task $previous, ?Task $next, ?string $comment, bool $hasIncidents, string $orgName, Metadata $metadata) + public function __construct( + int $id, + DateTime $createdAt, + DateTime $updatedAt, + string $type, + string $status, + Address $address, + DateTime $after, + DateTime $before, + ?int $previous, + ?int $next, + ?string $comment, + array $packages, + bool $hasIncidents, + string $orgName, + TaskMetadataDto $metadata) { $this->id = $id; $this->createdAt = $createdAt; @@ -90,43 +108,16 @@ public function __construct(int $id, DateTime $createdAt, DateTime $updatedAt, s $this->type = $type; $this->status = $status; $this->address = $address; - $this->doneAfter = $doneAfter; - $this->doneBefore = $doneBefore; - $this->after = $doneAfter; - $this->before = $doneBefore; + $this->doneAfter = $after; + $this->doneBefore = $before; + $this->after = $after; + $this->before = $before; $this->previous = $previous; $this->next = $next; $this->comment = $comment; + $this->packages = $packages; $this->hasIncidents = $hasIncidents; $this->orgName = $orgName; $this->metadata = $metadata; } } - -final class Metadata { - #[Groups(["task"])] - public readonly int $delivery_position; - - #[Groups(["task"])] - public readonly ?string $order_number; - - #[Groups(["task"])] - public readonly ?string $payment_method; - - #[Groups(["task"])] - public readonly ?int $order_total; - - /** - * @param int $delivery_position - * @param string|null $order_number - * @param string|null $payment_method - * @param int|null $order_total - */ - public function __construct(int $delivery_position, ?string $order_number, ?string $payment_method, ?int $order_total) - { - $this->delivery_position = $delivery_position; - $this->order_number = $order_number; - $this->payment_method = $payment_method; - $this->order_total = $order_total; - } -} diff --git a/src/Api/Dto/TaskMetadataDto.php b/src/Api/Dto/TaskMetadataDto.php new file mode 100644 index 0000000000..3188313a26 --- /dev/null +++ b/src/Api/Dto/TaskMetadataDto.php @@ -0,0 +1,34 @@ +delivery_position = $deliveryPosition; + $this->order_number = $orderNumber; + $this->payment_method = $paymentMethod; + $this->order_total = $orderTotal; + } +} diff --git a/src/Api/Dto/TaskPackageDto.php b/src/Api/Dto/TaskPackageDto.php new file mode 100644 index 0000000000..cb013bf095 --- /dev/null +++ b/src/Api/Dto/TaskPackageDto.php @@ -0,0 +1,22 @@ +name = $name; + $this->quantity = $quantity; + } +} diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index c78a63e978..440aff9802 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -2,9 +2,10 @@ namespace AppBundle\Entity; -use AppBundle\Api\Dto\Metadata; -use AppBundle\Api\Dto\MyTask; use AppBundle\Api\Dto\MyTaskList; +use AppBundle\Api\Dto\TaskDto; +use AppBundle\Api\Dto\TaskMetadataDto; +use AppBundle\Api\Dto\TaskPackageDto; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; @@ -12,7 +13,10 @@ class TaskListRepository extends ServiceEntityRepository { - public function __construct(ManagerRegistry $registry, private readonly EntityManagerInterface $entityManager) + public function __construct( + ManagerRegistry $registry, + private readonly EntityManagerInterface $entityManager + ) { parent::__construct($registry, TaskList::class); @@ -35,53 +39,53 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $queryResult = $this->entityManager->createQueryBuilder() ->select([ - 't.id', - 't.createdAt', - 't.updatedAt', - 't.type', - 't.status', -// 'a', //TODO - 't.doneAfter', - 't.doneBefore', -// 't.previous', //TODO -// 't.next', //TODO - 't.comments', - 't.metadata', + 't', + // objects listed here will be hydrated / pre-fetched by Doctrine + // https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/dql-doctrine-query-language.html#result-format + 'o.number AS orderNumber', 'o.total AS orderTotal', - 'org.name AS orgName', + 'org.name AS organizationName', + 'taskPackage', + 'package' ]) ->from(Task::class, 't') - ->leftJoin('t.address', 'a') ->leftJoin('t.delivery', 'd') ->leftJoin('d.order', 'o') ->leftJoin('t.organization', 'org') + ->leftJoin('t.packages', 'taskPackage') + ->leftJoin('taskPackage.package', 'package') ->where('t.id IN (:taskIds)') ->setParameter('taskIds', $taskIds) ->getQuery() - ->getArrayResult(); -// ->getResult(\AppBundle\Api\Dto\Task::class); + ->getResult(); //TODO; sort by position $tasks = array_map(function ($row) { - $taskDto = new MyTask( - $row['id'], - $row['createdAt'], - $row['updatedAt'], - $row['type'], - $row['status'], - null, //TODO: $row['address'], - $row['doneAfter'], - $row['doneBefore'], - null, //TODO: $row['previous'], - null, //TODO: $row['next'], - $row['comments'], + $task = $row[0]; + $taskDto = new TaskDto( + $task->getId(), + $task->getCreatedAt(), + $task->getUpdatedAt(), + $task->getType(), + $task->getStatus(), + $task->getAddress(), + $task->getDoneAfter(), + $task->getDoneBefore(), + $task->getPrevious()?->getId(), + $task->getNext()?->getId(), + $task->getComments(), + $task->getPackages()->map(function (Task\Package $package) { + return new TaskPackageDto( + $package->getPackage()->getName(), + $package->getQuantity()); + })->toArray(), false, //TODO - $row['orgName'], - new Metadata( - $row['metadata']['delivery_position'], - $row['metadata']['order_number'], - $row['metadata']['payment_method'], + $row['organizationName'], + new TaskMetadataDto( + $task->getMetadata()['delivery_position'] ?? null, //TODO extract from query + $row['orderNumber'], + $task->getMetadata()['payment_method'] ?? null, //TODO extract from query $row['orderTotal'] ), ); From 70a4e14982b09735280214d938c71d1c4c88bb61 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:01:28 -0700 Subject: [PATCH 05/34] removed not needed logs --- src/Serializer/MyTaskListNormalizer.php | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Serializer/MyTaskListNormalizer.php b/src/Serializer/MyTaskListNormalizer.php index 58555eae14..48e5101ca9 100644 --- a/src/Serializer/MyTaskListNormalizer.php +++ b/src/Serializer/MyTaskListNormalizer.php @@ -8,7 +8,6 @@ use AppBundle\Entity\Task; use AppBundle\Entity\TaskList; use AppBundle\Entity\Tour; -use Psr\Log\LoggerInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -17,8 +16,7 @@ class MyTaskListNormalizer implements NormalizerInterface public function __construct( protected ObjectNormalizer $normalizer, - protected IriConverterInterface $iriConverterInterface, - private LoggerInterface $checkoutLogger + protected IriConverterInterface $iriConverterInterface ) { $this->normalizer = $normalizer; @@ -46,8 +44,6 @@ public function __construct( public function normalize($object, $format = null, array $context = array()) { - $this->checkoutLogger->info('MyTaskListNormalizer::normalize'); - // legacy serialization for API endpoints that output TaskList.items as a list of tasks // look for "setTempLegacyTaskStorage" usage in the code. // known usage at the time of the writing : @@ -110,14 +106,6 @@ public function normalize($object, $format = null, array $context = array()) public function supportsNormalization($data, $format = null) { - $result = $this->normalizer->supportsNormalization($data, $format) && $data instanceof MyTaskList; - - $this->checkoutLogger->info('MyTaskListNormalizer::supportsNormalization', [ - 'result' => $result, - 'data' => $data, - 'format' => $format, - ]); - - return $result; + return $this->normalizer->supportsNormalization($data, $format) && $data instanceof MyTaskList; } } From 614764e2a68d7be8f2e30c200c6d92c01699ab85 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:54:54 -0700 Subject: [PATCH 06/34] added: more fields --- src/Api/Dto/{TaskDto.php => MyTaskDto.php} | 36 +++++++++------------- src/Api/Dto/MyTaskList.php | 2 +- src/Entity/TaskListRepository.php | 16 ++++++---- 3 files changed, 25 insertions(+), 29 deletions(-) rename src/Api/Dto/{TaskDto.php => MyTaskDto.php} (81%) diff --git a/src/Api/Dto/TaskDto.php b/src/Api/Dto/MyTaskDto.php similarity index 81% rename from src/Api/Dto/TaskDto.php rename to src/Api/Dto/MyTaskDto.php index 83697b8c58..5fb056b7cf 100644 --- a/src/Api/Dto/TaskDto.php +++ b/src/Api/Dto/MyTaskDto.php @@ -6,7 +6,7 @@ use DateTime; use Symfony\Component\Serializer\Annotation\Groups; -final class TaskDto +final class MyTaskDto { #[Groups(["task"])] public readonly int $id; @@ -17,13 +17,15 @@ final class TaskDto #[Groups(["task"])] public readonly DateTime $updatedAt; + #[Groups(["task"])] + public readonly string $orgName; + #[Groups(["task"])] public readonly string $type; #[Groups(["task"])] public readonly string $status; - //TODO; make non-nullable #[Groups(["task"])] public readonly Address $address; @@ -53,6 +55,12 @@ final class TaskDto #[Groups(["task"])] public readonly ?int $next; + #[Groups(["task"])] + public readonly array $tags; + + #[Groups(["task"])] + public readonly bool $doorstep; + #[Groups(["task"])] public readonly ?string $comment; @@ -62,29 +70,9 @@ final class TaskDto #[Groups(["task"])] public readonly bool $hasIncidents; - #[Groups(["task"])] - public readonly string $orgName; - #[Groups(["task"])] public readonly TaskMetadataDto $metadata; - /** - * @param int $id - * @param DateTime $createdAt - * @param DateTime $updatedAt - * @param string $type - * @param string $status - * @param Address $address - * @param DateTime $after - * @param DateTime $before - * @param int|null $previous - * @param int|null $next - * @param string|null $comment - * @param array $packages - * @param bool $hasIncidents - * @param string $orgName - * @param TaskMetadataDto $metadata - */ public function __construct( int $id, DateTime $createdAt, @@ -96,6 +84,8 @@ public function __construct( DateTime $before, ?int $previous, ?int $next, + array $tags, + bool $doorstep, ?string $comment, array $packages, bool $hasIncidents, @@ -114,6 +104,8 @@ public function __construct( $this->before = $before; $this->previous = $previous; $this->next = $next; + $this->tags = $tags; + $this->doorstep = $doorstep; $this->comment = $comment; $this->packages = $packages; $this->hasIncidents = $hasIncidents; diff --git a/src/Api/Dto/MyTaskList.php b/src/Api/Dto/MyTaskList.php index 39e034d845..4e80c14cd9 100644 --- a/src/Api/Dto/MyTaskList.php +++ b/src/Api/Dto/MyTaskList.php @@ -58,7 +58,7 @@ class MyTaskList * @param DateTime $updatedAt * @param DateTime $date * @param string $username - * @param TaskDto[] $items + * @param MyTaskDto[] $items * @param int $distance * @param int $duration * @param string $polyline diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 440aff9802..37bc61cf19 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -3,7 +3,7 @@ namespace AppBundle\Entity; use AppBundle\Api\Dto\MyTaskList; -use AppBundle\Api\Dto\TaskDto; +use AppBundle\Api\Dto\MyTaskDto; use AppBundle\Api\Dto\TaskMetadataDto; use AppBundle\Api\Dto\TaskPackageDto; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; @@ -40,13 +40,14 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $queryResult = $this->entityManager->createQueryBuilder() ->select([ 't', - // objects listed here will be hydrated / pre-fetched by Doctrine - // https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/dql-doctrine-query-language.html#result-format 'o.number AS orderNumber', 'o.total AS orderTotal', 'org.name AS organizationName', + // objects are listed below to force them being hydrated / pre-fetched by Doctrine + // https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/dql-doctrine-query-language.html#result-format 'taskPackage', - 'package' + 'package', + 'incidents', ]) ->from(Task::class, 't') ->leftJoin('t.delivery', 'd') @@ -54,6 +55,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa ->leftJoin('t.organization', 'org') ->leftJoin('t.packages', 'taskPackage') ->leftJoin('taskPackage.package', 'package') + ->leftJoin('t.incidents', 'incidents') ->where('t.id IN (:taskIds)') ->setParameter('taskIds', $taskIds) ->getQuery() @@ -63,7 +65,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $tasks = array_map(function ($row) { $task = $row[0]; - $taskDto = new TaskDto( + $taskDto = new MyTaskDto( $task->getId(), $task->getCreatedAt(), $task->getUpdatedAt(), @@ -74,13 +76,15 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $task->getDoneBefore(), $task->getPrevious()?->getId(), $task->getNext()?->getId(), + $task->getTags(), + $task->isDoorstep(), $task->getComments(), $task->getPackages()->map(function (Task\Package $package) { return new TaskPackageDto( $package->getPackage()->getName(), $package->getQuantity()); })->toArray(), - false, //TODO + $task->getHasIncidents(), $row['organizationName'], new TaskMetadataDto( $task->getMetadata()['delivery_position'] ?? null, //TODO extract from query From 9f63c6d6ea26a6ea8f31f4fd2ce0cb29ae5141a9 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:24:18 -0700 Subject: [PATCH 07/34] WIP: handle tours --- src/Entity/TaskListRepository.php | 51 +++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 37bc61cf19..3c50946ed4 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -24,20 +24,37 @@ public function __construct( public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTaskList { - $taskList = $this->findOneBy([ - 'courier' => $user, - 'date' => $date, - ]); + $taskListQueryResult = $this->entityManager->createQueryBuilder() + ->select([ + 'tl', + // objects are listed below to force them being hydrated / pre-fetched by Doctrine + // https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/dql-doctrine-query-language.html#result-format + 'item', + 'tour', + 'tourItem', + ]) + ->from(TaskList::class, 'tl') + ->leftJoin('tl.items', 'item') + ->leftJoin('item.tour', 'tour') + ->leftJoin('tour.items', 'tourItem') + ->where('tl.courier = :courier') + ->andWhere('tl.date = :date') + ->setParameter('courier', $user) + ->setParameter('date', $date) + ->getQuery() + ->getResult(); + + $taskList = $taskListQueryResult[0] ?? null; if (null === $taskList) { return null; } - $taskIds = $taskList->getItems()->map(function (TaskList\Item $item) { - return $item->getTask()->getId(); - }); + $orderedTaskIds = array_map(function (Task $task) { + return $task->getId(); + }, $taskList->getTasks()); - $queryResult = $this->entityManager->createQueryBuilder() + $tasksQueryResult = $this->entityManager->createQueryBuilder() ->select([ 't', 'o.number AS orderNumber', @@ -57,12 +74,10 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa ->leftJoin('taskPackage.package', 'package') ->leftJoin('t.incidents', 'incidents') ->where('t.id IN (:taskIds)') - ->setParameter('taskIds', $taskIds) + ->setParameter('taskIds', $orderedTaskIds) // using IN might cause problems with large number of tasks ->getQuery() ->getResult(); - //TODO; sort by position - $tasks = array_map(function ($row) { $task = $row[0]; $taskDto = new MyTaskDto( @@ -95,7 +110,17 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa ); return $taskDto; - }, $queryResult); + }, $tasksQueryResult); + + $tasksById = array_reduce($tasks, function ($carry, $task) { + $carry[$task->id] = $task; + return $carry; + }, []); + + //restore order of tasks + $orderedTasks = array_map(function ($taskId) use ($tasksById) { + return $tasksById[$taskId]; + }, $orderedTaskIds); $taskListDto = new MyTaskList( $taskList->getId(), @@ -103,7 +128,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $taskList->getUpdatedAt(), $taskList->getDate(), $user->getUsername(), - $tasks, + $orderedTasks, $taskList->getDistance(), $taskList->getDuration(), $taskList->getPolyline(), From 8cac6270397a6936cbf4066f21f08a8949c0f34f Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:41:25 -0700 Subject: [PATCH 08/34] introduce /me/tasks/v2 --- src/Action/MyTasks.php | 52 ++++++++++++++++++++++++++----------- src/Action/MyTasks2.php | 57 +++++++++++++++++++++++++++++++++++++++++ src/Entity/TaskList.php | 20 +++++++++++++++ 3 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 src/Action/MyTasks2.php diff --git a/src/Action/MyTasks.php b/src/Action/MyTasks.php index 5f75ac260d..428487ccc2 100644 --- a/src/Action/MyTasks.php +++ b/src/Action/MyTasks.php @@ -2,12 +2,11 @@ namespace AppBundle\Action; -use AppBundle\Entity\TaskListRepository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use AppBundle\Action\Utils\TokenStorageTrait; +use AppBundle\Entity\Task; use AppBundle\Entity\TaskList; -use Doctrine\ORM\EntityRepository; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -15,30 +14,23 @@ final class MyTasks { use TokenStorageTrait; - /** - * @var TaskListRepository - */ - private readonly EntityRepository $taskListRepository; - public function __construct( TokenStorageInterface $tokenStorage, - private readonly EntityManagerInterface $entityManager) + protected EntityManagerInterface $entityManager) { $this->tokenStorage = $tokenStorage; - $this->taskListRepository = $this->entityManager->getRepository(TaskList::class); } public function __invoke(Request $request) { - $user = $this->getUser(); $date = new \DateTime($request->get('date')); - $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); + $taskList = $this->loadExisting($date); - if (null === $taskListDto) { + if (null === $taskList) { $taskList = new TaskList(); - $taskList->setCourier($user); + $taskList->setCourier($this->getUser()); $taskList->setDate($date); try { @@ -48,10 +40,40 @@ public function __invoke(Request $request) // If 2 requests are received at the very same time, // we can have a race condition // @see https://github.com/coopcycle/coopcycle-app/issues/1265 - $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); + $taskList = $this->loadExisting($date); } } - return $taskListDto; + return $taskList; + } + + /** + * @param \DateTime $date + * @return TaskList|null + */ + private function loadExisting(\DateTime $date): ?TaskList + { + $taskList = $this->entityManager->getRepository(TaskList::class) + ->findOneBy([ + 'courier' => $this->getUser(), + 'date' => $date, + ]); + + if ($taskList) { + + // reset array index to 0 with array_values, otherwise you might get weird stuff in the serializer + $notCancelled = array_values( + array_filter(array_filter($taskList->getTasks(), function (Task $task) { + return !$task->isCancelled(); + })) + ); + + // supports the legacy display of TaskList as tasks for the app courier part + $taskList->setTempLegacyTaskStorage($notCancelled); + + return $taskList; + } + + return null; } } diff --git a/src/Action/MyTasks2.php b/src/Action/MyTasks2.php new file mode 100644 index 0000000000..71baf49f25 --- /dev/null +++ b/src/Action/MyTasks2.php @@ -0,0 +1,57 @@ +tokenStorage = $tokenStorage; + $this->taskListRepository = $this->entityManager->getRepository(TaskList::class); + } + + public function __invoke(Request $request) + { + $user = $this->getUser(); + $date = new \DateTime($request->get('date')); + + $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); + + if (null === $taskListDto) { + + $taskList = new TaskList(); + $taskList->setCourier($user); + $taskList->setDate($date); + + try { + $this->entityManager->persist($taskList); + $this->entityManager->flush(); + } catch (UniqueConstraintViolationException $e) { + // If 2 requests are received at the very same time, + // we can have a race condition + // @see https://github.com/coopcycle/coopcycle-app/issues/1265 + $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); + } + } + + return $taskListDto; + } +} diff --git a/src/Entity/TaskList.php b/src/Entity/TaskList.php index 66dccb3c5d..209dc1d161 100644 --- a/src/Entity/TaskList.php +++ b/src/Entity/TaskList.php @@ -3,6 +3,7 @@ namespace AppBundle\Entity; use AppBundle\Action\MyTasks as MyTasksController; +use AppBundle\Action\MyTasks2 as MyTasksController2; use AppBundle\Action\TaskList\Create as CreateTaskListController; use AppBundle\Action\TaskList\Optimize as OptimizeController; use AppBundle\Action\TaskList\SetItems as SetTaskListItemsController; @@ -82,6 +83,25 @@ * }} * } * }, + * "my_tasks_v2"={ + * "method"="GET", + * "path"="/me/tasks/v2/{date}", + * "controller"=MyTasksController2::class, + * "access_control"="is_granted('ROLE_ADMIN') or is_granted('ROLE_COURIER')", + * "read"=false, + * "write"=false, + * "normalization_context"={"groups"={"task_list", "task", "delivery", "address"}}, + * "openapi_context"={ + * "summary"="Retrieves the collection of Task resources assigned to the authenticated token.", + * "parameters"={{ + * "in"="path", + * "name"="date", + * "required"=true, + * "type"="string", + * "format"="date" + * }} + * } + * }, * "optimize"={ * "method"="GET", * "path"="/task_lists/{id}/optimize", From 240b60ed47a2afc9d35334c6516b2f8a540a8414 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:46:15 -0700 Subject: [PATCH 09/34] remove deprecated fields --- src/Api/Dto/MyTaskDto.php | 16 --------- src/Api/Dto/MyTaskList.php | 8 +---- src/Serializer/MyTaskListNormalizer.php | 44 ++++++++++++------------- 3 files changed, 23 insertions(+), 45 deletions(-) diff --git a/src/Api/Dto/MyTaskDto.php b/src/Api/Dto/MyTaskDto.php index 5fb056b7cf..04f3713fd8 100644 --- a/src/Api/Dto/MyTaskDto.php +++ b/src/Api/Dto/MyTaskDto.php @@ -29,20 +29,6 @@ final class MyTaskDto #[Groups(["task"])] public readonly Address $address; - /** - * @var DateTime - * @deprecated - */ - #[Groups(["task"])] - public readonly DateTime $doneAfter; - - /** - * @var DateTime - * @deprecated - */ - #[Groups(["task"])] - public readonly DateTime $doneBefore; - #[Groups(["task"])] public readonly DateTime $after; @@ -98,8 +84,6 @@ public function __construct( $this->type = $type; $this->status = $status; $this->address = $address; - $this->doneAfter = $after; - $this->doneBefore = $before; $this->after = $after; $this->before = $before; $this->previous = $previous; diff --git a/src/Api/Dto/MyTaskList.php b/src/Api/Dto/MyTaskList.php index 4e80c14cd9..8f50b74d75 100644 --- a/src/Api/Dto/MyTaskList.php +++ b/src/Api/Dto/MyTaskList.php @@ -25,7 +25,7 @@ class MyTaskList public readonly string $username; /** - * @var MyTask[] + * @var MyTaskDto[] */ #[Groups(["task_list"])] public readonly array $items; @@ -47,11 +47,6 @@ class MyTaskList #[Groups(["task_list"])] public readonly ?Trailer $trailer; - /** - * @deprecated - */ - public readonly bool $isTempLegacyTaskStorage; - /** * @param int $id * @param DateTime $createdAt @@ -89,6 +84,5 @@ public function __construct( $this->polyline = $polyline; $this->vehicle = $vehicle; $this->trailer = $trailer; - $this->isTempLegacyTaskStorage = true; } } diff --git a/src/Serializer/MyTaskListNormalizer.php b/src/Serializer/MyTaskListNormalizer.php index 48e5101ca9..47a928eca4 100644 --- a/src/Serializer/MyTaskListNormalizer.php +++ b/src/Serializer/MyTaskListNormalizer.php @@ -49,7 +49,7 @@ public function normalize($object, $format = null, array $context = array()) // known usage at the time of the writing : // - used for the rider/dispatcher smartphone app (does not display or handle tours) // - used for stores to access /api/task_lists/ and display only tasks linked to the org (it is only used by Tricargo coop and should be considered legacy) - if ($object->isTempLegacyTaskStorage) { +// if ($object->isTempLegacyTaskStorage) { $context[AbstractNormalizer::IGNORED_ATTRIBUTES] = ['items']; $data = $this->normalizer->normalize($object, $format, $context); @@ -73,27 +73,27 @@ public function normalize($object, $format = null, array $context = array()) return $taskData; }, $object->items ); - } - // legacy serialization for app and events - // see https://github.com/coopcycle/coopcycle-app/issues/1803 - else if (in_array('task', $context['groups'])) { - $context[AbstractNormalizer::IGNORED_ATTRIBUTES] = ['items']; - $data = $this->normalizer->normalize($object, $format, $context); - $data['items'] = array_map(function($task) { - return $this->normalizer->normalize( - $task, - 'jsonld', - ['groups' => ["task_list", "task_collection", "task", "delivery", "address"]] - ); - }, $object->getTasks() - ); - } else { - $data = $this->normalizer->normalize($object, $format, $context); - - if (isset($data['items'])) { - $data['items'] = $this->flattenItemsUris($data['items']); - } - } +// } +// // legacy serialization for app and events +// // see https://github.com/coopcycle/coopcycle-app/issues/1803 +// else if (in_array('task', $context['groups'])) { +// $context[AbstractNormalizer::IGNORED_ATTRIBUTES] = ['items']; +// $data = $this->normalizer->normalize($object, $format, $context); +// $data['items'] = array_map(function($task) { +// return $this->normalizer->normalize( +// $task, +// 'jsonld', +// ['groups' => ["task_list", "task_collection", "task", "delivery", "address"]] +// ); +// }, $object->getTasks() +// ); +// } else { +// $data = $this->normalizer->normalize($object, $format, $context); +// +// if (isset($data['items'])) { +// $data['items'] = $this->flattenItemsUris($data['items']); +// } +// } // Legacy if (isset($context['item_operation_name']) && $context['item_operation_name'] === 'my_tasks') { From 98f88fb3923bf8bb39f1656af45971e7bea74309 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:07:36 -0700 Subject: [PATCH 10/34] filter out cancelled tasks --- src/Entity/TaskListRepository.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 3c50946ed4..8d14f9095b 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -74,7 +74,9 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa ->leftJoin('taskPackage.package', 'package') ->leftJoin('t.incidents', 'incidents') ->where('t.id IN (:taskIds)') + ->andWhere('t.status != :statusCancelled') ->setParameter('taskIds', $orderedTaskIds) // using IN might cause problems with large number of tasks + ->setParameter('statusCancelled', Task::STATUS_CANCELLED) ->getQuery() ->getResult(); @@ -118,9 +120,14 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa }, []); //restore order of tasks - $orderedTasks = array_map(function ($taskId) use ($tasksById) { - return $tasksById[$taskId]; - }, $orderedTaskIds); + $orderedTasks = []; + foreach ($orderedTaskIds as $taskId) { + // skip tasks that are not returned by the query + // that can happen if a task is cancelled, for example + if (isset($tasksById[$taskId])) { + $orderedTasks[] = $tasksById[$taskId]; + } + } $taskListDto = new MyTaskList( $taskList->getId(), From c76394d3df50d72ee10dfd18048ade6ff1da6687 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:33:01 -0700 Subject: [PATCH 11/34] remove `hydra` view --- src/Serializer/MyTaskListNormalizer.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Serializer/MyTaskListNormalizer.php b/src/Serializer/MyTaskListNormalizer.php index 47a928eca4..96778c6ba6 100644 --- a/src/Serializer/MyTaskListNormalizer.php +++ b/src/Serializer/MyTaskListNormalizer.php @@ -95,12 +95,6 @@ public function normalize($object, $format = null, array $context = array()) // } // } - // Legacy - if (isset($context['item_operation_name']) && $context['item_operation_name'] === 'my_tasks') { - $data['hydra:member'] = $data['items']; - $data['hydra:totalItems'] = count($data['items']); - } - return $data; } From 6a3c53642e95cd2e91f151a4924ca10d17e3db6e Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:02:06 -0700 Subject: [PATCH 12/34] added: pickup packages and weight --- src/Api/Dto/MyTaskDto.php | 5 +++ src/Api/Dto/TaskPackageDto.php | 16 ++++++++-- src/Entity/TaskListRepository.php | 52 +++++++++++++++++++++++++++---- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/Api/Dto/MyTaskDto.php b/src/Api/Dto/MyTaskDto.php index 04f3713fd8..7e15d60c08 100644 --- a/src/Api/Dto/MyTaskDto.php +++ b/src/Api/Dto/MyTaskDto.php @@ -53,6 +53,9 @@ final class MyTaskDto #[Groups(["task"])] public readonly array $packages; + #[Groups(["task"])] + public readonly ?int $weight; + #[Groups(["task"])] public readonly bool $hasIncidents; @@ -74,6 +77,7 @@ public function __construct( bool $doorstep, ?string $comment, array $packages, + ?int $weight, bool $hasIncidents, string $orgName, TaskMetadataDto $metadata) @@ -92,6 +96,7 @@ public function __construct( $this->doorstep = $doorstep; $this->comment = $comment; $this->packages = $packages; + $this->weight = $weight; $this->hasIncidents = $hasIncidents; $this->orgName = $orgName; $this->metadata = $metadata; diff --git a/src/Api/Dto/TaskPackageDto.php b/src/Api/Dto/TaskPackageDto.php index cb013bf095..61494816c9 100644 --- a/src/Api/Dto/TaskPackageDto.php +++ b/src/Api/Dto/TaskPackageDto.php @@ -6,17 +6,27 @@ class TaskPackageDto { + #[Groups(["task"])] + public readonly string $shortCode; + #[Groups(["task"])] public readonly string $name; #[Groups(["task"])] - public readonly int $quantity; + public readonly int $averageVolumeUnits; - //todo; add more fields + #[Groups(["task"])] + public readonly int $quantity; - public function __construct(string $name, int $quantity) + public function __construct( + string $shortCode, + string $name, + int $averageVolumeUnits, + int $quantity) { + $this->shortCode = $shortCode; $this->name = $name; + $this->averageVolumeUnits = $averageVolumeUnits; $this->quantity = $quantity; } } diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 8d14f9095b..1cbd65b85f 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -19,7 +19,6 @@ public function __construct( ) { parent::__construct($registry, TaskList::class); - } public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTaskList @@ -57,6 +56,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $tasksQueryResult = $this->entityManager->createQueryBuilder() ->select([ 't', + 'd.id AS deliveryId', 'o.number AS orderNumber', 'o.total AS orderTotal', 'org.name AS organizationName', @@ -80,7 +80,43 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa ->getQuery() ->getResult(); - $tasks = array_map(function ($row) { + $tasksByDeliveryId = array_reduce($tasksQueryResult, function ($carry, $row) { + $deliveryId = $row['deliveryId'] ?? null; + + if (null === $deliveryId) { + return $carry; + } + + $task = $row[0]; + $carry[$deliveryId][] = $task; + return $carry; + }, []); + + $tasks = array_map(function ($row) use ($tasksByDeliveryId) { + $task = $row[0]; + $deliveryId = $row['deliveryId'] ?? null; + + $taskPackages = []; + $weight = null; + + $tasksInTheSameDelivery = $deliveryId ? $tasksByDeliveryId[$deliveryId] : []; + + if ($task->isPickup()) { + // for a pickup in a delivery, the serialized weight is the sum of the dropoff weight and + // the packages are the "sum" of the dropoffs packages + foreach ($tasksInTheSameDelivery as $task) { + if ($task->isPickup()) { + continue; + } + + $taskPackages = array_merge($taskPackages, $task->getPackages()->toArray()); + $weight += $task->getWeight(); + } + } else { + $taskPackages = $task->getPackages()->toArray(); + $weight = $task->getWeight(); + } + $task = $row[0]; $taskDto = new MyTaskDto( $task->getId(), @@ -96,11 +132,15 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $task->getTags(), $task->isDoorstep(), $task->getComments(), - $task->getPackages()->map(function (Task\Package $package) { + array_map(function (Task\Package $taskPackage) { + $package = $taskPackage->getPackage(); return new TaskPackageDto( - $package->getPackage()->getName(), - $package->getQuantity()); - })->toArray(), + $package->getShortCode(), + $package->getName(), + $package->getAverageVolumeUnits(), + $taskPackage->getQuantity()); + }, $taskPackages), + $weight, $task->getHasIncidents(), $row['organizationName'], new TaskMetadataDto( From 9dce435ac3e5dd2e023d8ed8a0bb4b072246b6ba Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:38:57 -0700 Subject: [PATCH 13/34] flatten metadata --- src/Api/Dto/MyTaskDto.php | 22 +++++++++++++++++--- src/Api/Dto/TaskMetadataDto.php | 34 ------------------------------- src/Entity/TaskListRepository.php | 11 ++++------ 3 files changed, 23 insertions(+), 44 deletions(-) delete mode 100644 src/Api/Dto/TaskMetadataDto.php diff --git a/src/Api/Dto/MyTaskDto.php b/src/Api/Dto/MyTaskDto.php index 7e15d60c08..c0c31c39fe 100644 --- a/src/Api/Dto/MyTaskDto.php +++ b/src/Api/Dto/MyTaskDto.php @@ -60,7 +60,16 @@ final class MyTaskDto public readonly bool $hasIncidents; #[Groups(["task"])] - public readonly TaskMetadataDto $metadata; + public readonly ?int $delivery_position; + + #[Groups(["task"])] + public readonly ?string $order_number; + + #[Groups(["task"])] + public readonly ?string $payment_method; + + #[Groups(["task"])] + public readonly ?int $order_total; public function __construct( int $id, @@ -80,7 +89,11 @@ public function __construct( ?int $weight, bool $hasIncidents, string $orgName, - TaskMetadataDto $metadata) + ?int $deliveryPosition, + ?string $orderNumber, + ?string $paymentMethod, + ?int $orderTotal + ) { $this->id = $id; $this->createdAt = $createdAt; @@ -99,6 +112,9 @@ public function __construct( $this->weight = $weight; $this->hasIncidents = $hasIncidents; $this->orgName = $orgName; - $this->metadata = $metadata; + $this->delivery_position = $deliveryPosition; + $this->order_number = $orderNumber; + $this->payment_method = $paymentMethod; + $this->order_total = $orderTotal; } } diff --git a/src/Api/Dto/TaskMetadataDto.php b/src/Api/Dto/TaskMetadataDto.php deleted file mode 100644 index 3188313a26..0000000000 --- a/src/Api/Dto/TaskMetadataDto.php +++ /dev/null @@ -1,34 +0,0 @@ -delivery_position = $deliveryPosition; - $this->order_number = $orderNumber; - $this->payment_method = $paymentMethod; - $this->order_total = $orderTotal; - } -} diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 1cbd65b85f..479794e8d7 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -4,7 +4,6 @@ use AppBundle\Api\Dto\MyTaskList; use AppBundle\Api\Dto\MyTaskDto; -use AppBundle\Api\Dto\TaskMetadataDto; use AppBundle\Api\Dto\TaskPackageDto; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\EntityManagerInterface; @@ -143,12 +142,10 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $weight, $task->getHasIncidents(), $row['organizationName'], - new TaskMetadataDto( - $task->getMetadata()['delivery_position'] ?? null, //TODO extract from query - $row['orderNumber'], - $task->getMetadata()['payment_method'] ?? null, //TODO extract from query - $row['orderTotal'] - ), + $task->getMetadata()['delivery_position'] ?? null, //FIXME extract from the query + $row['orderNumber'] ?? null, + $task->getMetadata()['payment_method'] ?? null, //FIXME extract from the query + $row['orderTotal'] ?? null, ); return $taskDto; From 1c0fc09d97f0d63786d93ebf9797f706e849b78b Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:43:23 -0700 Subject: [PATCH 14/34] renamed: MyTaskListDto --- app/config/services/serializer.yml | 2 +- src/Api/Dto/{MyTaskList.php => MyTaskListDto.php} | 2 +- src/Entity/TaskListRepository.php | 6 +++--- ...MyTaskListNormalizer.php => MyTaskListDtoNormalizer.php} | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) rename src/Api/Dto/{MyTaskList.php => MyTaskListDto.php} (99%) rename src/Serializer/{MyTaskListNormalizer.php => MyTaskListDtoNormalizer.php} (96%) diff --git a/app/config/services/serializer.yml b/app/config/services/serializer.yml index 3f386f9d5e..ea38de6569 100644 --- a/app/config/services/serializer.yml +++ b/app/config/services/serializer.yml @@ -30,7 +30,7 @@ services: - "@api_platform.jsonld.normalizer.item" tags: [ { name: serializer.normalizer, priority: 128 } ] - AppBundle\Serializer\MyTaskListNormalizer: + AppBundle\Serializer\MyTaskListDtoNormalizer: arguments: - "@api_platform.jsonld.normalizer.object" tags: [ { name: serializer.normalizer, priority: 128 } ] diff --git a/src/Api/Dto/MyTaskList.php b/src/Api/Dto/MyTaskListDto.php similarity index 99% rename from src/Api/Dto/MyTaskList.php rename to src/Api/Dto/MyTaskListDto.php index 8f50b74d75..43d11c30d7 100644 --- a/src/Api/Dto/MyTaskList.php +++ b/src/Api/Dto/MyTaskListDto.php @@ -7,7 +7,7 @@ use DateTime; use Symfony\Component\Serializer\Annotation\Groups; -class MyTaskList +class MyTaskListDto { #[Groups(["task_list"])] public readonly int $id; diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 479794e8d7..ce12a1b0aa 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -2,7 +2,7 @@ namespace AppBundle\Entity; -use AppBundle\Api\Dto\MyTaskList; +use AppBundle\Api\Dto\MyTaskListDto; use AppBundle\Api\Dto\MyTaskDto; use AppBundle\Api\Dto\TaskPackageDto; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; @@ -20,7 +20,7 @@ public function __construct( parent::__construct($registry, TaskList::class); } - public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTaskList + public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTaskListDto { $taskListQueryResult = $this->entityManager->createQueryBuilder() ->select([ @@ -166,7 +166,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa } } - $taskListDto = new MyTaskList( + $taskListDto = new MyTaskListDto( $taskList->getId(), $taskList->getCreatedAt(), $taskList->getUpdatedAt(), diff --git a/src/Serializer/MyTaskListNormalizer.php b/src/Serializer/MyTaskListDtoNormalizer.php similarity index 96% rename from src/Serializer/MyTaskListNormalizer.php rename to src/Serializer/MyTaskListDtoNormalizer.php index 96778c6ba6..9e96c0e088 100644 --- a/src/Serializer/MyTaskListNormalizer.php +++ b/src/Serializer/MyTaskListDtoNormalizer.php @@ -4,14 +4,14 @@ use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\JsonLd\Serializer\ObjectNormalizer; -use AppBundle\Api\Dto\MyTaskList; +use AppBundle\Api\Dto\MyTaskListDto; use AppBundle\Entity\Task; use AppBundle\Entity\TaskList; use AppBundle\Entity\Tour; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -class MyTaskListNormalizer implements NormalizerInterface +class MyTaskListDtoNormalizer implements NormalizerInterface { public function __construct( @@ -100,6 +100,6 @@ public function normalize($object, $format = null, array $context = array()) public function supportsNormalization($data, $format = null) { - return $this->normalizer->supportsNormalization($data, $format) && $data instanceof MyTaskList; + return $this->normalizer->supportsNormalization($data, $format) && $data instanceof MyTaskListDto; } } From 598ee1db3a248bb19f13aef93beaad3085bbbae0 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:43:38 -0700 Subject: [PATCH 15/34] set output type --- src/Entity/TaskList.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Entity/TaskList.php b/src/Entity/TaskList.php index 209dc1d161..b7f1d0c72e 100644 --- a/src/Entity/TaskList.php +++ b/src/Entity/TaskList.php @@ -8,6 +8,7 @@ use AppBundle\Action\TaskList\Optimize as OptimizeController; use AppBundle\Action\TaskList\SetItems as SetTaskListItemsController; use AppBundle\Entity\Task\CollectionInterface as TaskCollectionInterface; +use AppBundle\Api\Dto\MyTaskListDto; use ApiPlatform\Core\Annotation\ApiFilter; use ApiPlatform\Core\Annotation\ApiResource; use AppBundle\Api\Filter\DateFilter; @@ -88,6 +89,7 @@ * "path"="/me/tasks/v2/{date}", * "controller"=MyTasksController2::class, * "access_control"="is_granted('ROLE_ADMIN') or is_granted('ROLE_COURIER')", + * "output"=MyTaskListDto::class, * "read"=false, * "write"=false, * "normalization_context"={"groups"={"task_list", "task", "delivery", "address"}}, From 53b40e56e0bf7272f87f29ffa5ce0190a07c559b Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:44:13 -0700 Subject: [PATCH 16/34] wip: MyTaskDtoNormalizer --- app/config/services/serializer.yml | 5 ++ src/Serializer/MyTaskDtoNormalizer.php | 65 ++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/Serializer/MyTaskDtoNormalizer.php diff --git a/app/config/services/serializer.yml b/app/config/services/serializer.yml index ea38de6569..ef63a67d14 100644 --- a/app/config/services/serializer.yml +++ b/app/config/services/serializer.yml @@ -30,6 +30,11 @@ services: - "@api_platform.jsonld.normalizer.item" tags: [ { name: serializer.normalizer, priority: 128 } ] + AppBundle\Serializer\MyTaskDtoNormalizer: + arguments: + - "@api_platform.jsonld.normalizer.item" + tags: [ { name: serializer.normalizer, priority: 128 } ] + AppBundle\Serializer\MyTaskListDtoNormalizer: arguments: - "@api_platform.jsonld.normalizer.object" diff --git a/src/Serializer/MyTaskDtoNormalizer.php b/src/Serializer/MyTaskDtoNormalizer.php new file mode 100644 index 0000000000..d9c7483b6d --- /dev/null +++ b/src/Serializer/MyTaskDtoNormalizer.php @@ -0,0 +1,65 @@ +normalizer->normalize($object, $format, $context); + + if (!is_array($data)) { + + return $data; + } + + // Make sure "comments" is a string + if (array_key_exists('comments', $data) && null === $data['comments']) { + $data['comments'] = ''; + } + + if (isset($data['tags']) && is_array($data['tags']) && count($data['tags']) > 0) { + $data['tags'] = $this->tagManager->expand($data['tags']); + } + + $data['previous'] = null; + if ($object->hasPrevious()) { + $data['previous'] = $this->iriConverter->getIriFromItem($object->getPrevious()); + } + + $data['next'] = null; + if ($object->hasNext()) { + $data['next'] = $this->iriConverter->getIriFromItem($object->getNext()); + } + + //TODO; fix me +// if (array_key_exists('metadata', $data) && is_array($data['metadata'])) { +// if ($order = $object->getDelivery()?->getOrder()) { +// $data['metadata'] = array_merge($data['metadata'], ['zero_waste' => $order->isZeroWaste()]); +// if ($object->isDropoff()) { +// $data['metadata'] = array_merge($data['metadata'], ['has_loopeat_returns' => $order->hasLoopeatReturns()]); +// } +// } +// } + + return $data; + } + + public function supportsNormalization($data, $format = null) + { + return $this->normalizer->supportsNormalization($data, $format) && $data instanceof MyTaskDto; + } +} From aa037c1e795429d913b80185e331c49159eff09b Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:16:41 -0700 Subject: [PATCH 17/34] maintain v1 response fields --- src/Action/MyTasks.php | 52 ++++++++-------------------- src/Action/MyTasks2.php | 57 ------------------------------- src/Api/Dto/MyTaskDto.php | 34 +++++++++--------- src/Api/Dto/MyTaskMetadataDto.php | 32 +++++++++++++++++ src/Entity/TaskList.php | 22 +----------- src/Entity/TaskListRepository.php | 11 +++--- 6 files changed, 72 insertions(+), 136 deletions(-) delete mode 100644 src/Action/MyTasks2.php create mode 100644 src/Api/Dto/MyTaskMetadataDto.php diff --git a/src/Action/MyTasks.php b/src/Action/MyTasks.php index 428487ccc2..5f75ac260d 100644 --- a/src/Action/MyTasks.php +++ b/src/Action/MyTasks.php @@ -2,11 +2,12 @@ namespace AppBundle\Action; +use AppBundle\Entity\TaskListRepository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use AppBundle\Action\Utils\TokenStorageTrait; -use AppBundle\Entity\Task; use AppBundle\Entity\TaskList; +use Doctrine\ORM\EntityRepository; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -14,23 +15,30 @@ final class MyTasks { use TokenStorageTrait; + /** + * @var TaskListRepository + */ + private readonly EntityRepository $taskListRepository; + public function __construct( TokenStorageInterface $tokenStorage, - protected EntityManagerInterface $entityManager) + private readonly EntityManagerInterface $entityManager) { $this->tokenStorage = $tokenStorage; + $this->taskListRepository = $this->entityManager->getRepository(TaskList::class); } public function __invoke(Request $request) { + $user = $this->getUser(); $date = new \DateTime($request->get('date')); - $taskList = $this->loadExisting($date); + $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); - if (null === $taskList) { + if (null === $taskListDto) { $taskList = new TaskList(); - $taskList->setCourier($this->getUser()); + $taskList->setCourier($user); $taskList->setDate($date); try { @@ -40,40 +48,10 @@ public function __invoke(Request $request) // If 2 requests are received at the very same time, // we can have a race condition // @see https://github.com/coopcycle/coopcycle-app/issues/1265 - $taskList = $this->loadExisting($date); + $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); } } - return $taskList; - } - - /** - * @param \DateTime $date - * @return TaskList|null - */ - private function loadExisting(\DateTime $date): ?TaskList - { - $taskList = $this->entityManager->getRepository(TaskList::class) - ->findOneBy([ - 'courier' => $this->getUser(), - 'date' => $date, - ]); - - if ($taskList) { - - // reset array index to 0 with array_values, otherwise you might get weird stuff in the serializer - $notCancelled = array_values( - array_filter(array_filter($taskList->getTasks(), function (Task $task) { - return !$task->isCancelled(); - })) - ); - - // supports the legacy display of TaskList as tasks for the app courier part - $taskList->setTempLegacyTaskStorage($notCancelled); - - return $taskList; - } - - return null; + return $taskListDto; } } diff --git a/src/Action/MyTasks2.php b/src/Action/MyTasks2.php deleted file mode 100644 index 71baf49f25..0000000000 --- a/src/Action/MyTasks2.php +++ /dev/null @@ -1,57 +0,0 @@ -tokenStorage = $tokenStorage; - $this->taskListRepository = $this->entityManager->getRepository(TaskList::class); - } - - public function __invoke(Request $request) - { - $user = $this->getUser(); - $date = new \DateTime($request->get('date')); - - $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); - - if (null === $taskListDto) { - - $taskList = new TaskList(); - $taskList->setCourier($user); - $taskList->setDate($date); - - try { - $this->entityManager->persist($taskList); - $this->entityManager->flush(); - } catch (UniqueConstraintViolationException $e) { - // If 2 requests are received at the very same time, - // we can have a race condition - // @see https://github.com/coopcycle/coopcycle-app/issues/1265 - $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); - } - } - - return $taskListDto; - } -} diff --git a/src/Api/Dto/MyTaskDto.php b/src/Api/Dto/MyTaskDto.php index c0c31c39fe..e2e1452603 100644 --- a/src/Api/Dto/MyTaskDto.php +++ b/src/Api/Dto/MyTaskDto.php @@ -35,6 +35,18 @@ final class MyTaskDto #[Groups(["task"])] public readonly DateTime $before; + /** + * @deprecated use $after instead + */ + #[Groups(["task"])] + public readonly DateTime $doneAfter; + + /** + * @deprecated use $before instead + */ + #[Groups(["task"])] + public readonly DateTime $doneBefore; + #[Groups(["task"])] public readonly ?int $previous; @@ -60,16 +72,8 @@ final class MyTaskDto public readonly bool $hasIncidents; #[Groups(["task"])] - public readonly ?int $delivery_position; + public readonly MyTaskMetadataDto $metadata; - #[Groups(["task"])] - public readonly ?string $order_number; - - #[Groups(["task"])] - public readonly ?string $payment_method; - - #[Groups(["task"])] - public readonly ?int $order_total; public function __construct( int $id, @@ -89,10 +93,7 @@ public function __construct( ?int $weight, bool $hasIncidents, string $orgName, - ?int $deliveryPosition, - ?string $orderNumber, - ?string $paymentMethod, - ?int $orderTotal + MyTaskMetadataDto $metadata ) { $this->id = $id; @@ -103,6 +104,8 @@ public function __construct( $this->address = $address; $this->after = $after; $this->before = $before; + $this->doneAfter = $after; + $this->doneBefore = $before; $this->previous = $previous; $this->next = $next; $this->tags = $tags; @@ -112,9 +115,6 @@ public function __construct( $this->weight = $weight; $this->hasIncidents = $hasIncidents; $this->orgName = $orgName; - $this->delivery_position = $deliveryPosition; - $this->order_number = $orderNumber; - $this->payment_method = $paymentMethod; - $this->order_total = $orderTotal; + $this->metadata = $metadata; } } diff --git a/src/Api/Dto/MyTaskMetadataDto.php b/src/Api/Dto/MyTaskMetadataDto.php new file mode 100644 index 0000000000..8bf6e0a2c7 --- /dev/null +++ b/src/Api/Dto/MyTaskMetadataDto.php @@ -0,0 +1,32 @@ +delivery_position = $delivery_position; + $this->order_number = $order_number; + $this->payment_method = $payment_method; + $this->order_total = $order_total; + } +} diff --git a/src/Entity/TaskList.php b/src/Entity/TaskList.php index b7f1d0c72e..3611c6afbc 100644 --- a/src/Entity/TaskList.php +++ b/src/Entity/TaskList.php @@ -3,7 +3,6 @@ namespace AppBundle\Entity; use AppBundle\Action\MyTasks as MyTasksController; -use AppBundle\Action\MyTasks2 as MyTasksController2; use AppBundle\Action\TaskList\Create as CreateTaskListController; use AppBundle\Action\TaskList\Optimize as OptimizeController; use AppBundle\Action\TaskList\SetItems as SetTaskListItemsController; @@ -69,6 +68,7 @@ * "method"="GET", * "path"="/me/tasks/{date}", * "controller"=MyTasksController::class, + * "output"=MyTaskListDto::class, * "access_control"="is_granted('ROLE_ADMIN') or is_granted('ROLE_COURIER')", * "read"=false, * "write"=false, @@ -84,26 +84,6 @@ * }} * } * }, - * "my_tasks_v2"={ - * "method"="GET", - * "path"="/me/tasks/v2/{date}", - * "controller"=MyTasksController2::class, - * "access_control"="is_granted('ROLE_ADMIN') or is_granted('ROLE_COURIER')", - * "output"=MyTaskListDto::class, - * "read"=false, - * "write"=false, - * "normalization_context"={"groups"={"task_list", "task", "delivery", "address"}}, - * "openapi_context"={ - * "summary"="Retrieves the collection of Task resources assigned to the authenticated token.", - * "parameters"={{ - * "in"="path", - * "name"="date", - * "required"=true, - * "type"="string", - * "format"="date" - * }} - * } - * }, * "optimize"={ * "method"="GET", * "path"="/task_lists/{id}/optimize", diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index ce12a1b0aa..97f4572487 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -4,6 +4,7 @@ use AppBundle\Api\Dto\MyTaskListDto; use AppBundle\Api\Dto\MyTaskDto; +use AppBundle\Api\Dto\MyTaskMetadataDto; use AppBundle\Api\Dto\TaskPackageDto; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\EntityManagerInterface; @@ -142,10 +143,12 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $weight, $task->getHasIncidents(), $row['organizationName'], - $task->getMetadata()['delivery_position'] ?? null, //FIXME extract from the query - $row['orderNumber'] ?? null, - $task->getMetadata()['payment_method'] ?? null, //FIXME extract from the query - $row['orderTotal'] ?? null, + new MyTaskMetadataDto( + $task->getMetadata()['delivery_position'] ?? null, //FIXME extract from the query + $row['orderNumber'] ?? null, + $task->getMetadata()['payment_method'] ?? null, //FIXME extract from the query + $row['orderTotal'] ?? null, + ) ); return $taskDto; From 40ee2a27f07f967ee884bcc4e616f3d0adaa516e Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:30:12 -0700 Subject: [PATCH 18/34] added: has_loopeat_returns --- src/Api/Dto/MyTaskMetadataDto.php | 8 +++++++- src/Entity/TaskListRepository.php | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Api/Dto/MyTaskMetadataDto.php b/src/Api/Dto/MyTaskMetadataDto.php index 8bf6e0a2c7..ba2e1323f4 100644 --- a/src/Api/Dto/MyTaskMetadataDto.php +++ b/src/Api/Dto/MyTaskMetadataDto.php @@ -18,15 +18,21 @@ class MyTaskMetadataDto #[Groups(["task"])] public readonly ?int $order_total; + #[Groups(["task"])] + public readonly bool $has_loopeat_returns; + public function __construct( ?int $delivery_position, ?string $order_number, ?string $payment_method, - ?int $order_total) + ?int $order_total, + bool $has_loopeat_returns + ) { $this->delivery_position = $delivery_position; $this->order_number = $order_number; $this->payment_method = $payment_method; $this->order_total = $order_total; + $this->has_loopeat_returns = $has_loopeat_returns; } } diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 97f4572487..6981477fdd 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -60,6 +60,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa 'o.number AS orderNumber', 'o.total AS orderTotal', 'org.name AS organizationName', + 'loopeatDetails.returns AS loopeatReturns', // objects are listed below to force them being hydrated / pre-fetched by Doctrine // https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/dql-doctrine-query-language.html#result-format 'taskPackage', @@ -73,6 +74,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa ->leftJoin('t.packages', 'taskPackage') ->leftJoin('taskPackage.package', 'package') ->leftJoin('t.incidents', 'incidents') + ->leftJoin('o.loopeatDetails', 'loopeatDetails') ->where('t.id IN (:taskIds)') ->andWhere('t.status != :statusCancelled') ->setParameter('taskIds', $orderedTaskIds) // using IN might cause problems with large number of tasks @@ -148,6 +150,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $row['orderNumber'] ?? null, $task->getMetadata()['payment_method'] ?? null, //FIXME extract from the query $row['orderTotal'] ?? null, + $task->isDropoff() && $row['loopeatReturns'] && count($row['loopeatReturns']) > 0 ) ); From 814d8888c472416e31cb8e81c49c52e24ea2564d Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:02:07 -0700 Subject: [PATCH 19/34] re-added: isZeroWaste --- src/Api/Dto/MyTaskMetadataDto.php | 7 ++- src/Entity/TaskListRepository.php | 78 ++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/Api/Dto/MyTaskMetadataDto.php b/src/Api/Dto/MyTaskMetadataDto.php index ba2e1323f4..c1104d7583 100644 --- a/src/Api/Dto/MyTaskMetadataDto.php +++ b/src/Api/Dto/MyTaskMetadataDto.php @@ -21,12 +21,16 @@ class MyTaskMetadataDto #[Groups(["task"])] public readonly bool $has_loopeat_returns; + #[Groups(["task"])] + public readonly bool $zero_waste; + public function __construct( ?int $delivery_position, ?string $order_number, ?string $payment_method, ?int $order_total, - bool $has_loopeat_returns + bool $has_loopeat_returns, + bool $zero_waste ) { $this->delivery_position = $delivery_position; @@ -34,5 +38,6 @@ public function __construct( $this->payment_method = $payment_method; $this->order_total = $order_total; $this->has_loopeat_returns = $has_loopeat_returns; + $this->zero_waste = $zero_waste; } } diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 6981477fdd..30bee47a51 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -6,6 +6,7 @@ use AppBundle\Api\Dto\MyTaskDto; use AppBundle\Api\Dto\MyTaskMetadataDto; use AppBundle\Api\Dto\TaskPackageDto; +use AppBundle\Entity\Sylius\Order; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; @@ -49,6 +50,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa return null; } + $orderedTaskIds = array_map(function (Task $task) { return $task->getId(); }, $taskList->getTasks()); @@ -56,7 +58,8 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $tasksQueryResult = $this->entityManager->createQueryBuilder() ->select([ 't', - 'd.id AS deliveryId', + 'delivery.id AS deliveryId', + 'o.id AS orderId', 'o.number AS orderNumber', 'o.total AS orderTotal', 'org.name AS organizationName', @@ -65,23 +68,22 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa // https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/dql-doctrine-query-language.html#result-format 'taskPackage', 'package', - 'incidents', ]) ->from(Task::class, 't') - ->leftJoin('t.delivery', 'd') - ->leftJoin('d.order', 'o') + ->leftJoin('t.delivery', 'delivery') + ->leftJoin('delivery.order', 'o') ->leftJoin('t.organization', 'org') + ->leftJoin('o.loopeatDetails', 'loopeatDetails') ->leftJoin('t.packages', 'taskPackage') ->leftJoin('taskPackage.package', 'package') - ->leftJoin('t.incidents', 'incidents') - ->leftJoin('o.loopeatDetails', 'loopeatDetails') ->where('t.id IN (:taskIds)') ->andWhere('t.status != :statusCancelled') - ->setParameter('taskIds', $orderedTaskIds) // using IN might cause problems with large number of tasks + ->setParameter('taskIds', $orderedTaskIds) // using IN might cause problems with large number of items ->setParameter('statusCancelled', Task::STATUS_CANCELLED) ->getQuery() ->getResult(); + $tasksByDeliveryId = array_reduce($tasksQueryResult, function ($carry, $row) { $deliveryId = $row['deliveryId'] ?? null; @@ -94,9 +96,63 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa return $carry; }, []); - $tasks = array_map(function ($row) use ($tasksByDeliveryId) { + + $tasksWithIncidentsQueryResult = $this->entityManager->createQueryBuilder() + ->select([ + 't.id AS taskId', + 'COUNT(incidents.id) AS incidentCount', + ]) + ->from(Task::class, 't') + ->leftJoin('t.incidents', 'incidents') + ->where('t.id IN (:taskIds)') + ->setParameter('taskIds', $orderedTaskIds) // using IN might cause problems with large number of items + ->groupBy('t.id') + ->getQuery() + ->getResult(); + + $tasksWithIncidents = array_reduce($tasksWithIncidentsQueryResult, function ($carry, $row) { + $carry[$row['taskId']] = $row['incidentCount']; + return $carry; + }, []); + + + $orderIds = array_reduce($tasksQueryResult, function ($carry, $row) { + $orderId = $row['orderId'] ?? null; + + if (null === $orderId) { + return $carry; + } + + $carry[$orderId] = $orderId; // using an associative array to avoid duplicates + return $carry; + }, []); + + $zeroWasteOrdersQueryResult = $this->entityManager->createQueryBuilder() + ->select([ + 'o.id AS orderId', + 'COUNT(reusablePackaging.id) AS reusablePackagingCount', + ]) + ->from(Order::class, 'o') + ->leftJoin('o.items', 'orderItem') + ->leftJoin('orderItem.variant', 'productVariant') + ->leftJoin('productVariant.product', 'product') + ->leftJoin('product.reusablePackagings', 'reusablePackaging') + ->where('o.id IN (:orderIds)') + ->andWhere('product.reusablePackagingEnabled = TRUE') + ->setParameter('orderIds', $orderIds) // using IN might cause problems with large number of items + ->groupBy('o.id') + ->getQuery() + ->getResult(); + + $zeroWasteOrders = array_reduce($zeroWasteOrdersQueryResult, function ($carry, $row) { + $carry[$row['orderId']] = $row['reusablePackagingCount']; + return $carry; + }, []); + + $tasks = array_map(function ($row) use ($tasksByDeliveryId, $tasksWithIncidents, $zeroWasteOrders) { $task = $row[0]; $deliveryId = $row['deliveryId'] ?? null; + $orderId = $row['orderId'] ?? null; $taskPackages = []; $weight = null; @@ -119,7 +175,6 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $weight = $task->getWeight(); } - $task = $row[0]; $taskDto = new MyTaskDto( $task->getId(), $task->getCreatedAt(), @@ -143,14 +198,15 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $taskPackage->getQuantity()); }, $taskPackages), $weight, - $task->getHasIncidents(), + ($tasksWithIncidents[$task->getId()] ?? 0) > 0, $row['organizationName'], new MyTaskMetadataDto( $task->getMetadata()['delivery_position'] ?? null, //FIXME extract from the query $row['orderNumber'] ?? null, $task->getMetadata()['payment_method'] ?? null, //FIXME extract from the query $row['orderTotal'] ?? null, - $task->isDropoff() && $row['loopeatReturns'] && count($row['loopeatReturns']) > 0 + $task->isDropoff() && $row['loopeatReturns'] && count($row['loopeatReturns']) > 0, + $orderId && ($zeroWasteOrders[$orderId] ?? 0) > 0 ) ); From 5ba788d7ff8947c375595f5d31038a9618c6af2a Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:54:20 -0700 Subject: [PATCH 20/34] updated normalization logic --- app/config/services/serializer.yml | 10 +- src/Api/Dto/TaskPackageDto.php | 13 +- src/Serializer/MyTaskDtoNormalizer.php | 60 +++++----- src/Serializer/MyTaskListDtoNormalizer.php | 113 +++++------------- .../MyTaskMetadataDtoNormalizer.php | 42 +++++++ src/Serializer/TaskPackageDtoNormalizer.php | 42 +++++++ 6 files changed, 163 insertions(+), 117 deletions(-) create mode 100644 src/Serializer/MyTaskMetadataDtoNormalizer.php create mode 100644 src/Serializer/TaskPackageDtoNormalizer.php diff --git a/app/config/services/serializer.yml b/app/config/services/serializer.yml index ef63a67d14..a1d4cc2cd7 100644 --- a/app/config/services/serializer.yml +++ b/app/config/services/serializer.yml @@ -30,14 +30,16 @@ services: - "@api_platform.jsonld.normalizer.item" tags: [ { name: serializer.normalizer, priority: 128 } ] + AppBundle\Serializer\MyTaskMetadataDtoNormalizer: + tags: [ { name: serializer.normalizer, priority: 128 } ] + + AppBundle\Serializer\TaskPackageDtoNormalizer: + tags: [ { name: serializer.normalizer, priority: 128 } ] + AppBundle\Serializer\MyTaskDtoNormalizer: - arguments: - - "@api_platform.jsonld.normalizer.item" tags: [ { name: serializer.normalizer, priority: 128 } ] AppBundle\Serializer\MyTaskListDtoNormalizer: - arguments: - - "@api_platform.jsonld.normalizer.object" tags: [ { name: serializer.normalizer, priority: 128 } ] AppBundle\Serializer\TaskImageNormalizer: diff --git a/src/Api/Dto/TaskPackageDto.php b/src/Api/Dto/TaskPackageDto.php index 61494816c9..af4ece2274 100644 --- a/src/Api/Dto/TaskPackageDto.php +++ b/src/Api/Dto/TaskPackageDto.php @@ -7,13 +7,16 @@ class TaskPackageDto { #[Groups(["task"])] - public readonly string $shortCode; + public readonly string $short_code; #[Groups(["task"])] public readonly string $name; #[Groups(["task"])] - public readonly int $averageVolumeUnits; + public readonly string $type; + + #[Groups(["task"])] + public readonly int $volume_per_package; #[Groups(["task"])] public readonly int $quantity; @@ -24,9 +27,11 @@ public function __construct( int $averageVolumeUnits, int $quantity) { - $this->shortCode = $shortCode; + $this->short_code = $shortCode; $this->name = $name; - $this->averageVolumeUnits = $averageVolumeUnits; + //FIXME; why do we have name and type with the same value? + $this->type = $name; + $this->volume_per_package = $averageVolumeUnits; $this->quantity = $quantity; } } diff --git a/src/Serializer/MyTaskDtoNormalizer.php b/src/Serializer/MyTaskDtoNormalizer.php index d9c7483b6d..be08e51ad0 100644 --- a/src/Serializer/MyTaskDtoNormalizer.php +++ b/src/Serializer/MyTaskDtoNormalizer.php @@ -3,63 +3,67 @@ namespace AppBundle\Serializer; use ApiPlatform\Core\Api\IriConverterInterface; -use ApiPlatform\Core\JsonLd\Serializer\ItemNormalizer; use AppBundle\Api\Dto\MyTaskDto; +use AppBundle\Entity\Task; use AppBundle\Service\TagManager; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; -class MyTaskDtoNormalizer implements NormalizerInterface +class MyTaskDtoNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface { + use NormalizerAwareTrait; + + private const ALREADY_CALLED = 'MyTaskDtoNormalizer_ALREADY_CALLED'; + public function __construct( - private ItemNormalizer $normalizer, - private IriConverterInterface $iriConverter, - private TagManager $tagManager, + private readonly IriConverterInterface $iriConverter, + private readonly TagManager $tagManager, ) - {} + { + } public function normalize($object, $format = null, array $context = array()) { - $data = $this->normalizer->normalize($object, $format, $context); + $context[self::ALREADY_CALLED] = true; + $data = $this->normalizer->normalize($object, $format, $context); if (!is_array($data)) { - return $data; } + // override json-ld to match the existing API + $data['@context'] = '/api/contexts/Task'; + $data['@type'] = 'Task'; + $data['@id'] = "/api/tasks/" . $object->id; + // Make sure "comments" is a string if (array_key_exists('comments', $data) && null === $data['comments']) { $data['comments'] = ''; } - if (isset($data['tags']) && is_array($data['tags']) && count($data['tags']) > 0) { + if (isset($data['tags']) && count($data['tags']) > 0) { $data['tags'] = $this->tagManager->expand($data['tags']); } - $data['previous'] = null; - if ($object->hasPrevious()) { - $data['previous'] = $this->iriConverter->getIriFromItem($object->getPrevious()); + if ($object->previous) { + $data['previous'] = $this->iriConverter->getItemIriFromResourceClass(Task::class, ['id' => $object->previous]); } - $data['next'] = null; - if ($object->hasNext()) { - $data['next'] = $this->iriConverter->getIriFromItem($object->getNext()); + if ($object->next) { + $data['next'] = $this->iriConverter->getItemIriFromResourceClass(Task::class, ['id' => $object->next]); } - //TODO; fix me -// if (array_key_exists('metadata', $data) && is_array($data['metadata'])) { -// if ($order = $object->getDelivery()?->getOrder()) { -// $data['metadata'] = array_merge($data['metadata'], ['zero_waste' => $order->isZeroWaste()]); -// if ($object->isDropoff()) { -// $data['metadata'] = array_merge($data['metadata'], ['has_loopeat_returns' => $order->hasLoopeatReturns()]); -// } -// } -// } - return $data; } - public function supportsNormalization($data, $format = null) + public function supportsNormalization($data, ?string $format = null, array $context = []) { - return $this->normalizer->supportsNormalization($data, $format) && $data instanceof MyTaskDto; + // Make sure we're not called twice + if (isset($context[self::ALREADY_CALLED])) { + return false; + } + + return $data instanceof MyTaskDto; } } diff --git a/src/Serializer/MyTaskListDtoNormalizer.php b/src/Serializer/MyTaskListDtoNormalizer.php index 9e96c0e088..5f8116d0f3 100644 --- a/src/Serializer/MyTaskListDtoNormalizer.php +++ b/src/Serializer/MyTaskListDtoNormalizer.php @@ -2,104 +2,55 @@ namespace AppBundle\Serializer; -use ApiPlatform\Core\Api\IriConverterInterface; -use ApiPlatform\Core\JsonLd\Serializer\ObjectNormalizer; use AppBundle\Api\Dto\MyTaskListDto; -use AppBundle\Entity\Task; -use AppBundle\Entity\TaskList; -use AppBundle\Entity\Tour; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; -class MyTaskListDtoNormalizer implements NormalizerInterface +class MyTaskListDtoNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface { + use NormalizerAwareTrait; - public function __construct( - protected ObjectNormalizer $normalizer, - protected IriConverterInterface $iriConverterInterface - ) - { - $this->normalizer = $normalizer; - } - -// private function flattenItemsUris(array $items) -// { -// $itemsUris = []; -// foreach($items as $item) { -// if (isset($item['task'])) { -// array_push( -// $itemsUris, -// $item['task'] -// ); -// } else { -// array_push( -// $itemsUris, -// $item['tour']['@id'], // to the best of my knowledge, tour is eagerly fetch because we use the table inheritance for the tour table -// ); -// } -// } -// -// return $itemsUris; -// } + private const ALREADY_CALLED = 'MyTaskListDtoNormalizer_ALREADY_CALLED'; public function normalize($object, $format = null, array $context = array()) { - // legacy serialization for API endpoints that output TaskList.items as a list of tasks - // look for "setTempLegacyTaskStorage" usage in the code. - // known usage at the time of the writing : - // - used for the rider/dispatcher smartphone app (does not display or handle tours) - // - used for stores to access /api/task_lists/ and display only tasks linked to the org (it is only used by Tricargo coop and should be considered legacy) -// if ($object->isTempLegacyTaskStorage) { - $context[AbstractNormalizer::IGNORED_ATTRIBUTES] = ['items']; - $data = $this->normalizer->normalize($object, $format, $context); + $context[self::ALREADY_CALLED] = true; - // override json-ld to match the existing API - $data['@context'] = '/api/contexts/TaskList'; - $data['@type'] = 'TaskList'; - $data['@id'] = "/api/task_lists/" . $object->id; + $context[AbstractNormalizer::IGNORED_ATTRIBUTES] = ['date', 'items']; - $data['items'] = array_map(function($task) { - $taskData = $this->normalizer->normalize( - $task, - 'jsonld', - ['groups' => ["task_list", "task_collection", "task", "delivery", "address"]] - ); + $data = $this->normalizer->normalize($object, $format, $context); + if (!is_array($data)) { + return $data; + } - // override json-ld to match the existing API - $taskData['@context'] = '/api/contexts/Task'; - $taskData['@type'] = 'Task'; - $taskData['@id'] = "/api/tasks/" . $task->id; + // override json-ld to match the existing API + $data['@context'] = '/api/contexts/TaskList'; + $data['@type'] = 'TaskList'; + $data['@id'] = "/api/task_lists/" . $object->id; - return $taskData; - }, $object->items + $data['date'] = $object->date->format('Y-m-d'); + + $data['items'] = array_map(function ($task) use ($format) { + return $this->normalizer->normalize( + $task, + $format, + ['groups' => ["task_list", "task_collection", "task", "delivery", "address"]] ); -// } -// // legacy serialization for app and events -// // see https://github.com/coopcycle/coopcycle-app/issues/1803 -// else if (in_array('task', $context['groups'])) { -// $context[AbstractNormalizer::IGNORED_ATTRIBUTES] = ['items']; -// $data = $this->normalizer->normalize($object, $format, $context); -// $data['items'] = array_map(function($task) { -// return $this->normalizer->normalize( -// $task, -// 'jsonld', -// ['groups' => ["task_list", "task_collection", "task", "delivery", "address"]] -// ); -// }, $object->getTasks() -// ); -// } else { -// $data = $this->normalizer->normalize($object, $format, $context); -// -// if (isset($data['items'])) { -// $data['items'] = $this->flattenItemsUris($data['items']); -// } -// } + }, $object->items + ); return $data; } - public function supportsNormalization($data, $format = null) + public function supportsNormalization($data, ?string $format = null, array $context = []) { - return $this->normalizer->supportsNormalization($data, $format) && $data instanceof MyTaskListDto; + // Make sure we're not called twice + if (isset($context[self::ALREADY_CALLED])) { + return false; + } + + return $data instanceof MyTaskListDto; } } diff --git a/src/Serializer/MyTaskMetadataDtoNormalizer.php b/src/Serializer/MyTaskMetadataDtoNormalizer.php new file mode 100644 index 0000000000..e0ac77d62d --- /dev/null +++ b/src/Serializer/MyTaskMetadataDtoNormalizer.php @@ -0,0 +1,42 @@ +normalizer->normalize($object, $format, $context); + if (!is_array($data)) { + return $data; + } + + // override json-ld to match the existing API + unset($data['@context']); + unset($data['@type']); + unset($data['@id']); + + return $data; + } + + public function supportsNormalization($data, ?string $format = null, array $context = []) + { + // Make sure we're not called twice + if (isset($context[self::ALREADY_CALLED])) { + return false; + } + + return $data instanceof MyTaskMetadataDto; + } +} diff --git a/src/Serializer/TaskPackageDtoNormalizer.php b/src/Serializer/TaskPackageDtoNormalizer.php new file mode 100644 index 0000000000..301ffc4927 --- /dev/null +++ b/src/Serializer/TaskPackageDtoNormalizer.php @@ -0,0 +1,42 @@ +normalizer->normalize($object, $format, $context); + if (!is_array($data)) { + return $data; + } + + // override json-ld to match the existing API + unset($data['@context']); + unset($data['@type']); + unset($data['@id']); + + return $data; + } + + public function supportsNormalization($data, ?string $format = null, array $context = []) + { + // Make sure we're not called twice + if (isset($context[self::ALREADY_CALLED])) { + return false; + } + + return $data instanceof TaskPackageDto; + } +} From dda36602ffa24c611de1a5cd04154d435521be27 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:26:47 -0700 Subject: [PATCH 21/34] skip optional has_loopeat_returns field --- src/Api/Dto/MyTaskMetadataDto.php | 4 ++-- src/Entity/TaskListRepository.php | 11 ++++++++++- src/Serializer/MyTaskMetadataDtoNormalizer.php | 4 ++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Api/Dto/MyTaskMetadataDto.php b/src/Api/Dto/MyTaskMetadataDto.php index c1104d7583..afba66898f 100644 --- a/src/Api/Dto/MyTaskMetadataDto.php +++ b/src/Api/Dto/MyTaskMetadataDto.php @@ -19,7 +19,7 @@ class MyTaskMetadataDto public readonly ?int $order_total; #[Groups(["task"])] - public readonly bool $has_loopeat_returns; + public readonly ?bool $has_loopeat_returns; #[Groups(["task"])] public readonly bool $zero_waste; @@ -29,7 +29,7 @@ public function __construct( ?string $order_number, ?string $payment_method, ?int $order_total, - bool $has_loopeat_returns, + ?bool $has_loopeat_returns, bool $zero_waste ) { diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 30bee47a51..df68a6b4db 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -24,6 +24,15 @@ public function __construct( public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTaskListDto { + + /** + * IMPORTANT: The queries below are optimized for list operations. + * So that the number of queries is constant and does not depend on the number of tasks. + * Be careful when adding/modifying them + * (check Symfony Profiler/Doctine, the number of "Database Queries" + * should be equal to the number of "Different statements"). + */ + $taskListQueryResult = $this->entityManager->createQueryBuilder() ->select([ 'tl', @@ -205,7 +214,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $row['orderNumber'] ?? null, $task->getMetadata()['payment_method'] ?? null, //FIXME extract from the query $row['orderTotal'] ?? null, - $task->isDropoff() && $row['loopeatReturns'] && count($row['loopeatReturns']) > 0, + $task->isDropoff() ? ($row['loopeatReturns'] && count($row['loopeatReturns']) > 0) : null, $orderId && ($zeroWasteOrders[$orderId] ?? 0) > 0 ) ); diff --git a/src/Serializer/MyTaskMetadataDtoNormalizer.php b/src/Serializer/MyTaskMetadataDtoNormalizer.php index e0ac77d62d..89a34616b5 100644 --- a/src/Serializer/MyTaskMetadataDtoNormalizer.php +++ b/src/Serializer/MyTaskMetadataDtoNormalizer.php @@ -27,6 +27,10 @@ public function normalize($object, $format = null, array $context = array()) unset($data['@type']); unset($data['@id']); + if (null === $data['has_loopeat_returns']) { + unset($data['has_loopeat_returns']); + } + return $data; } From f71a563cae7554b3494e2602b2221f86b72a5c25 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:28:25 -0700 Subject: [PATCH 22/34] removed type --- src/Action/MyTasks.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Action/MyTasks.php b/src/Action/MyTasks.php index 5f75ac260d..c8147a4bec 100644 --- a/src/Action/MyTasks.php +++ b/src/Action/MyTasks.php @@ -15,9 +15,6 @@ final class MyTasks { use TokenStorageTrait; - /** - * @var TaskListRepository - */ private readonly EntityRepository $taskListRepository; public function __construct( From 1985ac4a2b1e24cbdaba81bbff7b54b54a1c85db Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:35:39 -0700 Subject: [PATCH 23/34] fix: phpStan error --- src/Action/MyTasks.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Action/MyTasks.php b/src/Action/MyTasks.php index c8147a4bec..cf7e3686e4 100644 --- a/src/Action/MyTasks.php +++ b/src/Action/MyTasks.php @@ -7,7 +7,6 @@ use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use AppBundle\Action\Utils\TokenStorageTrait; use AppBundle\Entity\TaskList; -use Doctrine\ORM\EntityRepository; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -15,14 +14,13 @@ final class MyTasks { use TokenStorageTrait; - private readonly EntityRepository $taskListRepository; - public function __construct( TokenStorageInterface $tokenStorage, - private readonly EntityManagerInterface $entityManager) + private readonly EntityManagerInterface $entityManager, + private readonly TaskListRepository $taskListRepository + ) { $this->tokenStorage = $tokenStorage; - $this->taskListRepository = $this->entityManager->getRepository(TaskList::class); } public function __invoke(Request $request) From 4b4828227793cf7aad9a967344ff933db86548ac Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:50:47 -0700 Subject: [PATCH 24/34] re-formatting --- src/Entity/TaskListRepository.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index df68a6b4db..78133c72f6 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -158,6 +158,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa return $carry; }, []); + $tasks = array_map(function ($row) use ($tasksByDeliveryId, $tasksWithIncidents, $zeroWasteOrders) { $task = $row[0]; $deliveryId = $row['deliveryId'] ?? null; @@ -222,6 +223,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa return $taskDto; }, $tasksQueryResult); + $tasksById = array_reduce($tasks, function ($carry, $task) { $carry[$task->id] = $task; return $carry; @@ -237,6 +239,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa } } + $taskListDto = new MyTaskListDto( $taskList->getId(), $taskList->getCreatedAt(), From 3ebd5bc8c198d492a084a3b871aa8bb01c5ee2cd Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:10:10 -0700 Subject: [PATCH 25/34] updated: get payment method from the database --- src/Entity/TaskListRepository.php | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 78133c72f6..30d96035d1 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -101,7 +101,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa } $task = $row[0]; - $carry[$deliveryId][] = $task; + $carry[$deliveryId][] = $task; // append to an array return $carry; }, []); @@ -136,6 +136,26 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa return $carry; }, []); + + $paymentMethodsQueryResult = $this->entityManager->createQueryBuilder() + ->select([ + 'o.id AS orderId', + 'paymentMethod.code AS paymentMethodCode', + ]) + ->from(Order::class, 'o') + ->leftJoin('o.payments', 'payment') + ->leftJoin('payment.method', 'paymentMethod') + ->where('o.id IN (:orderIds)') + ->setParameter('orderIds', $orderIds) // using IN might cause problems with large number of items + ->getQuery() + ->getResult(); + + $paymentMethodsByOrderId = array_reduce($paymentMethodsQueryResult, function ($carry, $row) { + $carry[$row['orderId']][] = $row['paymentMethodCode']; // append to an array + return $carry; + }, []); + + $zeroWasteOrdersQueryResult = $this->entityManager->createQueryBuilder() ->select([ 'o.id AS orderId', @@ -159,7 +179,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa }, []); - $tasks = array_map(function ($row) use ($tasksByDeliveryId, $tasksWithIncidents, $zeroWasteOrders) { + $tasks = array_map(function ($row) use ($tasksByDeliveryId, $tasksWithIncidents, $paymentMethodsByOrderId, $zeroWasteOrders) { $task = $row[0]; $deliveryId = $row['deliveryId'] ?? null; $orderId = $row['orderId'] ?? null; @@ -213,7 +233,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa new MyTaskMetadataDto( $task->getMetadata()['delivery_position'] ?? null, //FIXME extract from the query $row['orderNumber'] ?? null, - $task->getMetadata()['payment_method'] ?? null, //FIXME extract from the query + $paymentMethodsByOrderId[$orderId] ? $paymentMethodsByOrderId[$orderId][0] : null, //FIXME what payment method to show if there are multiple? $row['orderTotal'] ?? null, $task->isDropoff() ? ($row['loopeatReturns'] && count($row['loopeatReturns']) > 0) : null, $orderId && ($zeroWasteOrders[$orderId] ?? 0) > 0 From c0e79e630485c6f94884464667bed2cf0c08b287 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:46:09 -0700 Subject: [PATCH 26/34] fix: create a task list if it does not exist yet --- src/Action/MyTasks.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Action/MyTasks.php b/src/Action/MyTasks.php index cf7e3686e4..6f642aaa02 100644 --- a/src/Action/MyTasks.php +++ b/src/Action/MyTasks.php @@ -43,8 +43,9 @@ public function __invoke(Request $request) // If 2 requests are received at the very same time, // we can have a race condition // @see https://github.com/coopcycle/coopcycle-app/issues/1265 - $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); } + + $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); } return $taskListDto; From 1b86b276f6ed5d4d15efa456950fe252632d3012 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:08:49 -0700 Subject: [PATCH 27/34] fixed: task mapping --- src/Entity/TaskListRepository.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 30d96035d1..f19e3fb136 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -192,13 +192,13 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa if ($task->isPickup()) { // for a pickup in a delivery, the serialized weight is the sum of the dropoff weight and // the packages are the "sum" of the dropoffs packages - foreach ($tasksInTheSameDelivery as $task) { - if ($task->isPickup()) { + foreach ($tasksInTheSameDelivery as $t) { + if ($t->isPickup()) { continue; } - $taskPackages = array_merge($taskPackages, $task->getPackages()->toArray()); - $weight += $task->getWeight(); + $taskPackages = array_merge($taskPackages, $t->getPackages()->toArray()); + $weight += $t->getWeight(); } } else { $taskPackages = $task->getPackages()->toArray(); @@ -244,18 +244,19 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa }, $tasksQueryResult); - $tasksById = array_reduce($tasks, function ($carry, $task) { + $taskDtosById = array_reduce($tasks, function ($carry, $task) { $carry[$task->id] = $task; return $carry; }, []); + //restore order of tasks $orderedTasks = []; foreach ($orderedTaskIds as $taskId) { // skip tasks that are not returned by the query // that can happen if a task is cancelled, for example - if (isset($tasksById[$taskId])) { - $orderedTasks[] = $tasksById[$taskId]; + if (isset($taskDtosById[$taskId])) { + $orderedTasks[] = $taskDtosById[$taskId]; } } From 582833850742fc4d9f861e2be3935f4e35f3ff8e Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:18:31 -0700 Subject: [PATCH 28/34] publish MyTaskListDto in the Centrifugo event --- .../EventSubscriber/TaskListSubscriber.php | 30 +++++++------------ src/Domain/Task/Event/TaskListUpdated.php | 19 +++++++----- src/Domain/Task/Reactor/PublishLiveUpdate.php | 2 +- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/Doctrine/EventSubscriber/TaskListSubscriber.php b/src/Doctrine/EventSubscriber/TaskListSubscriber.php index 8c4906a375..427142260a 100644 --- a/src/Doctrine/EventSubscriber/TaskListSubscriber.php +++ b/src/Doctrine/EventSubscriber/TaskListSubscriber.php @@ -2,14 +2,11 @@ namespace AppBundle\Doctrine\EventSubscriber; -use AppBundle\Entity\Delivery; -use AppBundle\Entity\Task\CollectionInterface as TaskCollectionInterface; -use AppBundle\Entity\TaskCollection; -use AppBundle\Entity\TaskCollectionItem; use AppBundle\Entity\TaskList; use AppBundle\Domain\Task\Event\TaskListUpdated; use AppBundle\Domain\Task\Event\TaskListUpdatedv2; use AppBundle\Entity\TaskList\Item; +use AppBundle\Entity\TaskListRepository; use AppBundle\Message\PushNotification; use AppBundle\Service\RemotePushNotificationManager; use AppBundle\Service\RoutingInterface; @@ -27,25 +24,17 @@ class TaskListSubscriber implements EventSubscriber { - private $eventBus; - private $messageBus; - private $translator; - private $routing; - private $logger; private $taskLists = []; public function __construct( - MessageBus $eventBus, - MessageBusInterface $messageBus, - TranslatorInterface $translator, - RoutingInterface $routing, - LoggerInterface $logger) + private readonly MessageBus $eventBus, + private readonly MessageBusInterface $messageBus, + private readonly TranslatorInterface $translator, + private readonly RoutingInterface $routing, + private readonly TaskListRepository $taskListRepository, + private readonly LoggerInterface $logger + ) { - $this->eventBus = $eventBus; - $this->messageBus = $messageBus; - $this->translator = $translator; - $this->routing = $routing; - $this->logger = $logger; } public function getSubscribedEvents() @@ -151,7 +140,8 @@ public function postFlush(PostFlushEventArgs $args) // legacy event and new version of event // see https://github.com/coopcycle/coopcycle-app/issues/1803 - $this->eventBus->handle(new TaskListUpdated($taskList)); + $myTaskListDto = $this->taskListRepository->findMyTaskListAsDto($taskList->getCourier(), $taskList->getDate()); + $this->eventBus->handle(new TaskListUpdated($taskList->getCourier(), $myTaskListDto)); $this->eventBus->handle(new TaskListUpdatedv2($taskList)); $date = $taskList->getDate(); diff --git a/src/Domain/Task/Event/TaskListUpdated.php b/src/Domain/Task/Event/TaskListUpdated.php index 87447c2852..66003749d1 100644 --- a/src/Domain/Task/Event/TaskListUpdated.php +++ b/src/Domain/Task/Event/TaskListUpdated.php @@ -2,23 +2,28 @@ namespace AppBundle\Domain\Task\Event; +use AppBundle\Api\Dto\MyTaskListDto; use AppBundle\Domain\Event as BaseEvent; use AppBundle\Domain\SerializableEventInterface; -use AppBundle\Entity\TaskList; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Contracts\Translation\TranslatorInterface; class TaskListUpdated extends BaseEvent implements SerializableEventInterface { - protected $collection; - public function __construct(TaskList $collection) + public function __construct( + private readonly UserInterface $courier, + private readonly MyTaskListDto $collection + ) { - $this->collection = $collection; } - public function getTaskList(): TaskList + public function getCourier(): UserInterface + { + return $this->courier; + } + + public function getTaskList(): MyTaskListDto { return $this->collection; } @@ -26,7 +31,7 @@ public function getTaskList(): TaskList public function normalize(NormalizerInterface $serializer) { $normalized = $serializer->normalize($this->collection, 'jsonld', [ - 'resource_class' => TaskList::class, + 'resource_class' => MyTaskListDto::class, 'operation_type' => 'item', 'item_operation_name' => 'get', 'groups' => ['task_list', "task_collection", "task", "delivery", "address"] diff --git a/src/Domain/Task/Reactor/PublishLiveUpdate.php b/src/Domain/Task/Reactor/PublishLiveUpdate.php index d1ff07b482..cbceba9c7c 100644 --- a/src/Domain/Task/Reactor/PublishLiveUpdate.php +++ b/src/Domain/Task/Reactor/PublishLiveUpdate.php @@ -25,7 +25,7 @@ public function __invoke(Event $event) // legacy event and new version of event // see https://github.com/coopcycle/coopcycle-app/issues/1803 if ($event instanceof TaskListUpdated) { - $user = $event->getTaskList()->getCourier(); + $user = $event->getCourier(); $this->liveUpdates->toUsers([ $user ], $event); } else if ($event instanceof TaskListUpdatedv2) { // can be safely broadcasted both to riders and admins $this->liveUpdates->toAdmins($event); From 13d7fb1c3c92ce0fc3b205fe9c2e787100d62787 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:45:03 -0700 Subject: [PATCH 29/34] fixed: make orgName optional --- src/Api/Dto/MyTaskDto.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Api/Dto/MyTaskDto.php b/src/Api/Dto/MyTaskDto.php index e2e1452603..b60044599c 100644 --- a/src/Api/Dto/MyTaskDto.php +++ b/src/Api/Dto/MyTaskDto.php @@ -18,7 +18,7 @@ final class MyTaskDto public readonly DateTime $updatedAt; #[Groups(["task"])] - public readonly string $orgName; + public readonly ?string $orgName; #[Groups(["task"])] public readonly string $type; @@ -92,7 +92,7 @@ public function __construct( array $packages, ?int $weight, bool $hasIncidents, - string $orgName, + ?string $orgName, MyTaskMetadataDto $metadata ) { From 5aa20357166779c796c50f5338e48a4bc56b1140 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:45:37 -0700 Subject: [PATCH 30/34] extracted getPaymentMethod function --- src/Entity/TaskListRepository.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index f19e3fb136..377c883480 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -233,7 +233,7 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa new MyTaskMetadataDto( $task->getMetadata()['delivery_position'] ?? null, //FIXME extract from the query $row['orderNumber'] ?? null, - $paymentMethodsByOrderId[$orderId] ? $paymentMethodsByOrderId[$orderId][0] : null, //FIXME what payment method to show if there are multiple? + $this->getPaymentMethod($paymentMethodsByOrderId, $orderId), $row['orderTotal'] ?? null, $task->isDropoff() ? ($row['loopeatReturns'] && count($row['loopeatReturns']) > 0) : null, $orderId && ($zeroWasteOrders[$orderId] ?? 0) > 0 @@ -276,4 +276,20 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa ); return $taskListDto; } + + private function getPaymentMethod($paymentMethodsByOrderId, ?int $orderId): ?string + { + if (null === $orderId) { + return null; + } + + $paymentMethods = $paymentMethodsByOrderId[$orderId] ?? null; + + if (null === $paymentMethods || count($paymentMethods) === 0) { + return null; + } + + //FIXME what payment method to show if there are multiple? + return $paymentMethods[0]; + } } From 04b5c8f2dfe8cac16938b7f5eb586f2501513ea5 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:49:54 -0700 Subject: [PATCH 31/34] fixed: output format --- features/tasks.feature | 98 +------------------ src/Api/Dto/MyTaskDto.php | 6 +- src/Api/Dto/MyTaskMetadataDto.php | 4 +- src/Entity/TaskListRepository.php | 26 ++++- src/Serializer/MyTaskDtoNormalizer.php | 5 + .../MyTaskMetadataDtoNormalizer.php | 20 +++- 6 files changed, 55 insertions(+), 104 deletions(-) diff --git a/features/tasks.feature b/features/tasks.feature index 88e2433682..19ee84bc2d 100644 --- a/features/tasks.feature +++ b/features/tasks.feature @@ -204,82 +204,6 @@ Feature: Tasks "@id":"@string@.startsWith('/api/task_lists/')", "id": "@integer@", "@type":"TaskList", - "hydra:member":[ - { - "@id":"@string@.startsWith('/api/tasks')", - "@context": "/api/contexts/Task", - "@type":"Task", - "id":@integer@, - "type":"DROPOFF", - "status":"TODO", - "address":{ - "streetAddress": "@string@", - "@type":"http://schema.org/Place", - "geo":{ - "@type":"GeoCoordinates", - "latitude":48.846656, - "longitude":2.369052 - }, - "@*@":"@*@" - }, - "after":"@string@.isDateTime().startsWith('2018-03-02T11:30:00')", - "before":"@string@.isDateTime().startsWith('2018-03-02T12:00:00')", - "doneAfter":"@string@.isDateTime().startsWith('2018-03-02T11:30:00')", - "doneBefore":"@string@.isDateTime().startsWith('2018-03-02T12:00:00')", - "comments":"#bob", - "updatedAt":"@string@.isDateTime()", - "isAssigned":true, - "assignedTo":"bob", - "previous":null, - "group":{"@*@":"@*@"}, - "tags":@array@, - "doorstep":@boolean@, - "ref":null, - "recurrenceRule":null, - "metadata":[], - "weight":null, - "hasIncidents": false, - "incidents": [], - "orgName":"", - "images":[], - "next":null, - "packages":[], - "createdAt":"@string@.isDateTime()" - }, - { - "@id":"@string@.startsWith('/api/tasks')", - "@context": "/api/contexts/Task", - "@type":"Task", - "id":@integer@, - "type":"DROPOFF", - "status":"DONE", - "address":{"@*@":"@*@"}, - "after":"@string@.isDateTime().startsWith('2018-03-02T12:00:00')", - "before":"@string@.isDateTime().startsWith('2018-03-02T12:30:00')", - "doneAfter":"@string@.isDateTime().startsWith('2018-03-02T12:00:00')", - "doneBefore":"@string@.isDateTime().startsWith('2018-03-02T12:30:00')", - "comments":"#bob", - "updatedAt":"@string@.isDateTime()", - "isAssigned":true, - "assignedTo":"bob", - "previous":null, - "group":null, - "tags":@array@, - "doorstep":@boolean@, - "ref":null, - "recurrenceRule":null, - "metadata":[], - "weight":null, - "hasIncidents": false, - "incidents": [], - "orgName":"", - "images":[], - "next":null, - "packages":[], - "createdAt":"@string@.isDateTime()" - } - ], - "hydra:totalItems":2, "items":[ { "@id":"@string@.startsWith('/api/tasks')", @@ -295,20 +219,14 @@ Feature: Tasks "doneBefore":"@string@.isDateTime().startsWith('2018-03-02T12:00:00')", "comments":"#bob", "updatedAt":"@string@.isDateTime()", - "isAssigned":true, - "assignedTo":"bob", "previous":null, - "group":{"@*@":"@*@"}, "tags":@array@, "doorstep":@boolean@, - "ref":null, - "recurrenceRule":null, - "metadata":[], + "metadata": { + }, "weight":null, "hasIncidents": false, - "incidents": [], "orgName":"", - "images":[], "next":null, "packages":[], "createdAt":"@string@.isDateTime()" @@ -327,20 +245,14 @@ Feature: Tasks "doneBefore":"@string@.isDateTime().startsWith('2018-03-02T12:30:00')", "comments":"#bob", "updatedAt":"@string@.isDateTime()", - "isAssigned":true, - "assignedTo":"bob", "previous":null, - "group":null, "tags":@array@, "doorstep":@boolean@, - "ref":null, - "recurrenceRule":null, - "metadata":[], + "metadata": { + }, "weight":null, "hasIncidents": false, - "incidents": [], "orgName":"", - "images":[], "next":null, "packages":[], "createdAt":"@string@.isDateTime()" @@ -380,8 +292,6 @@ Feature: Tasks "@id":"@string@.startsWith('/api/task_lists')", "id": "@integer@", "@type":"TaskList", - "hydra:member":[], - "hydra:totalItems":0, "items":[], "distance":@integer@, "duration":@integer@, diff --git a/src/Api/Dto/MyTaskDto.php b/src/Api/Dto/MyTaskDto.php index b60044599c..55e0000285 100644 --- a/src/Api/Dto/MyTaskDto.php +++ b/src/Api/Dto/MyTaskDto.php @@ -60,7 +60,7 @@ final class MyTaskDto public readonly bool $doorstep; #[Groups(["task"])] - public readonly ?string $comment; + public readonly ?string $comments; #[Groups(["task"])] public readonly array $packages; @@ -88,7 +88,7 @@ public function __construct( ?int $next, array $tags, bool $doorstep, - ?string $comment, + ?string $comments, array $packages, ?int $weight, bool $hasIncidents, @@ -110,7 +110,7 @@ public function __construct( $this->next = $next; $this->tags = $tags; $this->doorstep = $doorstep; - $this->comment = $comment; + $this->comments = $comments; $this->packages = $packages; $this->weight = $weight; $this->hasIncidents = $hasIncidents; diff --git a/src/Api/Dto/MyTaskMetadataDto.php b/src/Api/Dto/MyTaskMetadataDto.php index afba66898f..709c9bdc84 100644 --- a/src/Api/Dto/MyTaskMetadataDto.php +++ b/src/Api/Dto/MyTaskMetadataDto.php @@ -22,7 +22,7 @@ class MyTaskMetadataDto public readonly ?bool $has_loopeat_returns; #[Groups(["task"])] - public readonly bool $zero_waste; + public readonly ?bool $zero_waste; public function __construct( ?int $delivery_position, @@ -30,7 +30,7 @@ public function __construct( ?string $payment_method, ?int $order_total, ?bool $has_loopeat_returns, - bool $zero_waste + ?bool $zero_waste ) { $this->delivery_position = $delivery_position; diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 377c883480..4e7fe4fbe6 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -235,8 +235,8 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $row['orderNumber'] ?? null, $this->getPaymentMethod($paymentMethodsByOrderId, $orderId), $row['orderTotal'] ?? null, - $task->isDropoff() ? ($row['loopeatReturns'] && count($row['loopeatReturns']) > 0) : null, - $orderId && ($zeroWasteOrders[$orderId] ?? 0) > 0 + $this->getLoopeatReturns($orderId, $task, $row), + $this->getIsZeroWaste($orderId, $zeroWasteOrders) ) ); @@ -292,4 +292,26 @@ private function getPaymentMethod($paymentMethodsByOrderId, ?int $orderId): ?str //FIXME what payment method to show if there are multiple? return $paymentMethods[0]; } + + private function getLoopeatReturns(?int $orderId, $task, $row): ?bool + { + if (null === $orderId) { + return null; + } + + if (!$task->isDropoff()) { + return false; + } + + return $row['loopeatReturns'] && count($row['loopeatReturns']) > 0; + } + + private function getIsZeroWaste(?int $orderId, $zeroWasteOrders): ?bool + { + if (null === $orderId) { + return null; + } + + return ($zeroWasteOrders[$orderId] ?? 0) > 0; + } } diff --git a/src/Serializer/MyTaskDtoNormalizer.php b/src/Serializer/MyTaskDtoNormalizer.php index be08e51ad0..a14751a7a7 100644 --- a/src/Serializer/MyTaskDtoNormalizer.php +++ b/src/Serializer/MyTaskDtoNormalizer.php @@ -54,6 +54,11 @@ public function normalize($object, $format = null, array $context = array()) $data['next'] = $this->iriConverter->getItemIriFromResourceClass(Task::class, ['id' => $object->next]); } + // Make sure "orgName" is a string + if (array_key_exists('orgName', $data) && null === $data['orgName']) { + $data['orgName'] = ''; + } + return $data; } diff --git a/src/Serializer/MyTaskMetadataDtoNormalizer.php b/src/Serializer/MyTaskMetadataDtoNormalizer.php index 89a34616b5..41e9de2245 100644 --- a/src/Serializer/MyTaskMetadataDtoNormalizer.php +++ b/src/Serializer/MyTaskMetadataDtoNormalizer.php @@ -27,13 +27,27 @@ public function normalize($object, $format = null, array $context = array()) unset($data['@type']); unset($data['@id']); - if (null === $data['has_loopeat_returns']) { - unset($data['has_loopeat_returns']); - } + $this->unsetIfNull($data, [ + 'delivery_position', + 'order_number', + 'payment_method', + 'order_total', + 'has_loopeat_returns', + 'zero_waste' + ]); return $data; } + private function unsetIfNull(&$data, $fields): void + { + foreach ($fields as $field) { + if (null === $data[$field]) { + unset($data[$field]); + } + } + } + public function supportsNormalization($data, ?string $format = null, array $context = []) { // Make sure we're not called twice From 002fb55d18292dcbaea80ce93f8901fdf5af4640 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 18 Oct 2024 14:37:51 -0700 Subject: [PATCH 32/34] remove: unused fields --- features/tasks.feature | 8 ++------ src/Api/Dto/MyTaskListDto.php | 17 ++--------------- src/Entity/TaskListRepository.php | 2 -- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/features/tasks.feature b/features/tasks.feature index 19ee84bc2d..2523310216 100644 --- a/features/tasks.feature +++ b/features/tasks.feature @@ -264,9 +264,7 @@ Feature: Tasks "date":"2018-03-02", "username":"bob", "createdAt":"@string@.isDateTime()", - "updatedAt":"@string@.isDateTime()", - "vehicle": null, - "trailer": null + "updatedAt":"@string@.isDateTime()" } """ @@ -299,9 +297,7 @@ Feature: Tasks "date":"2020-03-02", "username":"bob", "createdAt":"@string@.isDateTime()", - "updatedAt":"@string@.isDateTime()", - "vehicle": null, - "trailer": null + "updatedAt":"@string@.isDateTime()" } """ diff --git a/src/Api/Dto/MyTaskListDto.php b/src/Api/Dto/MyTaskListDto.php index 43d11c30d7..6421d9494d 100644 --- a/src/Api/Dto/MyTaskListDto.php +++ b/src/Api/Dto/MyTaskListDto.php @@ -39,14 +39,6 @@ class MyTaskListDto #[Groups(["task_list"])] public readonly string $polyline; - //TODO - #[Groups(["task_list"])] - public readonly ?Vehicle $vehicle; - - //TODO - #[Groups(["task_list"])] - public readonly ?Trailer $trailer; - /** * @param int $id * @param DateTime $createdAt @@ -57,8 +49,6 @@ class MyTaskListDto * @param int $distance * @param int $duration * @param string $polyline - * @param Vehicle|null $vehicle - * @param Trailer|null $trailer */ public function __construct( int $id, @@ -69,9 +59,8 @@ public function __construct( array $items, int $distance, int $duration, - string $polyline, - ?Vehicle $vehicle, - ?Trailer $trailer) + string $polyline + ) { $this->id = $id; $this->createdAt = $createdAt; @@ -82,7 +71,5 @@ public function __construct( $this->distance = $distance; $this->duration = $duration; $this->polyline = $polyline; - $this->vehicle = $vehicle; - $this->trailer = $trailer; } } diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php index 4e7fe4fbe6..a9b16a70c7 100644 --- a/src/Entity/TaskListRepository.php +++ b/src/Entity/TaskListRepository.php @@ -271,8 +271,6 @@ public function findMyTaskListAsDto(UserInterface $user, \DateTime $date): ?MyTa $taskList->getDistance(), $taskList->getDuration(), $taskList->getPolyline(), - $taskList->getVehicle(), - $taskList->getTrailer(), ); return $taskListDto; } From a936c5e3f5dd8facc5c8a4c7db344c2825c93382 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 18 Oct 2024 14:49:52 -0700 Subject: [PATCH 33/34] removed: duplicate code --- src/Api/Dto/MyTaskListDto.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Api/Dto/MyTaskListDto.php b/src/Api/Dto/MyTaskListDto.php index 6421d9494d..7b5acaf6ab 100644 --- a/src/Api/Dto/MyTaskListDto.php +++ b/src/Api/Dto/MyTaskListDto.php @@ -38,18 +38,7 @@ class MyTaskListDto #[Groups(["task_list"])] public readonly string $polyline; - - /** - * @param int $id - * @param DateTime $createdAt - * @param DateTime $updatedAt - * @param DateTime $date - * @param string $username - * @param MyTaskDto[] $items - * @param int $distance - * @param int $duration - * @param string $polyline - */ + public function __construct( int $id, DateTime $createdAt, From 07fae57146a854d82425413c984498011e9a902f Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:12:42 -0700 Subject: [PATCH 34/34] move metadata generation into TaskListRepository --- src/Serializer/TaskNormalizer.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Serializer/TaskNormalizer.php b/src/Serializer/TaskNormalizer.php index 8912f56ed2..762b255f64 100644 --- a/src/Serializer/TaskNormalizer.php +++ b/src/Serializer/TaskNormalizer.php @@ -124,15 +124,6 @@ public function normalize($object, $format = null, array $context = array()) ->getResult(); } - if (array_key_exists('metadata', $data) && is_array($data['metadata'])) { - if ($order = $object->getDelivery()?->getOrder()) { - $data['metadata'] = array_merge($data['metadata'], ['zero_waste' => $order->isZeroWaste()]); - if ($object->isDropoff()) { - $data['metadata'] = array_merge($data['metadata'], ['has_loopeat_returns' => $order->hasLoopeatReturns()]); - } - } - } - return $data; }