From 86362891a26418361fc01ead5ff23987d10077eb Mon Sep 17 00:00:00 2001 From: nkamuo Date: Sun, 8 Oct 2023 23:53:57 +0100 Subject: [PATCH] Driver Rating, LoadBoard Filtering, --- config/packages/messenger.yaml | 1 + migrations/Version20231008174204.php | 31 +++++ migrations/Version20231008213009.php | 31 +++++ src/Entity/Account/Driver.php | 14 +++ src/Entity/Account/ReviewSummary.php | 49 ++++++++ src/Entity/Shipment/ShipmentOrder.php | 32 ++++++ .../Resolver/ClientShipmentResolver.php | 6 + .../Resolver/DriverShipmentOrderResolver.php | 2 + .../Resolver/DriverShipmentResolver.php | 107 ++++++++++++++++-- .../Filter/ShipmentAddressFilterEntry.php | 62 ++++++++++ .../ShipmentAddressFilterEntryConnection.php | 11 ++ .../Filter/ShipmentAddressFilterEntryEdge.php | 12 ++ .../Type/Filter/ShipmentAddressSequence.php | 11 ++ src/Message/Account/CalculateDriverRating.php | 20 ++++ .../Account/CalculateDriverRatingHandler.php | 74 ++++++++++++ src/Util/Rsql/RSQLHelper.php | 25 +++- 16 files changed, 476 insertions(+), 12 deletions(-) create mode 100644 migrations/Version20231008174204.php create mode 100644 migrations/Version20231008213009.php create mode 100644 src/Entity/Account/ReviewSummary.php create mode 100644 src/GraphQL/Shipment/Type/Filter/ShipmentAddressFilterEntry.php create mode 100644 src/GraphQL/Shipment/Type/Filter/ShipmentAddressFilterEntryConnection.php create mode 100644 src/GraphQL/Shipment/Type/Filter/ShipmentAddressFilterEntryEdge.php create mode 100644 src/GraphQL/Shipment/Type/Filter/ShipmentAddressSequence.php create mode 100644 src/Message/Account/CalculateDriverRating.php create mode 100644 src/MessageHandler/Account/CalculateDriverRatingHandler.php diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index 587083a..a2954b0 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -19,6 +19,7 @@ framework: Symfony\Component\Mailer\Messenger\SendEmailMessage: async Symfony\Component\Notifier\Message\ChatMessage: async Symfony\Component\Notifier\Message\SmsMessage: async + App\Message\Account\CalculateDriverRating: async # Route your messages to the transports # 'App\Message\YourMessage': async diff --git a/migrations/Version20231008174204.php b/migrations/Version20231008174204.php new file mode 100644 index 0000000..8cdcb17 --- /dev/null +++ b/migrations/Version20231008174204.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE shipment_order ADD pickedup_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', ADD delivered_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE shipment_order DROP pickedup_at, DROP delivered_at'); + } +} diff --git a/migrations/Version20231008213009.php b/migrations/Version20231008213009.php new file mode 100644 index 0000000..51513df --- /dev/null +++ b/migrations/Version20231008213009.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE driver ADD review_rating NUMERIC(2, 1) DEFAULT NULL, ADD review_count INT DEFAULT 0 NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE driver DROP review_rating, DROP review_count'); + } +} diff --git a/src/Entity/Account/Driver.php b/src/Entity/Account/Driver.php index 0c094a7..e22b73f 100644 --- a/src/Entity/Account/Driver.php +++ b/src/Entity/Account/Driver.php @@ -61,6 +61,10 @@ class Driver #[ORM\OneToOne(inversedBy: 'driver', cascade: ['persist', 'remove'])] private ?DriverLicense $drivingLicense = null; + #[GQL\Field()] + #[ORM\Embedded(columnPrefix: 'review_')] + private ?ReviewSummary $review = null; + public function __construct() { $this->createdAt = new \DateTimeImmutable(); @@ -179,4 +183,14 @@ public function setDrivingLicense(?DriverLicense $drivingLicense): static return $this; } + + + public function getReview(): ?ReviewSummary{ + return $this->review; + } + + public function setReview(ReviewSummary $review): static{ + $this->review = $review; + return $this; + } } diff --git a/src/Entity/Account/ReviewSummary.php b/src/Entity/Account/ReviewSummary.php new file mode 100644 index 0000000..72cc546 --- /dev/null +++ b/src/Entity/Account/ReviewSummary.php @@ -0,0 +1,49 @@ + 0 + ] + )] + private ?int $count = 0; + + public function getRating(): ?string + { + return $this->rating; + } + + public function setRating(?string $rating): static + { + $this->rating = $rating; + + return $this; + } + + public function getCount(): ?int + { + return $this->count; + } + + public function setCount(int $count): static + { + $this->count = $count; + + return $this; + } +} diff --git a/src/Entity/Shipment/ShipmentOrder.php b/src/Entity/Shipment/ShipmentOrder.php index c0b072c..8986784 100644 --- a/src/Entity/Shipment/ShipmentOrder.php +++ b/src/Entity/Shipment/ShipmentOrder.php @@ -133,6 +133,14 @@ class ShipmentOrder #[ORM\Column] private ?int $documentsCount = 0; + #[GQL\Field(type: 'DateTime')] + #[ORM\Column(nullable: true)] + private ?\DateTimeImmutable $pickedupAt = null; + + #[GQL\Field(type: 'DateTime')] + #[ORM\Column(nullable: true)] + private ?\DateTimeImmutable $deliveredAt = null; + public function __construct() { @@ -518,4 +526,28 @@ public function getDocumentsCount(): ?int { return $this->documentsCount; } + + public function getPickedupAt(): ?\DateTimeImmutable + { + return $this->pickedupAt; + } + + public function setPickedupAt(?\DateTimeImmutable $pickedupAt): static + { + $this->pickedupAt = $pickedupAt; + + return $this; + } + + public function getDeliveredAt(): ?\DateTimeImmutable + { + return $this->deliveredAt; + } + + public function setDeliveredAt(?\DateTimeImmutable $deliveredAt): static + { + $this->deliveredAt = $deliveredAt; + + return $this; + } } diff --git a/src/GraphQL/Shipment/Resolver/ClientShipmentResolver.php b/src/GraphQL/Shipment/Resolver/ClientShipmentResolver.php index fa51b7c..6282cbf 100644 --- a/src/GraphQL/Shipment/Resolver/ClientShipmentResolver.php +++ b/src/GraphQL/Shipment/Resolver/ClientShipmentResolver.php @@ -2,6 +2,7 @@ namespace App\GraphQL\Shipment\Resolver; +use App\CQRS\CommandBusInterface; use App\Entity\Account\User; use App\Entity\Addressing\UserAddress; use App\Entity\Catalog\UserProduct; @@ -23,6 +24,7 @@ use App\GraphQL\Shipment\Type\ShipmentDriverBidConnection; use App\GraphQL\Shipment\Type\ShipmentDriverBidEdge; use App\GraphQL\Shipment\Type\ShipmentEdge; +use App\Message\Account\CalculateDriverRating; use App\Repository\Addressing\UserAddressRepository; use App\Repository\Catalog\UserProductRepository; use App\Repository\Shipment\Assessment\AssessmentParameterRepository; @@ -63,6 +65,7 @@ public function __construct( private AssessmentParameterRepository $assessmentParameterRepository, private DirectionsServiceInterface $directionsService, private CodeGeneratorInterface $codeGenerator, + private CommandBusInterface $commandBus, ) { } @@ -301,6 +304,9 @@ public function reviewShipment(Ulid $id, ?ShipmentOrderReviewInput $input): Ship $this->entityManager->persist($shipment); $this->entityManager->flush(); + $driver = $order->getDriver(); + $this->commandBus->dispatch(new CalculateDriverRating($driver)); + return $shipment; } diff --git a/src/GraphQL/Shipment/Resolver/DriverShipmentOrderResolver.php b/src/GraphQL/Shipment/Resolver/DriverShipmentOrderResolver.php index 998779b..da023b3 100644 --- a/src/GraphQL/Shipment/Resolver/DriverShipmentOrderResolver.php +++ b/src/GraphQL/Shipment/Resolver/DriverShipmentOrderResolver.php @@ -232,6 +232,7 @@ public function executeShipmentOrderPickup(Ulid $id, ShipmentOrderNodeExecutionI input: $input->document, ); $order->setPickupConfirmation($document); + $order->setPickedupAt(new DateTimeImmutable()); $order->setStatus(ShipmentOrderStatus::INTRANSIT); $shipment->setStatus(ShipmentStatus::INTRANSIT); @@ -264,6 +265,7 @@ public function executeShipmentOrderDelivery(Ulid $id, ShipmentOrderNodeExecutio input: $input->document, ); $order->setProofOfDelivery($document); + $order->setDeliveredAt(new DateTimeImmutable()); $order->setStatus(ShipmentOrderStatus::DELIVERED); $shipment->setStatus(ShipmentStatus::DELIVERED); diff --git a/src/GraphQL/Shipment/Resolver/DriverShipmentResolver.php b/src/GraphQL/Shipment/Resolver/DriverShipmentResolver.php index bf11e56..e262b83 100644 --- a/src/GraphQL/Shipment/Resolver/DriverShipmentResolver.php +++ b/src/GraphQL/Shipment/Resolver/DriverShipmentResolver.php @@ -9,6 +9,10 @@ use App\Entity\Shipment\ShipmentStatus; use App\Entity\Vehicle\Vehicle; use App\GraphQL\Shipment\Input\ShipmentBidCreationInput; +use App\GraphQL\Shipment\Type\Filter\ShipmentAddressFilterEntry; +use App\GraphQL\Shipment\Type\Filter\ShipmentAddressFilterEntryConnection; +use App\GraphQL\Shipment\Type\Filter\ShipmentAddressFilterEntryEdge; +use App\GraphQL\Shipment\Type\Filter\ShipmentAddressSequence; use App\GraphQL\Shipment\Type\ShipmentConnection; use App\GraphQL\Shipment\Type\ShipmentDriverBidConnection; use App\GraphQL\Shipment\Type\ShipmentDriverBidEdge; @@ -74,7 +78,6 @@ public function getShipmentItem( #[GQL\Query(name: "get_load_list")] - // #[GQL\Access("isGranted('ROLE_USER')")] public function getShipmentConnection( ?int $first, ?String $after, @@ -91,7 +94,7 @@ public function getShipmentConnection( $qb = $this->shipmentRepository ->createQueryBuilder('shipment') ->andWhere('shipment.status = :status') - ->setParameter('status',ShipmentStatus::PUBLISHED); + ->setParameter('status', ShipmentStatus::PUBLISHED); QueryBuilderHelper::applyCriteria($qb, $filter, 'shipment'); @@ -111,6 +114,93 @@ public function getShipmentConnection( + + #[GQL\Query(name: "get_load_address_filter_list")] + public function getShipmentAddressFilterConnection( + ShipmentAddressSequence $sequence, + ?int $first, + ?String $after, + ?String $filter, + ?String $sort, + ): ShipmentAddressFilterEntryConnection { + + $cb = new ConnectionBuilder( + null, + fn ($edges, PageInfoInterface $pageInfo) => new ShipmentAddressFilterEntryConnection($edges, $pageInfo), + fn (string $coursor, array $res, int $index) => new ShipmentAddressFilterEntryEdge($coursor, ShipmentAddressFilterEntry::create($res)) + ); + + // ShipmentAddressFilterEntry + $qb = $this->shipmentRepository + ->createQueryBuilder('shipment') + ->andWhere('shipment.status = :status') + ->setParameter('status', ShipmentStatus::PUBLISHED); + + if ($sequence == ShipmentAddressSequence::ORIGIN) { + $qb->innerJoin('shipment.originAddress', 'address'); + } elseif ($sequence == ShipmentAddressSequence::DESTINATION) { + $qb->innerJoin('shipment.destinationAddress', 'address'); + } + + QueryBuilderHelper::applyCriteria($qb, $filter, 'shipment'); + + + $options = ['address.countryCode', 'address.provinceCode', 'address.city',]; + $coalease = implode(',\'-\',', $options); + $reference = sprintf('CONCAT(%s)', $coalease); + $qb + // ->select(sprintf('%s AS HIDDEN %s', $reference, 'reference')) + ->select($options) + ->addSelect('COUNT(shipment.id) AS count') + // ->addGroupBy(sprintf('CONCAT(%s)', $coalease,)) + // ->groupBy('reference') + // ->addSelect('DISTINCT address_label ') + ; + + + $countQuery = (clone $qb)->select(sprintf('COUNT(DISTINCT %s)',$reference))->getQuery(); + $dql = $countQuery->getDQL(); + $total = fn () => (int) $countQuery->getSingleScalarResult(); + $paginator = new Paginator(function (?int $offset, ?int $limit) use ($qb, $options) { + + foreach ($options as $option) { + $qb->addGroupBy("{$option}"); + } + + $query = $qb + ->setFirstResult($offset) + ->setMaxResults($limit) + ->getQuery(); + + $dql = $query->getDQL(); + + $result = $query->getResult(); + + return $result; + + + // $final = []; + + // foreach ($result as $entry) { + // $reference = $entry['reference']; + // list($country, $province, $city) = explode('-', $reference); + // $final[] = [ + // 'city' => $city, + // 'province' => $province, + // 'country' => $country, + // 'count' => $entry['count'], + // ]; + // } + + // return $final; + }, false, $cb); + + return $paginator->auto(new Argument(['first' => $first, 'after' => $after]), $total); + } + + + + ///////////////////////////////////////// // SHIPMENT BIDDING AND LISTING FROM HERE ///////////////////////////////// @@ -128,7 +218,7 @@ public function getShipmentBidItem( return $bid; } - + #[GQL\Query(name: "get_load_bid_list")] // #[GQL\Access("isGranted('ROLE_USER')")] public function getShipmentDriverBidConnection( @@ -184,8 +274,7 @@ public function createNewShipmentBid(ShipmentBidCreationInput $input): ShipmentD ->setDriver($driver) ->setVehicle($vehicle) ->setTitle($input->title) - ->setDescription($input->description) - ; + ->setDescription($input->description); if ($input->pickupAt) { $bid->setPickupAt(DateTimeImmutable::createFromInterface($input->pickupAt)); @@ -289,15 +378,17 @@ private function getDriverVehicleById(Ulid $id): Vehicle - private function getBiddableShipmentById(Ulid $id): Shipment{ + private function getBiddableShipmentById(Ulid $id): Shipment + { $shipment = $this->getShipmentById($id); //TODO: Confirm that a bid can be placed against this shipment otherwise throw return $shipment; } - private function getShipmentById(Ulid $id): Shipment{ + private function getShipmentById(Ulid $id): Shipment + { $shipment = $this->shipmentRepository->find($id); - if(null == $shipment){ + if (null == $shipment) { throw new UserError("Cannot find shipment with [id: {$id}]"); } return $shipment; diff --git a/src/GraphQL/Shipment/Type/Filter/ShipmentAddressFilterEntry.php b/src/GraphQL/Shipment/Type/Filter/ShipmentAddressFilterEntry.php new file mode 100644 index 0000000..053904f --- /dev/null +++ b/src/GraphQL/Shipment/Type/Filter/ShipmentAddressFilterEntry.php @@ -0,0 +1,62 @@ +city = $data['city']; + $this->province = $data['provinceCode']; + $this->country = $data['countryCode']; + $this->count = $data['count']; + } + + + public static function create(array $data): static + { + return new static($data); + } + + public function getCity(): ?string + { + return $this->city; + } + + public function getProvince(): ?string + { + return $this->province; + } + + public function getCountry(): ?string + { + return $this->country; + } + + + public function getCount(): ?int + { + return $this->count; + } +} diff --git a/src/GraphQL/Shipment/Type/Filter/ShipmentAddressFilterEntryConnection.php b/src/GraphQL/Shipment/Type/Filter/ShipmentAddressFilterEntryConnection.php new file mode 100644 index 0000000..b036933 --- /dev/null +++ b/src/GraphQL/Shipment/Type/Filter/ShipmentAddressFilterEntryConnection.php @@ -0,0 +1,11 @@ +driverId = $driver->getId(); + } + + public function getDriverId(): Ulid + { + return $this->driverId; + } +} diff --git a/src/MessageHandler/Account/CalculateDriverRatingHandler.php b/src/MessageHandler/Account/CalculateDriverRatingHandler.php new file mode 100644 index 0000000..6e6f722 --- /dev/null +++ b/src/MessageHandler/Account/CalculateDriverRatingHandler.php @@ -0,0 +1,74 @@ +getDriverById($command->getDriverId()); + if(null === $driver) + return; + + $qb = $this->reviewRepository->createQueryBuilder('review'); + $qb + ->innerJoin('review.shipmentOrder', 'shipmentOrder') + ->innerJoin('shipmentOrder.driver', 'driver') + ->andWhere('driver.id = :driver') + ->setParameter('driver', $driver->getId(), UlidType::NAME); + + /** @var Collection|Review[] */ + $reviews = $qb->getQuery()->getResult(); + + $total = 0; + $count = 0; + foreach ($reviews as $review) { + $total += $review->getRating(); + $count++; + } + + $rating = 0; + if ($count == 0) { + } else { + $rating = $total / $count; + } + + $summary = $driver->getReview(); + + $summary + ->setRating($rating) + ->setCount($count); + + $this->entityManager->persist($driver); + $this->entityManager->flush(); + } + + + + private function getDriverById(Ulid $id): ?Driver + { + return $this->driverRepository->find($id); + } +} diff --git a/src/Util/Rsql/RSQLHelper.php b/src/Util/Rsql/RSQLHelper.php index 85f8f72..6bd52bf 100644 --- a/src/Util/Rsql/RSQLHelper.php +++ b/src/Util/Rsql/RSQLHelper.php @@ -328,7 +328,7 @@ private function getOrJoinField(QueryBuilder $qb, string $path, mixed $value): m $case = $enumType::from($value); } catch (\Throwable $e) { $cases = []; - foreach($enumType::cases() as $v){ + foreach ($enumType::cases() as $v) { $cases[$v->name] = $v; } @@ -493,14 +493,31 @@ private function setup() }); - $this->addOperator('=sameweek=', function (QueryBuilder $qb, string $rootName, string $alias, ?string $type, ?string $value) { - // list($rootName, $alias, $type) = $this->getOrJoinField($qb, $attrName); - + $this->addOperator('=sameweek=', function (QueryBuilder $qb, string $rootName, string $alias, ?string $type, string|\DateTimeInterface $value) { $param = uniqid(':param_'); $qb->setParameter($param, $value); // $field = "{$rootName}{$alias}"; return "WEEK($field) = WEEK($param) AND YEAR($field) = YEAR($param)"; // }); + + + $this->addOperator('=sameday=', function (QueryBuilder $qb, string $rootName, string $alias, ?string $type, string|\DateTimeInterface $value) { + $param = uniqid(':param_'); + $qb->setParameter($param, $value); + // + $field = "{$rootName}{$alias}"; + return "DATE($field) = DATE($param)"; // + }); + + + + $this->addOperator('=year=', function (QueryBuilder $qb, string $rootName, string $alias, ?string $type, string|\DateTimeInterface $value) { + $param = uniqid(':param_'); + $qb->setParameter($param, $value); + // + $field = "{$rootName}{$alias}"; + return "YEAR($field) = YEAR($param)"; // + }); } }