diff --git a/.env.dist b/.env.dist index f6d35eec..09f03372 100644 --- a/.env.dist +++ b/.env.dist @@ -80,3 +80,5 @@ API_VERSION=insert_api_version_here #SSL_ENABLE for websockets SSL_ENABLE=set_to_true_or_false + +HUB_JO_KEY=hub_jo_count diff --git a/config/services.yaml b/config/services.yaml index fb4870ba..12ee9e94 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -275,3 +275,22 @@ services: App\Service\PromoLogger: arguments: $em: "@doctrine.orm.entity_manager" + + # hub service + App\Service\HubSelector: + arguments: + $em: "@doctrine.orm.entity_manager" + $im: "@App\\Service\\InventoryManager" + $hub_distributor: "@App\\Service\\HubDistributor" + $hub_filter_logger: "@App\\Service\\HubFilterLogger" + + # hub distributor + App\Service\HubDistributor: + arguments: + $redis: "@App\\Service\\RedisClientProvider" + $hub_jo_key: "%env(HUB_JO_KEY)%" + + # hub filter logger + App\Service\HubFilterLogger: + arguments: + $em: "@doctrine.orm.entity_manager" diff --git a/src/Command/ResetHubJoCountCommand.php b/src/Command/ResetHubJoCountCommand.php new file mode 100644 index 00000000..070acf27 --- /dev/null +++ b/src/Command/ResetHubJoCountCommand.php @@ -0,0 +1,40 @@ +redis = $redis->getRedisClient(); + + parent::__construct(); + } + + protected function configure() + { + $this->setName('hub:jo:reset') + ->setDescription('Reset hub\'s job order count') + ->setHelp('Reset hub\'s job order count'); + } + + + protected function execute(InputInterface $input, OutputInterface $output) + { + $key = 'hub_jo_count'; + + $this->redis->del($key); + + return 0; + } +} + diff --git a/src/Controller/APIController.php b/src/Controller/APIController.php index 6c275ead..4e636e14 100644 --- a/src/Controller/APIController.php +++ b/src/Controller/APIController.php @@ -29,6 +29,7 @@ use App\Ramcar\JOEventType; use App\Ramcar\AdvanceOrderSlot; use App\Ramcar\AutoAssignStatus; use App\Ramcar\WarrantySource; +use App\Ramcar\HubCriteria; use App\Service\InvoiceGeneratorInterface; use App\Service\RisingTideGateway; @@ -40,6 +41,9 @@ use App\Service\InventoryManager; use App\Service\RiderAssignmentHandlerInterface; use App\Service\WarrantyAPILogger; use App\Service\PromoLogger; +use App\Service\HubSelector; +use App\Service\HubDistributor; +use App\Service\HubFilterLogger; use App\Entity\MobileSession; use App\Entity\Customer; @@ -847,7 +851,8 @@ class APIController extends Controller implements LoggedController public function requestJobOrder(Request $req, InvoiceGeneratorInterface $ic, GeofenceTracker $geo, MapTools $map_tools, InventoryManager $im, MQTTClient $mclient, - RiderAssignmentHandlerInterface $rah, PromoLogger $promo_logger) + RiderAssignmentHandlerInterface $rah, PromoLogger $promo_logger, + HubSelector $hub_select, HubDistributor $hub_dist, HubFilterLogger $hub_filter_logger) { // check required parameters and api key $required_params = [ @@ -1012,49 +1017,68 @@ class APIController extends Controller implements LoggedController $invoice = $ic->generateInvoice($icrit); $jo->setInvoice($invoice); - // assign hub and rider - if (($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) || - ($jo->getServicetype() == ServiceType::BATTERY_REPLACEMENT_WARRANTY)) - { - // get nearest hub - // $nearest_hub = $this->findNearestHubWithInventory($jo, $batt, $em, $map_tools, $im); - $nearest_hub = $this->findNearestHub($jo, $em, $map_tools); - } - else - { - $nearest_hub = $this->findNearestHub($jo, $em, $map_tools); - } + // TODO: set this properly, since the other flags + // are on default values + $hub_criteria = new HubCriteria(); + $hub_criteria->setPoint($jo->getCoordinates()) + ->setJoType($jo->getServiceType()); - if (!empty($nearest_hub)) + // add battery to items + $sku = $batt->getSAPCode(); + if (!empty($sku)) + $hub_criteria->addItem($batt->getSAPCode(), 1); + + // find nearest hubs + $nearest_hubs = $hub_select->find($hub_criteria); + + if (!empty($nearest_hubs)) { + // go through the hub list, find the nearest hub + // with an available rider //error_log('found nearest hub ' . $nearest_hub->getID()); - // assign rider - $available_riders = $nearest_hub->getAvailableRiders(); - if (count($available_riders) > 0) + foreach ($nearest_hubs as $nearest_hub) { - $assigned_rider = null; - if (count($available_riders) > 1) + $available_riders = $nearest_hub['hub']->getAvailableRiders(); + if (count($available_riders) >= 1) { - // TODO: the setting of riders into an array - // will no longer be necessary when the contents - // of randomizeRider changes - $riders = []; - foreach ($available_riders as $rider) + $assigned_rider = null; + if (count($available_riders) == 1) { - $riders[] = $rider; + $assigned_rider = $available_riders[0]; + } + else + { + // TODO: the setting of riders into an array + // will no longer be necessary when the contents + // of randomizeRider changes + $riders = []; + foreach ($available_riders as $rider) + { + $riders[] = $rider; + } + + $assigned_rider = $this->randomizeRider($riders); } - $assigned_rider = $this->randomizeRider($riders); + $jo->setHub($nearest_hub['hub']); + $jo->setRider($assigned_rider); + $jo->setStatus(JOStatus::ASSIGNED); + $jo->setStatusAutoAssign(AutoAssignStatus::HUB_AND_RIDER_ASSIGNED); + + $assigned_rider->setAvailable(false); + + // update redis hub_jo_count for hub + $hub_dist->incrementJoCountForHub($nearest_hub['hub']); + + // break out of loop + break; } else - $assigned_rider = $available_riders[0]; - - //error_log('found rider ' . $assigned_rider->getID()); - $jo->setHub($nearest_hub); - $jo->setRider($assigned_rider); - $jo->setStatus(JOStatus::ASSIGNED); - - $assigned_rider->setAvailable(false); + { + // log hub into hub_filter_log + $hub_filter_logger->logFilteredHub($nearest_hub['hub'], 'no_available_rider'); + // continue to go through list to find hub with an available rider + } } } @@ -2364,7 +2388,8 @@ class APIController extends Controller implements LoggedController public function newRequestJobOrder(Request $req, InvoiceGeneratorInterface $ic, GeofenceTracker $geo, MapTools $map_tools, InventoryManager $im, MQTTClient $mclient, - RiderAssignmentHandlerInterface $rah, PromoLogger $promo_logger) + RiderAssignmentHandlerInterface $rah, PromoLogger $promo_logger, + HubSelector $hub_select, HubDistributor $hub_dist, HubFilterLogger $hub_filter_logger) { // check required parameters and api key $required_params = [ @@ -2549,50 +2574,68 @@ class APIController extends Controller implements LoggedController // check if hub is null if ($hub == null) { - // find nearest hub - if (($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) || - ($jo->getServicetype() == ServiceType::BATTERY_REPLACEMENT_WARRANTY)) - { - // get nearest hub - // $nearest_hub = $this->findNearestHubWithInventory($jo, $batt, $em, $map_tools, $im); - $nearest_hub = $this->findNearestHub($jo, $em, $map_tools); - } - else - { - $nearest_hub = $this->findNearestHub($jo, $em, $map_tools); - } + // TODO: set this properly, since the other flags + // are on default values + $hub_criteria = new HubCriteria(); + $hub_criteria->setPoint($jo->getCoordinates()) + ->setJoType($jo->getServiceType()); - if (!empty($nearest_hub)) + // add battery to items + $sku = $batt->getSAPCode(); + if (!empty($sku)) + $hub_criteria->addItem($batt->getSAPCode(), 1); + + // find nearest hubs + $nearest_hubs = $hub_select->find($hub_criteria); + + if (!empty($nearest_hubs)) { + // go through the hub list, find the nearest hub + // with an available rider //error_log('found nearest hub ' . $nearest_hub->getID()); - // assign rider - $available_riders = $nearest_hub->getAvailableRiders(); - if (count($available_riders) > 0) + foreach ($nearest_hubs as $nearest_hub) { - $assigned_rider = null; - if (count($available_riders) > 1) + $available_riders = $nearest_hub['hub']->getAvailableRiders(); + if (count($available_riders) >= 1) { - // TODO: the setting of riders into an array - // will no longer be necessary when the contents - // of randomizeRider changes - $riders = []; - foreach ($available_riders as $rider) + $assigned_rider = null; + if (count($available_riders) == 1) { - $riders[] = $rider; + $assigned_rider = $available_riders[0]; + } + else + { + // TODO: the setting of riders into an array + // will no longer be necessary when the contents + // of randomizeRider changes + $riders = []; + foreach ($available_riders as $rider) + { + $riders[] = $rider; + } + + $assigned_rider = $this->randomizeRider($riders); } - $assigned_rider = $this->randomizeRider($riders); + $jo->setHub($nearest_hub['hub']); + $jo->setRider($assigned_rider); + $jo->setStatus(JOStatus::ASSIGNED); + $jo->setStatusAutoAssign(AutoAssignStatus::HUB_AND_RIDER_ASSIGNED); + + $assigned_rider->setAvailable(false); + + // update redis hub_jo_count for hub + $hub_dist->incrementJoCountForHub($nearest_hub['hub']); + + // break out of loop + break; } else - $assigned_rider = $available_riders[0]; - - //error_log('found rider ' . $assigned_rider->getID()); - $jo->setHub($nearest_hub); - $jo->setRider($assigned_rider); - $jo->setStatus(JOStatus::ASSIGNED); - $jo->setStatusAutoAssign(AutoAssignStatus::HUB_AND_RIDER_ASSIGNED); - - $assigned_rider->setAvailable(false); + { + // log hub into hub_filter_log + $hub_filter_logger->logFilteredHub($nearest_hub['hub'], 'no_available_rider'); + // continue to go through list to find hub with an available rider + } } } } @@ -2615,7 +2658,10 @@ class APIController extends Controller implements LoggedController $jo->setStatusAutoAssign(AutoAssignStatus::HUB_ASSIGNED); if ($date_schedule != null) - $jo->setDateSchedule($date_schedule); + $jo->setDateSchedule($date_schedule); + + // update redis hub_jo_count for hub + $hub_dist->incrementJoCountForHub($hub); } $em->persist($jo); diff --git a/src/Controller/HubController.php b/src/Controller/HubController.php index edc6e599..21c4758d 100644 --- a/src/Controller/HubController.php +++ b/src/Controller/HubController.php @@ -17,8 +17,12 @@ use DateTime; use Catalyst\MenuBundle\Annotation\Menu; -use App\Service\MapTools; use App\Service\RiderTracker; +use App\Service\HubSelector; +use App\Service\RisingTideGateway; + +use App\Ramcar\HubCriteria; +use App\Ramcar\ModeOfPayment; class HubController extends Controller { @@ -128,6 +132,7 @@ class HubController extends Controller $params = []; $params['obj'] = new Hub(); $params['mode'] = 'create'; + $params['payment_methods'] = ModeOfPayment::getCollection(); // response return $this->render('hub/form.html.twig', $params); @@ -154,7 +159,17 @@ class HubController extends Controller ->setBranchCode($req->request->get('branch_code', '')) ->setStatusOpen($req->request->get('status_open', false)) ->setRiderSlots($req->request->get('rider_slots', 0)) - ->setHubViewFlag($req->request->get('flag_hub_view', false)); + ->setHubViewFlag($req->request->get('flag_hub_view', false)) + ->setNotifNumber($req->request->get('notif_number')) + ->clearPaymentMethods(); + + // set payment methods + $payment_methods = $req->request->get('payment_methods'); + + if (!empty($payment_methods)) + { + $obj->setPaymentMethods($payment_methods); + } } protected function setQueryFilters($datatable, QueryBuilder $query) @@ -166,7 +181,7 @@ class HubController extends Controller } } - public function addSubmit(Request $req, EncoderFactoryInterface $ef, ValidatorInterface $validator) + public function addSubmit(Request $req, EncoderFactoryInterface $ef, ValidatorInterface $validator, RisingTideGateway $rt) { $this->denyAccessUnlessGranted('hub.add', null, 'No access.'); @@ -176,14 +191,23 @@ class HubController extends Controller $em = $this->getDoctrine()->getManager(); $obj = new Hub(); + // initialize error list + $error_array = []; + + // validate the notification number + $mobile = $req->request->get('notif_number'); + if (!empty($mobile)) + { + $is_valid = $rt->validatePhoneNumber($mobile); + if (!$is_valid) + $error_array['notif_number'] = 'Invalid notification number'; + } + $this->setObject($obj, $req); // validate $errors = $validator->validate($obj); - // initialize error list - $error_array = []; - // add errors to list foreach ($errors as $error) { $error_array[$error->getPropertyPath()] = $error->getMessage(); @@ -226,12 +250,14 @@ class HubController extends Controller $params = []; $params['obj'] = $obj; $params['mode'] = 'update'; + $params['payment_methods'] = ModeOfPayment::getCollection(); // response return $this->render('hub/form.html.twig', $params); } - public function updateSubmit(Request $req, EncoderFactoryInterface $ef, ValidatorInterface $validator, $id) + public function updateSubmit(Request $req, EncoderFactoryInterface $ef, ValidatorInterface $validator, $id, + RisingTideGateway $rt) { $this->denyAccessUnlessGranted('hub.update', null, 'No access.'); @@ -243,14 +269,23 @@ class HubController extends Controller if (empty($obj)) throw $this->createNotFoundException('The item does not exist'); + // initialize error list + $error_array = []; + + // validate the notification number + $mobile = $req->request->get('notif_number'); + if (!empty($mobile)) + { + $is_valid = $rt->validatePhoneNumber($mobile); + if (!$is_valid) + $error_array['notif_number'] = 'Invalid notification number'; + } + $this->setObject($obj, $req); // validate $errors = $validator->validate($obj); - // initialize error list - $error_array = []; - // add errors to list foreach ($errors as $error) { $error_array[$error->getPropertyPath()] = $error->getMessage(); @@ -295,7 +330,7 @@ class HubController extends Controller $response->send(); } - public function nearest(MapTools $map_tools, Request $req) + public function nearest(HubSelector $hub_selector, Request $req) { // get lat / long $lat = $req->query->get('lat'); @@ -303,7 +338,11 @@ class HubController extends Controller // get nearest hubs according to position $point = new Point($long, $lat); - $result = $map_tools->getClosestHubs($point, 10, date("H:i:s")); + + // set up hub criteria + $hub_criteria = new HubCriteria(); + $hub_criteria->setPoint($point); + $hub_selector->find($hub_criteria); $hubs = []; foreach ($result as $hub_res) diff --git a/src/Controller/JobOrderController.php b/src/Controller/JobOrderController.php index 11c7edf0..fbab3cb2 100644 --- a/src/Controller/JobOrderController.php +++ b/src/Controller/JobOrderController.php @@ -23,6 +23,7 @@ use App\Service\MapTools; use App\Service\MQTTClient; use App\Service\APNSClient; use App\Service\InventoryManager; +use App\Service\HubSelector; use App\Service\RiderTracker; use App\Service\MotivConnector; @@ -315,13 +316,13 @@ class JobOrderController extends Controller /** * @Menu(selected="jo_proc") */ - public function processingForm(MapTools $map_tools, $id, JobOrderHandlerInterface $jo_handler, GISManagerInterface $gis, MotivConnector $motiv) + public function processingForm(HubSelector $hub_selector, $id, JobOrderHandlerInterface $jo_handler, GISManagerInterface $gis, MotivConnector $motiv) { $this->denyAccessUnlessGranted('jo_proc.list', null, 'No access.'); try { - $params = $jo_handler->initializeProcessingForm($id, $map_tools, $motiv); + $params = $jo_handler->initializeProcessingForm($id, $hub_selector, $motiv); } catch (AccessDeniedHttpException $e) { @@ -504,14 +505,14 @@ class JobOrderController extends Controller /** * @Menu(selected="jo_open") */ - public function openHubForm(MapTools $map_tools, $id, JobOrderHandlerInterface $jo_handler, + public function openHubForm(HubSelector $hub_selector, $id, JobOrderHandlerInterface $jo_handler, GISManagerInterface $gis) { $this->denyAccessUnlessGranted('jo_open.list', null, 'No access.'); try { - $params = $jo_handler->initializeHubForm($id, $map_tools); + $params = $jo_handler->initializeHubForm($id, $hub_selector); } catch (NotFoundHttpException $e) { @@ -1207,6 +1208,8 @@ class JobOrderController extends Controller /** * @Menu(selected="jo_autoassign") */ + // this is uncalled or does not display in admin panel + /* public function autoAssignForm(GISManagerInterface $gis, JobOrderHandlerInterface $jo_handler) { $this->denyAccessUnlessGranted('jo_autoassign.test', null, 'No access.'); @@ -1375,6 +1378,6 @@ class JobOrderController extends Controller $selected_rider = $riders[$selected_index]; return $selected_rider; - } + } */ } diff --git a/src/Entity/Hub.php b/src/Entity/Hub.php index 0b1769b2..2d742b73 100644 --- a/src/Entity/Hub.php +++ b/src/Entity/Hub.php @@ -68,6 +68,18 @@ class Hub */ protected $flag_hub_view; + // notification number to send SMS to if hub doesn't have item + /** + * @ORM\Column(type="string", length=30) + */ + protected $notif_number; + + // payment methods + /** + * @ORM\Column(type="array", nullable=true) + */ + protected $payment_methods; + public function __construct() { $this->time_open = new DateTime(); @@ -76,6 +88,8 @@ class Hub $this->outlets = new ArrayCollection(); $this->status_open = true; $this->flag_hub_view = false; + $this->notif_number = ''; + $this->payment_methods = new ArrayCollection(); } public function getRiders() @@ -185,5 +199,37 @@ class Hub return $this->flag_hub_view; } + public function setNotifNumber($notif_number) + { + $this->notif_number = $notif_number; + return $this; + } + public function getNotifNumber() + { + return $this->notif_number; + } + + public function getPaymentMethods() + { + return $this->payment_methods; + } + + public function setPaymentMethods(array $payment_methods) + { + $this->payment_methods = new ArrayCollection(); + + foreach ($payment_methods as $payment_method) + { + $this->payment_methods->add($payment_method); + } + + return $this; + } + + public function clearPaymentMethods() + { + $this->payment_methods = new ArrayCollection(); + return $this; + } } diff --git a/src/Entity/HubFilterLog.php b/src/Entity/HubFilterLog.php new file mode 100644 index 00000000..2c4d8584 --- /dev/null +++ b/src/Entity/HubFilterLog.php @@ -0,0 +1,84 @@ +date_create = new DateTime(); + } + + public function getID() + { + return $this->id; + } + + public function getDateCreate() + { + return $this->date_create; + } + + public function setHub(Hub $hub) + { + $this->hub = $hub; + return $this; + } + + public function getHub() + { + return $this->hub; + } + + public function setFilterTypeId($filter_type_id) + { + $this->filter_type_id = $filter_type_id; + return $this; + } + + public function getFilterTypeId() + { + return $this->filter_type_id; + } +} + diff --git a/src/Ramcar/HubCriteria.php b/src/Ramcar/HubCriteria.php new file mode 100644 index 00000000..cd10325d --- /dev/null +++ b/src/Ramcar/HubCriteria.php @@ -0,0 +1,135 @@ +limit_results = 10; + $this->limit_distance = 500; + $this->jo_type = ''; + $this->date_time = null; + $this->has_inventory = false; + $this->items = []; + $this->payment_method = ''; + $flag_emergency = false; + } + + public function setPoint(Point $point) + { + $this->point = $point; + return $this; + } + + public function getPoint() + { + return $this->point; + } + + public function setLimitResults($limit_results) + { + $this->limit_results = $limit_results; + return $this; + } + + public function getLimitResults() + { + return $this->limit_results; + } + + public function setLimitDistance($limit_distance) + { + $this->limit_distance = $limit_distance; + return $this; + } + + public function getLimitDistance() + { + return $this->limit_distance; + } + + public function setInventoryCheck($flag_inventory_check = true) + { + $this->flag_inventory_check = $flag_inventory_check; + return $this; + } + + public function hasInventoryCheck() + { + return $this->flag_inventory_check; + } + + public function setJoType($jo_type) + { + // TODO: validate the jo type + $this->jo_type = $jo_type; + return $this; + } + + public function getJoType() + { + return $this->jo_type; + } + + public function setDateTime(DateTime $date_time) + { + $this->date_time = $date_time; + return $this; + } + + public function getDateTime() + { + return $this->date_time; + } + + public function addItem($sku, $quantity) + { + // at this point, sku is assumed to be a valid item + $this->items[$sku] = $quantity; + return $this; + } + + public function getItems() + { + return $this->items; + } + + public function setPaymentMethod($payment_method) + { + $this->payment_method = $payment_method; + return $this; + } + + public function getPaymentMethod() + { + return $this->payment_method; + } + + public function setEmergency($flag_emergency = true) + { + $this->flag_emergency = $flag_emergency; + return $this; + } + + public function isEmergency() + { + return $this->flag_emergency; + } +} + diff --git a/src/Service/HubDistributor.php b/src/Service/HubDistributor.php new file mode 100644 index 00000000..2b32cce5 --- /dev/null +++ b/src/Service/HubDistributor.php @@ -0,0 +1,110 @@ +redis = $redis->getRedisClient(); + $this->hub_jo_key = $hub_jo_key; + } + + public function incrementJoCountForHub(Hub $hub) + { + $key = $hub->getID(); + + // get current count + $jo_count = $this->redis->hget($this->hub_jo_key, $key); + if ($jo_count == false) + { + // hub not in hash + // add hub to hash + // set to 1 since this is first jo for hub + $this->redis->hset($this->hub_jo_key, $key, 1); + } + else + { + // hub exist in hash + // add to count + $this->redis->hset($this->hub_jo_key, $key, $jo_count + 1); + } + } + + public function decrementJoCountForHub(Hub $hub) + { + $key = $hub->getID(); + + // get current count + $jo_count = $this->redis->hget($this->hub_jo_key, $key); + if ($jo_count) + { + // hub exist in hash + // decrement count + $this->redis->hset($this->hub_jo_key, $key, $jo_count - 1); + } + } + + public function arrangeHubs($hubs) + { + if (count($hubs) == 1) + return $hubs; + + $arranged_hubs = []; + + foreach ($hubs as $hub_data) + { + $hub = $hub_data['hub']; + + // need the id of hub + $key = $hub->getID(); + + // get jo count of hub + $hub_jo_count = $this->redis->hget($this->hub_jo_key, $key); + + // check if hub is in hash. if not, hub has no jobs + // but should still be added to results + if ($hub_jo_count != null) + { + $arranged_hubs[] = [ + 'hub' => $hub, + 'db_distance' => $hub_data['db_distance'], + 'distance' => $hub_data['distance'], + 'duration' => $hub_data['duration'], + 'jo_count' => $hub_jo_count, + ]; + } + else + { + $arranged_hubs[] = [ + 'hub' => $hub, + 'db_distance' => $hub_data['db_distance'], + 'distance' => $hub_data['distance'], + 'duration' => $hub_data['duration'], + 'jo_count' => 0, + ]; + } + } + + usort($arranged_hubs, function($a, $b) { + if ($a['jo_count'] == $b['jo_count']) + return 0; + if ($a['jo_count'] < $b['jo_count']) + return -1; + else + return 1; + }); + + //error_log('arranged hubs ' . json_encode($arranged_hubs)); + + return $arranged_hubs; + } +} + diff --git a/src/Service/HubFilterLogger.php b/src/Service/HubFilterLogger.php new file mode 100644 index 00000000..74744a39 --- /dev/null +++ b/src/Service/HubFilterLogger.php @@ -0,0 +1,30 @@ +em = $em; + } + + public function logFilteredHub(Hub $hub, $filter_type) + { + $hub_filter_log = new HubFilterLog(); + + $hub_filter_log->setHub($hub) + ->setFilterTypeId($filter_type); + + $this->em->persist($hub_filter_log); + $this->em->flush(); + } +} + diff --git a/src/Service/HubSelector.php b/src/Service/HubSelector.php new file mode 100644 index 00000000..c276000a --- /dev/null +++ b/src/Service/HubSelector.php @@ -0,0 +1,442 @@ +em = $em; + $this->im = $im; + $this->hub_distributor = $hub_distributor; + $this->hub_filter_logger = $hub_filter_logger; + $this->trans = $trans; + $this->rt = $rt; + } + + public function find(HubCriteria $criteria) + { + $point = $criteria->getPoint(); + $limit_results = $criteria->getLimitResults(); + $limit_distance = $criteria->getLimitDistance(); + $jo_type = $criteria->getJoType(); + $flag_inventory_check = $criteria->hasInventoryCheck(); + $items = $criteria->getItems(); + $date_time = $criteria->getDateTime(); + $payment_method = $criteria->getPaymentMethod(); + $flag_emergency = $criteria->isEmergency(); + + $results = []; + + // get all the hubs within distance + $filtered_hubs = $this->getClosestHubs($point, $limit_distance); + + error_log('closest hubs ' . json_encode($filtered_hubs)); + + if (!$flag_emergency) + { + // filter the first hub results for date and opening times + $hubs_date_time = $this->filterHubsByDateAndTime($filtered_hubs, $date_time); + $filtered_hubs = $hubs_date_time; + + // filter jo types + $hubs_jo_type = $this->filterHubsByJoType($filtered_hubs, $jo_type); + $filtered_hubs = $hubs_jo_type; + + // filter hubs by payment methods + $hubs_payment_method = $this->filterHubsByPaymentMethod($filtered_hubs, $payment_method); + $filtered_hubs = $hubs_payment_method; + + // inventory filter + $hubs_inventory = $this->filterHubsByInventory($filtered_hubs, $flag_inventory_check, + $jo_type, $items); + $filtered_hubs = $hubs_inventory; + + // round robin filter + $hubs_round_robin = $this->filterHubsByRoundRobin($filtered_hubs); + $filtered_hubs = $hubs_round_robin; + + error_log(json_encode($filtered_hubs)); + + // max results filter + $hubs_max_result = $this->filterHubsByMaxResults($filtered_hubs, $limit_results); + $filtered_hubs = $hubs_max_result; + } + + $results = $filtered_hubs; + + error_log(json_encode($results)); + + return $results; + } + + protected function filterHubsByRoundRobin($hubs) + { + if (empty($hubs)) + return $hubs; + + $results = []; + // call hub distributor service + $arranged_hubs = $this->hub_distributor->arrangeHubs($hubs); + $results = $arranged_hubs; + + return $results; + + } + + protected function filterHubsByMaxResults($hubs, $limit_result) + { + if (empty($hubs)) + return $hubs; + if (empty($limit_result)) + return $hubs; + + $results = []; + for ($i = 0; $i < count($hubs); $i++) + { + if ($i < $limit_result) + $results[] = $hubs[$i]; + else + $this->hub_filter_logger->logFilteredHub($hubs[$i]['hub'], 'max_results'); + } + + return $results; + } + + protected function filterHubsByJoType($hubs, $jo_type) + { + if (empty($hubs)) + return $hubs; + if (empty($jo_type)) + return $hubs; + + $results = []; + foreach ($hubs as $hub_data) + { + $hub = $hub_data['hub']; + + // TODO: for now, have this return true + $has_jo_type = true; + // check if hub offers the jo_type + // TODO: add service to hub + if ($has_jo_type) + $results[] = [ + 'hub' => $hub, + 'db_distance' => $hub_data['db_distance'], + 'distance' => $hub_data['distance'], + 'duration' => $hub_data['duration'], + ]; + else + $this->hub_filter_logger->logFilteredHub($hub, 'job_order_type'); + } + + return $results; + } + + protected function filterHubsByPaymentMethod($hubs, $payment_method) + { + if (empty($hubs)) + return $hubs; + if (empty($payment_method)) + return $hubs; + + $results = []; + foreach ($hubs as $hub_data) + { + $hub = $hub_data['hub']; + + // name of payment method is what is saved + $payment_methods = $hub->getPaymentMethods(); + if ($payment_methods != null) + { + $flag_found_pmethod = false; + foreach ($payment_methods as $pmethod) + { + if ($pmethod == $payment_method) + { + $results[] = [ + 'hub' => $hub, + 'db_distance' => $hub_data['db_distance'], + 'distance' => $hub_data['distance'], + 'duration' => $hub_data['duration'], + ]; + } + $flag_found_pmethod = true; + } + + if (!$flag_found_pmethod) + $this->hub_filter_logger->logFilteredHub($hub, 'no_payment_method'); + } + else + $this->hub_filter_logger->logFilteredHub($hub, 'no_payment_method'); + } + + return $results; + } + + protected function filterHubsByDateAndTime($hubs, $date_time) + { + if (empty($hubs)) + return $hubs; + + if ($date_time == null) + return $hubs; + + $results = []; + + foreach ($hubs as $hub_data) + { + // go through each hub's opening times to check if hub is open + // for the specified time + // get hub opening and closing times + // TODO: maybe in the future, might also have to check if hub + // is open/available on date/day + $hub = $hub_data['hub']; + + $time_open = $hub->getTimeOpen()->format("H:i:s"); + $time_close = $hub->getTimeClose()->format("H:i:s"); + + $filter_time = $date_time->format("H:i:s"); + + if (($filter_time >= $time_open) && + ($filter_time <= $time_close)) + { + $results[] = [ + 'hub' => $hub, + 'db_distance' => $hub_data['db_distance'], + 'distance' => $hub_data['distance'], + 'duration' => $hub_data['duration'], + ]; + } + else + $this->hub_filter_logger->logFilteredHub($hub, 'date_and_time'); + } + + return $results; + } + + protected function filterHubsByInventory($hubs, $flag_inventory_check, $jo_type, $items) + { + if (empty($hubs)) + return $hubs; + + if (!$flag_inventory_check) + return $hubs; + + $results = []; + if ($flag_inventory_check) + { + foreach ($hubs as $hub_data) + { + $hub = $hub_data['hub']; + + if ($jo_type == ServiceType::BATTERY_REPLACEMENT_NEW) + { + // call inventory + $has_items = $this->checkInventory($items, $hub); + if ($has_items) + $results[] = [ + 'hub' => $hub, + 'db_distance' => $hub_data['db_distance'], + 'distance' => $hub_data['distance'], + 'duration' => $hub_data['duration'], + ]; + else + { + // get the skus for the message + $sku_text = ''; + foreach ($items as $key => $value) + { + $sku_text .= ' ' . $key; + } + // send SMS to hub + $message = str_replace('item_display', trim($sku_text), $this->trans->trans('no_inventory_message')); + error_log($message); + $this->sendSMSMessage($hub, $items); + + $this->hub_filter_logger->logFilteredHub($hub, 'no_inventory'); + } + } + if ($jo_type == ServiceType::BATTERY_REPLACEMENT_WARRANTY) + { + // call inventory + $has_items = $this->checkInventory($items, $hub); + if ($has_items) + $results[] = [ + 'hub' => $hub, + 'db_distance' => $hub_data['db_distance'], + 'distance' => $hub_data['distance'], + 'duration' => $hub_data['duration'], + ]; + else + { + // get the skus for the message + $sku_text = ''; + foreach ($items as $key => $value) + { + $sku_text .= ' ' . $key; + } + // send SMS to hub + $message = str_replace('item_display', trim($sku_text), $this->trans->trans('no_inventory_message')); + error_log($message); + $this->sendSMSMessage($hub, $items); + + $this->hub_filter_logger->logFilteredHub($hub, 'no_inventory'); + } + } + } + } + + return $results; + } + + protected function getClosestHubs(Point $point, $limit_distance) + { + // get closest hubs based on st_distance function from db + $query_string = 'SELECT h, st_distance(h.coordinates, point(:lng, :lat)) as dist FROM App\Entity\Hub h WHERE h.status_open = true ORDER BY dist'; + + $query = $this->em->createQuery($query_string) + ->setParameter('lat', $point->getLatitude()) + ->setParameter('lng', $point->getLongitude()); + + // error_log($query->getSql()); + $result = $query->getResult(); + + $hubs = []; + $hubs_data = []; + foreach ($result as $row) + { + $hubs[] = $row[0]; + + // get coordinates of hub + $hub_coordinates = $row[0]->getCoordinates(); + + $cust_lat = $point->getLatitude(); + $cust_lng = $point->getLongitude(); + + $hub_lat = $hub_coordinates->getLatitude(); + $hub_lng = $hub_coordinates->getLongitude(); + + // get distance in kilometers from customer point to hub point + $dist = $this->distance($cust_lat, $cust_lng, $hub_lat, $hub_lng); + + if ($dist < $limit_distance) + { + $hubs_data[] = [ + 'hub' => $row[0], + 'db_distance' => $row['dist'], + 'distance' => $dist, + 'duration' => 0, + ]; + } + else + { + $this->hub_filter_logger->logFilteredHub($row[0], 'distance'); + } + } + + return $hubs_data; + } + + protected function checkInventory($items, $hub) + { + // check if hub has all items + $skus = []; + $branch_codes[] = $hub->getBranchCode(); + $result = false; + + foreach ($items as $key=> $value) + { + // add sap code of item/battery into array since + // getBranchesInventory takes in an array of hubs/branches + // and an array of skus + // $items as format: $items[sku] = quantity + $skus[] = $key; + } + + // call InventoryManager's getBranchesInventory to check if hub has all items + $branches_with_items = $this->im->getBranchesInventory($branch_codes, $skus); + + if (!empty($branches_with_items)) + { + // check if branch has enough quantity for item + foreach ($branches_with_items as $branch) + { + // get quantity from call + $qty_available = $branch['Quantity']; + + // get the quantity request + $sku_requested = $branch['SapCode']; + $qty_requested = $items[$sku_requested]; + if ($qty_available >= $qty_requested) + $result = true; + } + } + + // return true or false + return $result; + } + + // convert db distance to kilometers + protected function distance($lat1, $lon1, $lat2, $lon2) + { + if (($lat1 == $lat2) && ($lon1 == $lon2)) + return 0; + + $theta = $lon1 - $lon2; + $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta)); + $dist = acos($dist); + $dist = rad2deg($dist); + $miles = $dist * 60 * 1.1515; + + return round(($miles * 1.609344), 1); + } + + protected function sendSMSMessage($hub, $items) + { + // compose message + // get the skus for the message + $sku_text = ''; + foreach ($items as $key => $value) + { + $sku_text .= ' ' . $key; + } + $message = str_replace('item_display', trim($sku_text), $this->trans->trans('no_inventory_message')); + + // get hub notification number + $mobile_number = $hub->getNotifNumber(); + + if (!empty($mobile_number)) + { + // send SMS message + error_log('sending sms to - ' . $mobile_number); + $this->rt->sendSMS($mobile_number, 'MOTOLITE', $message); + } + } +} + diff --git a/src/Service/JobOrderHandler/ResqJobOrderHandler.php b/src/Service/JobOrderHandler/ResqJobOrderHandler.php index 2ec57b40..6c94509a 100644 --- a/src/Service/JobOrderHandler/ResqJobOrderHandler.php +++ b/src/Service/JobOrderHandler/ResqJobOrderHandler.php @@ -42,6 +42,7 @@ use App\Ramcar\CustomerNotWaitReason; use App\Ramcar\NoTradeInReason; use App\Ramcar\WillingToWaitContent; use App\Ramcar\WarrantySource; +use App\Ramcar\HubCriteria; use App\Service\InvoiceGeneratorInterface; use App\Service\JobOrderHandlerInterface; @@ -52,6 +53,8 @@ use App\Service\APNSClient; use App\Service\MapTools; use App\Service\RisingTideGateway; use App\Service\PromoLogger; +use App\Service\HubSelector; +use App\Service\HubDistributor; use CrEOF\Spatial\PHP\Types\Geometry\Point; @@ -74,6 +77,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface protected $wh; protected $rt; protected $promo_logger; + protected $hub_dist; protected $template_hash; @@ -81,7 +85,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface InvoiceGeneratorInterface $ic, ValidatorInterface $validator, TranslatorInterface $translator, RiderAssignmentHandlerInterface $rah, string $country_code, WarrantyHandler $wh, RisingTideGateway $rt, - PromoLogger $promo_logger) + PromoLogger $promo_logger, HubDistributor $hub_dist) { $this->em = $em; $this->ic = $ic; @@ -93,6 +97,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface $this->wh = $wh; $this->rt = $rt; $this->promo_logger = $promo_logger; + $this->hub_dist = $hub_dist; $this->loadTemplates(); } @@ -799,6 +804,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface 'event' => 'outlet_assign' ]; $mclient->sendEvent($obj, $payload); + + // update redis hub jo count + $this->hub_dist->incrementJoCountForHub($hub); } return $error_array; @@ -1170,6 +1178,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface $more_reason = $req->request->get('not_wait_notes'); } + // get previously assigned hub, if any + $old_hub = $obj->getHub(); + if (empty($error_array)) { // rider mqtt event @@ -1243,6 +1254,13 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface 'event' => 'outlet_assign' ]; $mclient->sendEvent($obj, $payload); + + // update redis hub_jo_count for hub + // decrement old hub's count and increment new hub's count + if ($old_hub != null) + $this->hub_dist->decrementJoCountForHub($old_hub); + if ($hub != null) + $this->hub_dist->incrementJoCountForHub($hub); } return $error_array; @@ -1664,6 +1682,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface return $params; } + // CMB code public function initializeOneStepForm() { $params['obj'] = new JobOrder(); @@ -1679,6 +1698,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface return $params; } + // CMB code public function initializeOneStepEditForm($id, $map_tools) { $em = $this->em; @@ -1859,7 +1879,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface } // initialize dispatch/processing job order form - public function initializeProcessingForm($id, $map_tools, $motiv) + public function initializeProcessingForm($id, HubSelector $hub_selector, $motiv) { $em = $this->em; @@ -1925,8 +1945,43 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface // get rejection reasons $params['rejection_reasons'] = JORejectionReason::getCollection(); + // get battery (if any) + $skus = []; + $items = []; + $invoice = $obj->getInvoice(); + $inv_items = $invoice->getItems(); + foreach ($inv_items as $inv_item) + { + $batt = $inv_item->getBattery(); + if ($batt == null) + continue; + + $skus[] = $batt->getSapCode(); + $item_count = 1; + if (!empty($batt->getSapCode())) + { + $sap_code = $batt->getSapCode(); + if (isset($items[$sap_code])) + $items[$sap_code] = $item_count + 1; + else + $items[$sap_code] = $item_count; + } + } + // get closest hubs - $hubs = $map_tools->getClosestHubs($obj->getCoordinates(), 50, date("H:i:s")); + // set hub criteria + $hub_criteria = new HubCriteria(); + $hub_criteria->setPoint($obj->getCoordinates()) + ->setLimitResults(50) + ->setPaymentMethod($obj->getModeOfPayment()) + ->setJoType($obj->getServiceType()); + + // check if emergency or not + $willing_to_wait = $obj->getWillWait(); + if ($willing_to_wait == WillingToWaitContent::NOT_WILLING_TO_WAIT) + $hub_criteria->setEmergency(true); + + $hubs = $hub_selector->find($hub_criteria); $params['hubs'] = []; @@ -2150,7 +2205,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface } // initialize hub form - public function initializeHubForm($id, $map_tools) + public function initializeHubForm($id, HubSelector $hub_selector) { $em = $this->em; @@ -2175,7 +2230,10 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface $params['rejection_reasons'] = JORejectionReason::getCollection(); // get closest hubs - $hubs = $map_tools->getClosestHubs($obj->getCoordinates(), 50, date("H:i:s")); + $hub_criteria = new HubCriteria(); + $hub_criteria->setPoint($obj->getCoordinates()) + ->setLimitResults(50); + $hubs = $hub_selector->find($hub_criteria); $params['status_cancelled'] = JOStatus::CANCELLED; $params['hubs'] = []; diff --git a/src/Service/JobOrderHandlerInterface.php b/src/Service/JobOrderHandlerInterface.php index 59aea943..cff06242 100644 --- a/src/Service/JobOrderHandlerInterface.php +++ b/src/Service/JobOrderHandlerInterface.php @@ -7,6 +7,7 @@ use Symfony\Component\HttpFoundation\Request; use App\Service\MQTTClient; use App\Service\APNSClient; use App\Service\MapTools; +use App\Service\HubSelector; use App\Entity\JobOrder; @@ -67,7 +68,7 @@ interface JobOrderHandlerInterface public function initializeAllForm(int $id); // initialize dispatch/processing job order form - public function initializeProcessingForm(int $id, MapTools $map_tools, $motiv); + public function initializeProcessingForm(int $id, HubSelector $hub_selector, $motiv); // initialize assign job order form public function initializeAssignForm(int $id); @@ -76,7 +77,7 @@ interface JobOrderHandlerInterface public function initializeFulfillmentForm(int $id); // initialize hub form - public function initializeHubForm(int $id, MapTools $map_tools); + public function initializeHubForm(int $id, HubSelector $hub_selector); // initialize rider form public function initializeRiderForm(int $id); diff --git a/src/Service/RisingTideGateway.php b/src/Service/RisingTideGateway.php index 38276619..acd7d4d5 100644 --- a/src/Service/RisingTideGateway.php +++ b/src/Service/RisingTideGateway.php @@ -83,4 +83,28 @@ class RisingTideGateway error_log($result); } + + public function validatePhoneNumber($mobile) + { + // check valid number + $num = trim($mobile); + + // should be 10 digits + if (strlen($num) != 10) + return false; + + // should start with '9' + if ($num[0] != '9') + return false; + + // should be numeric + if (!is_numeric($num)) + return false; + + // should not be 9900000000 + if ($num == '9900000000') + return false; + + return true; + } } diff --git a/templates/hub/form.html.twig b/templates/hub/form.html.twig index d7101260..3c8947c1 100644 --- a/templates/hub/form.html.twig +++ b/templates/hub/form.html.twig @@ -90,6 +90,18 @@
+