Compare commits

...

4 commits

Author SHA1 Message Date
Ramon Gutierrez
f4520f6987 Merge branch 'master' into 810-auto-dispatch-revamp 2024-12-09 01:10:26 +08:00
Ramon Gutierrez
aa042a435b Move supported area query to hub selector #810 2024-12-08 21:37:58 +08:00
Ramon Gutierrez
3c79d1fe28 Configure all hub filter services by namespace #810 2024-12-08 17:51:24 +08:00
Ramon Gutierrez
0f909ad7e0 Further modularize hub filtering flow #810 2024-12-08 17:38:45 +08:00
14 changed files with 252 additions and 695 deletions

View file

@ -65,11 +65,6 @@ cust_api_battery_list:
controller: App\Controller\CustomerAppAPI\VehicleController::getCompatibleBatteries controller: App\Controller\CustomerAppAPI\VehicleController::getCompatibleBatteries
methods: [GET] methods: [GET]
cust_api_jo_request:
path: /apiv2/job_order
controller: App\Controller\CustomerAppAPI\JobOrderController::requestJobOrder
methods: [POST]
cust_api_estimate: cust_api_estimate:
path: /apiv2/estimate path: /apiv2/estimate
controller: App\Controller\CustomerAppAPI\EstimateController::getEstimate controller: App\Controller\CustomerAppAPI\EstimateController::getEstimate

View file

@ -27,6 +27,11 @@ services:
# fetching services directly from the container via $container->get() won't work. # fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway. # The best practice is to be explicit about your dependencies anyway.
_instanceof:
# make every hub filter public
App\Service\HubFilter\HubFilterInterface:
public: true
# makes classes in src/ available to be used as services # makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name # this creates a service per class whose id is the fully-qualified class name
App\: App\:
@ -332,27 +337,10 @@ services:
$rt: "@App\\Service\\RisingTideGateway" $rt: "@App\\Service\\RisingTideGateway"
$trans: "@Symfony\\Contracts\\Translation\\TranslatorInterface" $trans: "@Symfony\\Contracts\\Translation\\TranslatorInterface"
App\Service\HubFilter\Filters\DateAndTimeHubFilter:
public: true
App\Service\HubFilter\Filters\JoTypeHubFilter:
public: true
App\Service\HubFilter\Filters\MaxResultsHubFilter:
public: true
App\Service\HubFilter\Filters\PaymentMethodHubFilter:
public: true
App\Service\HubFilter\Filters\RiderAvailabilityHubFilter:
public: true
App\Service\HubFilter\Filters\InventoryHubFilter: App\Service\HubFilter\Filters\InventoryHubFilter:
public: true
arguments: arguments:
$im: "@App\\Service\\InventoryManager" $im: "@App\\Service\\InventoryManager"
App\Service\HubFilter\Filters\RoundRobinHubFilter: App\Service\HubFilter\Filters\RoundRobinHubFilter:
public: true
arguments: arguments:
$hub_distributor: "@App\\Service\\HubDistributor" $hub_distributor: "@App\\Service\\HubDistributor"

View file

@ -995,411 +995,6 @@ class JobOrderController extends ApiController
]); ]);
} }
// TODO: remove later
// mobile app no longer calls this
public function requestJobOrder(
Request $req,
InvoiceGeneratorInterface $ic,
GeofenceTracker $geo,
MapTools $map_tools,
InventoryManager $im,
MQTTClientApiv2 $mclientv2,
FCMSender $fcmclient,
RiderAssignmentHandlerInterface $rah,
PromoLogger $promo_logger,
HubSelector $hub_select,
HubDistributor $hub_dist,
HubFilterLogger $hub_filter_logger,
HubFilteringGeoChecker $hub_geofence,
JobOrderManager $jo_manager,
PriceTierManager $pt_manager
) {
// validate params
$validity = $this->validateRequest($req, [
'service_type',
'cv_id',
// 'batt_id',
'long',
'lat',
'warranty',
'mode_of_payment',
]);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// trade in type
$trade_in_batt = $req->request->get('trade_in_batt');
$trade_in_type = $req->request->get('trade_in_type', '');
// address
$address = $req->request->get('delivery_address', 'Set by mobile application');
// instructions
$instructions = $req->request->get('delivery_instructions', '');
// longitude and latitude
$long = $req->request->get('long');
$lat = $req->request->get('lat');
// geofence
$is_covered = $geo->isCovered($long, $lat);
if (!$is_covered) {
// TODO: put geofence error message in config file somewhere
return new ApiResponse(false, $this->getGeoErrorMessage());
}
$jo = new JobOrder();
$jo->setSource(TransactionOrigin::MOBILE_APP)
->setStatus(JOStatus::PENDING)
->setDeliveryInstructions('')
->setTier1Notes('')
->setTier2Notes('')
->setDeliveryAddress($address)
->setTradeInType($trade_in_type)
->setDeliveryInstructions($instructions)
// TODO: error check for valid mode of payment
->setModeOfPayment($req->request->get('mode_of_payment'));
// customer
$cust = $this->session->getCustomer();
if ($cust == null) {
return new ApiResponse(false, 'No customer information found.');
}
// check if customer has more than one job order already
$flag_cust_new = false;
$cust_jo_count = $jo_manager->getCustomerJobOrderCount($cust->getID());
if ($cust_jo_count <= 1)
$flag_cust_new = true;
$jo->setCustomer($cust);
$jo->setCustNew($flag_cust_new);
// validate service type
$stype = $req->request->get('service_type');
if (!ServiceType::validate($stype)) {
return new ApiResponse(false, 'Invalid service type.');
}
$jo->setServiceType($stype);
// validate warranty
$warr = $req->request->get('warranty');
if (!WarrantyClass::validate($warr)) {
return new ApiResponse(false, 'Invalid warranty class.');
}
$jo->setWarrantyClass($warr);
// set coordinates
$point = new Point($long, $lat);
$jo->setCoordinates($point);
// make invoice criteria
$icrit = new InvoiceCriteria();
$icrit->setServiceType($stype);
// check promo
$promo_id = $req->request->get('promo_id');
if (!empty($promo_id)) {
$promo = $this->em->getRepository(Promo::class)->find($promo_id);
if ($promo == null) {
return new ApiResponse(false, 'Invalid promo id.');
}
// put in criteria
$icrit->addPromo($promo);
}
// check customer vehicle
$cv = $this->em->getRepository(CustomerVehicle::class)->find($req->request->get('cv_id'));
if ($cv == null) {
return new ApiResponse(false, 'Invalid customer vehicle id.');
}
$icrit->setCustomerVehicle($cv);
$jo->setCustomerVehicle($cv);
// check if customer owns vehicle
if ($cust->getID() != $cv->getCustomer()->getID()) {
return new ApiResponse(false, 'Customer does not own vehicle.');
}
// check battery
$batt_id = $req->request->get('batt_id');
if ($batt_id != null) {
$batt = $this->em->getRepository(Battery::class)->find($batt_id);
if ($batt == null) {
return new ApiResponse(false, 'Invalid battery id.');
}
} else
$batt = null;
/*
// put battery in criteria
$icrit->addBattery($batt);
*/
// check trade-in
// only allow motolite, other, none
switch ($trade_in_type) {
case TradeInType::MOTOLITE:
case TradeInType::OTHER:
break;
default:
$trade_in_type = '';
break;
}
// add the actual battery item first
$icrit->addEntry($batt, null, 1);
// if we have a trade in, add it as well
if (!empty($trade_in_type) && !empty($trade_in_batt)) {
$ti_batt_obj = $this->em->getRepository(Battery::class)->find($trade_in_batt);
if (!empty($ti_batt_obj)) {
$battery_size = $ti_batt_obj->getSize();
$icrit->addTradeInEntry($battery_size, $trade_in_type, 1);
}
}
// set taxable
$icrit->setIsTaxable();
// set JO source
$icrit->setSource(TransactionOrigin::MOBILE_APP);
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// send to invoice generator
$invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice);
// set more hub criteria fields
$hub_criteria = new HubCriteria();
$hub_criteria->setPoint($jo->getCoordinates());
// get distance limit for mobile from env
$limit_distance = $_ENV['CUST_DISTANCE_LIMIT'];
// set distance limit
$hub_criteria->setLimitDistance($limit_distance);
if ($hub_geofence->isCovered($long, $lat)) {
// TODO: set this properly, since the other flags
// are on default values.
// if true, set other values for HubCriteria
// error_log('Area is covered by hub filtering');
$hub_criteria->setJoType($jo->getServiceType())
->setPaymentMethod($jo->getModeOfPayment())
->setRoundRobin(true);
}
// add battery to items
$sku = $batt->getSAPCode();
if (!empty($sku))
$hub_criteria->addItem($batt->getSAPCode(), 1);
// get customer id. No JO id at this point
$customer_id = $cust->getID();
$hub_criteria->setCustomerId($customer_id);
// find nearest hubs
$nearest_hubs = $hub_select->find($hub_criteria);
$assigned_rider = null;
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());
foreach ($nearest_hubs as $nearest_hub) {
$available_riders = $nearest_hub['hub']->getAvailableRiders();
if (count($available_riders) >= 1) {
if (count($available_riders) == 1) {
$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);
}
$jo->setHub($nearest_hub['hub']);
$jo->setRider($assigned_rider);
$jo->setStatus(JOStatus::ASSIGNED);
$jo->setStatusAutoAssign(AutoAssignStatus::HUB_AND_RIDER_ASSIGNED);
$assigned_rider->setAvailable(false);
// set rider's current job order
$assigned_rider->setCurrentJobOrder($jo);
// update redis hub_jo_count for hub
$hub_dist->incrementJoCountForHub($nearest_hub['hub']);
// break out of loop
break;
} else {
// log hub into hub_filter_log
$hub_filter_logger->logFilteredHub($nearest_hub['hub'], 'no_available_rider', null, $cust->getID());
// continue to go through list to find hub with an available rider
}
}
}
$this->em->persist($jo);
$this->em->persist($invoice);
// add event log for JO
$event = new JOEvent();
$event->setDateHappen(new DateTime())
->setTypeID(JOEventType::CREATE)
->setJobOrder($jo);
$this->em->persist($event);
// check JO status
if ($jo->getStatus() == JOStatus::ASSIGNED) {
// add event logs for hub and rider assignments
$hub_assign_event = new JOEvent();
$hub_assign_event->setDateHappen(new DateTime())
->setTypeID(JOEventType::HUB_ASSIGN)
->setJobOrder($jo);
$this->em->persist($hub_assign_event);
$rider_assign_event = new JOEvent();
$rider_assign_event->setDateHappen(new DateTime())
->setTypeID(JOEventType::RIDER_ASSIGN)
->setJobOrder($jo);
$this->em->persist($rider_assign_event);
// user mqtt event
$payload = [
'event' => 'outlet_assign',
];
$mclientv2->sendEvent($jo, $payload);
$fcmclient->sendJoEvent($jo, "jo_fcm_title_outlet_assign", "jo_fcm_body_outlet_assign");
$rah->assignJobOrder($jo, $jo->getRider());
}
$this->em->flush();
// make invoice json data
$invoice_data = [
'total_price' => $invoice->getTotalPrice(),
'vat_ex_price' => (float) $invoice->getVATExclusivePrice(),
'vat' => $invoice->getVAT(),
'discount' => $invoice->getDiscount(),
'trade_in' => $invoice->getTradeIn(),
];
$items = $invoice->getItems();
$items_data = [];
foreach ($items as $item) {
$items_data[] = [
'title' => $item->getTitle(),
'qty' => $item->getQuantity() + 0,
'price' => $item->getPrice() + 0.0,
];
}
$invoice_data['items'] = $items_data;
// check service type
if ($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) {
$customer = $cv->getCustomer();
$customer_tags = $customer->getCustomerTagObjects();
if (!empty($customer_tags)) {
foreach ($customer_tags as $customer_tag) {
// TODO: not too comfy with this being hardcoded
if ($customer_tag->getID() == $invoice->getUsedCustomerTagId()) {
// remove associated entity
$customer->removeCustomerTag($customer_tag);
// log the availment of promo from customer
$created_by = $req->query->get('session_key');
$cust_id = $jo->getCustomer()->getID();
$cust_fname = $jo->getCustomer()->getFirstName();
$cust_lname = $jo->getCustomer()->getLastName();
$jo_id = $jo->getID();
$invoice_id = $jo->getInvoice()->getID();
// TODO: check if we store total price of invoice or just the discounted amount
$amount = $jo->getInvoice()->getTotalPrice();
$promo_logger->logPromoInfo(
$created_by,
$cust_id,
$cust_fname,
$cust_lname,
$jo_id,
$invoice_id,
$amount
);
}
}
}
}
// response
return new ApiResponse(true, '', [
'jo_id' => $jo->getID(),
'invoice' => $invoice_data,
]);
}
// commenting it out. Modify the getJOHistory instead to just get the fulfilled
// and cancelled job orders, since ongoing is not yet part of history
/*
public function getCompletedJobOrders(Request $req, EntityManagerInterface $em, RiderTracker $rt)
{
// 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.');
}
$completed_jos = $this->getCompletedJOs($cust);
// initialize data
$jo_data = [];
foreach ($completed_jos as $jo)
{
$jo_data[] = $this->generateJobOrderData($req, $jo, $rt);
}
// response
return new ApiResponse(true, '', [
'completed_job_orders' => $jo_data,
]);
}
protected function getCompletedJOs($cust)
{
$completed_jos = $this->em->getRepository(JobOrder::class)->findBy([
'customer' => $cust,
'status' => [JOStatus::CANCELLED, JOStatus::FULFILLED],
], ['date_schedule' => 'desc']);
return $completed_jos;
}
*/
protected function generateJobOrderData($req, $jo, $rt) protected function generateJobOrderData($req, $jo, $rt)
{ {
$status = $jo->getStatus(); $status = $jo->getStatus();

View file

@ -2,6 +2,7 @@
namespace App\Service\HubFilter; namespace App\Service\HubFilter;
use App\Entity\Customer;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -12,18 +13,19 @@ use App\Entity\JORejection;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
use App\Ramcar\JORejectionReason; use App\Ramcar\JORejectionReason;
use App\Service\RisingTideGateway; use App\Service\RisingTideGateway;
use App\Ramcar\HubCriteria;
use DateTime; use DateTime;
class BaseHubFilter class BaseHubFilter
{ {
protected $id; protected $id;
protected $jo_id; protected $cust;
protected $customer_id;
protected $hub_filter_logger; protected $hub_filter_logger;
protected $em; protected $em;
protected $rt; protected $rt;
protected $trans; protected $trans;
protected $crit;
public function __construct(HubFilterLogger $hub_filter_logger, EntityManagerInterface $em, RisingTideGateway $rt, TranslatorInterface $trans) public function __construct(HubFilterLogger $hub_filter_logger, EntityManagerInterface $em, RisingTideGateway $rt, TranslatorInterface $trans)
{ {
@ -37,36 +39,22 @@ class BaseHubFilter
error_log("-------------------"); error_log("-------------------");
} }
public function initialize(HubCriteria $crit, Customer $cust): void
{
$this->crit = $crit;
$this->cust = $cust;
}
public function getID(): string public function getID(): string
{ {
return $this->id; return $this->id;
} }
public function setJOID(int $jo_id)
{
$this->jo_id = $jo_id;
return $this;
}
public function getJOID(): int
{
return $this->jo_id;
}
public function setCustomerID(int $customer_id)
{
$this->customer_id = $customer_id;
return $this;
}
public function getCustomerID(): int
{
return $this->customer_id;
}
public function log(Hub $hub): void public function log(Hub $hub): void
{ {
$this->hub_filter_logger->logFilteredHub($hub, $this->getID(), $this->getJOID(), $this->getCustomerID()); $jo_id = $this->crit->getJobOrderId();
$this->hub_filter_logger->logFilteredHub($hub, $this->getID(), $jo_id, $this->cust->getID());
// log to file // log to file
$filename = '/../../../var/log/hub_rejection.log'; $filename = '/../../../var/log/hub_rejection.log';
@ -74,7 +62,7 @@ class BaseHubFilter
// build log entry // build log entry
$entry = implode("", [ $entry = implode("", [
"[JO: " . $this->getJOID() . "]", "[JO: " . $jo_id . "]",
"[" . $date . "]", "[" . $date . "]",
"[" . $this->getID() . "]", "[" . $this->getID() . "]",
" " . $hub->getName() . " (ID: " . $hub->getID() . ")", " " . $hub->getName() . " (ID: " . $hub->getID() . ")",
@ -86,7 +74,12 @@ class BaseHubFilter
protected function createRejectionEntry($hub, $reason, $remarks = ""): JORejection protected function createRejectionEntry($hub, $reason, $remarks = ""): JORejection
{ {
$jo = $this->em->getRepository(JobOrder::class)->find($this->getJOID()); $jo_id = $this->crit->getJobOrderId();
$jo = null;
if (!empty($jo_id)) {
$jo = $this->em->getRepository(JobOrder::class)->find($jo_id);
}
$robj = new JORejection(); $robj = new JORejection();
$robj->setDateCreate(new DateTime()) $robj->setDateCreate(new DateTime())
@ -103,7 +96,7 @@ class BaseHubFilter
protected function sendSMSMessage($hub, $order_date, $service_type, $rejection, $reason = "", $remarks = ""): void protected function sendSMSMessage($hub, $order_date, $service_type, $rejection, $reason = "", $remarks = ""): void
{ {
$jo_id = $this->getJOID(); $jo_id = $this->crit->getJobOrderId();
// check if we already have a rejection record for this hub and JO. this also means an SMS was already sent // check if we already have a rejection record for this hub and JO. this also means an SMS was already sent
$rejection_count = $this->em->createQueryBuilder() $rejection_count = $this->em->createQueryBuilder()

View file

@ -4,21 +4,22 @@ namespace App\Service\HubFilter\Filters;
use App\Service\HubFilter\BaseHubFilter; use App\Service\HubFilter\BaseHubFilter;
use App\Service\HubFilter\HubFilterInterface; use App\Service\HubFilter\HubFilterInterface;
use App\Ramcar\HubCriteria;
class DateAndTimeHubFilter extends BaseHubFilter implements HubFilterInterface class DateAndTimeHubFilter extends BaseHubFilter implements HubFilterInterface
{ {
protected $id = 'date_and_time'; protected $id = 'date_and_time';
public function getRequestedParams() : array public function isApplicable() : bool
{ {
return [ return true;
'date_time',
];
} }
public function filter(array $hubs, array $params = []) : array public function filter(array $hubs) : array
{ {
if ($params['date_time'] == null) $filter_time = $this->crit->getDateTime();
if ($filter_time == null)
return $hubs; return $hubs;
$results = []; $results = [];
@ -32,22 +33,27 @@ class DateAndTimeHubFilter extends BaseHubFilter implements HubFilterInterface
// is open/available on date/day // is open/available on date/day
$hub = $hub_data['hub']; $hub = $hub_data['hub'];
$time_open = $hub->getTimeOpen()->format("H:i:s"); $time_open = $hub->getTimeOpen();
$time_close = $hub->getTimeClose()->format("H:i:s"); $time_close = $hub->getTimeClose();
$filter_time = $params['date_time']->format("H:i:s"); // normalize dates to be the same as filter time
$time_open->setDate(
$filter_time->format('Y'),
$filter_time->format('m'),
$filter_time->format('d')
);
$time_close->setDate(
$filter_time->format('Y'),
$filter_time->format('m'),
$filter_time->format('d')
);
// check filter time falls within operating hours
if (($filter_time >= $time_open) && if (($filter_time >= $time_open) &&
($filter_time <= $time_close)) ($filter_time <= $time_close))
{ {
$results[] = [ $results[] = $hub_data;
'hub' => $hub,
'db_distance' => $hub_data['db_distance'],
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => 0,
'inventory' => $hub_data['inventory'],
];
} }
else else
$this->log($hub); $this->log($hub);

View file

@ -0,0 +1,59 @@
<?php
namespace App\Service\HubFilter\Filters;
use App\Entity\Hub;
use App\Service\HubFilter\BaseHubFilter;
use App\Service\HubFilter\HubFilterInterface;
class DistanceHubFilter extends BaseHubFilter implements HubFilterInterface
{
protected $id = 'not_within_distance';
public function isApplicable() : bool
{
return !$this->crit->isEmergency();
}
public function filter(array $hubs) : array
{
$results = [];
foreach ($hubs as $hub_data)
{
$hub = $hub_data['hub'];
$dist = $hub_data['distance'];
// check if within distance threshold
if ($dist < $this->crit->getLimitDistance()) {
// log to legacy file
$this->logHubWithinDistance($hub, $dist);
// add to results
$results[] = $hub_data;
} else {
$this->log($hub);
}
}
return $results;
}
// NOTE: pulled from old hub selector class to maintain the log file
protected function logHubWithinDistance(Hub $hub, float $distance): void
{
// log to file
$filename = '/../../../../var/log/closest_hubs_selected.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("", [
"[JO: " . $this->crit->getJobOrderId() . "]",
"[" . $date . "]",
"[Distance: " . $distance . " vs " . $this->crit->getLimitDistance() . "]",
" " . $hub->getName() . " (ID: " . $hub->getID() . ")",
"\r\n",
]);
@file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
}
}

View file

@ -28,45 +28,40 @@ class InventoryHubFilter extends BaseHubFilter implements HubFilterInterface
$this->im = $im; $this->im = $im;
} }
public function getRequestedParams() : array public function isApplicable() : bool
{ {
return [ return true;
'flag_inventory_check',
'customer_class',
'jo_type',
'jo_origin',
'order_date',
'service_type',
'items',
];
} }
public function filter(array $hubs, array $params = []) : array public function filter(array $hubs) : array
{ {
$jo_id = $this->crit->getJobOrderId();
$jo_type = $this->crit->getJoType();
// check if this is enabled // check if this is enabled
if (!$params['flag_inventory_check']) { if (!$this->crit->hasInventoryCheck()) {
error_log("INVENTORY CHECK " . $this->getJOID() . ": DISABLED"); error_log("INVENTORY CHECK " . $jo_id . ": DISABLED");
return $hubs; return $hubs;
} }
// check customer class // check customer class
if ((!empty($params['customer_class']) && $params['customer_class'] == CustomerClassification::VIP) || if ((!empty($params['customer_class']) && $this->crit->getCustomerClass() == CustomerClassification::VIP) ||
$params['jo_origin'] === TransactionOrigin::VIP) { $this->crit->getJoOrigin() === TransactionOrigin::VIP) {
error_log("INVENTORY CHECK " . $this->getJOID() . ": VIP CLASS"); error_log("INVENTORY CHECK " . $jo_id . ": VIP CLASS");
return $hubs; return $hubs;
} }
// check item list is not empty // check item list is not empty
if (empty($params['items'])) { if (empty($params['items'])) {
error_log("INVENTORY CHECK " . $this->getJOID() . ": NO ITEMS"); error_log("INVENTORY CHECK " . $jo_id . ": NO ITEMS");
return $hubs; return $hubs;
} }
// check this is a battery item related JO // check this is a battery item related JO
if ($params['jo_type'] != ServiceType::BATTERY_REPLACEMENT_NEW && if ($jo_type != ServiceType::BATTERY_REPLACEMENT_NEW &&
$params['jo_type'] != ServiceType::BATTERY_REPLACEMENT_WARRANTY $jo_type != ServiceType::BATTERY_REPLACEMENT_WARRANTY
) { ) {
error_log("INVENTORY CHECK " . $this->getJOID() . ": INVALID SERVICE TYPE: " . $params['jo_type']); error_log("INVENTORY CHECK " . $jo_id . ": INVALID SERVICE TYPE: " . $jo_type);
return $hubs; return $hubs;
} }
@ -84,7 +79,8 @@ class InventoryHubFilter extends BaseHubFilter implements HubFilterInterface
$qtys = []; $qtys = [];
// call inventory manager for all hubs for selected SKUs // call inventory manager for all hubs for selected SKUs
$skus = array_keys($params['items']); $items = $this->crit->getItems();
$skus = array_keys($items);
error_log("CHECKING INVENTORY FOR " . count($skus) . " ITEM(S) ON HUBS " . count($branch_codes) . "..."); error_log("CHECKING INVENTORY FOR " . count($skus) . " ITEM(S) ON HUBS " . count($branch_codes) . "...");
@ -96,7 +92,7 @@ class InventoryHubFilter extends BaseHubFilter implements HubFilterInterface
foreach ($branches as $branch) { foreach ($branches as $branch) {
if (isset($branch['BranchCode'])) { if (isset($branch['BranchCode'])) {
// filter out branch if it does not have sufficient inventory // filter out branch if it does not have sufficient inventory
if (!isset($params['items'][$branch['SapCode']]) || $branch['Quantity'] < $params['items'][$branch['SapCode']] && if (!isset($params['items'][$branch['SapCode']]) || $branch['Quantity'] < $items[$branch['SapCode']] &&
!isset($hubs_to_filter[$branch['BranchCode']]) !isset($hubs_to_filter[$branch['BranchCode']])
) { ) {
error_log("FILTERING BRANCH WITH NO INVENTORY: " . $branch['BranchCode']); error_log("FILTERING BRANCH WITH NO INVENTORY: " . $branch['BranchCode']);
@ -124,8 +120,6 @@ class InventoryHubFilter extends BaseHubFilter implements HubFilterInterface
// check if we are filtering this hub // check if we are filtering this hub
if (isset($hubs_to_filter[$branch_code]) || empty($branch_code) || !isset($qtys[$branch_code])) { if (isset($hubs_to_filter[$branch_code]) || empty($branch_code) || !isset($qtys[$branch_code])) {
// if we have a JO, create rejection record and notify // if we have a JO, create rejection record and notify
$jo_id = $this->getJOID();
if (!empty($jo_id)) { if (!empty($jo_id)) {
// create rejection report entry // create rejection report entry
$robj = $this->createRejectionEntry( $robj = $this->createRejectionEntry(
@ -137,8 +131,8 @@ class InventoryHubFilter extends BaseHubFilter implements HubFilterInterface
// build SMS message // build SMS message
$this->sendSMSMessage( $this->sendSMSMessage(
$hub, $hub,
$params['order_date'], $this->crit->getOrderDate(),
$params['service_type'], $this->crit->getServiceType(),
$robj, $robj,
JORejectionReason::getName(JORejectionReason::NO_STOCK_SALES), JORejectionReason::getName(JORejectionReason::NO_STOCK_SALES),
"Requested SKU(s) - " . $battery_string "Requested SKU(s) - " . $battery_string

View file

@ -9,20 +9,17 @@ class JoTypeHubFilter extends BaseHubFilter implements HubFilterInterface
{ {
protected $id = 'job_order_type'; protected $id = 'job_order_type';
public function getRequestedParams() : array public function isApplicable() : bool
{ {
return [ return true;
'flag_emergency',
'jo_type',
];
} }
public function filter(array $hubs, array $params = []) : array public function filter(array $hubs) : array
{ {
if ($params['flag_emergency']) if ($this->crit->isEmergency())
return $hubs; return $hubs;
if (empty($params['jo_type'])) if (empty($this->crit->getJoType()))
return $hubs; return $hubs;
$results = []; $results = [];

View file

@ -9,22 +9,22 @@ class MaxResultsHubFilter extends BaseHubFilter implements HubFilterInterface
{ {
protected $id = 'max_results'; protected $id = 'max_results';
public function getRequestedParams() : array public function isApplicable() : bool
{ {
return [ return true;
'limit_results',
];
} }
public function filter(array $hubs, array $params = []) : array public function filter(array $hubs) : array
{ {
if (empty($params['limit_results'])) $limit_results = $this->crit->getLimitResults();
if (empty($limit_results))
return $hubs; return $hubs;
$results = []; $results = [];
for ($i = 0; $i < count($hubs); $i++) for ($i = 0; $i < count($hubs); $i++)
{ {
if ($i < $params['limit_results']) if ($i < $limit_results)
$results[] = $hubs[$i]; $results[] = $hubs[$i];
else else
$this->log($hubs[$i]['hub']); $this->log($hubs[$i]['hub']);

View file

@ -9,20 +9,17 @@ class PaymentMethodHubFilter extends BaseHubFilter implements HubFilterInterface
{ {
protected $id = 'no_payment_method'; protected $id = 'no_payment_method';
public function getRequestedParams() : array public function isApplicable() : bool
{ {
return [ return true;
'flag_emergency',
'payment_method',
];
} }
public function filter(array $hubs, array $params = []) : array public function filter(array $hubs) : array
{ {
if ($params['flag_emergency']) if ($this->crit->isEmergency())
return $hubs; return $hubs;
if (empty($params['payment_method'])) if (empty($this->crit->getPaymentMethod()))
return $hubs; return $hubs;
$results = []; $results = [];
@ -37,7 +34,7 @@ class PaymentMethodHubFilter extends BaseHubFilter implements HubFilterInterface
$flag_found_pmethod = false; $flag_found_pmethod = false;
foreach ($payment_methods as $pmethod) foreach ($payment_methods as $pmethod)
{ {
if ($pmethod == $params['payment_method']) if ($pmethod == $this->crit->getPaymentMethod())
{ {
$results[] = [ $results[] = [
'hub' => $hub, 'hub' => $hub,

View file

@ -12,26 +12,24 @@ class RiderAvailabilityHubFilter extends BaseHubFilter implements HubFilterInter
{ {
protected $id = 'no_available_rider'; protected $id = 'no_available_rider';
public function getRequestedParams() : array public function isApplicable() : bool
{ {
return [ return true;
'flag_riders_check',
'customer_class',
'order_date',
'service_type',
];
} }
public function filter(array $hubs, array $params = []) : array public function filter(array $hubs) : array
{ {
$cust_class = $this->crit->getCustomerClass();
$jo_id = $this->crit->getJobOrderId();
// check if this is enabled // check if this is enabled
if (!$params['flag_riders_check']) { if (!$this->crit->hasRidersCheck()) {
return $hubs; return $hubs;
} }
// check customer class // check customer class
if (!empty($params['customer_class']) && $params['customer_class'] == CustomerClassification::VIP) { if (!empty($cust_class) && $cust_class == CustomerClassification::VIP) {
error_log("RIDER CHECK " . $this->getJOID() . ": VIP CLASS"); error_log("RIDER CHECK " . $jo_id . ": VIP CLASS");
return $hubs; return $hubs;
} }
@ -45,8 +43,6 @@ class RiderAvailabilityHubFilter extends BaseHubFilter implements HubFilterInter
error_log("TOTAL RIDERS: " . $available_riders); error_log("TOTAL RIDERS: " . $available_riders);
if ($available_riders === 0) { if ($available_riders === 0) {
// if we have a JO, create rejection record and notify // if we have a JO, create rejection record and notify
$jo_id = $this->getJOID();
if (!empty($jo_id)) { if (!empty($jo_id)) {
// create rejection report entry // create rejection report entry
$robj = $this->createRejectionEntry($hub, JORejectionReason::NO_RIDER_AVAILABLE); $robj = $this->createRejectionEntry($hub, JORejectionReason::NO_RIDER_AVAILABLE);
@ -54,8 +50,8 @@ class RiderAvailabilityHubFilter extends BaseHubFilter implements HubFilterInter
// build SMS message // build SMS message
$this->sendSMSMessage( $this->sendSMSMessage(
$hub, $hub,
$params['order_date'], $this->crit->getOrderDate(),
$params['service_type'], $this->crit->getServiceType(),
$robj, $robj,
JORejectionReason::getName(JORejectionReason::NO_RIDER_AVAILABLE), JORejectionReason::getName(JORejectionReason::NO_RIDER_AVAILABLE),
); );

View file

@ -23,16 +23,14 @@ class RoundRobinHubFilter extends BaseHubFilter implements HubFilterInterface
$this->hub_distributor = $hub_distributor; $this->hub_distributor = $hub_distributor;
} }
public function getRequestedParams() : array public function isApplicable() : bool
{ {
return [ return true;
'flag_round_robin',
];
} }
public function filter(array $hubs, array $params = []) : array public function filter(array $hubs) : array
{ {
if (!$params['flag_round_robin']) if (!$this->crit->isRoundRobin())
return $hubs; return $hubs;
$results = []; $results = [];

View file

@ -2,19 +2,16 @@
namespace App\Service\HubFilter; namespace App\Service\HubFilter;
use App\Entity\Customer;
use App\Ramcar\HubCriteria;
interface HubFilterInterface interface HubFilterInterface
{ {
public function initialize(HubCriteria $crit, Customer $cust);
public function getID() : string; public function getID() : string;
public function filter(array $hubs, array $params = []) : array; public function isApplicable() : bool;
public function setJOID(int $jo_id); public function filter(array $hubs) : array;
public function getJOID() : int;
public function setCustomerID(int $customer_id);
public function getCustomerID() : int;
public function getRequestedParams() : array;
} }

View file

@ -2,6 +2,7 @@
namespace App\Service; namespace App\Service;
use App\Entity\Customer;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -15,6 +16,7 @@ use App\Service\HubFilterLogger;
use App\Service\RisingTideGateway; use App\Service\RisingTideGateway;
use App\Ramcar\HubCriteria; use App\Ramcar\HubCriteria;
use App\Service\HubFilter\HubFilterInterface;
class HubSelector class HubSelector
{ {
@ -26,9 +28,7 @@ class HubSelector
protected $trans; protected $trans;
protected $rt; protected $rt;
public function __construct(ContainerInterface $container, EntityManagerInterface $em, InventoryManager $im, public function __construct(ContainerInterface $container, EntityManagerInterface $em, InventoryManager $im, HubDistributor $hub_distributor, HubFilterLogger $hub_filter_logger, TranslatorInterface $trans, RisingTideGateway $rt)
HubDistributor $hub_distributor, HubFilterLogger $hub_filter_logger,
TranslatorInterface $trans, RisingTideGateway $rt)
{ {
$this->container = $container; $this->container = $container;
$this->em = $em; $this->em = $em;
@ -51,84 +51,87 @@ class HubSelector
return $enabled_filters; return $enabled_filters;
} }
public function find(HubCriteria $criteria) public function find(HubCriteria $criteria): array
{ {
$point = $criteria->getPoint(); // get all the hubs
$limit_results = $criteria->getLimitResults(); $hubs = $this->getHubList($criteria->getPoint());
$limit_distance = $criteria->getLimitDistance();
$jo_type = $criteria->getJoType();
$flag_inventory_check = $criteria->hasInventoryCheck();
$flag_riders_check = $criteria->hasRidersCheck();
$items = $criteria->getItems();
$date_time = $criteria->getDateTime();
$payment_method = $criteria->getPaymentMethod();
$flag_emergency = $criteria->isEmergency();
$flag_round_robin = $criteria->isRoundRobin();
$jo_id = $criteria->getJobOrderId();
$jo_origin = $criteria->getJoOrigin();
$customer_id = $criteria->getCustomerId();
$customer_class = $criteria->getCustomerClass();
// needed for JORejection records and SMS notifs // get customer record
$order_date = $criteria->getOrderDate(); $cust = $this->em->getRepository(Customer::class)->find($criteria->getCustomerId());
$service_type = $criteria->getServiceType();
// error_log('payment methods ' . $payment_method); // get all areas that cover the JO location
// error_log('distance limit ' . $limit_distance); $areas = $this->getAreaCoverage($criteria);
// error_log('emergency flag ' . $flag_emergency);
// get all the hubs within distance
$filtered_hubs = $this->getClosestHubs($point, $limit_distance, $jo_id, $customer_id);
// build param list
$params = [
'date_time' => $date_time,
'flag_inventory_check' => $flag_inventory_check,
'customer_class' => $customer_class,
'jo_type' => $jo_type,
'jo_origin' => $jo_origin,
'order_date' => $order_date,
'service_type' => $service_type,
'items' => $items,
'flag_emergency' => $flag_emergency,
'limit_results' => $limit_results,
'payment_method' => $payment_method,
'flag_riders_check' => $flag_riders_check,
'flag_round_robin' => $flag_round_robin,
];
// loop through all enabled filters // loop through all enabled filters
foreach ($this->getActiveFilters() as $hub_filter) { foreach ($this->getActiveFilters() as $hub_filter) {
// no hubs left to filter // no hubs left to filter
if (empty($filtered_hubs)) { if (empty($hubs)) {
break; break;
} }
// initialize the filter
$f = $this->container->get($hub_filter); $f = $this->container->get($hub_filter);
$f->initialize($criteria, $cust);
// check if supported area is exempted from this filter // check if supported area is exempted from this filter
if ($this->isExemptedByArea($f->getID(), $point)) { if ($this->isExemptedByArea($areas, $f)) {
continue; continue;
} }
$f->setJOID($jo_id); // run the filter
$f->setCustomerID($customer_id); $hubs = $f->filter($hubs, $criteria);
// get requested params only
$req_params = array_intersect_key($params, array_flip($f->getRequestedParams()));
// filter hub list
$filtered_hubs = $f->filter($filtered_hubs, $req_params);
// error_log($f->getID() . ' hubs ' . json_encode($filtered_hubs));
} }
// error_log('final hub list ' . json_encode($filtered_hubs)); // error_log('final hub list ' . json_encode($hubs));
return $filtered_hubs; return $hubs;
} }
protected function getClosestHubs(Point $point, $limit_distance, $jo_id, $customer_id) protected function isExemptedByArea(array $areas, HubFilterInterface $filter): bool
{
$is_exempted = false;
if (!empty($areas)) {
// check if at least one area has this filter enabled
$has_support = false;
foreach ($areas as $area) {
// get all exceptions
$exceptions = $area->getHubFilterExceptions();
// if any area has this filter enabled, consider it enabled and move on
if (!isset($exceptions[$filter->getID()])) {
$has_support = true;
break;
}
}
// none of the areas have this filter enabled, consider it exempted
if (!$has_support) {
error_log("skipping filter " . $filter->getID() . " due to exempted area");
$is_exempted = true;
}
}
// filter is in place
return $is_exempted;
}
protected function getAreaCoverage(HubCriteria $criteria): array
{
$point = $criteria->getPoint();
$long = $point->getLongitude();
$lat = $point->getLatitude();
// get supported area given a set of coordinates
$query = $this->em->createQuery('SELECT s from App\Entity\SupportedArea s where st_contains(s.coverage_area, point(:long, :lat)) = true');
return $query->setParameter('long', $long)
->setParameter('lat', $lat)
->getResult();
}
protected function getHubList(Point $point): array
{ {
// get closest hubs based on st_distance function from db // 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_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';
@ -141,49 +144,34 @@ class HubSelector
$result = $query->getResult(); $result = $query->getResult();
$hubs = []; $hubs = [];
$hubs_data = [];
foreach ($result as $row)
{
$hubs[] = $row[0];
// get coordinates of hub foreach ($result as $row) {
$hub_coordinates = $row[0]->getCoordinates(); $hub = $row[0];
$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 // get distance in kilometers from customer point to hub point
$dist = $this->distance($cust_lat, $cust_lng, $hub_lat, $hub_lng); $dist = $this->getKmDistance($point, $hub->getCoordinates());
if ($dist < $limit_distance) $hubs[] = [
{ 'hub' => $hub,
$hubs_data[] = [ 'db_distance' => $row['dist'],
'hub' => $row[0], 'distance' => $dist,
'db_distance' => $row['dist'], 'duration' => 0,
'distance' => $dist, 'jo_count' => 0,
'duration' => 0, 'inventory' => 0,
'jo_count' => 0, ];
'inventory' => 0,
];
// log to file
$this->logClosestHubResult($jo_id, $row[0], $dist, $limit_distance);
}
else
{
$this->hub_filter_logger->logFilteredHub($row[0], 'not_within_distance', $jo_id, $customer_id);
}
} }
return $hubs_data; return $hubs;
} }
// convert db distance to kilometers protected function getKmDistance(Point $point1, Point $point2): float
protected function distance($lat1, $lon1, $lat2, $lon2)
{ {
// get coordinates in radians
$lat1 = $point1->getLatitude();
$lon1 = $point1->getLongitude();
$lat2 = $point2->getLatitude();
$lon2 = $point2->getLongitude();
if (($lat1 == $lat2) && ($lon1 == $lon2)) if (($lat1 == $lat2) && ($lon1 == $lon2))
return 0; return 0;
@ -193,53 +181,7 @@ class HubSelector
$dist = rad2deg($dist); $dist = rad2deg($dist);
$miles = $dist * 60 * 1.1515; $miles = $dist * 60 * 1.1515;
return round(($miles * 1.609344), 1); return round($miles * 1.609344, 1);
}
protected function isExemptedByArea(string $filter_id, Point $coordinates): bool
{
$long = $coordinates->getLongitude();
$lat = $coordinates->getLatitude();
// get supported area given a set of coordinates
$query = $this->em->createQuery('SELECT s from App\Entity\SupportedArea s where st_contains(s.coverage_area, point(:long, :lat)) = true');
$area = $query->setParameter('long', $long)
->setParameter('lat', $lat)
->setMaxResults(1)
->getOneOrNullResult();
if ($area !== null) {
// get all exceptions
$exceptions = $area->getHubFilterExceptions();
if (isset($exceptions[$filter_id])) {
error_log("FILTER " . $filter_id . " DISABLED FOR AREA: " . $area->getName());
// disable this filter for this area
return true;
}
}
// filter is in place
return false;
}
protected function logClosestHubResult($jo_id, $hub, $distance, $limit_distance)
{
// log to file
$filename = '/../../var/log/closest_hubs_selected.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("", [
"[JO: " . $jo_id . "]",
"[" . $date . "]",
"[Distance: " . $distance . " vs " . $limit_distance . "]",
" " . $hub->getName() . " (ID: " . $hub->getID() . ")",
"\r\n",
]);
@file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
} }
} }