resq/src/Controller/CustomerAppAPI/LocationController.php
2023-03-26 00:43:45 +08:00

633 lines
22 KiB
PHP

<?php
namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request;
use Catalyst\ApiBundle\Component\Response as ApiResponse;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Ramcar\JOStatus;
use App\Service\GeofenceTracker;
use App\Service\InventoryManager;
use App\Service\MapTools;
use App\Entity\Hub;
use App\Entity\Battery;
use App\Entity\CustomerMetadata;
use DateTime;
use DateInterval;
class LocationController extends ApiController
{
public function locationSupport(Request $req, GeofenceTracker $geo)
{
// validate params
$validity = $this->validateRequest($req, [
'longitude',
'latitude',
]);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
$long = $req->query->get('longitude');
$lat = $req->query->get('latitude');
// NOTE: had to add this for promo tag
$cust = $this->session->getCustomer();
if ($cust == null) {
return new ApiResponse(false, 'No customer information found.');
}
$is_covered = false;
// check if customer still has promo
if (($cust->getCustomerTag('TAG_CAR_CLUB_OFFICER_PROMO')) ||
($cust->getCustomerTag('TAG_CAR_CLUB_MEMBER_PROMO'))
) {
// if has customer tag, customer has not availed of promo
$is_covered = true;
} else {
// geofence
$is_covered = $geo->isCovered($long, $lat);
}
// geofence
// $is_covered = $geo->isCovered($long, $lat);
$data = [
'longitude' => $long,
'latitude' => $lat,
'supported' => $is_covered,
];
// check if is_covered is false. If so, we need to set the error part in the response
if (!$is_covered) {
return new ApiResponse(false, $this->getGeoErrorMessage(), $data);
}
// response
return new ApiResponse(true, '', $data);
}
public function getNearestHubAndSlots(
Request $req,
MapTools $map_tools
) {
// validate params
$validity = $this->validateRequest($req, [
'longitude',
'latitude',
]);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
$coordinates = new Point($req->query->get('longitude'), $req->query->get('latitude'));
// add checking if customer has a pre-registered hub
$cust = $this->session->getCustomer();
if ($cust == null) {
return new ApiResponse(false, 'No customer information found.');
}
// check if customer has customer tag promo
if (($cust->getCustomerTag('TAG_CAR_CLUB_OFFICER_PROMO')) ||
($cust->getCustomerTag('TAG_CAR_CLUB_MEMBER_PROMO'))
) {
// if has customer tag, customer has not availed of promo, get the hub where customer is pre-registered
$car_club_cust_hub = $cust->getCarClubCustomerHub();
if ($car_club_cust_hub != null) {
// need to get the rider slots for the pre-registered hub
$hub = $car_club_cust_hub->getHub();
$nearest_hub_slots = $this->findAdvanceNearestHubAndSlots($coordinates, $map_tools, $hub);
} else {
$nearest_hub_slots = $this->findAdvanceNearestHubAndSlots($coordinates, $map_tools);
if (empty($nearest_hub_slots['hub'])) {
return new ApiResponse(false, 'Thank you for reaching out to us. Please expect a call from us and we will assist you with your request. Thank you and stay safe!');
}
}
} else {
$nearest_hub_slots = $this->findAdvanceNearestHubAndSlots($coordinates, $map_tools);
if (empty($nearest_hub_slots['hub'])) {
return new ApiResponse(false, 'Thank you for reaching out to us. Please expect a call from us and we will assist you with your request. Thank you and stay safe!');
}
}
// response
return new ApiResponse(true, '', [
'hub_id' => $nearest_hub_slots['hub']->getID(),
'hub_slots' => $nearest_hub_slots['slots'],
]);
}
public function addLocation(Request $req)
{
// validate params
$validity = $this->validateRequest($req, [
'name',
'address',
'longitude',
'latitude',
'landmark',
]);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// get customer
$cust = $this->session->getCustomer();
if ($cust == null) {
return new ApiResponse(false, 'No customer information found.');
}
// get the information
$name = $req->request->get('name');
$address = $req->request->get('address');
$lng = $req->request->get('longitude');
$lat = $req->request->get('latitude');
$landmark = $req->request->get('landmark');
$loc_info = [
'address' => $address,
'longitude' => $lng,
'latitude' => $lat,
'landmark' => $landmark,
];
// check if customer already has existing metadata
$c_meta = $this->em->getRepository(CustomerMetadata::class)->findOneBy(['customer' => $cust]);
if ($c_meta == null) {
// create new customer meta
$cust_meta = new CustomerMetadata();
$cust_meta->setCustomer($cust);
$cust_meta->addMetaInfo($name, $loc_info);
$this->em->persist($cust_meta);
} else {
// limit locations to 6. If more than 6, pop the first one out
// add location to existing customer meta
$meta_count = count($c_meta->getAllMetaInfo());
if ($meta_count >= 6)
$c_meta->popMetaInfo();
$c_meta->addMetaInfo($name, $loc_info);
}
$this->em->flush();
// response
return new ApiResponse();
}
public function removeLocation($id, Request $req)
{
// validate params
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// get customer
$cust = $this->session->getCustomer();
if ($cust == null) {
return new ApiResponse(false, 'No customer information found.');
}
// find customer metadata and delete entry if present
$cv = $this->em->getRepository(CustomerMetadata::class)->findOneBy(['customer' => $cust]);
if ($cv != null) {
$cv->deleteMetadataInfo(base64_decode($id));
}
$this->em->flush();
// response
return new ApiResponse();
}
public function getLocations(Request $req)
{
// validate params
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// get customer
$cust = $this->session->getCustomer();
if ($cust == null) {
return new ApiResponse(false, 'No customer information found.');
}
// get the customer meta for customer
$locations = [];
$cust_meta = $this->em->getRepository(CustomerMetadata::class)->findOneBy(['customer' => $cust]);
if ($cust_meta != null) {
$locations = $cust_meta->getAllMetaInfo();
}
$data = [
'locations' => $locations,
];
// response
return new ApiResponse(true, '', $data);
}
protected function findAdvanceNearestHubAndSlots(Point $coordinates, MapTools $map_tools, $hub = null)
{
$hub_data = [];
if ($hub != null) {
// get the slots of hub
$hub_slots = $this->getHubRiderSlots($hub);
$slots = $hub_slots['slot_data'];
$hub_data = [
'hub' => $hub,
'slots' => $slots,
];
return $hub_data;
}
// get the nearest 10 hubs
$nearest_hubs_with_distance = [];
$hubs = $map_tools->getClosestOpenHubs($coordinates, 10);
foreach ($hubs as $hub) {
$nearest_hubs_with_distance[] = $hub;
// TODO: insert checking for branch code here when inventory manager is up
}
$nearest = null;
$hub_slots = [];
$slot_found = false;
// find the nearest hub
if (!empty($nearest_hubs_with_distance)) {
// get slots of nearest hub right after getting nearest hub.
// then check if hub has available slots. If not, get next nearest hub.
foreach ($nearest_hubs_with_distance as $nhd) {
if (empty($nearest)) {
// get the slots for the hub to check if hub is available for assignment
$hub_slots = $this->getHubRiderSlots($nhd['hub']);
$flag_hub_available = $hub_slots['flag_hub_available'];
if ($flag_hub_available == true) {
$nearest = $nhd;
}
} else {
if ($nhd['distance'] < $nearest['distance']) {
// get the slots for nearest which is nhd right now
$hub_slots = $this->getHubRiderSlots($nhd['hub']);
$flag_hub_available = $hub_slots['flag_hub_available'];
// if hub is available, set hub to nearest
if ($flag_hub_available == true) {
$nearest = $nhd;
}
}
}
}
}
if ($nearest != null) {
// set hub data to what is in nearest
$hub_data = [
'hub' => $nearest['hub'],
'slots' => $hub_slots['slot_data'],
];
}
return $hub_data;
}
protected function getHubRiderSlots(Hub $hub)
{
// check hub's advance orders for the day
/*
// get number of advance orders for the next day if request came in before midnight
// or for current day if request came in after midnight
// check request_time
$request_time = time();
$midnight = strtotime('00:00');
*/
$start_date = new DateTime();
$end_date = new DateTime();
// to keep things simple, just start on next day regardless of midnight timer
$start_date->add(new DateInterval('P1D'));
$end_date->add(new DateInterval('P3D'));
/*
if ($request_time < $midnight)
{
// add +1 to start date to get the next day
// add +3 to date to end date to get the advance orders for the next three days
$start_date->add(new DateInterval('P1D'));
$end_date->add(new DateInterval('P1D'));
}
$end_date->add(new DateInterval('P2D'));
*/
// set time bounds for the start and end date
$start_date->setTime(0, 1);
$end_date->setTime(23, 59);
// NOTE: get advance orders via query
// get JOs assigned to hub that are advance orders and scheduled for the next three days with
// for hub assignment status
$query = $this->em->createQuery('select jo from App\Entity\JobOrder jo where jo.hub = :hub and jo.flag_advance = true and
jo.date_schedule >= :date_start and jo.date_schedule <= :date_end and jo.status != :status_cancelled
and jo.status != :status_fulfilled');
$jos_advance_orders = $query->setParameters([
'hub' => $hub,
'date_start' => $start_date,
'date_end' => $end_date,
'status_cancelled' => JOStatus::CANCELLED,
'status_fulfilled' => JOStatus::FULFILLED,
])
->getResult();
// check request_time
// define slots
$slots = [
'08_09' => '8:00 AM',
'09_10' => '9:00 AM',
'10_11' => '10:00 AM',
'11_12' => '11:00 AM',
'12_13' => '12:00 PM',
'13_14' => '1:00 PM',
'14_15' => '2:00 PM',
'15_16' => '3:00 PM',
'16_17' => '4:00 PM',
];
// get the dates for the next three days
$first_date = $start_date->format('Y-m-d');
$second_date = $start_date->add(new DateInterval('P1D'));
$sec_date = $second_date->format('Y-m-d');
$third_date = $end_date->format('Y-m-d');
// define days
$days = [
$first_date => $first_date,
$sec_date => $sec_date,
$third_date => $third_date,
];
// initialize hub rider slots
$hub_rider_slots = [];
foreach ($days as $day) {
foreach ($slots as $slot_key => $slot) {
$hub_rider_slots[$day][$slot_key] = $hub->getRiderSlots();
}
}
// check each JO's date_schedule, decrement rider_slots if date schedule falls in that slot
foreach ($jos_advance_orders as $jo) {
// get date key
$date_sched = $jo->getDateSchedule();
$date_string = $date_sched->format('Y-m-d');
$hour = $date_sched->format('H');
$slot_id = sprintf('%02d_%02d', $hour, $hour + 1);
// error_log("SLOT - $date_string - $slot_id");
// decrement rider slot
if (isset($hub_rider_slots[$date_string][$slot_id]))
$hub_rider_slots[$date_string][$slot_id]--;
// check if it goes through next slot (10 min allowance)
$mins = $date_sched->format('i');
if ($mins > 10) {
$next_slot_id = sprintf('%02d_%02d', $hour + 1, $hour + 2);
// error_log("NEXT SLOT - $date_string - $next_slot_id");
// decrement rider slot
if (isset($hub_rider_slots[$date_string][$next_slot_id]))
$hub_rider_slots[$date_string][$next_slot_id]--;
}
}
// error_log(print_r($hub_rider_slots, true));
$hub_slots = $this->generateHubSlots($hub_rider_slots, $slots);
// error_log(print_r($hub_slots, true));
return $hub_slots;
}
protected function generateHubSlots($rider_slots, $slots)
{
$data = [];
$total_rslots = 0;
$total_unavailable_rslots = 0;
foreach ($rider_slots as $day_id => $rslot) {
$data[$day_id] = [];
foreach ($rslot as $slot_id => $avail_slots) {
// increment total rider slots
$total_rslots++;
$slot_data = [
'id' => $slot_id,
'label' => $slots[$slot_id],
'available' => true,
];
// mark unavailable ones
if ($avail_slots <= 0) { // increment total number of unavailable slots
$total_unavailable_rslots++;
$slot_data['available'] = false;
}
// add to day data
$data[$day_id][] = $slot_data;
}
}
// check if hub has available slots
$hub_available = true;
// error_log('total rider slots ' . $total_rslots);
// error_log('total unavailable slots ' . $total_unavailable_rslots);
if ($total_rslots == $total_unavailable_rslots) {
// error_log('hub has no available slots');
$hub_available = false;
}
$hs_data = [
'flag_hub_available' => $hub_available,
'slot_data' => $data,
];
return $hs_data;
}
protected function findNearestHub($jo, MapTools $map_tools)
{
// get the nearest 10 hubs
$selected_hub = null;
$hubs = $map_tools->getClosestOpenHubs($jo->getCoordinates(), 10, date("H:i:s"));
$nearest_hubs_with_distance = [];
$nearest_branch_codes = [];
foreach ($hubs as $hub) {
$nearest_hubs_with_distance[] = $hub;
//if (!empty($hub['hub']->getBranchCode()))
// $nearest_branch_codes[] = $hub['hub']->getBranchCode();
}
// check if nearest hubs have branch codes
//if (count($nearest_branch_codes) == 0)
// return $selected_hub;
// assume all 10 have stock
// find the nearest hub with available riders
$nearest = null;
foreach ($nearest_hubs_with_distance as $nhd) {
// get number of available riders
$count_riders = count($nhd['hub']->getAvailableRiders());
// get number of advance orders in the next 3 hours
$time_now = new DateTime();
$date_end = new DateTime();
$date_end->add(new DateInterval('PT2H'));
// NOTE: get advance orders via query
// get JOs assigned to hub that are advance orders and scheduled within X hours with
// for rider assignment status
$query = $this->em->createQuery('select count(jo) from App\Entity\JobOrder jo where jo.hub = :hub and jo.flag_advance = true and jo.date_schedule <= :date_end and jo.status = :status');
$count_advance_orders = $query->setParameters([
'hub' => $nhd['hub'],
'date_end' => $date_end,
'status' => JOStatus::RIDER_ASSIGN,
])
->setMaxResults(1)
->getSingleScalarResult();
// error_log('HUB - ' . $nhd['hub']->getID());
// error_log('RIDER COUNT - ' . $count_riders);
// error_log('ADVANCE ORDER COUNT - ' . $count_advance_orders);
// if (count($nhd['hub']->getAvailableRiders()) > 0)
// if we have more riders than we have advance orders
if ($count_riders - $count_advance_orders > 0) {
if (empty($nearest))
$nearest = $nhd;
else {
if ($nhd['distance'] < $nearest['distance'])
$nearest = $nhd;
}
}
}
$selected_hub = $nearest['hub'];
return $selected_hub;
}
protected function findNearestHubWithInventory(
$jo,
Battery $batt,
MapTools $map_tools,
InventoryManager $im
) {
// get the nearest 10 hubs
$selected_hub = null;
$hubs = $map_tools->getClosestOpenHubs($jo->getCoordinates(), 10, date("H:i:s"));
$nearest_hubs_with_distance = [];
$nearest_branch_codes = [];
foreach ($hubs as $hub) {
$nearest_hubs_with_distance[] = $hub;
//if (!empty($hub['hub']->getBranchCode()))
// $nearest_branch_codes[] = $hub['hub']->getBranchCode();
}
// check if nearest hubs have branch codes
//if (count($nearest_branch_codes) == 0)
// return $selected_hub;
// assume all 10 have stock
// find the nearest hub with available riders
$nearest = null;
foreach ($nearest_hubs_with_distance as $nhd) {
if (count($nhd['hub']->getAvailableRiders()) > 0) {
if (empty($nearest))
$nearest = $nhd;
else {
if ($nhd['distance'] < $nearest['distance'])
$nearest = $nhd;
}
}
}
$selected_hub = $nearest['hub'];
// uncomment this snippet when inventory check becomes active
// get battery sku
/*
if ($batt != null)
{
$skus[] = $batt->getSAPCode();
// api call to check inventory
// pass the list of branch codes of nearest hubs and the skus
// go through returned list of branch codes
// bypass inventory check for now
// $hubs_with_inventory = $im->getBranchesInventory($nearest_branch_codes, $skus);
if (!empty($hubs_with_inventory))
{
$nearest = [];
$flag_hub_found = false;
foreach ($hubs_with_inventory as $hub_with_inventory)
{
// find hub according to branch code
$found_hub = $this->em->getRepository(Hub::class)->findOneBy(['branch_code' => $hub_with_inventory['BranchCode']]);
if ($found_hub != null)
{
// check rider availability
if (count($found_hub->getAvailableRiders()) > 0)
{
// check against nearest hubs with distance
foreach ($nearest_hubs_with_distance as $nhd)
{
// get distance of hub from location, compare with $nearest. if less, replace nearest
if ($found_hub->getID() == $nhd['hub']->getID())
{
if (empty($nearest))
{
$nearest = $nhd;
$flag_hub_found = true;
}
else
{
if ($nhd['distance'] < $nearest['distance'])
{
$nearest = $nhd;
$flag_hub_found = true;
}
}
}
}
}
}
}
$selected_hub = $nearest['hub'];
}
} */
return $selected_hub;
}
}