From e61fc9dce9d50c2b4fa990dc0eaba9aa0cd8500f Mon Sep 17 00:00:00 2001 From: Joeri van Veen Date: Tue, 17 Dec 2024 09:47:34 +0100 Subject: [PATCH] feat(webhooks): link shipment to order (#327) Allows showing shipment details including barcode in order mode, when label is printed in backoffice. INT-627 --- .../Shipment/UpdateShipmentsAction.php | 8 +- .../Order/Collection/PdkOrderCollection.php | 2 +- .../Repository/AbstractPdkOrderRepository.php | 21 ++++ .../Hook/ShipmentStatusChangeWebhook.php | 17 +++- tests/Bootstrap/MockPdkOrderRepository.php | 5 + .../AbstractPdkOrderRepositoryTest.php | 32 +++++++ .../Hook/ShipmentStatusChangeWebhookTest.php | 96 +++++++++++++++++++ 7 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 tests/Unit/App/Webhook/Hook/ShipmentStatusChangeWebhookTest.php diff --git a/src/App/Action/Backend/Shipment/UpdateShipmentsAction.php b/src/App/Action/Backend/Shipment/UpdateShipmentsAction.php index c380bb30a..648c28609 100644 --- a/src/App/Action/Backend/Shipment/UpdateShipmentsAction.php +++ b/src/App/Action/Backend/Shipment/UpdateShipmentsAction.php @@ -55,6 +55,13 @@ public function handle(Request $request): Response $orders = $this->pdkOrderRepository->getMany($this->getOrderIds($request)); $shipments = $this->shipmentRepository->getShipments($this->getShipmentIds($request, $orders)); + if ($request->get('linkFirstShipmentToFirstOrder') + && $orders->isNotEmpty() + && $shipments->isNotEmpty() + ) { + $shipments->first()->orderId = $orders->first()->getExternalIdentifier(); + } + if ($orders->isNotEmpty()) { $orders->updateShipments($shipments); $this->pdkOrderRepository->updateMany($orders); @@ -103,4 +110,3 @@ private function addBarcodeNotes(ShipmentCollection $shipments): void }); } } - diff --git a/src/App/Order/Collection/PdkOrderCollection.php b/src/App/Order/Collection/PdkOrderCollection.php index e882f3e7b..7ece5334d 100644 --- a/src/App/Order/Collection/PdkOrderCollection.php +++ b/src/App/Order/Collection/PdkOrderCollection.php @@ -154,6 +154,7 @@ private function mergeShipmentsById(ShipmentCollection $shipments, PdkOrder $ord return $orderShipments->values(); } + /** * @param \MyParcelNL\Pdk\Shipment\Collection\ShipmentCollection $shipments * @param \MyParcelNL\Pdk\App\Order\Model\PdkOrder $order @@ -170,4 +171,3 @@ private function mergeShipmentsByOrder(ShipmentCollection $shipments, PdkOrder $ return $merged; } } - diff --git a/src/App/Order/Repository/AbstractPdkOrderRepository.php b/src/App/Order/Repository/AbstractPdkOrderRepository.php index bf44078a6..cf054903c 100644 --- a/src/App/Order/Repository/AbstractPdkOrderRepository.php +++ b/src/App/Order/Repository/AbstractPdkOrderRepository.php @@ -9,6 +9,7 @@ use MyParcelNL\Pdk\App\Order\Model\PdkOrder; use MyParcelNL\Pdk\Base\Repository\Repository; use MyParcelNL\Pdk\Base\Support\Utils; +use MyParcelNL\Pdk\Facade\Logger; abstract class AbstractPdkOrderRepository extends Repository implements PdkOrderRepositoryInterface { @@ -19,6 +20,25 @@ abstract class AbstractPdkOrderRepository extends Repository implements PdkOrder */ abstract public function get($input): PdkOrder; + /** + * TODO: v3.0.0 make method abstract to force implementation + * + * @param string $uuid + * + * @return null|\MyParcelNL\Pdk\App\Order\Model\PdkOrder + */ + public function getByApiIdentifier(string $uuid): ?PdkOrder + { + Logger::notice( + 'Implement getByApiIdentifier, in PDK v3 it will be required.', + [ + 'class' => self::class, + ] + ); + + return $this->get(['order_id' => $uuid]); + } + /** * @param string|string[] $orderIds * @@ -39,6 +59,7 @@ public function update(PdkOrder $order): PdkOrder return $this->save($order->externalIdentifier, $order); } + /** * @param \MyParcelNL\Pdk\App\Order\Collection\PdkOrderCollection $collection * diff --git a/src/App/Webhook/Hook/ShipmentStatusChangeWebhook.php b/src/App/Webhook/Hook/ShipmentStatusChangeWebhook.php index 3a1e688e0..ca6c1c415 100644 --- a/src/App/Webhook/Hook/ShipmentStatusChangeWebhook.php +++ b/src/App/Webhook/Hook/ShipmentStatusChangeWebhook.php @@ -5,7 +5,9 @@ namespace MyParcelNL\Pdk\App\Webhook\Hook; use MyParcelNL\Pdk\App\Api\Backend\PdkBackendActions; +use MyParcelNL\Pdk\App\Order\Contract\PdkOrderRepositoryInterface; use MyParcelNL\Pdk\Facade\Actions; +use MyParcelNL\Pdk\Facade\Pdk; use MyParcelNL\Pdk\Webhook\Model\WebhookSubscription; use Symfony\Component\HttpFoundation\Request; @@ -20,12 +22,19 @@ public function handle(Request $request): void { $content = $this->getHookBody($request); - Actions::execute(PdkBackendActions::UPDATE_SHIPMENTS, [ - 'orderIds' => [$content['shipment_reference_identifier']], - 'shipmentIds' => [$content['shipment_id']], - ]); + // translate order_id (which is api identifier / uuid) to local order id for db + $order = Pdk::get(PdkOrderRepositoryInterface::class)->getByApiIdentifier($content['order_id']); + + if ($order) { + Actions::execute(PdkBackendActions::UPDATE_SHIPMENTS, [ + 'orderIds' => [$order->getExternalIdentifier()], + 'shipmentIds' => [$content['shipment_id']], + 'linkFirstShipmentToFirstOrder' => true, + ]); + } } + /** * @return string */ diff --git a/tests/Bootstrap/MockPdkOrderRepository.php b/tests/Bootstrap/MockPdkOrderRepository.php index f0325dfd3..0cb85da61 100644 --- a/tests/Bootstrap/MockPdkOrderRepository.php +++ b/tests/Bootstrap/MockPdkOrderRepository.php @@ -43,6 +43,11 @@ public function get($input): PdkOrder }); } + public function getByApiIdentifier(string $uuid): ?PdkOrder + { + return new PdkOrder(['externalIdentifier' => 197]); + } + protected function getKeyPrefix(): string { return static::class; diff --git a/tests/Unit/App/Order/Repository/AbstractPdkOrderRepositoryTest.php b/tests/Unit/App/Order/Repository/AbstractPdkOrderRepositoryTest.php index 5d760bfa7..a04337fc8 100644 --- a/tests/Unit/App/Order/Repository/AbstractPdkOrderRepositoryTest.php +++ b/tests/Unit/App/Order/Repository/AbstractPdkOrderRepositoryTest.php @@ -8,7 +8,9 @@ use MyParcelNL\Pdk\App\Order\Contract\PdkOrderRepositoryInterface; use MyParcelNL\Pdk\App\Order\Model\PdkOrder; use MyParcelNL\Pdk\Facade\Pdk; +use MyParcelNL\Pdk\Storage\Contract\StorageInterface; use MyParcelNL\Pdk\Tests\Uses\UsesMockPdkInstance; +use Psr\Log\LoggerInterface; use function MyParcelNL\Pdk\Tests\usesShared; usesShared(new UsesMockPdkInstance()); @@ -44,3 +46,33 @@ expect($newOrder)->toBeInstanceOf(PdkOrder::class); }); + +it('gets order by api identifier', function () { + /** @var \MyParcelNL\Pdk\Tests\Bootstrap\MockLogger $logger */ + $logger = Pdk::get(LoggerInterface::class); + class MockPdkOrderRepository extends AbstractPdkOrderRepository + { + public function get($input): PdkOrder + { + return new PdkOrder(); + } + } + $repository = new MockPdkOrderRepository(Pdk::get(StorageInterface::class)); + $order = $repository->getByApiIdentifier('123'); + + expect($order) + ->toBeInstanceOf(PdkOrder::class) + ->and($logger->getLogs()) + ->toEqual([ + [ + 'level' => 'notice', + 'message' => '[PDK]: Implement getByApiIdentifier, in PDK v3 it will be required.', + 'context' => + [ + 'class' => 'MyParcelNL\\Pdk\\App\\Order\\Repository\\AbstractPdkOrderRepository', + ], + ], + ] + ); +}); + diff --git a/tests/Unit/App/Webhook/Hook/ShipmentStatusChangeWebhookTest.php b/tests/Unit/App/Webhook/Hook/ShipmentStatusChangeWebhookTest.php new file mode 100644 index 000000000..69fd1a747 --- /dev/null +++ b/tests/Unit/App/Webhook/Hook/ShipmentStatusChangeWebhookTest.php @@ -0,0 +1,96 @@ +group('webhook'); + +usesShared(new UsesMockPdkInstance(), new UsesMockEachCron(), new UsesMockEachLogger()); + +it('handles an api request', function (string $hook, string $expectedClass, array $hookBody) { + /** @var PdkWebhooksRepositoryInterface $repository */ + $repository = Pdk::get(PdkWebhooksRepositoryInterface::class); + /** @var PdkWebhookManagerInterface $webhookManager */ + $webhookManager = Pdk::get(PdkWebhookManagerInterface::class); + /** @var \MyParcelNL\Pdk\Tests\Bootstrap\MockCronService $cronService */ + $cronService = Pdk::get(CronServiceInterface::class); + /** @var \MyParcelNL\Pdk\Tests\Bootstrap\MockLogger $logger */ + $logger = Pdk::get(LoggerInterface::class); + + $repository->storeHashedUrl('https://example.com/hook/1234567890abcdef'); + $repository->store(new WebhookSubscriptionCollection([['hook' => $hook, 'url' => $repository->getHashedUrl()]])); + MockApi::enqueue(new ExampleGetShipmentsResponse()); + + $request = Request::create( + $repository->getHashedUrl(), + Request::METHOD_POST, + [], + [], + [], + ['HTTP_X_MYPARCEL_HOOK' => $hook], + json_encode([ + 'data' => [ + 'hooks' => [ + array_merge(['event' => $hook], $hookBody), + ], + ], + ]) + ); + + $webhookManager->call($request); + $cronService->executeScheduledTask(); + + $logs = (new Collection($logger->getLogs()))->map(function (array $log) { + // Omit the request from the logs. + unset($log['context']['request']); + return $log; + }); + // Omit the shipment response from the logs. + unset($logs[1]); + + expect(array_values($logs->toArray()))->toBe([ + [ + 'level' => 'debug', + 'message' => '[PDK]: Webhook received', + 'context' => [], + ], + [ + 'level' => 'debug', + 'message' => '[PDK]: Webhook processed', + 'context' => ['hook' => $expectedClass], + ], + ]); +})->with([ + 'shipment updated' => [ + 'hook' => WebhookSubscription::SHIPMENT_STATUS_CHANGE, + 'class' => ShipmentStatusChangeWebhook::class, + 'body' => [ + 'shipment_id' => 192031595, + 'account_id' => 162450, + 'order_id' => 'api-uuid-string', + 'shop_id' => 83287, + 'status' => 2, + 'barcode' => '3SHOHR763563926', + 'shipment_reference_identifier' => '', + ], + ], +]); +