diff --git a/UnitTestFiles/Test/OrderTests.php b/UnitTestFiles/Test/OrderTests.php index 76e5453..bebae13 100644 --- a/UnitTestFiles/Test/OrderTests.php +++ b/UnitTestFiles/Test/OrderTests.php @@ -173,8 +173,8 @@ public function testToArray() 'name' => 'Bill Soul', ], ], - ] - ); + ], + ]); } public function testGetOrders() @@ -253,6 +253,8 @@ public function testAddOrdersToOptimization() public function testAddOrdersToRoute() { + $this->markTestSkipped('Read old data.'); + $body = json_decode(file_get_contents(dirname(__FILE__).'/data/add_order_to_route_data.json'), true); $routeId = self::$createdProblems[0]->routes[0]->route_id; @@ -350,6 +352,23 @@ public function testGetOrderByID() self::assertInstanceOf(Order::class, Order::fromArray($response)); } + public function testGetOrderByUUID() + { + $order = new Order(); + + $orderUUID = self::$createdOrders[0]['order_uuid']; + + // Get an order + $orderParameters = Order::fromArray([ + 'order_id' => $orderUUID, + ]); + + $response = $order->getOrder($orderParameters); + + self::assertNotNull($response); + self::assertInstanceOf(Order::class, Order::fromArray($response)); + } + public function testGetOrderByInsertedDate() { $orderParameters = Order::fromArray([ @@ -517,6 +536,23 @@ public function testUpdateOrderWithCustomFiel() $this->assertEquals(true, $response['custom_user_fields'][0]['order_custom_field_value']); } + public function testDeleteOrderByUuid() + { + $lastOrder = array_pop(self::$createdOrders); + if ($lastOrder != null) { + $order = new Order(); + $ids = [ + "order_ids" => [$lastOrder['order_uuid']] + ]; + + $response = $order->removeOrder($ids); + + if (!is_null($response) && isset($response['status']) && $response['status']) { + echo "The test order removed by UUID
"; + } + } + } + public static function tearDownAfterClass() { if (sizeof(self::$createdOrders)) { diff --git a/UnitTestFiles/Test/V5/OrderUnitTests.php b/UnitTestFiles/Test/V5/OrderUnitTests.php new file mode 100644 index 0000000..13df50a --- /dev/null +++ b/UnitTestFiles/Test/V5/OrderUnitTests.php @@ -0,0 +1,371 @@ +assertInstanceOf(CustomData::class, new CustomData()); + } + + public function testCustomDataCanBeCreateFromArray() : void + { + $this->assertInstanceOf(CustomData::class, new CustomData([ + 'barcode' => '1', + 'airbillno' => '2', + 'sorted_on_date' => '3', + 'sorted_on_utc' => 1 + ])); + } + + public function testCustomFieldCanBeCreateEmpty() : void + { + $this->assertInstanceOf(CustomField::class, new CustomField()); + } + + public function testCustomFieldCanBeCreateFromArray() : void + { + $this->assertInstanceOf(CustomField::class, new CustomField([ + 'order_custom_field_uuid' => 'uuid', + 'order_custom_field_value' => 'value' + ])); + } + + public function testGPSCoordsCanBeCreateEmpty() : void + { + $this->assertInstanceOf(GPSCoords::class, new GPSCoords()); + } + + public function testGPSCoordsCanBeCreateByConstructor() : void + { + $coords = new GPSCoords(40.5, 90.0); + $this->assertInstanceOf(GPSCoords::class, $coords); + $this->assertEquals($coords->lat, 40.5); + $this->assertEquals($coords->lng, 90.0); + } + + public function testGPSCoordsCanBeCreateFromArray() : void + { + $coords = new GPSCoords([ + 'lat' => 40.5, + 'lng' => 90.0 + ]); + $this->assertInstanceOf(GPSCoords::class, $coords); + $this->assertEquals($coords->lat, 40.5); + $this->assertEquals($coords->lng, 90.0); + } + + public function testLocalTimeWindowCanBeCreateEmpty() : void + { + $this->assertInstanceOf(LocalTimeWindow::class, new LocalTimeWindow()); + } + + public function testLocalTimeWindowCanBeCreateByConstructor() : void + { + $coords = new LocalTimeWindow(1, 2); + $this->assertInstanceOf(LocalTimeWindow::class, $coords); + $this->assertEquals($coords->start, 1); + $this->assertEquals($coords->end, 2); + } + + public function testLocalTimeWindowCanBeCreateFromArray() : void + { + $coords = new LocalTimeWindow([ + 'start' => 1, + 'end' => 2 + ]); + $this->assertInstanceOf(LocalTimeWindow::class, $coords); + $this->assertEquals($coords->start, 1); + $this->assertEquals($coords->end, 2); + } + + public function testOrderCanBeCreateEmpty() : void + { + $this->assertInstanceOf(Order::class, new Order()); + } + + public function testOrderCanBeCreateFromArray() : void + { + $order = new Order([ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'some@company.com' + ]); + $this->assertInstanceOf(Order::class, $order); + $this->assertEquals($order->first_name, 'John'); + $this->assertEquals($order->last_name, 'Doe'); + } + + public function testResponseOrderCanBeCreateEmpty() : void + { + $this->assertInstanceOf(Order::class, new Order()); + } + + public function testResponseOrderCanBeCreateFromArray() : void + { + $order = new ResponseOrder([ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'some@company.com' + ]); + $this->assertInstanceOf(ResponseOrder::class, $order); + $this->assertEquals($order->first_name, 'John'); + $this->assertEquals($order->last_name, 'Doe'); + } + + public function testCreateMustReturnResponseOrder() : void + { + $orders = new Orders(); + $order = $orders->create([ + 'address_1' => '1358 E Luzerne St, Philadelphia, PA 19124, US', + 'address_alias' => 'Auto test address', + 'address_city' => 'Philadelphia', + 'address_geo' => [ + 'lat' => 48.335991, + 'lng' => 31.18287 + ], + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'some@company.com' + ]); + + $this->assertInstanceOf(ResponseOrder::class, $order); + $this->assertNotNull($order->first_name); + $this->assertEquals($order->first_name, 'John'); + + self::$created_uuid = $order->order_uuid; + } + + public function testGetMustReturnResponseOrder() : void + { + if (self::$created_uuid !== null) { + $orders = new Orders(); + $order = $orders->get(self::$created_uuid); + + $this->assertInstanceOf(ResponseOrder::class, $order); + $this->assertNotNull($order->first_name); + $this->assertEquals($order->first_name, 'John'); + } + } + + public function testUpdateMustReturnResponseOrder() : void + { + if (self::$created_uuid !== null) { + $orders = new Orders(); + $order = $orders->update(self::$created_uuid, ['first_name' => 'Jane']); + + $this->assertInstanceOf(ResponseOrder::class, $order); + $this->assertNotNull($order->first_name); + $this->assertEquals($order->first_name, 'Jane'); + } + } + + public function testSearchMustReturnResponseSearchWithoutParams() : void + { + $orders = new Orders(); + $res = $orders->search(); + + $this->assertInstanceOf(ResponseSearch::class, $res); + $this->assertNotNull($res->total); + } + + public function testSearchMustReturnResponseSearchWithParams() : void + { + if (self::$created_uuid !== null) { + $orders = new Orders(); + $params = [ + "filters" => [ + "order_ids" => [self::$created_uuid] + ] + ]; + $res = $orders->search($params); + + $this->assertInstanceOf(ResponseSearch::class, $res); + $this->assertNotNull($res->total); + $this->assertNotNull($res->results); + } + } + + public function testBatchCreateMustReturnTrue() : void + { + $this->markTestSkipped('must be revisited, cannot get back IDs or created Orders.'); + + $orders = new Orders(); + $params = [ + [ + 'address_1' => '1358 E Luzerne St, Philadelphia, PA 19124, US', + 'address_alias' => 'Address for batch workflow 0', + 'address_city' => 'Philadelphia', + 'address_geo' => [ + 'lat' => 48.335991, + 'lng' => 31.18287 + ], + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'some@company.com' + ], [ + 'address_1' => '1358 E Luzerne St, Philadelphia, PA 19124, US', + 'address_alias' => 'Address for batch workflow 1', + 'address_city' => 'Philadelphia', + 'address_geo' => [ + 'lat' => 48.335991, + 'lng' => 31.18287 + ], + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'some@company.com' + ] + ]; + $res = $orders->batchCreate($params); + + $this->assertIsBool($res); + $this->assertTrue($res); + } + + public function testBatchUpdateMustReturnArray() : void + { + $this->markTestSkipped('must be revisited, cannot get back IDs or created Orders.'); + + $orders = new Orders(); + $orderIds = ['', '']; + $data = [ + 'first_name' => 'Jane' + ]; + $res = $orders->batchUpdate($orderIds, $data); + + $this->assertIsArray($res); + } + + public function testBatchUpdateByFiltersMustReturnTrue() : void + { + $this->markTestSkipped('must be revisited, cannot get back IDs or created Orders.'); + + $orders = new Orders(); + $params = [ + 'data' => [ + 'first_name' => 'John' + ], + 'filters' => [ + 'orderIds' => ['', ''] + ] + ]; + $res = $orders->batchUpdateByFilters($params); + + $this->assertIsBool($res); + $this->assertTrue($res); + } + + public function testBatchDeleteMustReturnTrue() : void + { + $this->markTestSkipped('must be revisited, cannot get back IDs or created Orders.'); + + $orders = new Orders(); + $orderIds = ['', '']; + $res = $orders->batchDelete($orderIds); + + $this->assertIsBool($res); + $this->assertTrue($res); + } + + public function testGetOrderCustomFieldsReturnArray() : void + { + $orders = new Orders(); + $res = $orders->getOrderCustomFields(); + + $this->assertIsArray($res); + } + + public function testCreateOrderCustomFieldMustReturnCustomField() : void + { + $orders = new Orders(); + $field = $orders->createOrderCustomField([ + 'order_custom_field_name' => 'CustomField1', + 'order_custom_field_label' => 'Custom Field 1', + 'order_custom_field_type' => 'checkbox', + 'order_custom_field_type_info' => [ + 'short_label' => 'cFl1' + ] + ]); + + $this->assertInstanceOf(CustomField::class, $field); + $this->assertNotNull($field->order_custom_field_label); + $this->assertEquals($field->order_custom_field_label, 'Custom Field 1'); + + self::$created_field_uuid = $field->order_custom_field_uuid; + } + + public function testUpdateOrderCustomFieldMustReturnCustomField() : void + { + if (self::$created_field_uuid !== null) { + $orders = new Orders(); + $field = $orders->updateOrderCustomField(self::$created_field_uuid, [ + 'order_custom_field_label' => 'Custom Field New', + 'order_custom_field_type' => 'checkbox', + 'order_custom_field_type_info' => [ + 'short_label' => 'cFl1' + ] + ]); + + $this->assertInstanceOf(CustomField::class, $field); + $this->assertNotNull($field->order_custom_field_label); + $this->assertEquals($field->order_custom_field_label, 'Custom Field New'); + } + } + + public function testDeleteOrderCustomFieldMustReturnCustomField() : void + { + if (self::$created_field_uuid !== null) { + $orders = new Orders(); + $field = $orders->deleteOrderCustomField(self::$created_field_uuid); + + $this->assertInstanceOf(CustomField::class, $field); + self::$created_field_uuid = null; + } + } + + public function testDeleteMustReturnTrue() : void + { + if (self::$created_uuid !== null) { + $orders = new Orders(); + $result = $orders->delete(self::$created_uuid); + + $this->assertTrue($result); + self::$created_uuid = null; + } + } + + public static function tearDownAfterClass() : void + { + sleep(1); + + if (self::$created_uuid !== null || self::$created_field_uuid !== null) { + $orders = new Orders(); + if (self::$created_uuid !== null) { + $orders->delete(self::$created_uuid); + } + if (self::$created_field_uuid !== null) { + $orders->deleteOrderCustomField(self::$created_field_uuid); + } + } + } +} diff --git a/composer.json b/composer.json index 211bf5e..6aeed78 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "route4me/route4me-php", "description": "Access Route4Me's logistics-as-a-service API using our PHP SDK", "minimum-stability": "stable", - "version": "1.2.11", + "version": "1.3.1", "authors": [ { "name": "Igor Route4Me", diff --git a/examples/Order/GetOrderByUuid.php b/examples/Order/GetOrderByUuid.php new file mode 100644 index 0000000..234ae1e --- /dev/null +++ b/examples/Order/GetOrderByUuid.php @@ -0,0 +1,72 @@ + '1358 E Luzerne St, Philadelphia, PA 19124, US', + 'cached_lat' => 48.335991, + 'cached_lng' => 31.18287, + 'address_alias' => 'Auto test address', + 'address_city' => 'Philadelphia', + 'EXT_FIELD_first_name' => 'John', + 'EXT_FIELD_last_name' => 'Doe', + 'EXT_FIELD_email' => 'some@company.com' +]); + +$order = null; +$orderId = null; + +try { + $order = new Order(); + + // create order + $newOrder = $order->addOrder($orderParams); + $orderId = $newOrder['order_id']; + $orderUuid = $newOrder['order_uuid']; + echo "Create Order with id='" . $orderId . "' and uuid='" . $orderUuid . "'" . PHP_EOL; + + // get order by ID + $orderById = $order->getOrder(['order_id' => $orderId]); + echo "Read Order by id, id='" . $orderId . "' and uuid='" . $orderUuid . "'" . PHP_EOL; + + // get order by UUID + $orderByUuid = $order->getOrder(['order_id' => $orderUuid]); + echo "Read Order by uuid, id='" . $orderId . "' and uuid='" . $orderUuid . "'" . PHP_EOL; + + // compare orders + if ($orderById == $orderByUuid) { + echo "The Orders are equal." . PHP_EOL; + } else { + echo "The Orders are not equal." . PHP_EOL; + } + + // delete order + $res = $order->removeOrder(['order_ids' => [$orderUuid]]); + if ($res) { + echo "Order with uuid='" . $orderUuid . "' was deleted successful." . PHP_EOL; + } else { + echo "Order with uuid='" . $orderUuid . "' was not deleted." . PHP_EOL; + } +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . PHP_EOL; + + if ($order && $orderId) { + try { + $order->removeOrder(['order_ids' => [$orderUuid]]); + echo "Order with uuid='" . $orderUuid . "' was cleaned up successful." . PHP_EOL; + } catch (Exception $e) { + echo "Order with uuid='" . $orderUuid . "' was not cleaned up." . PHP_EOL; + } + } +} diff --git a/examples/Order_V5/CreateBatchOfOrders.php b/examples/Order_V5/CreateBatchOfOrders.php new file mode 100644 index 0000000..fda1167 --- /dev/null +++ b/examples/Order_V5/CreateBatchOfOrders.php @@ -0,0 +1,55 @@ + '1358 E Luzerne St, Philadelphia, PA 19124, US', + 'address_alias' => 'Address for batch workflow 0', + 'address_city' => 'Philadelphia', + 'address_geo' => [ + 'lat' => 48.335991, + 'lng' => 31.18287 + ], + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'some@company.com' + ], [ + 'address_1' => '1358 E Luzerne St, Philadelphia, PA 19124, US', + 'address_alias' => 'Address for batch workflow 1', + 'address_city' => 'Philadelphia', + 'address_geo' => [ + 'lat' => 48.335991, + 'lng' => 31.18287 + ], + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'some@company.com' + ] +]; + +try { + $orders = new Orders(); + + // create a batch of orders + $res = $orders->batchCreate($ordersParams); + if ($res) { + echo "Create a batch of orders." . PHP_EOL; + } else { + echo "Error to create a batch of orders." . PHP_EOL; + } +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . PHP_EOL; +} diff --git a/examples/Order_V5/CreateOrder.php b/examples/Order_V5/CreateOrder.php new file mode 100644 index 0000000..53ae506 --- /dev/null +++ b/examples/Order_V5/CreateOrder.php @@ -0,0 +1,70 @@ + '1358 E Luzerne St, Philadelphia, PA 19124, US', + 'address_alias' => 'Auto test address', + 'address_city' => 'Philadelphia', + 'address_geo' => [ + 'lat' => 48.335991, + 'lng' => 31.18287 + ], + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'some@company.com' +]); + +$orders = null; +$orderId = null; + +try { + $orders = new Orders(); + + // create order + $newOrder = $orders->create($order); + $orderId = $newOrder->order_uuid; + echo "Create Order with uuid='" . $orderId . "'" . PHP_EOL; + + // read order + $readOrder = $orders->get($orderId); + echo "Read Order first_name is '" . $readOrder->first_name . "'" . PHP_EOL; + + // update order + $params = [ + 'first_name' => 'Jane' + ]; + $updateOrder = $orders->update($orderId, $params); + echo "Update Order first_name is '" . $updateOrder->first_name . "'" . PHP_EOL; + + // delete order + if ($orders->delete($orderId)) { + echo "Order with uuid='" . $orderId . "' was deleted successful." . PHP_EOL; + } else { + echo "Order with uuid='" . $orderId . "' was not deleted." . PHP_EOL; + } +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . PHP_EOL; + + if ($orders && $orderId) { + try { + $orders->delete($orderId); + echo "Order with uuid='" . $orderId . "' was cleaned up successful." . PHP_EOL; + } catch (Exception $e) { + echo "Order with uuid='" . $orderId . "' was not cleaned up." . PHP_EOL; + } + } +} diff --git a/examples/Order_V5/CreateOrderCustomField.php b/examples/Order_V5/CreateOrderCustomField.php new file mode 100644 index 0000000..ca10f24 --- /dev/null +++ b/examples/Order_V5/CreateOrderCustomField.php @@ -0,0 +1,70 @@ + 'CustomField1', + 'order_custom_field_label' => 'Custom Field 1', + 'order_custom_field_type' => 'checkbox', + 'order_custom_field_type_info' => [ + 'short_label' => 'cFl1' + ] +]); + +print_r($customField); + +$orders = null; +$uuid = null; + +try { + $orders = new Orders(); + + // create custom field + $newField = $orders->createOrderCustomField($customField); + $uuid = $newField->order_custom_field_uuid; + echo "Create Custom field, label is '" . $newField->order_custom_field_label . "'" . PHP_EOL; + + // update custom field + $customField->order_custom_field_label = 'Custom Field New'; + $updateField = $orders->updateOrderCustomField($uuid, $customField); + echo "Update Custom field, label is '" . $updateField->order_custom_field_label . "'" . PHP_EOL; + + // read custom fields + $readFields = $orders->getOrderCustomFields(); + foreach ($readFields as $key => $field) { + if ($field->order_custom_field_uuid === $uuid) { + echo "Found Custom field with label '" . $field->order_custom_field_label . "'" . PHP_EOL; + break; + } + } + // delete custom field + if ($orders->deleteOrderCustomField($uuid)) { + echo "Custom field with uuid='" . $uuid . "' was deleted successful." . PHP_EOL; + } else { + echo "Custom field with uuid='" . $uuid . "' was not deleted." . PHP_EOL; + } +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . PHP_EOL; + + if ($orders && $uuid) { + try { + $orders->deleteOrderCustomField($uuid); + echo "Custom field with uuid='" . $uuid . "' was cleaned up successful." . PHP_EOL; + } catch (Exception $e) { + echo "Custom field with uuid='" . $uuid . "' was not cleaned up." . PHP_EOL; + } + } +} diff --git a/examples/Order_V5/GetOrderByUuid.php b/examples/Order_V5/GetOrderByUuid.php new file mode 100644 index 0000000..39f2373 --- /dev/null +++ b/examples/Order_V5/GetOrderByUuid.php @@ -0,0 +1,76 @@ + '1358 E Luzerne St, Philadelphia, PA 19124, US', + 'address_alias' => 'Auto test address', + 'address_city' => 'Philadelphia', + 'address_geo' => [ + 'lat' => 48.335991, + 'lng' => 31.18287 + ], + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'some@company.com' +]); + +$orders = null; +$orderId = null; + +try { + $orders = new Orders(); + + // create order + $newOrder = $orders->create($order); + $orderId = $newOrder->order_id; + $orderUuid = $newOrder->order_uuid; + echo "Create Order with id='" . $orderId . "' and uuid='" . $orderUuid . "'" . PHP_EOL; + + // get order by ID + $orderById = $orders->get($orderId); + echo "Read Order by id, id='" . $orderId . "' and uuid='" . $orderUuid . "'" . PHP_EOL; + + // get order by UUID + $orderByUuid = $orders->get($orderUuid); + echo "Read Order by uuid, id='" . $orderId . "' and uuid='" . $orderUuid . "'" . PHP_EOL; + + // compare orders + if ($orderById == $orderByUuid) { + echo "The Orders are equal." . PHP_EOL; + } else { + echo "The Orders are not equal." . PHP_EOL; + } + + // delete order + if ($orders->delete($orderUuid)) { + echo "Order with uuid='" . $orderUuid . "' was deleted successful." . PHP_EOL; + } else { + echo "Order with uuid='" . $orderUuid . "' was not deleted." . PHP_EOL; + } +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . PHP_EOL; + + if ($orders && $orderId) { + try { + $orders->delete($orderUuid); + echo "Order with uuid='" . $orderUuid . "' was cleaned up successful." . PHP_EOL; + } catch (Exception $e) { + echo "Order with uuid='" . $orderUuid . "' was not cleaned up." . PHP_EOL; + } + } +} diff --git a/examples/Order_V5/SearchOrders.php b/examples/Order_V5/SearchOrders.php new file mode 100644 index 0000000..bf86291 --- /dev/null +++ b/examples/Order_V5/SearchOrders.php @@ -0,0 +1,45 @@ +search(); + + // Search orders by known IDs + $params = [ + "filters" => [ + "order_ids" => ["B3CBB9C07D37406997EE73D9CEC18264", "D91F4962CC4C468A9563896A93DBE4D7"] + ] + ]; + $ordersByIds = $orders->search($params); + + // Search all the orders with specific address_alias, return only specific fields in response. + $params = [ + "return_provided_fields_as_map" => true, + "fields" => ["order_uuid", "address_alias", "email", "first_name", "phone"], + "search" => [ + "matches" => ["address_alias" => "Auto test address"] + ] + ]; + $ordersWithAddress = $orders->search($params); + + print_r($ordersWithAddress); +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . PHP_EOL; +} diff --git a/phpunit.xml b/phpunit.xml index bf578d1..b74246d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -33,6 +33,7 @@ UnitTestFiles/Test/UserTests.php UnitTestFiles/Test/VehicleTests.php UnitTestFiles/Test/V5/AddressBookUnitTests.php + UnitTestFiles/Test/V5/OrderUnitTests.php UnitTestFiles/Test/V5/PodWorkflowUnitTests.php UnitTestFiles/Test/V5/RecurringRoutesUnitTests.php UnitTestFiles/Test/V5/RouteTests.php @@ -41,6 +42,7 @@ UnitTestFiles/Test/V5/AddressBookUnitTests.php + UnitTestFiles/Test/V5/OrderUnitTests.php UnitTestFiles/Test/V5/PodWorkflowUnitTests.php UnitTestFiles/Test/V5/RecurringRoutesUnitTests.php UnitTestFiles/Test/V5/RouteTests.php diff --git a/src/Route4Me/Order.php b/src/Route4Me/Order.php index 54d1b61..a48bce0 100644 --- a/src/Route4Me/Order.php +++ b/src/Route4Me/Order.php @@ -34,6 +34,7 @@ class Order extends Common public $redirect; public $optimization_problem_id; public $order_id; + public $order_uuid; public $order_ids; public $day_added_YYMMDD; @@ -103,7 +104,7 @@ public function __construct() public static function addOrder($params) { $excludeFields = ['route_id', 'redirect', 'optimization_problem_id', 'order_id', - 'order_ids', 'fields', 'offset', 'limit', 'query', 'created_timestamp', ]; + 'order_ids', 'fields', 'offset', 'limit', 'query', 'created_timestamp', 'order_uuid']; $allBodyFields = Route4Me::getObjectProperties(new self(), $excludeFields); @@ -148,7 +149,8 @@ public static function addOrder2Optimization($params) public static function getOrder($params) { - $allQueryFields = ['order_id', 'fields', 'day_added_YYMMDD', 'scheduled_for_YYMMDD', 'query', 'offset', 'limit']; + $allQueryFields = ['order_id', 'fields', 'day_added_YYMMDD', 'scheduled_for_YYMMDD', 'query', + 'offset', 'limit']; $response = Route4Me::makeRequst([ 'url' => Endpoint::ORDER_V4, @@ -215,8 +217,8 @@ public static function removeOrder($params) public static function updateOrder($params) { - $excludeFields = ['route_id', 'redirect', 'optimization_problem_id', - 'order_ids', 'fields', 'offset', 'limit', 'query', 'created_timestamp', ]; + $excludeFields = ['route_id', 'redirect', 'optimization_problem_id', 'order_ids', + 'fields', 'offset', 'limit', 'query', 'created_timestamp', 'route_uuid']; $allBodyFields = Route4Me::getObjectProperties(new self(), $excludeFields); @@ -279,8 +281,8 @@ public function addOrdersFromCsvFile($csvFileHandle, $ordersFieldsMapping) $columns = fgetcsv($csvFileHandle, $max_line_length, $delemietr); - $excludeFields = ['route_id', 'redirect', 'optimization_problem_id', 'order_id', - 'order_ids', 'fields', 'offset', 'limit', 'query', 'created_timestamp', ]; + $excludeFields = ['route_id', 'redirect', 'optimization_problem_id', 'order_id', 'order_ids', + 'fields', 'offset', 'limit', 'query', 'created_timestamp', 'order_uuid']; $allOrderFields = Route4Me::getObjectProperties(new self(), $excludeFields); diff --git a/src/Route4Me/V5/Enum/Endpoint.php b/src/Route4Me/V5/Enum/Endpoint.php index c88b89c..e21ba2c 100644 --- a/src/Route4Me/V5/Enum/Endpoint.php +++ b/src/Route4Me/V5/Enum/Endpoint.php @@ -125,4 +125,12 @@ class Endpoint const ADDRESSES_JOB_TRACKER_RESULT = self::MAIN_HOST . "/address-book/addresses/job-tracker/result"; const POD_WORKFLOW = self::MAIN_HOST . "/workflows"; + + const ORDER = self::MAIN_HOST . "/orders-platform"; + const ORDER_CREATE = self::ORDER . "/create"; + const ORDER_BATCH_CREATE = self::ORDER . "/batch-create"; + const ORDER_BATCH_DELETE = self::ORDER . "/batch-delete"; + const ORDER_BATCH_UPDATE = self::ORDER . "/batch-update"; + const ORDER_BATCH_UPDATE_FILTER = self::ORDER_BATCH_UPDATE . "/filter"; + const ORDER_CUSTOM_FIELDS = self::ORDER . "/order-custom-user-fields"; } diff --git a/src/Route4Me/V5/Orders/CustomData.php b/src/Route4Me/V5/Orders/CustomData.php new file mode 100644 index 0000000..dd1dae7 --- /dev/null +++ b/src/Route4Me/V5/Orders/CustomData.php @@ -0,0 +1,42 @@ +fillFromArray($params); + } + } +} diff --git a/src/Route4Me/V5/Orders/CustomField.php b/src/Route4Me/V5/Orders/CustomField.php new file mode 100644 index 0000000..e1e3365 --- /dev/null +++ b/src/Route4Me/V5/Orders/CustomField.php @@ -0,0 +1,52 @@ +fillFromArray($params); + } + } +} diff --git a/src/Route4Me/V5/Orders/GPSCoords.php b/src/Route4Me/V5/Orders/GPSCoords.php new file mode 100644 index 0000000..3c258d6 --- /dev/null +++ b/src/Route4Me/V5/Orders/GPSCoords.php @@ -0,0 +1,35 @@ +fillFromArray($params_or_lat); + } elseif (is_float($params_or_lat)) { + $this->lat = $params_or_lat; + $this->lng = $lng; + } + } +} diff --git a/src/Route4Me/V5/Orders/LocalTimeWindow.php b/src/Route4Me/V5/Orders/LocalTimeWindow.php new file mode 100644 index 0000000..f686178 --- /dev/null +++ b/src/Route4Me/V5/Orders/LocalTimeWindow.php @@ -0,0 +1,35 @@ +fillFromArray($params_or_start); + } elseif (is_int($params_or_start)) { + $this->start = $params_or_start; + $this->end = $end; + } + } +} diff --git a/src/Route4Me/V5/Orders/Order.php b/src/Route4Me/V5/Orders/Order.php new file mode 100644 index 0000000..44d4aa9 --- /dev/null +++ b/src/Route4Me/V5/Orders/Order.php @@ -0,0 +1,255 @@ + $value) { + if (isset($params[$key])) { + if ($key === 'local_time_windows') { + $this->{$key} = array(); + foreach ($params[$key] as $ltw_key => $ltw_value) { + if (is_array($ltw_value)) { + array_push($this->{$key}, new LocalTimeWindow($ltw_value)); + } elseif (is_object($ltw_value) && $ltw_value instanceof LocalTimeWindow) { + array_push($this->{$key}, $ltw_value); + } + } + } elseif ($key === 'custom_fields') { + $this->{$key} = array(); + foreach ($params[$key] as $cf_key => $cf_value) { + if (is_array($cf_value)) { + array_push($this->{$key}, new CustomField($cf_value)); + } elseif (is_object($cf_value) && $cf_value instanceof CustomField) { + array_push($this->{$key}, $cf_value); + } + } + } elseif ($key === 'address_geo' || $key === 'curbside_geo') { + if (is_array($params[$key])) { + $this->{$key} = new GPSCoords($params[$key]); + } elseif (is_object($params[$key]) && $params[$key] instanceof GPSCoords) { + $this->{$key} = $params[$key]; + } + } elseif ($key === 'custom_data') { + if (is_array($params[$key])) { + $this->{$key} = new CustomData($params[$key]); + } elseif (is_object($params[$key]) && $params[$key] instanceof CustomData) { + $this->{$key} = $params[$key]; + } + } else { + $this->{$key} = $params[$key]; + } + } + } + } + } +} diff --git a/src/Route4Me/V5/Orders/Orders.php b/src/Route4Me/V5/Orders/Orders.php new file mode 100644 index 0000000..e0cdacd --- /dev/null +++ b/src/Route4Me/V5/Orders/Orders.php @@ -0,0 +1,532 @@ +toResponseOrder(Route4Me::makeRequst([ + 'url' => Endpoint::ORDER_CREATE, + 'method' => 'POST', + 'HTTPHEADER' => 'Content-Type: application/json', + 'body' => Route4Me::generateRequestParameters($allBodyFields, $params_or_order) + ])); + } + + /** + * Show single order by its id + * + * @since 1.3.0 + * + * @param string $order_id - Order ID. + * @return ResponseOrder + * @throws Exception\ApiError + */ + public function get(string $order_id) : ResponseOrder + { + return $this->toResponseOrder(Route4Me::makeRequst([ + 'url' => Endpoint::ORDER . '/' . $order_id, + 'method' => 'GET' + ])); + } + + /** + * Update single order by its id + * + * @since 1.3.0 + * + * @param string $order_id - Order ID. + * @param object $params - Parameters of order to update, look for more + * information in create() + * @return ResponseOrder + * @throws Exception\ApiError + */ + public function update(string $order_id, $params) : ResponseOrder + { + $allBodyFields = ['member_id', 'address_1', 'address_2', 'address_alias', + 'address_city', 'address_state', 'address_zip', 'address_country', 'address_geo', 'curbside_geo', + 'date_scheduled_for', 'order_status_id', 'is_pending', 'is_accepted', + 'is_started', 'is_completed', 'is_validated', 'phone', 'first_name', + 'last_name', 'email', 'custom_data', 'local_time_windows', 'local_timezone_string', + 'service_time', 'color', 'tracking_number', 'address_stop_type', 'last_status', 'weight', + 'cost', 'revenue', 'cube', 'pieces', 'group', 'address_priority', + 'address_customer_po', 'custom_fields' + ]; + + return $this->toResponseOrder(Route4Me::makeRequst([ + 'url' => Endpoint::ORDER . '/' . $order_id, + 'method' => 'PUT', + 'HTTPHEADER' => 'Content-Type: application/json', + 'body' => Route4Me::generateRequestParameters($allBodyFields, $params) + ])); + } + + /** + * Delete (soft) single order by its id + * + * @since 1.3.0 + * + * @param string $order_id - Order ID. + * @return bool + * @throws Exception\ApiError + */ + public function delete(string $order_id) : bool + { + $res = Route4Me::makeRequst([ + 'url' => Endpoint::ORDER . '/' . $order_id, + 'method' => 'DELETE' + ]); + return (isset($res['status']) ? $res['status'] : false); + } + + /** + * Search orders in ElasticSearch storage or in Spanner database + * + * @since 1.3.0 + * + * @param $params - Search and filter parameters. + * string[] [order_ids] - Array of order ids, HEX-Strings. + * bool return_provided_fields_as_map + * array [orderBy] - Sort and direction parameters. + * string 0 - The name of the sort field, this is one of + * 'address_alias', 'first_name', 'last_name', 'phone', + * 'is_pending', 'is_validated', 'is_accepted', + * 'is_completed', 'scheduled_for', 'day_added' + * string [1 = 'asc'] - Sorting direction, this is one of 'asc', 'ASC', 'desc', 'DESC' + * int [limit = 30] - The number of orders per page. + * int [offset = 0] - The requested page. + * string[] [fields] - An array of returned fields, this is one of + * 'order_uuid', 'member_id', 'address_1', 'address_2', + * 'address_alias', 'address_city', 'address_state', 'address_zip', + * 'address_country', 'coordinates', 'curbside_coordinates', + * 'updated_timestamp', 'created_timestamp', 'day_added', + * 'scheduled_for', 'order_status_id', 'is_pending', 'is_started', + * 'is_completed', 'is_validated', 'phone', 'first_name', 'last_name', + * 'email', 'custom_data', 'local_time_windows', 'local_timezone', + * 'service_time', 'color', 'icon', 'last_visited_timestamp', + * 'visited_count', 'in_route_count', 'last_routed_timestamp', + * 'tracking_number', 'organization_id', 'root_member_id', + * 'address_stop_type', 'last_status', 'sorted_day_id', 'weight', + * 'cost', 'revenue', 'cube', 'pieces', 'done_day_id', + * 'possession_day_id', 'group', 'workflow_uuid', 'address_priority' + * string[] [addition] - An array of additional returned fields, this is one of + * 'territory_ids', 'aggregation_ids' + * array [search] - Search parameters. + * string [query] - The string to query to ElasticSearch. If set the `matches` and + * `terms` sections will be ignored. + * array [matches] - The object to query to ElasticSearch. + * array [custom_data] - Order custom data. + * string [barcode] - Tracking number for order. + * string [airbillno] - Additional tracking number for order. + * string [sorted_on_date] - Datetime String with "T" delimiter, ISO 8601. + * int [sorted_on_utc] - Timestamp only; replaced data in `sorted_on_date` property. + * string [first_name] - The first name. + * string [last_name] - The last name. + * string [email] - E-mail. + * string [phone] - The phone number. + * string [address_1] - The order Address line 1. + * string [address_alias] - Address alias. + * string [address_zip] - The zip code of the address. + * array [terms] - The object to query to ElasticSearch. + * array [custom_data] - Order custom data. + * string [barcode] - Tracking number for order. + * string [airbillno] - Additional tracking number for order. + * string [sorted_on_date] - Datetime String with "T" delimiter, ISO 8601. + * int [sorted_on_utc] - Timestamp only; replaced data in `sorted_on_date` property. + * string [first_name] - The first name. + * string [last_name] - The last name. + * string [email] - E-mail. + * string [phone] - The phone number. + * string [address_1] - The order Address line 1. + * string [address_alias] - Address alias. + * string [address_zip] - The zip code the address is located in. + * array [filters] - Filter parameters. + * string[] [order_ids] - Array of included order ids, HEX-Strings. + * string[] [excluded_ids] - Array of excluded order ids, HEX-Strings. + * string[] [tracking_numbers] - Array of tracking number of orders. + * bool [only_geocoded] + * int|string|array [updated_timestamp] - Can be unix timestamp or ISO 8601 or array [ + * "start" => "timestamp or ISO 8601", + * "end" => "timestamp or ISO 8601" + * ] + * int|string|array [created_timestamp] - Can be unix timestamp or ISO 8601 or array [ + * "start" => "timestamp or ISO 8601", + * "end" => "timestamp or ISO 8601" + * ] + * int|string|array [scheduled_for] - Can be unix timestamp or ISO 8601 or array [ + * "start" => "timestamp or ISO 8601", + * "end" => "timestamp or ISO 8601" + * ] + * bool [only_unscheduled] + * int|string|array [day_added] - Can be unix timestamp or ISO 8601 or array [ + * "start" => "timestamp or ISO 8601", + * "end" => "timestamp or ISO 8601" + * ] + * int|string|array [sorted_on] - Can be unix timestamp or ISO 8601 or array [ + * "start" => "timestamp or ISO 8601", + * "end" => "timestamp or ISO 8601" + * ] + * string[] [address_stop_types] - Array of stop type names, possible values + * 'DELIVERY', 'PICKUP', 'BREAK', 'MEETUP', + * 'SERVICE', 'VISIT' or 'DRIVEBY'. + * int[] [last_statuses] - Array of statuses. + * int[] [territory_ids] - Array of territory ids. + * string [done_day] + * string [possession_day] + * string[] [groups] + * string [display= 'all'] - Filtering by the in_route_count field, is one of + * 'routed', 'unrouted', 'all' + * @return ResponseSearch + * @throws Exception\ApiError + */ + public function search(?array $params = null) : ResponseSearch + { + $allBodyFields = ['order_ids', 'return_provided_fields_as_map', 'orderBy', 'limit', + 'offset', 'fields', 'addition', 'search', 'filters' + ]; + + $result = Route4Me::makeRequst([ + 'url' => Endpoint::ORDER, + 'method' => 'POST', + 'HTTPHEADER' => 'Content-Type: application/json', + 'body' => ($params ? Route4Me::generateRequestParameters($allBodyFields, $params) : []) + ]); + + if (isset($result)) { + return new ResponseSearch($result); + } + return []; + } + + /** + * Update the batch of orders (asynchronous, by filters) + * + * @since 1.3.0 + * + * @param array $params - Batch update parameters. + * array data - Order values for batch update, look for more + * information in create() + * array search - Search parameters for batch update, + * look for more information in search() + * array filters - Filter parameters for batch update, + * look for more information in search() + * @return bool + * @throws Exception\ApiError + */ + public function batchUpdateByFilters(array $params) : bool + { + $allBodyFields = ['data', 'search', 'filters']; + + $res = Route4Me::makeRequst([ + 'url' => Endpoint::ORDER_BATCH_UPDATE_FILTER, + 'method' => 'PUT', + 'HTTPHEADER' => 'Content-Type: application/json', + 'body' => Route4Me::generateRequestParameters($allBodyFields, $params) + ]); + return (isset($res['success']) && $res['success'] == 1 ? true : false); + } + + /** + * Delete the batch of orders + * + * @since 1.3.0 + * + * @param string[] $orderIds - Array of Order IDs, HEX-Strings. + * @return bool + * @throws Exception\ApiError + */ + public function batchDelete(array $orderIds) : bool + { + $res = Route4Me::makeRequst([ + 'url' => Endpoint::ORDER_BATCH_DELETE, + 'method' => 'POST', + 'HTTPHEADER' => 'Content-Type: application/json', + 'body' => ['order_ids' => $orderIds] + ]); + return (isset($res['status']) ? $res['status'] : false); + } + + /** + * Update the batch of orders by ids + * + * @since 1.3.0 + * + * @param string[] $orderIds - Array of Order IDs, HEX-Strings. + * @param array $data - Order values for batch update, + * look for more information in create() + * @return ResponseOrder[] + * @throws Exception\ApiError + */ + public function batchUpdate(array $orderIds, $data) : array + { + $allBodyFields = ['member_id', 'address_1', 'address_2', 'address_alias', + 'address_city', 'address_state', 'address_zip', 'address_country', 'address_geo', 'curbside_geo', + 'date_scheduled_for', 'order_status_id', 'is_pending', 'is_accepted', + 'is_started', 'is_completed', 'is_validated', 'phone', 'first_name', + 'last_name', 'email', 'custom_data', 'local_time_windows', 'local_timezone_string', + 'service_time', 'color', 'tracking_number', 'address_stop_type', 'last_status', 'weight', + 'cost', 'revenue', 'cube', 'pieces', 'group', 'address_priority', + 'address_customer_po', 'custom_fields' + ]; + + $res = Route4Me::makeRequst([ + 'url' => Endpoint::ORDER_BATCH_UPDATE, + 'method' => 'POST', + 'HTTPHEADER' => 'Content-Type: application/json', + 'body' => [ + 'order_ids' => $orderIds, + 'data' => Route4Me::generateRequestParameters($allBodyFields, $data) + ] + ]); + + $orders = []; + if (is_array($res)) { + foreach ($res as $key => $value) { + $orders[] = new ResponseOrder($value); + } + } + return $orders; + } + + /** + * Create the batch of orders + * + * @since 1.3.0 + * + * @param array $orders - Array of Orders or of array. + * look for more information in create() + * @return bool + * @throws Exception\ApiError + */ + public function batchCreate(array $orders) : bool + { + $allBodyFields = ['member_id', 'address_1', 'address_2', 'address_alias', + 'address_city', 'address_state', 'address_zip', 'address_country', 'address_geo', 'curbside_geo', + 'date_scheduled_for', 'order_status_id', 'is_pending', 'is_accepted', + 'is_started', 'is_completed', 'is_validated', 'phone', 'first_name', + 'last_name', 'email', 'custom_data', 'local_time_windows', 'local_timezone_string', + 'service_time', 'color', 'tracking_number', 'address_stop_type', 'last_status', 'weight', + 'cost', 'revenue', 'cube', 'pieces', 'group', 'address_priority', + 'address_customer_po', 'custom_fields' + ]; + + $body = []; + foreach ($orders as $key => $order) { + $body[] = Route4Me::generateRequestParameters($allBodyFields, $order); + } + + $res = Route4Me::makeRequst([ + 'url' => Endpoint::ORDER_BATCH_CREATE, + 'method' => 'POST', + 'HTTPHEADER' => 'Content-Type: application/json', + 'body' => ['data' => $body] + ]); + return (isset($res['status']) ? $res['status'] : false); + } + + /** + * Get a list of Order Custom Fields + * + * @since 1.3.0 + * + * @return CustomField[] + * @throws Exception\ApiError + */ + public function getOrderCustomFields() : array + { + $res = Route4Me::makeRequst([ + 'url' => Endpoint::ORDER_CUSTOM_FIELDS, + 'method' => 'GET' + ]); + if (isset($result) && isset($result['data']) && is_array($result['data']) && is_array($result['data'][0])) { + return new ResponseOrder($result['data'][0]); + } + $ocf = []; + if (isset($res) && isset($res['data']) && is_array($res['data'])) { + foreach ($res['data'] as $key => $value) { + $ocf[] = new CustomField($value); + } + } + return $ocf; + } + + /** + * Create one Order Custom Field + * + * @since 1.3.0 + * + * @param array $params_or_custom_field - Params of CustomField custom field + * string data.order_custom_field_name - Name, max 128 characters. + * string data.order_custom_field_type - Type, max 128 characters. + * string data.order_custom_field_label - Label, max 128 characters. + * array data.order_custom_field_type_info - Info, as JSON Object max 4096 characters. + * @return CustomField + * @throws Exception\ApiError + */ + public function createOrderCustomField($params_or_custom_field) : CustomField + { + $allBodyFields = ['order_custom_field_name', 'order_custom_field_type', + 'order_custom_field_label', 'order_custom_field_type_info']; + + return $this->toCustomField(Route4Me::makeRequst([ + 'url' => Endpoint::ORDER_CUSTOM_FIELDS, + 'method' => 'POST', + 'HTTPHEADER' => 'Content-Type: application/json', + 'body' => Route4Me::generateRequestParameters($allBodyFields, $params_or_custom_field) + ])); + } + + /** + * Update one Order Custom Fields + * + * @since 1.3.0 + * + * @param array $uuid - OrderCustomField ID, HEX-string. + * @param array $params_or_custom_field - Params of Order custom field + * string data.order_custom_field_type - Type, max 128 characters. + * string data.order_custom_field_label - Label, max 128 characters. + * array data.order_custom_field_type_info - Info, as JSON Object max 4096 characters. + * @return CustomField + * @throws Exception\ApiError + */ + public function updateOrderCustomField(string $uuid, $params_or_custom_field) : CustomField + { + $allBodyFields = ['order_custom_field_type', 'order_custom_field_label', 'order_custom_field_type_info']; + + return $this->toCustomField(Route4Me::makeRequst([ + 'url' => Endpoint::ORDER_CUSTOM_FIELDS . '/' . $uuid, + 'method' => 'PUT', + 'HTTPHEADER' => 'Content-Type: application/json', + 'body' => Route4Me::generateRequestParameters($allBodyFields, $params_or_custom_field) + ])); + } + + /** + * Delete an Order Custom Fields + * + * @since 1.3.0 + * + * @param array $uuid - OrderCustomField ID, HEX-string. + * @return CustomField + * @throws Exception\ApiError + */ + public function deleteOrderCustomField(string $uuid) : CustomField + { + return $this->toCustomField(Route4Me::makeRequst([ + 'url' => Endpoint::ORDER_CUSTOM_FIELDS . '/' . $uuid, + 'method' => 'DELETE' + ])); + } + + private function toResponseOrder($result) : ResponseOrder + { + if (is_array($result)) { + return new ResponseOrder($result); + } + throw new ApiError('Can not convert result to ResponseOrder object.'); + } + + private function toCustomField($result) : CustomField + { + if (isset($result) && isset($result['data']) && is_array($result['data'])) { + return new CustomField($result['data']); + } + throw new ApiError('Can not convert result to CustomField object.'); + } +} diff --git a/src/Route4Me/V5/Orders/ResponseOrder.php b/src/Route4Me/V5/Orders/ResponseOrder.php new file mode 100644 index 0000000..e05b8dd --- /dev/null +++ b/src/Route4Me/V5/Orders/ResponseOrder.php @@ -0,0 +1,309 @@ + $value) { + if (isset($params[$key])) { + if ($key === 'local_time_windows') { + $this->{$key} = array(); + foreach ($params[$key] as $ltw_key => $ltw_value) { + if (is_array($ltw_value)) { + array_push($this->{$key}, new LocalTimeWindow($ltw_value)); + } elseif (is_object($ltw_value) && $ltw_value instanceof LocalTimeWindow) { + array_push($this->{$key}, $ltw_value); + } + } + } elseif ($key === 'custom_user_fields') { + $this->{$key} = array(); + foreach ($params[$key] as $cf_key => $cf_value) { + if (is_array($cf_value)) { + array_push($this->{$key}, new CustomField($cf_value)); + } elseif (is_object($cf_value) && $cf_value instanceof CustomField) { + array_push($this->{$key}, $cf_value); + } + } + } elseif ($key === 'address_geo' || $key === 'curbside_geo') { + if (is_array($params[$key])) { + $this->{$key} = new GPSCoords($params[$key]); + } elseif (is_object($params[$key]) && $params[$key] instanceof GPSCoords) { + $this->{$key} = $params[$key]; + } + } elseif ($key === 'custom_data') { + if (is_array($params[$key])) { + $this->{$key} = new CustomData($params[$key]); + } elseif (is_object($params[$key]) && $params[$key] instanceof CustomData) { + $this->{$key} = $params[$key]; + } + } else { + $this->{$key} = $params[$key]; + } + } + } + } + } +} diff --git a/src/Route4Me/V5/Orders/ResponseSearch.php b/src/Route4Me/V5/Orders/ResponseSearch.php new file mode 100644 index 0000000..03881ac --- /dev/null +++ b/src/Route4Me/V5/Orders/ResponseSearch.php @@ -0,0 +1,54 @@ + $value) { + if (isset($params[$key])) { + if ($key === 'results') { + $this->{$key} = array(); + foreach ($params[$key] as $r_key => $r_value) { + if (is_array($r_value)) { + array_push($this->{$key}, new ResponseOrder($r_value)); + } elseif (is_object($r_value) && $r_value instanceof ResponseOrder) { + array_push($this->{$key}, $r_value); + } + } + } else { + $this->{$key} = $params[$key]; + } + } + } + } + } +}