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 diff --git a/app/config/services.yml b/app/config/services.yml index 393abe5e0e..8e783c359a 100755 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -626,6 +626,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..a1d4cc2cd7 100644 --- a/app/config/services/serializer.yml +++ b/app/config/services/serializer.yml @@ -30,6 +30,18 @@ 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: + tags: [ { name: serializer.normalizer, priority: 128 } ] + + AppBundle\Serializer\MyTaskListDtoNormalizer: + tags: [ { name: serializer.normalizer, priority: 128 } ] + AppBundle\Serializer\TaskImageNormalizer: arguments: $normalizer: '@api_platform.jsonld.normalizer.item' diff --git a/features/tasks.feature b/features/tasks.feature index 88e2433682..2523310216 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()" @@ -352,9 +264,7 @@ Feature: Tasks "date":"2018-03-02", "username":"bob", "createdAt":"@string@.isDateTime()", - "updatedAt":"@string@.isDateTime()", - "vehicle": null, - "trailer": null + "updatedAt":"@string@.isDateTime()" } """ @@ -380,8 +290,6 @@ Feature: Tasks "@id":"@string@.startsWith('/api/task_lists')", "id": "@integer@", "@type":"TaskList", - "hydra:member":[], - "hydra:totalItems":0, "items":[], "distance":@integer@, "duration":@integer@, @@ -389,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/Action/MyTasks.php b/src/Action/MyTasks.php index 7d8fd775a8..6f642aaa02 100644 --- a/src/Action/MyTasks.php +++ b/src/Action/MyTasks.php @@ -2,10 +2,10 @@ 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 Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -16,21 +16,24 @@ final class MyTasks public function __construct( TokenStorageInterface $tokenStorage, - protected EntityManagerInterface $entityManager) + private readonly EntityManagerInterface $entityManager, + private readonly TaskListRepository $taskListRepository + ) { $this->tokenStorage = $tokenStorage; } 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 +43,11 @@ 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); } - } - - 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; + $taskListDto = $this->taskListRepository->findMyTaskListAsDto($user, $date); } - return null; + return $taskListDto; } } diff --git a/src/Api/Dto/MyTaskDto.php b/src/Api/Dto/MyTaskDto.php new file mode 100644 index 0000000000..55e0000285 --- /dev/null +++ b/src/Api/Dto/MyTaskDto.php @@ -0,0 +1,120 @@ +id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + $this->type = $type; + $this->status = $status; + $this->address = $address; + $this->after = $after; + $this->before = $before; + $this->doneAfter = $after; + $this->doneBefore = $before; + $this->previous = $previous; + $this->next = $next; + $this->tags = $tags; + $this->doorstep = $doorstep; + $this->comments = $comments; + $this->packages = $packages; + $this->weight = $weight; + $this->hasIncidents = $hasIncidents; + $this->orgName = $orgName; + $this->metadata = $metadata; + } +} diff --git a/src/Api/Dto/MyTaskListDto.php b/src/Api/Dto/MyTaskListDto.php new file mode 100644 index 0000000000..7b5acaf6ab --- /dev/null +++ b/src/Api/Dto/MyTaskListDto.php @@ -0,0 +1,64 @@ +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; + } +} diff --git a/src/Api/Dto/MyTaskMetadataDto.php b/src/Api/Dto/MyTaskMetadataDto.php new file mode 100644 index 0000000000..709c9bdc84 --- /dev/null +++ b/src/Api/Dto/MyTaskMetadataDto.php @@ -0,0 +1,43 @@ +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; + $this->zero_waste = $zero_waste; + } +} diff --git a/src/Api/Dto/TaskPackageDto.php b/src/Api/Dto/TaskPackageDto.php new file mode 100644 index 0000000000..af4ece2274 --- /dev/null +++ b/src/Api/Dto/TaskPackageDto.php @@ -0,0 +1,37 @@ +short_code = $shortCode; + $this->name = $name; + //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/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); diff --git a/src/Entity/TaskList.php b/src/Entity/TaskList.php index 66dccb3c5d..3611c6afbc 100644 --- a/src/Entity/TaskList.php +++ b/src/Entity/TaskList.php @@ -7,6 +7,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; @@ -67,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, diff --git a/src/Entity/TaskListRepository.php b/src/Entity/TaskListRepository.php new file mode 100644 index 0000000000..a9b16a70c7 --- /dev/null +++ b/src/Entity/TaskListRepository.php @@ -0,0 +1,315 @@ +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; + } + + + $orderedTaskIds = array_map(function (Task $task) { + return $task->getId(); + }, $taskList->getTasks()); + + $tasksQueryResult = $this->entityManager->createQueryBuilder() + ->select([ + 't', + 'delivery.id AS deliveryId', + 'o.id AS orderId', + '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', + 'package', + ]) + ->from(Task::class, 't') + ->leftJoin('t.delivery', 'delivery') + ->leftJoin('delivery.order', 'o') + ->leftJoin('t.organization', 'org') + ->leftJoin('o.loopeatDetails', 'loopeatDetails') + ->leftJoin('t.packages', 'taskPackage') + ->leftJoin('taskPackage.package', 'package') + ->where('t.id IN (:taskIds)') + ->andWhere('t.status != :statusCancelled') + ->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; + + if (null === $deliveryId) { + return $carry; + } + + $task = $row[0]; + $carry[$deliveryId][] = $task; // append to an array + return $carry; + }, []); + + + $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; + }, []); + + + $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', + '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, $paymentMethodsByOrderId, $zeroWasteOrders) { + $task = $row[0]; + $deliveryId = $row['deliveryId'] ?? null; + $orderId = $row['orderId'] ?? 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 $t) { + if ($t->isPickup()) { + continue; + } + + $taskPackages = array_merge($taskPackages, $t->getPackages()->toArray()); + $weight += $t->getWeight(); + } + } else { + $taskPackages = $task->getPackages()->toArray(); + $weight = $task->getWeight(); + } + + $taskDto = new MyTaskDto( + $task->getId(), + $task->getCreatedAt(), + $task->getUpdatedAt(), + $task->getType(), + $task->getStatus(), + $task->getAddress(), + $task->getDoneAfter(), + $task->getDoneBefore(), + $task->getPrevious()?->getId(), + $task->getNext()?->getId(), + $task->getTags(), + $task->isDoorstep(), + $task->getComments(), + array_map(function (Task\Package $taskPackage) { + $package = $taskPackage->getPackage(); + return new TaskPackageDto( + $package->getShortCode(), + $package->getName(), + $package->getAverageVolumeUnits(), + $taskPackage->getQuantity()); + }, $taskPackages), + $weight, + ($tasksWithIncidents[$task->getId()] ?? 0) > 0, + $row['organizationName'], + new MyTaskMetadataDto( + $task->getMetadata()['delivery_position'] ?? null, //FIXME extract from the query + $row['orderNumber'] ?? null, + $this->getPaymentMethod($paymentMethodsByOrderId, $orderId), + $row['orderTotal'] ?? null, + $this->getLoopeatReturns($orderId, $task, $row), + $this->getIsZeroWaste($orderId, $zeroWasteOrders) + ) + ); + + return $taskDto; + }, $tasksQueryResult); + + + $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($taskDtosById[$taskId])) { + $orderedTasks[] = $taskDtosById[$taskId]; + } + } + + + $taskListDto = new MyTaskListDto( + $taskList->getId(), + $taskList->getCreatedAt(), + $taskList->getUpdatedAt(), + $taskList->getDate(), + $user->getUsername(), + $orderedTasks, + $taskList->getDistance(), + $taskList->getDuration(), + $taskList->getPolyline(), + ); + 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]; + } + + 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/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/MyTaskDtoNormalizer.php b/src/Serializer/MyTaskDtoNormalizer.php new file mode 100644 index 0000000000..a14751a7a7 --- /dev/null +++ b/src/Serializer/MyTaskDtoNormalizer.php @@ -0,0 +1,74 @@ +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']) && count($data['tags']) > 0) { + $data['tags'] = $this->tagManager->expand($data['tags']); + } + + if ($object->previous) { + $data['previous'] = $this->iriConverter->getItemIriFromResourceClass(Task::class, ['id' => $object->previous]); + } + + if ($object->next) { + $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; + } + + 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 MyTaskDto; + } +} diff --git a/src/Serializer/MyTaskListDtoNormalizer.php b/src/Serializer/MyTaskListDtoNormalizer.php new file mode 100644 index 0000000000..5f8116d0f3 --- /dev/null +++ b/src/Serializer/MyTaskListDtoNormalizer.php @@ -0,0 +1,56 @@ +normalizer->normalize($object, $format, $context); + if (!is_array($data)) { + return $data; + } + + // override json-ld to match the existing API + $data['@context'] = '/api/contexts/TaskList'; + $data['@type'] = 'TaskList'; + $data['@id'] = "/api/task_lists/" . $object->id; + + $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"]] + ); + }, $object->items + ); + + 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 MyTaskListDto; + } +} diff --git a/src/Serializer/MyTaskMetadataDtoNormalizer.php b/src/Serializer/MyTaskMetadataDtoNormalizer.php new file mode 100644 index 0000000000..41e9de2245 --- /dev/null +++ b/src/Serializer/MyTaskMetadataDtoNormalizer.php @@ -0,0 +1,60 @@ +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']); + + $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 + if (isset($context[self::ALREADY_CALLED])) { + return false; + } + + return $data instanceof MyTaskMetadataDto; + } +} 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; } 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; + } +}