Resolve "Aggregate Rider Rating" #1689

Open
korina.cordero wants to merge 92 commits from 764-aggregate-rider-rating into 746-resq-2-0-final
70 changed files with 4275 additions and 144 deletions

3
.gitignore vendored
View file

@ -12,3 +12,6 @@
*.swp *.swp
/public/warranty_uploads/* /public/warranty_uploads/*
.vscode
*__pycache__
/public/assets/images/insurance-premiums.png

View file

@ -634,6 +634,45 @@ catalyst_auth:
- id: service_offering.delete - id: service_offering.delete
label: Delete label: Delete
- id: price_tier
label: Price Tier
acls:
- id: price_tier.menu
label: Menu
- id: price_tier.list
label: List
- id: price_tier.add
label: Add
- id: price_tier.update
label: Update
- id: price_tier.delete
label: Delete
- id: item_type
label: Item Type
acls:
- id: item_type.menu
label: Menu
- id: item_type.list
label: List
- id: item_type.add
label: Add
- id: item_type.update
label: Update
- id: item_type.delete
label: Delete
- id: item
label: Item
acls:
- id: item.menu
label: Menu
- id: item_pricing
label: Item Pricing
acls:
- id: item_pricing.update
label: Update
api: api:
user_entity: "App\\Entity\\ApiUser" user_entity: "App\\Entity\\ApiUser"
acl_data: acl_data:

View file

@ -177,7 +177,7 @@ catalyst_menu:
acl: support.menu acl: support.menu
label: '[menu.support]' label: '[menu.support]'
icon: flaticon-support icon: flaticon-support
order: 10 order: 11
- id: customer_list - id: customer_list
acl: customer.list acl: customer.list
label: '[menu.support.customers]' label: '[menu.support.customers]'
@ -223,7 +223,7 @@ catalyst_menu:
acl: service.menu acl: service.menu
label: '[menu.service]' label: '[menu.service]'
icon: flaticon-squares icon: flaticon-squares
order: 11 order: 12
- id: service_list - id: service_list
acl: service.list acl: service.list
label: '[menu.service.services]' label: '[menu.service.services]'
@ -233,7 +233,7 @@ catalyst_menu:
acl: partner.menu acl: partner.menu
label: '[menu.partner]' label: '[menu.partner]'
icon: flaticon-network icon: flaticon-network
order: 12 order: 13
- id: partner_list - id: partner_list
acl: partner.list acl: partner.list
label: '[menu.partner.partners]' label: '[menu.partner.partners]'
@ -247,7 +247,7 @@ catalyst_menu:
acl: motolite_event.menu acl: motolite_event.menu
label: '[menu.motolite_event]' label: '[menu.motolite_event]'
icon: flaticon-event-calendar-symbol icon: flaticon-event-calendar-symbol
order: 13 order: 14
- id: motolite_event_list - id: motolite_event_list
acl: motolite_event.list acl: motolite_event.list
label: '[menu.motolite_event.events]' label: '[menu.motolite_event.events]'
@ -257,7 +257,7 @@ catalyst_menu:
acl: analytics.menu acl: analytics.menu
label: '[menu.analytics]' label: '[menu.analytics]'
icon: flaticon-graphic icon: flaticon-graphic
order: 14 order: 15
- id: analytics_forecast_form - id: analytics_forecast_form
acl: analytics.forecast acl: analytics.forecast
label: '[menu.analytics.forecasting]' label: '[menu.analytics.forecasting]'
@ -267,7 +267,7 @@ catalyst_menu:
acl: database.menu acl: database.menu
label: '[menu.database]' label: '[menu.database]'
icon: fa fa-database icon: fa fa-database
order: 15 order: 16
- id: ticket_type_list - id: ticket_type_list
acl: ticket_type.menu acl: ticket_type.menu
label: '[menu.database.tickettypes]' label: '[menu.database.tickettypes]'
@ -288,3 +288,21 @@ catalyst_menu:
acl: service_offering.menu acl: service_offering.menu
label: '[menu.database.serviceofferings]' label: '[menu.database.serviceofferings]'
parent: database parent: database
- id: item_type_list
acl: item_type.menu
label: '[menu.database.itemtypes]'
parent: database
- id: item
acl: item.menu
label: Item Management
icon: fa fa-boxes
order: 10
- id: price_tier_list
acl: price_tier.list
label: Price Tiers
parent: item
- id: item_pricing
acl: item_pricing.update
label: Item Pricing
parent: item

View file

@ -303,3 +303,13 @@ apiv2_insurance_application_create:
path: /apiv2/insurance/application path: /apiv2/insurance/application
controller: App\Controller\CustomerAppAPI\InsuranceController::createApplication controller: App\Controller\CustomerAppAPI\InsuranceController::createApplication
methods: [POST] methods: [POST]
apiv2_insurance_premiums_banner:
path: /apiv2/insurance/premiums_banner
controller: App\Controller\CustomerAppAPI\InsuranceController::getPremiumsBanner
methods: [GET]
apiv2_insurance_body_types:
path: /apiv2/insurance/body_types
controller: App\Controller\CustomerAppAPI\InsuranceController::getBodyTypes
methods: [GET]

View file

@ -94,3 +94,24 @@ capi_rider_jo_start:
path: /rider_api/start path: /rider_api/start
controller: App\Controller\CAPI\RiderAppController::startJobOrder controller: App\Controller\CAPI\RiderAppController::startJobOrder
methods: [POST] methods: [POST]
# trade-ins
capi_rider_battery_sizes:
path: /rider_api/battery_sizes
controller: App\Controller\CAPI\RiderAppController::getBatterySizes
methods: [GET]
capi_rider_trade_in_types:
path: /rider_api/trade_in_types
controller: App\Controller\CAPI\RiderAppController::getTradeInTypes
methods: [GET]
capi_rider_battery_info:
path: /rider_api/battery/{serial}
controller: App\Controller\CAPI\RiderAppController::getBatteryInfo
methods: [GET]
capi_rider_update_jo:
path: /rider_api/job_order/update
controller: App\Controller\CAPI\RiderAppController::updateJobOrder
methods: [POST]

View file

@ -0,0 +1,14 @@
item_pricing:
path: /item-pricing
controller: App\Controller\ItemPricingController::index
methods: [GET]
item_pricing_update:
path: /item-pricing
controller: App\Controller\ItemPricingController::formSubmit
methods: [POST]
item_pricing_prices:
path: /item-pricing/{pt_id}/{it_id}/prices
controller: App\Controller\ItemPricingController::itemPrices
methods: [GET]

View file

@ -0,0 +1,34 @@
item_type_list:
path: /item-types
controller: App\Controller\ItemTypeController::index
methods: [GET]
item_type_rows:
path: /item-types/rowdata
controller: App\Controller\ItemTypeController::datatableRows
methods: [POST]
item_type_add_form:
path: /item-types/newform
controller: App\Controller\ItemTypeController::addForm
methods: [GET]
item_type_add_submit:
path: /item-types
controller: App\Controller\ItemTypeController::addSubmit
methods: [POST]
item_type_update_form:
path: /item-types/{id}
controller: App\Controller\ItemTypeController::updateForm
methods: [GET]
item_type_update_submit:
path: /item-types/{id}
controller: App\Controller\ItemTypeController::updateSubmit
methods: [POST]
item_type_delete:
path: /item-types/{id}
controller: App\Controller\ItemTypeController::deleteSubmit
methods: [DELETE]

View file

@ -0,0 +1,34 @@
price_tier_list:
path: /price-tiers
controller: App\Controller\PriceTierController::index
methods: [GET]
price_tier_rows:
path: /price-tiers/rows
controller: App\Controller\PriceTierController::datatableRows
methods: [POST]
price_tier_add_form:
path: /price-tiers/newform
controller: App\Controller\PriceTierController::addForm
methods: [GET]
price_tier_add_submit:
path: /price-tiers
controller: App\Controller\PriceTierController::addSubmit
methods: [POST]
price_tier_update_form:
path: /price-tiers/{id}
controller: App\Controller\PriceTierController::updateForm
methods: [GET]
price_tier_update_submit:
path: /price-tiers/{id}
controller: App\Controller\PriceTierController::updateSubmit
methods: [POST]
price_tier_delete:
path: /price-tiers/{id}
controller: App\Controller\PriceTierController::deleteSubmit
methods: [DELETE]

View file

@ -51,7 +51,7 @@ tapi_vehicle_make_list:
tapi_battery_list: tapi_battery_list:
path: /tapi/vehicles/{vid}/compatible_batteries path: /tapi/vehicles/{vid}/compatible_batteries
controller: App\Controller\TAPI\BatteryController::getCompatibleBatteries controller: App\Controller\TAPI\BatteryController::getCompatibleBatteries
methods: [GET] methods: [POST]
# promos # promos
tapi_promo_list: tapi_promo_list:

View file

@ -15,6 +15,7 @@ parameters:
api_version: "%env(API_VERSION)%" api_version: "%env(API_VERSION)%"
android_app_version: "%env(ANDROID_APP_VERSION)%" android_app_version: "%env(ANDROID_APP_VERSION)%"
ios_app_version: "%env(IOS_APP_VERSION)%" ios_app_version: "%env(IOS_APP_VERSION)%"
insurance_premiums_banner_url: "%env(INSURANCE_PREMIUMS_BANNER_URL)%"
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
@ -310,3 +311,8 @@ services:
arguments: arguments:
$server_key: "%env(FCM_SERVER_KEY)%" $server_key: "%env(FCM_SERVER_KEY)%"
$sender_id: "%env(FCM_SENDER_ID)%" $sender_id: "%env(FCM_SENDER_ID)%"
# price tier manager
App\Service\PriceTierManager:
arguments:
$em: "@doctrine.orm.entity_manager"

View file

@ -0,0 +1,123 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\AggregatedRiderRating;
use PDO;
class LoadAggregateRiderRatingsComand extends Command
{
protected $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
parent::__construct();
}
protected function configure()
{
$this->setName('aggregated_rider_rating:load')
->setDescription('Add rider ratings to aggregated rider rating.')
->setHelp('Add rider ratings to aggregated rider rating.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// get the ids of all riders
$rider_id_list = $this->getRiderIds();
$this->processAggregateRiderRating($rider_id_list);
return 0;
}
protected function processAggregateRiderRating($rider_id_list)
{
$db = $this->em->getConnection();
$agg_rider_ratings = [];
// get all rider ratings per rider
$rr_query_sql = 'SELECT rating FROM rider_rating WHERE rider_id = :id';
$rr_query_stmt = $db->prepare($rr_query_sql);
foreach ($rider_id_list as $key => $rider_id)
{
$rr_query_stmt->bindValue('id', $rider_id);
$results = $rr_query_stmt->executeQuery();
$total_jos = 0;
$total_rating = 0;
while ($row = $results->fetchAssociative())
{
$rating = $row['rating'];
$total_rating = bcadd($total_rating, $rating, 2);
// increment number of JOs per rider
$total_jos++;
}
// compute average
$agg_rating = 0;
if ($total_jos > 0)
$agg_rating = bcdiv($total_rating, $total_jos, 2);
$agg_rider_ratings[$rider_id] = [
'agg_rating' => $agg_rating,
'agg_count' => $total_jos,
];
}
// create aggregated rider rating
$this->createAggregatedRiderRating($agg_rider_ratings);
}
protected function createAggregatedRiderRating($agg_rider_ratings)
{
// error_log(print_r($agg_rider_ratings, true));
foreach ($agg_rider_ratings as $key => $data)
{
// create new AggregatedRiderRating object
$obj = new AggregatedRiderRating();
// set fields
$obj->setRiderId($key)
->setAggregateRating($data['agg_rating'])
->setAggregateCount($data['agg_count']);
// save to database
$this->em->persist($obj);
}
$this->em->flush();
}
protected function getRiderIds()
{
$rider_ids = [];
$db = $this->em->getConnection();
$query_sql = 'SELECT id FROM rider';
$query_stmt = $db->prepare($query_sql);
$results = $query_stmt->executeQuery();
while ($row = $results->fetchAssociative())
{
$rider_ids[] = $row['id'];
}
return $rider_ids;
}
}

View file

@ -50,6 +50,7 @@ use App\Service\HubFilterLogger;
use App\Service\HubFilteringGeoChecker; use App\Service\HubFilteringGeoChecker;
use App\Service\HashGenerator; use App\Service\HashGenerator;
use App\Service\JobOrderManager; use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use App\Entity\MobileSession; use App\Entity\MobileSession;
use App\Entity\Customer; use App\Entity\Customer;
@ -70,6 +71,7 @@ use App\Entity\Hub;
use App\Entity\SAPBattery; use App\Entity\SAPBattery;
use App\Entity\WarrantySerial; use App\Entity\WarrantySerial;
use App\Entity\CustomerMetadata; use App\Entity\CustomerMetadata;
use App\Entity\AggregatedRiderRating;
use DateTime; use DateTime;
use DateInterval; use DateInterval;
@ -1714,7 +1716,13 @@ class APIController extends Controller implements LoggedController
$em->persist($rating); $em->persist($rating);
$em->flush(); $em->flush();
// TODO: set average rating in rider entity // need to update or add aggregated rider rating
$average_rating = $this->updateAggregatedRiderRating($em, $rider, $rating_num);
// TODO: preliminary rating computation on the entity for now
$rider->updateRatingAverage($average_rating);
$em->persist($rider);
$em->flush();
$res->setData([]); $res->setData([]);
@ -2911,6 +2919,10 @@ class APIController extends Controller implements LoggedController
// old app doesn't have separate jumpstart // old app doesn't have separate jumpstart
$icrit->setSource(TransactionOrigin::CALL); $icrit->setSource(TransactionOrigin::CALL);
// set price tier
$pt_id = $this->pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// check promo // check promo
$promo_id = $req->request->get('promo_id'); $promo_id = $req->request->get('promo_id');
if (!empty($promo_id)) if (!empty($promo_id))
@ -4844,6 +4856,60 @@ class APIController extends Controller implements LoggedController
return $jo_data; return $jo_data;
} }
protected function updateAggregatedRiderRating($em, $rider, $rating_num)
{
$rider_id = $rider->getID();
$agg_rating = 0;
// check if rider is in the the aggregated rider rating table
$agg_rider_rating = $em->getRepository(AggregatedRiderRating::class)->findOneBy(['rider_id' => $rider_id]);
if ($agg_rider_rating == null)
{
// new rider, new entry
$old_rating = 0;
$old_count = 0;
$new_count = 1;
$agg_rating = $this->computeAggregatedRiderRating($old_rating, $rating_num, $old_count, $new_count);
$obj = new AggregatedRiderRating();
$obj->setRiderId($rider_id)
->setAggregateRating($agg_rating)
->setAggregateCount($new_count);
$em->persist($obj);
}
else
{
// existing rider, update entry
$r_rating = $agg_rider_rating->getAggregateRating();
$r_count = $agg_rider_rating->getAggregateCount();
$new_count = $r_count + 1;
$agg_rating = $this->computeAggregatedRiderRating($r_rating, $rating_num, $r_count, $new_count);
// set updated values for entry
$agg_rider_rating->setAggregateRating($agg_rating)
->setAggregateCount($new_count);
}
$em->flush();
return $agg_rating;
}
protected function computeAggregatedRiderRating($old_rating, $new_rating, $r_count, $new_count)
{
// ((existing aggregate rating * existing aggregate count) + new rating) / new count
$agg_comp = bcmul($old_rating, $r_count, 2);
$rating = bcadd($agg_comp, $new_rating, 2);
$agg_rating = bcdiv($rating, $new_count, 2);
return $agg_rating;
}
protected function normalizeString($string) protected function normalizeString($string)
{ {
return trim(strtolower($string)); return trim(strtolower($string));

View file

@ -23,7 +23,9 @@ use App\Entity\BatterySize;
use App\Entity\RiderAPISession; use App\Entity\RiderAPISession;
use App\Entity\User; use App\Entity\User;
use App\Entity\ApiUser as APIUser; use App\Entity\ApiUser as APIUser;
use App\Entity\JobOrder;
use App\Entity\SAPBattery;
use App\Entity\WarrantySerial;
use App\Service\RedisClientProvider; use App\Service\RedisClientProvider;
use App\Service\RiderCache; use App\Service\RiderCache;
use App\Service\MQTTClient; use App\Service\MQTTClient;
@ -34,6 +36,7 @@ use App\Service\JobOrderHandlerInterface;
use App\Service\InvoiceGeneratorInterface; use App\Service\InvoiceGeneratorInterface;
use App\Service\RisingTideGateway; use App\Service\RisingTideGateway;
use App\Service\RiderTracker; use App\Service\RiderTracker;
use App\Service\PriceTierManager;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType; use App\Ramcar\TradeInType;
@ -286,8 +289,9 @@ class RiderAppController extends ApiController
// do we have a job order? // do we have a job order?
// $jo = $rider->getActiveJobOrder(); // $jo = $rider->getActiveJobOrder();
// NOTE: we do not include job orders that have been cancelled
$jo = $rider->getCurrentJobOrder(); $jo = $rider->getCurrentJobOrder();
if ($jo == null) if ($jo == null || $jo->getStatus() == JOStatus::CANCELLED)
{ {
$data = [ $data = [
'job_order' => null 'job_order' => null
@ -408,6 +412,9 @@ class RiderAppController extends ApiController
if (!empty($msg)) if (!empty($msg))
return new APIResponse(false, $msg); return new APIResponse(false, $msg);
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// TODO: refactor this into a jo handler class, so we don't have to repeat for control center // TODO: refactor this into a jo handler class, so we don't have to repeat for control center
// set jo status to in transit // set jo status to in transit
@ -458,6 +465,9 @@ class RiderAppController extends ApiController
// TODO: this is a workaround for requeue, because rider app gets stuck in accept / decline screen // TODO: this is a workaround for requeue, because rider app gets stuck in accept / decline screen
return new APIResponse(true, $msg); return new APIResponse(true, $msg);
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// requeue it, instead of cancelling it // requeue it, instead of cancelling it
$jo->requeue(); $jo->requeue();
@ -516,6 +526,9 @@ class RiderAppController extends ApiController
// get rider's current job order // get rider's current job order
$jo = $rider->getCurrentJobOrder(); $jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// set delivery status // set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB); $jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB);
@ -556,6 +569,9 @@ class RiderAppController extends ApiController
// get rider's current job order // get rider's current job order
$jo = $rider->getCurrentJobOrder(); $jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// set delivery status // set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB_PRE_JO); $jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB_PRE_JO);
@ -596,6 +612,9 @@ class RiderAppController extends ApiController
// get rider's current job order // get rider's current job order
$jo = $rider->getCurrentJobOrder(); $jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// set delivery status // set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB_PRE_JO); $jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB_PRE_JO);
@ -636,6 +655,9 @@ class RiderAppController extends ApiController
// get rider's current job order // get rider's current job order
$jo = $rider->getCurrentJobOrder(); $jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// set delivery status // set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_START); $jo->setDeliveryStatus(DeliveryStatus::RIDER_START);
@ -677,6 +699,9 @@ class RiderAppController extends ApiController
// set jo status to in progress // set jo status to in progress
$jo->setStatus(JOStatus::IN_PROGRESS); $jo->setStatus(JOStatus::IN_PROGRESS);
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// set delivery status // set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE); $jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE);
@ -735,6 +760,9 @@ class RiderAppController extends ApiController
// get rider's current job order // get rider's current job order
$jo = $rider->getCurrentJobOrder(); $jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// set delivery status // set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB); $jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB);
@ -758,6 +786,54 @@ class RiderAppController extends ApiController
return new APIResponse(true, 'Rider arrive at hub.', $data); return new APIResponse(true, 'Rider arrive at hub.', $data);
} }
public function getBatterySizes(Request $req, EntityManagerInterface $em)
{
// get capi user
$capi_user = $this->getUser();
if ($capi_user == null)
return new APIResponse(false, 'User not found.');
// get rider id from capi user metadata
$rider = $this->getRiderFromCAPI($capi_user, $em);
if ($rider == null)
return new APIResponse(false, 'No rider found.');
// get sizes
$qb = $em->getRepository(BatterySize::class)
->createQueryBuilder('bs');
$sizes = $qb->select('bs.id, bs.name')
->orderBy('bs.name', 'asc')
->getQuery()
->getResult();
// response
return new APIResponse(true, '', [
'sizes' => $sizes,
]);
}
public function getTradeInTypes(Request $req, EntityManagerInterface $em)
{
// get capi user
$capi_user = $this->getUser();
if ($capi_user == null)
return new APIResponse(false, 'User not found.');
// get rider id from capi user metadata
$rider = $this->getRiderFromCAPI($capi_user, $em);
if ($rider == null)
return new APIResponse(false, 'No rider found.');
// get trade-in types
$types = TradeInType::getCollection();
// response
return new APIResponse(true, '', [
'types' => $types,
]);
}
public function payment(Request $req, EntityManagerInterface $em, JobOrderHandlerInterface $jo_handler, public function payment(Request $req, EntityManagerInterface $em, JobOrderHandlerInterface $jo_handler,
RisingTideGateway $rt, WarrantyHandler $wh, MQTTClient $mclient, MQTTClientApiv2 $mclientv2, FCMSender $fcmclient, TranslatorInterface $translator) RisingTideGateway $rt, WarrantyHandler $wh, MQTTClient $mclient, MQTTClientApiv2 $mclientv2, FCMSender $fcmclient, TranslatorInterface $translator)
{ {
@ -777,6 +853,20 @@ class RiderAppController extends ApiController
if (!empty($msg)) if (!empty($msg))
return new APIResponse(false, $msg); return new APIResponse(false, $msg);
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// need to check if service type is battery sales
// if so, serial is a required parameter
$serial = $req->request->get('serial', '');
if ($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW)
{
/*
if (empty($serial))
return new APIResponse(false, 'Missing parameter(s): serial');
*/
}
// set invoice to paid // set invoice to paid
$jo->getInvoice()->setStatus(InvoiceStatus::PAID); $jo->getInvoice()->setStatus(InvoiceStatus::PAID);
@ -828,7 +918,6 @@ class RiderAppController extends ApiController
// create warranty // create warranty
if($jo_handler->checkIfNewBattery($jo)) if($jo_handler->checkIfNewBattery($jo))
{ {
$serial = null;
$warranty_class = $jo->getWarrantyClass(); $warranty_class = $jo->getWarrantyClass();
$first_name = $jo->getCustomer()->getFirstName(); $first_name = $jo->getCustomer()->getFirstName();
$last_name = $jo->getCustomer()->getLastName(); $last_name = $jo->getCustomer()->getLastName();
@ -912,6 +1001,9 @@ class RiderAppController extends ApiController
// get rider's current job order // get rider's current job order
$jo = $rider->getCurrentJobOrder(); $jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// set delivery status // set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB_POST_JO); $jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB_POST_JO);
@ -953,6 +1045,9 @@ class RiderAppController extends ApiController
// get rider's current job order // get rider's current job order
$jo = $rider->getCurrentJobOrder(); $jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// set delivery status // set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB_POST_JO); $jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB_POST_JO);
@ -1099,7 +1194,167 @@ class RiderAppController extends ApiController
return new APIResponse(true, 'Batteries found.', $data); return new APIResponse(true, 'Batteries found.', $data);
} }
public function changeService(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic) public function getBatteryInfo(Request $req, $serial, EntityManagerInterface $em)
{
if (empty($serial))
{
return new APIResponse(false, 'Missing parameter(s): serial');
}
// get capi user
$capi_user = $this->getUser();
if ($capi_user == null)
return new APIResponse(false, 'User not found.');
// get rider id from capi user metadata
$rider = $this->getRiderFromCAPI($capi_user, $em);
if ($rider == null)
return new APIResponse(false, 'No rider found.');
// find battery given serial/sap_code and flag_active is true
$serial = $em->getRepository(WarrantySerial::class)->find($serial);
if (empty($serial)) {
return new APIResponse(false, 'Warranty serial number not found.');
}
$sap_battery = $em->getRepository(SAPBattery::class)->find($serial->getSKU());
if (empty($sap_battery)) {
return new APIResponse(false, 'No battery info found.');
}
$battery = [
'id' => $sap_battery->getID(),
'brand' => $sap_battery->getBrand()->getName(),
'size' => $sap_battery->getSize()->getName(),
'size_id' => $sap_battery->getSize()->getID(),
'trade_in_type' => TradeInType::MOTOLITE,
'container_size' => $sap_battery->getContainerSize()->getName(),
];
return new APIResponse(true, 'Battery info found.', [
'battery' => $battery,
]);
}
public function updateJobOrder(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager)
{
$items = json_decode(file_get_contents('php://input'), true);
// get job order id
if (!isset($items['jo_id']))
return new APIResponse(false, 'Missing parameter(s): jo_id');
// validate jo_id
$jo_id = $items['jo_id'];
if (empty($jo_id) || $jo_id == null)
return new APIResponse(false, 'Missing parameter(s): jo_id');
// get capi user
$capi_user = $this->getUser();
if ($capi_user == null)
return new APIResponse(false, 'User not found.');
// get rider id from capi user metadata
$rider = $this->getRiderFromCAPI($capi_user, $em);
if ($rider == null)
return new APIResponse(false, 'No rider found.');
// get the job order
$jo = $em->getRepository(JobOrder::class)->find($jo_id);
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// check if we have trade in items
$ti_items = [];
if (isset($items['trade_in_items']))
{
// validate the trade in items first
$ti_items = $items['trade_in_items'];
$msg = $this->validateTradeInItems($em, $ti_items);
if (!empty($msg))
return new APIResponse(false, $msg);
}
// get the service type
if (!isset($items['stype_id']))
return new APIResponse(false, 'Missing parameter(s): stype_id');
// validate service type
$stype_id = $items['stype_id'];
if (!ServiceType::validate($stype_id))
return new APIResponse(false, 'Invalid service type - ' . $stype_id);
// save service type
$jo->setServiceType($stype_id);
// validate promo if any. Promo not required
$promo = null;
if (isset($items['promo_id']))
{
$promo_id = $items['promo_id'];
$promo = $em->getRepository(Promo::class)->find($promo_id);
if ($promo == null)
return new APIResponse(false, 'Invalid promo id - ' . $promo_id);
}
// get other parameters, if any: has motolite battery, has warranty doc, with coolant, payment method
if (isset($items['flag_motolite_battery']))
{
// get customer vehicle from jo
$cv = $jo->getCustomerVehicle();
$has_motolite = $items['flag_motolite_battery'];
if ($has_motolite == 'true')
$cv->setHasMotoliteBattery(true);
else
$cv->setHasMotoliteBattery(false);
$em->persist($cv);
}
if (isset($items['flag_warranty_doc']))
{
// TODO: what do we do?
}
if (isset($items['flag_coolant']))
{
$has_coolant = $items['flag_coolant'];
if ($has_coolant == 'true')
$jo->setHasCoolant(true);
else
$jo->setHasCoolant(false);
}
if (isset($items['mode_of_payment']))
{
$payment_method = $items['payment_method'];
if (!ModeOfPayment::validate($payment_method))
$payment_method = ModeOfPayment::CASH;
$jo->setModeOfPayment($payment_method);
}
// get capi user
$capi_user = $this->getUser();
if ($capi_user == null)
return new APIResponse(false, 'User not found.');
// get rider id from capi user metadata
$rider = $this->getRiderFromCAPI($capi_user, $em);
if ($rider == null)
return new APIResponse(false, 'No rider found.');
// need to get the existing invoice items using jo id and invoice id
$existing_ii = $this->getInvoiceItems($em, $jo);
$this->generateUpdatedInvoice($em, $ic, $jo, $existing_ii, $ti_items, $promo, $pt_manager);
$data = [];
return new APIResponse(true, 'Job order updated.', $data);
}
public function changeService(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager)
{ {
// $this->debugRequest($req); // $this->debugRequest($req);
@ -1120,6 +1375,9 @@ class RiderAppController extends ApiController
if (!empty($msg)) if (!empty($msg))
return new APIResponse(false, $msg); return new APIResponse(false, $msg);
// check if JO can be modified first
$this->checkJOProgressionAllowed($jo, $rider);
// check service type // check service type
$stype_id = $req->request->get('stype_id'); $stype_id = $req->request->get('stype_id');
if (!ServiceType::validate($stype_id)) if (!ServiceType::validate($stype_id))
@ -1203,6 +1461,10 @@ class RiderAppController extends ApiController
$crit->setHasCoolant($jo->hasCoolant()); $crit->setHasCoolant($jo->hasCoolant());
$crit->setIsTaxable(); $crit->setIsTaxable();
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$crit->setPriceTier($pt_id);
if ($promo != null) if ($promo != null)
$crit->addPromo($promo); $crit->addPromo($promo);
@ -1241,6 +1503,164 @@ class RiderAppController extends ApiController
return new APIResponse(true, 'Job order service changed.', $data); return new APIResponse(true, 'Job order service changed.', $data);
} }
protected function generateUpdatedInvoice(EntityManagerInterface $em, InvoiceGeneratorInterface $ic, JobOrder $jo, $existing_ii, $trade_in_items, $promo, PriceTierManager $pt_manager)
{
// get the service type
$stype = $jo->getServiceType();
// get the source
$source = $jo->getSource();
// get the customer vehicle
$cv = $jo->getCustomerVehicle();
// get coolant if any
$flag_coolant = $jo->hasCoolant();
// check if new promo is null
if ($promo == null)
{
// promo not updated from app so check existing invoice
// get the promo id from existing invoice item
$promo_id = $existing_ii['promo_id'];
if ($promo_id == null)
$promo = null;
else
$promo = $em->getRepository(Promo::class)->find($promo_id);
}
// populate Invoice Criteria
$icrit = new InvoiceCriteria();
$icrit->setServiceType($stype)
->setCustomerVehicle($cv)
->setSource($source)
->setHasCoolant($flag_coolant)
->setIsTaxable();
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// at this point, all information should be valid
// assuming JO information is already valid since this
// is in the system already
// add promo if any to criteria
if ($promo != null)
$icrit->addPromo($promo);
// get the battery purchased from existing invoice items
// add the batteries ordered to criteria
$ii_items = $existing_ii['invoice_items'];
foreach ($ii_items as $ii_item)
{
$batt_id = $ii_item['batt_id'];
$qty = $ii_item['qty'];
$battery = $em->getRepository(Battery::class)->find($batt_id);
$icrit->addEntry($battery, null, $qty);
}
// add the trade in items to the criteria
foreach ($trade_in_items as $ti_item)
{
$batt_size_id = $ti_item['battery_size_id'];
$qty = $ti_item['qty'];
$trade_in_type = $ti_item['trade_in_type'];
$batt_size = $em->getRepository(BatterySize::class)->find($batt_size_id);
$icrit->addTradeInEntry($batt_size, $trade_in_type, $qty);
}
// call generateInvoice
$invoice = $ic->generateInvoice($icrit);
// remove previous invoice
$old_invoice = $jo->getInvoice();
$em->remove($old_invoice);
$em->flush();
// save new invoice
$jo->setInvoice($invoice);
$em->persist($invoice);
// log event?
$event = new JOEvent();
$event->setDateHappen(new DateTime())
->setTypeID(JOEventType::RIDER_EDIT)
->setJobOrder($jo)
->setRider($jo->getRider());
$em->persist($event);
$em->flush();
}
protected function getInvoiceItems(EntityManagerInterface $em, JobOrder $jo)
{
$jo_id = $jo->getID();
$conn = $em->getConnection();
// need to get the ordered battery id and quantity from invoice item
// and the promo from invoice
$query_sql = 'SELECT ii.battery_id AS battery_id, ii.qty AS qty, i.promo_id AS promo_id
FROM invoice_item ii, invoice i
WHERE ii.invoice_id = i.id
AND i.job_order_id = :jo_id
AND ii.battery_id IS NOT NULL';
$query_stmt = $conn->prepare($query_sql);
$query_stmt->bindValue('jo_id', $jo_id);
$results = $query_stmt->executeQuery();
$promo_id = null;
$invoice_items = [];
while ($row = $results->fetchAssociative())
{
$promo_id = $row['promo_id'];
$invoice_items[] = [
'batt_id' => $row['battery_id'],
'qty' => $row['qty'],
'trade_in' => ''
];
}
$data = [
'promo_id' => $promo_id,
'invoice_items' => $invoice_items
];
return $data;
}
protected function validateTradeInItems(EntityManagerInterface $em, $ti_items)
{
$msg = '';
foreach ($ti_items as $ti_item)
{
$bs_id = $ti_item['battery_size_id'];
$ti_type = $ti_item['trade_in_type'];
// validate the battery size id
$batt_size = $em->getRepository(BatterySize::class)->find($bs_id);
if ($batt_size == null)
{
$msg = 'Invalid battery size for trade in: ' . $bs_id;
return $msg;
}
// validate the trade in type
if (!TradeInType::validate($ti_type))
{
$msg = 'Invalid trade in type: ' . $ti_type;
return $msg;
}
}
return $msg;
}
protected function getCAPIUser($id, EntityManagerInterface $em) protected function getCAPIUser($id, EntityManagerInterface $em)
{ {
$capi_user = $em->getRepository(APIUser::class)->find($id); $capi_user = $em->getRepository(APIUser::class)->find($id);
@ -1320,6 +1740,24 @@ class RiderAppController extends ApiController
return $msg; return $msg;
} }
protected function checkJOProgressionAllowed(JobOrder $jo, $rider)
{
// TODO: add more statuses to block if needed, hence. this is a failsafe in case MQTT is not working.
switch ($jo->getStatus())
{
case JOStatus::CANCELLED:
// if this is the rider's current JO, set to null
if ($rider->getCurrentJobOrder() === $jo) {
$rider->setCurrentJobOrder();
}
return new APIResponse(false, 'Job order can no longer be modified.');
break;
default:
return true;
}
}
protected function debugRequest(Request $req) protected function debugRequest(Request $req)
{ {
$all = $req->request->all(); $all = $req->request->all();

View file

@ -162,6 +162,6 @@ class ApiController extends BaseApiController
protected function getGeoErrorMessage() protected function getGeoErrorMessage()
{ {
return 'Oops! Our service is limited to some areas in Metro Manila, Laguna, Cavite, Pampanga and Baguio only. We will update you as soon as we are able to cover your area'; return 'Our services are currently limited to some areas in Metro Manila, Baguio, Batangas, Laguna, Cavite, Pampanga, and Palawan. We will update you as soon as we are available in your area. Thank you for understanding. Keep safe!';
} }
} }

View file

@ -18,6 +18,7 @@ use App\Ramcar\InsuranceApplicationStatus;
use App\Ramcar\InsuranceMVType; use App\Ramcar\InsuranceMVType;
use App\Ramcar\InsuranceClientType; use App\Ramcar\InsuranceClientType;
use App\Ramcar\TransactionStatus; use App\Ramcar\TransactionStatus;
use App\Ramcar\InsuranceBodyType;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use DateTime; use DateTime;
@ -293,6 +294,45 @@ class InsuranceController extends ApiController
]); ]);
} }
public function getPremiumsBanner(Request $req)
{
// validate params
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
return new ApiResponse(true, '', [
'url' => $this->getParameter('insurance_premiums_banner_url'),
]);
}
public function getBodyTypes(Request $req)
{
// validate params
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
$bt_collection = InsuranceBodyType::getCollection();
$body_types = [];
// NOTE: formatting it this way to match how insurance third party API returns their own stuff, so it's all handled one way on the app
foreach ($bt_collection as $bt_key => $bt_name) {
$body_types[] = [
'id' => $bt_key,
'name' => $bt_name,
];
}
return new ApiResponse(true, '', [
'body_types' => $body_types,
]);
}
protected function getLineType($mv_type_id, $vehicle_use_type, $is_public = false) protected function getLineType($mv_type_id, $vehicle_use_type, $is_public = false)
{ {
$line = ''; $line = '';

View file

@ -4,18 +4,22 @@ namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Catalyst\ApiBundle\Component\Response as ApiResponse; use Catalyst\ApiBundle\Component\Response as ApiResponse;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Service\InvoiceGeneratorInterface; use App\Service\InvoiceGeneratorInterface;
use App\Service\PriceTierManager;
use App\Ramcar\InvoiceCriteria; use App\Ramcar\InvoiceCriteria;
use App\Ramcar\TradeInType; use App\Ramcar\TradeInType;
use App\Ramcar\TransactionOrigin; use App\Ramcar\TransactionOrigin;
use App\Entity\CustomerVehicle; use App\Entity\CustomerVehicle;
use App\Entity\Promo; use App\Entity\Promo;
use App\Entity\Battery; use App\Entity\Battery;
use App\Entity\Customer;
use App\Entity\CustomerMetadata;
class InvoiceController extends ApiController class InvoiceController extends ApiController
{ {
public function getEstimate(Request $req, InvoiceGeneratorInterface $ic) public function getEstimate(Request $req, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager)
{ {
// $this->debugRequest($req); // $this->debugRequest($req);
@ -36,6 +40,18 @@ class InvoiceController extends ApiController
return new ApiResponse(false, 'No customer information found.'); return new ApiResponse(false, 'No customer information found.');
} }
// get customer location from customer_metadata using customer id
$lng = $req->request->get('longitude');
$lat = $req->request->get('latitude');
if ((empty($lng)) || (empty($lat)))
{
// use customer metadata location as basis
$coordinates = $this->getCustomerMetadata($cust);
}
else
$coordinates = new Point($lng, $lat);
// make invoice criteria // make invoice criteria
$icrit = new InvoiceCriteria(); $icrit = new InvoiceCriteria();
$icrit->setServiceType($req->request->get('service_type')); $icrit->setServiceType($req->request->get('service_type'));
@ -113,6 +129,18 @@ class InvoiceController extends ApiController
// set JO source // set JO source
$icrit->setSource(TransactionOrigin::MOBILE_APP); $icrit->setSource(TransactionOrigin::MOBILE_APP);
// set price tier
$pt_id = 0;
if ($coordinates != null)
{
error_log('coordinates are not null');
$pt_id = $pt_manager->getPriceTier($coordinates);
}
else
error_log('null?');
$icrit->setPriceTier($pt_id);
// send to invoice generator // send to invoice generator
$invoice = $ic->generateInvoice($icrit); $invoice = $ic->generateInvoice($icrit);
@ -148,4 +176,28 @@ class InvoiceController extends ApiController
// response // response
return new ApiResponse(true, '', $data); return new ApiResponse(true, '', $data);
} }
protected function getCustomerMetadata(Customer $cust)
{
$coordinates = null;
// check if customer already has existing metadata
$c_meta = $this->em->getRepository(CustomerMetadata::class)->findOneBy(['customer' => $cust]);
if ($c_meta != null)
{
$meta_data = $c_meta->getAllMetaInfo();
foreach ($meta_data as $m_info)
{
if ((isset($m_info['longitude'])) && (isset($m_info['latitude'])))
{
$lng = $m_info['longitude'];
$lat = $m_info['latitude'];
$coordinates = new Point($lng, $lat);
}
}
}
return $coordinates;
}
} }

View file

@ -21,6 +21,7 @@ use App\Service\HubDistributor;
use App\Service\HubFilterLogger; use App\Service\HubFilterLogger;
use App\Service\HubFilteringGeoChecker; use App\Service\HubFilteringGeoChecker;
use App\Service\JobOrderManager; use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
use App\Ramcar\APIRiderStatus; use App\Ramcar\APIRiderStatus;
use App\Ramcar\InvoiceCriteria; use App\Ramcar\InvoiceCriteria;
@ -484,7 +485,8 @@ class JobOrderController extends ApiController
HubDistributor $hub_dist, HubDistributor $hub_dist,
HubFilterLogger $hub_filter_logger, HubFilterLogger $hub_filter_logger,
HubFilteringGeoChecker $hub_geofence, HubFilteringGeoChecker $hub_geofence,
JobOrderManager $jo_manager JobOrderManager $jo_manager,
PriceTierManager $pt_manager
) { ) {
// validate params // validate params
$validity = $this->validateRequest($req, [ $validity = $this->validateRequest($req, [
@ -698,6 +700,10 @@ class JobOrderController extends ApiController
// set JO source // set JO source
$icrit->setSource(TransactionOrigin::MOBILE_APP); $icrit->setSource(TransactionOrigin::MOBILE_APP);
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// send to invoice generator // send to invoice generator
$invoice = $ic->generateInvoice($icrit); $invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice); $jo->setInvoice($invoice);
@ -970,7 +976,8 @@ class JobOrderController extends ApiController
HubDistributor $hub_dist, HubDistributor $hub_dist,
HubFilterLogger $hub_filter_logger, HubFilterLogger $hub_filter_logger,
HubFilteringGeoChecker $hub_geofence, HubFilteringGeoChecker $hub_geofence,
JobOrderManager $jo_manager JobOrderManager $jo_manager,
PriceTierManager $pt_manager
) { ) {
// validate params // validate params
$validity = $this->validateRequest($req, [ $validity = $this->validateRequest($req, [
@ -1127,6 +1134,10 @@ class JobOrderController extends ApiController
// set JO source // set JO source
$icrit->setSource(TransactionOrigin::MOBILE_APP); $icrit->setSource(TransactionOrigin::MOBILE_APP);
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// send to invoice generator // send to invoice generator
$invoice = $ic->generateInvoice($icrit); $invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice); $jo->setInvoice($invoice);

View file

@ -9,6 +9,7 @@ use App\Ramcar\JOStatus;
use App\Ramcar\APIRiderStatus; use App\Ramcar\APIRiderStatus;
use App\Entity\RiderRating; use App\Entity\RiderRating;
use App\Entity\JobOrder; use App\Entity\JobOrder;
use App\Entity\AggregatedRiderRating;
use App\Service\RiderTracker; use App\Service\RiderTracker;
use Exception; use Exception;
@ -230,12 +231,69 @@ class RiderController extends ApiController
$this->em->persist($rating); $this->em->persist($rating);
$this->em->flush(); $this->em->flush();
// need to update or add aggregated rider rating
$average_rating = $this->updateAggregatedRiderRating($rider, $rating_num);
// TODO: preliminary rating computation on the entity for now // TODO: preliminary rating computation on the entity for now
$rider->updateRatingAverage(); $rider->updateRatingAverage($average_rating);
$this->em->persist($rider); $this->em->persist($rider);
$this->em->flush(); $this->em->flush();
// response // response
return new ApiResponse(); return new ApiResponse();
} }
protected function updateAggregatedRiderRating($rider, $rating_num)
{
$rider_id = $rider->getID();
$agg_rating = 0;
// check if rider is in the the aggregated rider rating table
$agg_rider_rating = $this->em->getRepository(AggregatedRiderRating::class)->findOneBy(['rider_id' => $rider_id]);
if ($agg_rider_rating == null)
{
// new rider, new entry
$old_rating = 0;
$old_count = 0;
$new_count = 1;
$agg_rating = $this->computeAggregatedRiderRating($old_rating, $rating_num, $old_count, $new_count);
$obj = new AggregatedRiderRating();
$obj->setRiderId($rider_id)
->setAggregateRating($agg_rating)
->setAggregateCount($new_count);
$this->em->persist($obj);
}
else
{
// existing rider, update entry
$r_rating = $agg_rider_rating->getAggregateRating();
$r_count = $agg_rider_rating->getAggregateCount();
$new_count = $r_count + 1;
$agg_rating = $this->computeAggregatedRiderRating($r_rating, $rating_num, $r_count, $new_count);
// set updated values for entry
$agg_rider_rating->setAggregateRating($agg_rating)
->setAggregateCount($new_count);
}
$this->em->flush();
return $agg_rating;
}
protected function computeAggregatedRiderRating($old_rating, $new_rating, $r_count, $new_count)
{
// ((existing aggregate rating * existing aggregate count) + new rating) / new count
$agg_comp = bcmul($old_rating, $r_count, 2);
$rating = bcadd($agg_comp, $new_rating, 2);
$agg_rating = bcdiv($rating, $new_count, 2);
return $agg_rating;
}
} }

View file

@ -4,16 +4,19 @@ namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Catalyst\ApiBundle\Component\Response as ApiResponse; use Catalyst\ApiBundle\Component\Response as ApiResponse;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Entity\CustomerVehicle; use App\Entity\CustomerVehicle;
use App\Entity\JobOrder; use App\Entity\JobOrder;
use App\Entity\VehicleManufacturer; use App\Entity\VehicleManufacturer;
use App\Entity\Vehicle; use App\Entity\Vehicle;
use App\Entity\ItemType;
use App\Ramcar\JOStatus; use App\Ramcar\JOStatus;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType; use App\Ramcar\TradeInType;
use App\Ramcar\InsuranceApplicationStatus; use App\Ramcar\InsuranceApplicationStatus;
use App\Service\PayMongoConnector; use App\Service\PayMongoConnector;
use App\Service\PriceTierManager;
use DateTime; use DateTime;
class VehicleController extends ApiController class VehicleController extends ApiController
@ -237,7 +240,7 @@ class VehicleController extends ApiController
]); ]);
} }
public function getCompatibleBatteries(Request $req, $vid) public function getCompatibleBatteries(Request $req, $vid, PriceTierManager $pt_manager)
{ {
// validate params // validate params
$validity = $this->validateRequest($req); $validity = $this->validateRequest($req);
@ -252,11 +255,43 @@ class VehicleController extends ApiController
return new ApiResponse(false, 'Invalid vehicle.'); return new ApiResponse(false, 'Invalid vehicle.');
} }
// get location from request
$lng = $req->query->get('longitude', '');
$lat = $req->query->get('latitude', '');
$batts = $vehicle->getActiveBatteries();
$pt_id = 0;
if ((!(empty($lng))) && (!(empty($lat))))
{
// get the price tier
$coordinates = new Point($lng, $lat);
$pt_id = $pt_manager->getPriceTier($coordinates);
}
// batteries // batteries
$batt_list = []; $batt_list = [];
$batts = $vehicle->getBatteries();
foreach ($batts as $batt) { foreach ($batts as $batt) {
// TODO: Add warranty_tnv to battery information // TODO: Add warranty_tnv to battery information
// check if customer location is in a price tier location
if ($pt_id == 0)
$price = $batt->getSellingPrice();
else
{
// get item type for battery
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
if ($item_type == null)
$price = $batt->getSellingPrice();
else
{
$item_type_id = $item_type->getID();
$batt_id = $batt->getID();
// find the item price given price tier id and battery id
$price = $pt_manager->getItemPrice($pt_id, $item_type_id, $batt_id);
}
}
$batt_list[] = [ $batt_list[] = [
'id' => $batt->getID(), 'id' => $batt->getID(),
'mfg_id' => $batt->getManufacturer()->getID(), 'mfg_id' => $batt->getManufacturer()->getID(),
@ -265,7 +300,7 @@ class VehicleController extends ApiController
'model_name' => $batt->getModel()->getName(), 'model_name' => $batt->getModel()->getName(),
'size_id' => $batt->getSize()->getID(), 'size_id' => $batt->getSize()->getID(),
'size_name' => $batt->getSize()->getName(), 'size_name' => $batt->getSize()->getName(),
'price' => $batt->getSellingPrice(), 'price' => $price,
'wty_private' => $batt->getWarrantyPrivate(), 'wty_private' => $batt->getWarrantyPrivate(),
'wty_commercial' => $batt->getWarrantyCommercial(), 'wty_commercial' => $batt->getWarrantyCommercial(),
'image_url' => $this->getBatteryImageURL($req, $batt), 'image_url' => $this->getBatteryImageURL($req, $batt),

View file

@ -4,6 +4,7 @@ namespace App\Controller;
use App\Ramcar\InsuranceApplicationStatus; use App\Ramcar\InsuranceApplicationStatus;
use App\Service\FCMSender; use App\Service\FCMSender;
use App\Service\InsuranceConnector;
use App\Entity\InsuranceApplication; use App\Entity\InsuranceApplication;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -15,11 +16,13 @@ use DateTime;
class InsuranceController extends Controller class InsuranceController extends Controller
{ {
protected $ic;
protected $em; protected $em;
protected $fcmclient; protected $fcmclient;
public function __construct(EntityManagerInterface $em, FCMSender $fcmclient) public function __construct(InsuranceConnector $ic, EntityManagerInterface $em, FCMSender $fcmclient)
{ {
$this->ic = $ic;
$this->em = $em; $this->em = $em;
$this->fcmclient = $fcmclient; $this->fcmclient = $fcmclient;
} }
@ -28,17 +31,8 @@ class InsuranceController extends Controller
{ {
$payload = $req->request->all(); $payload = $req->request->all();
// DEBUG // log this callback
@file_put_contents(__DIR__ . '/../../var/log/insurance.log', print_r($payload, true) . "\r\n----------------------------------------\r\n\r\n", FILE_APPEND); $this->ic->log('CALLBACK', "[]", json_encode($payload), 'callback');
error_log(print_r($payload, true));
/*
return $this->json([
'success' => true,
]);
*/
// END DEBUG
// if no transaction code given, silently fail // if no transaction code given, silently fail
if (empty($payload['transaction_code'])) { if (empty($payload['transaction_code'])) {

View file

@ -0,0 +1,269 @@
<?php
namespace App\Controller;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Catalyst\MenuBundle\Annotation\Menu;
use App\Entity\PriceTier;
use App\Entity\Battery;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Entity\ItemPrice;
class ItemPricingController extends Controller
{
/**
* @Menu(selected="item_pricing")
* @IsGranted("item_pricing.update")
*/
public function index (EntityManagerInterface $em)
{
// get all the price tiers
$price_tiers = $em->getRepository(PriceTier::class)->findAll();
// get all item types
$item_types = $em->getRepository(ItemType::class)->findBy([], ['name' => 'asc']);
// get all the items/batteries
// load only batteries upon initial loading
$items = $this->getBatteries($em);
// set the default item type to battery
$default_it = $em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
$params = [
'sets' => [
'price_tiers' => $price_tiers,
'item_types' => $item_types,
],
'items' => $items,
'default_item_type_id' => $default_it->getID(),
];
return $this->render('item-pricing/form.html.twig', $params);
}
/**
* @Menu(selected="item_pricing")
* @IsGranted("item_pricing.update")
*/
public function formSubmit(Request $req, EntityManagerInterface $em)
{
$pt_id = $req->request->get('price_tier_id');
$it_id = $req->request->get('item_type_id');
$prices = $req->request->get('price');
// get the item type
$item_type = $em->getRepository(ItemType::class)->find($it_id);
if ($item_type->getCode() == 'battery')
{
// get batteries
$items = $em->getRepository(Battery::class)->findBy(['flag_active' => true], ['id' => 'asc']);
}
else
{
// get service offerings
$items = $em->getRepository(ServiceOffering::class)->findBy([], ['id' => 'asc']);
}
// on default price tier
if ($pt_id == 0)
{
// default price tier, update battery or service offering, depending on item type
// NOTE: battery and service offering prices or fees are stored as decimal.
foreach ($items as $item)
{
$item_id = $item->getID();
if (isset($prices[$item_id]))
{
// check item type
if ($item_type->getCode() == 'battery')
$item->setSellingPrice($prices[$item_id]);
else
$item->setFee($prices[$item_id]);
}
}
}
else
{
// get the price tier
$price_tier = $em->getRepository(PriceTier::class)->find($pt_id);
$item_prices = $price_tier->getItemPrices();
// clear the tier's item prices for the specific item type
foreach ($item_prices as $ip)
{
if ($ip->getItemType() == $item_type)
$em->remove($ip);
}
// update the tier's item prices
foreach ($items as $item)
{
$item_id = $item->getID();
$item_price = new ItemPrice();
$item_price->setItemType($item_type)
->setPriceTier($price_tier)
->setItemID($item_id);
if (isset($prices[$item_id]))
{
$item_price->setPrice($prices[$item_id] * 100);
}
else
{
$item_price->setPrice($item->getPrice() * 100);
}
// save
$em->persist($item_price);
}
}
$em->flush();
return $this->redirectToRoute('item_pricing');
}
/**
* @IsGranted("item_pricing.update")
*/
public function itemPrices(EntityManagerInterface $em, $pt_id, $it_id)
{
$pt_prices = [];
// get the item type
$it = $em->getRepository(ItemType::class)->find($it_id);
// check if default prices are needed
if ($pt_id != 0)
{
// get the price tier
$pt = $em->getRepository(PriceTier::class)->find($pt_id);
// get the items under the price tier
$pt_items = $pt->getItemPrices();
foreach ($pt_items as $pt_item)
{
// make item price hash
$pt_prices[$pt_item->getItemID()] = $pt_item->getPrice();
}
}
// get the prices from battery or service offering, depending on item type
if ($it->getCode() == 'battery')
{
// get batteries
$items = $em->getRepository(Battery::class)->findBy(['flag_active' => true], ['id' => 'asc']);
}
else
{
// get service offerings
$items = $em->getRepository(ServiceOffering::class)->findBy([], ['id' => 'asc']);
}
$data_items = [];
foreach ($items as $item)
{
$item_id = $item->getID();
// get default price
if ($it->getCode() == 'battery')
{
$price = $item->getSellingPrice();
$name = $item->getModel()->getName() . ' ' . $item->getSize()->getName();
}
else
{
$price = $item->getFee();
$name = $item->getName();
}
// check if tier has price for item
if (isset($pt_prices[$item_id]))
{
$pt_price = $pt_prices[$item_id];
// actual price
$price = number_format($pt_price / 100, 2, '.', '');
}
$actual_price = $price;
$data_items[] = [
'id' => $item_id,
'name' => $name,
'item_type_id' => $it->getID(),
'item_type' => $it->getName(),
'price' => $actual_price,
];
}
// response
return new JsonResponse([
'items' => $data_items,
]);
}
protected function getBatteries(EntityManagerInterface $em)
{
// get the item type for battery
$batt_item_type = $em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
// get all active batteries
$batts = $em->getRepository(Battery::class)->findBy(['flag_active' => true], ['id' => 'asc']);
foreach ($batts as $batt)
{
$batt_set[$batt->getID()] = [
'name' => $batt->getModel()->getName() . ' ' . $batt->getSize()->getName(),
'item_type_id' => $batt_item_type->getID(),
'item_type' => $batt_item_type->getName(),
'price' => $batt->getSellingPrice(),
];
}
return [
'items' => $batt_set,
];
}
protected function getServiceOfferings(EntityeManagerInterface $em)
{
// get the item type for service offering
$service_item_type = $em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
// get all service offerings
$services = $em->getRepository(ServiceOffering::class)->findBy([], ['id' => 'asc']);
$service_set = [];
foreach ($services as $service)
{
$service_set[$service->getID()] = [
'name' => $service->getName(),
'item_type_id' => $service_item_type->getID(),
'item_type' => $service_item_type->getName(),
'price' => $service->getFee(),
];
}
return [
'items' => $service_set,
];
}
}

View file

@ -0,0 +1,251 @@
<?php
namespace App\Controller;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use App\Entity\ItemType;
use Catalyst\MenuBundle\Annotation\Menu;
class ItemTypeController extends Controller
{
/**
* @Menu(selected="item_type_list")
* @IsGranted("item_type.list")
*/
public function index ()
{
return $this->render('item-type/list.html.twig');
}
/**
* @IsGranted("item_type.list")
*/
public function datatableRows(Request $req)
{
// get query builder
$qb = $this->getDoctrine()
->getRepository(ItemType::class)
->createQueryBuilder('q');
// get datatable params
$datatable = $req->request->get('datatable');
// count total records
$tquery = $qb->select('COUNT(q)');
$this->setQueryFilters($datatable, $tquery);
$total = $tquery->getQuery()
->getSingleScalarResult();
// get current page number
$page = $datatable['pagination']['page'] ?? 1;
$perpage = $datatable['pagination']['perpage'];
$offset = ($page - 1) * $perpage;
// add metadata
$meta = [
'page' => $page,
'perpage' => $perpage,
'pages' => ceil($total / $perpage),
'total' => $total,
'sort' => 'asc',
'field' => 'id'
];
// build query
$query = $qb->select('q');
$this->setQueryFilters($datatable, $query);
// check if sorting is present, otherwise use default
if (isset($datatable['sort']['field']) && !empty($datatable['sort']['field'])) {
$order = $datatable['sort']['sort'] ?? 'asc';
$query->orderBy('q.' . $datatable['sort']['field'], $order);
} else {
$query->orderBy('q.id', 'asc');
}
// get rows for this page
$obj_rows = $query->setFirstResult($offset)
->setMaxResults($perpage)
->getQuery()
->getResult();
// process rows
$rows = [];
foreach ($obj_rows as $orow) {
// add row data
$row['id'] = $orow->getID();
$row['name'] = $orow->getName();
// add row metadata
$row['meta'] = [
'update_url' => '',
'delete_url' => ''
];
// add crud urls
if ($this->isGranted('item_type.update'))
$row['meta']['update_url'] = $this->generateUrl('item_type_update_form', ['id' => $row['id']]);
if ($this->isGranted('item_type.delete'))
$row['meta']['delete_url'] = $this->generateUrl('item_type_delete', ['id' => $row['id']]);
$rows[] = $row;
}
// response
return $this->json([
'meta' => $meta,
'data' => $rows
]);
}
/**
* @Menu(selected="item_type.list")
* @IsGranted("item_type.add")
*/
public function addForm()
{
$item_type = new ItemType();
$params = [
'obj' => $item_type,
'mode' => 'create',
];
// response
return $this->render('item-type/form.html.twig', $params);
}
/**
* @IsGranted("item_type.add")
*/
public function addSubmit(Request $req, EntityManagerInterface $em, ValidatorInterface $validator)
{
$item_type = new ItemType();
$this->setObject($item_type, $req);
// validate
$errors = $validator->validate($item_type);
// initialize error list
$error_array = [];
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
// validated! save the entity
$em->persist($item_type);
$em->flush();
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
/**
* @Menu(selected="item_type_list")
* @ParamConverter("item_type", class="App\Entity\ItemType")
* @IsGranted("item_type.update")
*/
public function updateForm($id, EntityManagerInterface $em, ItemType $item_type)
{
$params = [];
$params['obj'] = $item_type;
$params['mode'] = 'update';
// response
return $this->render('item-type/form.html.twig', $params);
}
/**
* @ParamConverter("item_type", class="App\Entity\ItemType")
* @IsGranted("item_type.update")
*/
public function updateSubmit(Request $req, EntityManagerInterface $em, ValidatorInterface $validator, ItemType $item_type)
{
$this->setObject($item_type, $req);
// validate
$errors = $validator->validate($item_type);
// initialize error list
$error_array = [];
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
// validated! save the entity
$em->flush();
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
/**
* @ParamConverter("item_type", class="App\Entity\ItemType")
* @IsGranted("item_type.delete")
*/
public function deleteSubmit(EntityManagerInterface $em, ItemType $item_type)
{
// delete this object
$em->remove($item_type);
$em->flush();
// response
$response = new Response();
$response->setStatusCode(Response::HTTP_OK);
$response->send();
}
protected function setObject(ItemType $obj, Request $req)
{
// set and save values
$obj->setName($req->request->get('name'))
->setCode($req->request->get('code'));
}
protected function setQueryFilters($datatable, QueryBuilder $query)
{
if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) {
$query->where('q.name LIKE :filter')
->setParameter('filter', '%' . $datatable['query']['data-rows-search'] . '%');
}
}
}

View file

@ -30,6 +30,7 @@ use App\Service\HubSelector;
use App\Service\RiderTracker; use App\Service\RiderTracker;
use App\Service\MotivConnector; use App\Service\MotivConnector;
use App\Service\PriceTierManager;
use App\Service\GeofenceTracker; use App\Service\GeofenceTracker;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -42,6 +43,8 @@ use Doctrine\ORM\EntityManagerInterface;
use Catalyst\MenuBundle\Annotation\Menu; use Catalyst\MenuBundle\Annotation\Menu;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
class JobOrderController extends Controller class JobOrderController extends Controller
{ {
public function getJobOrders(Request $req, JobOrderHandlerInterface $jo_handler) public function getJobOrders(Request $req, JobOrderHandlerInterface $jo_handler)
@ -741,7 +744,7 @@ class JobOrderController extends Controller
} }
public function generateInvoice(Request $req, InvoiceGeneratorInterface $ic) public function generateInvoice(Request $req, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager)
{ {
// error_log('generating invoice...'); // error_log('generating invoice...');
$error = false; $error = false;
@ -752,6 +755,19 @@ class JobOrderController extends Controller
$cvid = $req->request->get('cvid'); $cvid = $req->request->get('cvid');
$service_charges = $req->request->get('service_charges', []); $service_charges = $req->request->get('service_charges', []);
// coordinates
// need to check if lng and lat are set
$lng = $req->request->get('coord_lng', 0);
$lat = $req->request->get('coord_lat', 0);
$price_tier = 0;
if (($lng != 0) && ($lat != 0))
{
$coordinates = new Point($req->request->get('coord_lng'), $req->request->get('coord_lat'));
$price_tier = $pt_manager->getPriceTier($coordinates);
}
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
// get customer vehicle // get customer vehicle
@ -767,8 +783,8 @@ class JobOrderController extends Controller
$criteria->setServiceType($stype) $criteria->setServiceType($stype)
->setCustomerVehicle($cv) ->setCustomerVehicle($cv)
->setIsTaxable() ->setIsTaxable()
->setSource(TransactionOrigin::CALL); ->setSource(TransactionOrigin::CALL)
->setPriceTier($price_tier);
/* /*
// if it's a jumpstart or troubleshoot only, we know what to charge already // if it's a jumpstart or troubleshoot only, we know what to charge already

View file

@ -4,6 +4,7 @@ namespace App\Controller;
use App\Entity\GatewayTransaction; use App\Entity\GatewayTransaction;
use App\Ramcar\TransactionStatus; use App\Ramcar\TransactionStatus;
use App\Service\PayMongoConnector;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -12,10 +13,12 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class PayMongoController extends Controller class PayMongoController extends Controller
{ {
protected $pm;
protected $em; protected $em;
public function __construct(EntityManagerInterface $em) public function __construct(PayMongoConnector $pm, EntityManagerInterface $em)
{ {
$this->pm = $pm;
$this->em = $em; $this->em = $em;
} }
@ -23,16 +26,8 @@ class PayMongoController extends Controller
{ {
$payload = json_decode($req->getContent(), true); $payload = json_decode($req->getContent(), true);
// DEBUG // log this callback
@file_put_contents(__DIR__ . '/../../var/log/paymongo.log', print_r($payload, true) . "\r\n----------------------------------------\r\n\r\n", FILE_APPEND); $this->pm->log('CALLBACK', "[]", $req->getContent(), 'callback');
/*
return $this->json([
'success' => true,
]);
*/
// END DEBUG
// if no event type given, silently fail // if no event type given, silently fail
if (empty($payload['data'])) { if (empty($payload['data'])) {

View file

@ -0,0 +1,355 @@
<?php
namespace App\Controller;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Catalyst\MenuBundle\Annotation\Menu;
use App\Entity\PriceTier;
use App\Entity\SupportedArea;
class PriceTierController extends Controller
{
/**
* @Menu(selected="price_tier_list")
* @IsGranted("price_tier.list")
*/
public function index()
{
return $this->render('price-tier/list.html.twig');
}
/**
* @IsGranted("price_tier.list")
*/
public function datatableRows(Request $req)
{
// get query builder
$qb = $this->getDoctrine()
->getRepository(PriceTier::class)
->createQueryBuilder('q');
// get datatable params
$datatable = $req->request->get('datatable');
// count total records
$tquery = $qb->select('COUNT(q)');
$this->setQueryFilters($datatable, $tquery);
$total = $tquery->getQuery()
->getSingleScalarResult();
// get current page number
$page = $datatable['pagination']['page'] ?? 1;
$perpage = $datatable['pagination']['perpage'];
$offset = ($page - 1) * $perpage;
// add metadata
$meta = [
'page' => $page,
'perpage' => $perpage,
'pages' => ceil($total / $perpage),
'total' => $total,
'sort' => 'asc',
'field' => 'id'
];
// build query
$query = $qb->select('q');
$this->setQueryFilters($datatable, $query);
// check if sorting is present, otherwise use default
if (isset($datatable['sort']['field']) && !empty($datatable['sort']['field'])) {
$order = $datatable['sort']['sort'] ?? 'asc';
$query->orderBy('q.' . $datatable['sort']['field'], $order);
} else {
$query->orderBy('q.id', 'asc');
}
// get rows for this page
$obj_rows = $query->setFirstResult($offset)
->setMaxResults($perpage)
->getQuery()
->getResult();
// process rows
$rows = [];
foreach ($obj_rows as $orow) {
// add row data
$row['id'] = $orow->getID();
$row['name'] = $orow->getName();
// add row metadata
$row['meta'] = [
'update_url' => '',
'delete_url' => ''
];
// add crud urls
if ($this->isGranted('price_tier.update'))
$row['meta']['update_url'] = $this->generateUrl('price_tier_update_form', ['id' => $row['id']]);
if ($this->isGranted('service_offering.delete'))
$row['meta']['delete_url'] = $this->generateUrl('price_tier_delete', ['id' => $row['id']]);
$rows[] = $row;
}
// response
return $this->json([
'meta' => $meta,
'data' => $rows
]);
}
/**
* @Menu(selected="price_tier.list")
* @IsGranted("price_tier.add")
*/
public function addForm(EntityManagerInterface $em)
{
$pt = new PriceTier();
// get the supported areas
$sets = $this->generateFormSets($em);
$params = [
'obj' => $pt,
'sets' => $sets,
'mode' => 'create',
];
// response
return $this->render('price-tier/form.html.twig', $params);
}
/**
* @IsGranted("price_tier.add")
*/
public function addSubmit(Request $req, EntityManagerInterface $em, ValidatorInterface $validator)
{
// initialize error list
$error_array = [];
$pt = new PriceTier();
$error_array = $this->validateRequest($em, $req);
$this->setObject($pt, $req);
// validate
$errors = $validator->validate($pt);
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
// validated! save the entity
$em->persist($pt);
// set the price tier id for the selected supported areas
$this->updateSupportedAreas($em, $pt, $req);
$em->flush();
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
/**
* @Menu(selected="price_tier_list")
* @ParamConverter("pt", class="App\Entity\PriceTier")
* @IsGranted("price_tier.update")
*/
public function updateForm($id, EntityManagerInterface $em, PriceTier $pt)
{
// get the supported areas
$sets = $this->generateFormSets($em, $pt);
$params = [
'obj' => $pt,
'sets' => $sets,
'mode' => 'update',
];
// response
return $this->render('price-tier/form.html.twig', $params);
}
/**
* @ParamConverter("pt", class="App\Entity\PriceTier")
* @IsGranted("price_tier.update")
*/
public function updateSubmit(Request $req, EntityManagerInterface $em, ValidatorInterface $validator, PriceTier $pt)
{
// initialize error list
$error_array = [];
// clear supported areas of price tier
$this->clearPriceTierSupportedAreas($em, $pt);
$error_array = $this->validateRequest($em, $req);
$this->setObject($pt, $req);
// validate
$errors = $validator->validate($pt);
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
// set the price tier id for the selected supported areas
$this->updateSupportedAreas($em, $pt, $req);
// validated! save the entity
$em->flush();
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
/**
* @ParamConverter("pt", class="App\Entity\PriceTier")
* @IsGranted("price_tier.delete")
*/
public function deleteSubmit(EntityManagerInterface $em, PriceTier $pt)
{
// clear supported areas of price tier
$this->clearPriceTierSupportedAreas($em, $pt);
// delete this object
$em->remove($pt);
$em->flush();
// response
$response = new Response();
$response->setStatusCode(Response::HTTP_OK);
$response->send();
}
protected function validateRequest(EntityManagerInterface $em, Request $req)
{
// get areas
$areas = $req->request->get('areas');
// check if no areas selected aka empty
if (!empty($areas))
{
foreach ($areas as $area_id)
{
$supported_area = $em->getRepository(SupportedArea::class)->find($area_id);
if ($supported_area == null)
return ['areas' => 'Invalid area'];
// check if supported area already belongs to a price tier
if ($supported_area->getPriceTier() != null)
return ['areas' => 'Area already belongs to a price tier.'];
}
}
return null;
}
protected function setObject(PriceTier $obj, Request $req)
{
// clear supported areas first
$obj->clearSupportedAreas();
$obj->setName($req->request->get('name'));
}
protected function clearPriceTierSupportedAreas(EntityManagerInterface $em, PriceTier $obj)
{
// find the supported areas set with the price tier
$areas = $em->getRepository(SupportedArea::class)->findBy(['price_tier' => $obj]);
if (!empty($areas))
{
// set the price tier id for the supported areas to null
foreach ($areas as $area)
{
$area->setPriceTier(null);
}
$em->flush();
}
}
protected function updateSupportedAreas(EntityManagerInterface $em, PriceTier $obj, Request $req)
{
// get the selected areas
$areas = $req->request->get('areas');
// check if no areas selected aka empty
if (!empty($areas))
{
foreach ($areas as $area_id)
{
// get supported area
$supported_area = $em->getRepository(SupportedArea::class)->find($area_id);
if ($supported_area != null)
$supported_area->setPriceTier($obj);
}
}
}
protected function generateFormSets(EntityManagerInterface $em, PriceTier $pt = null)
{
// get the supported areas with no price tier id or price tier id is set to the one that is being updated
$areas = $em->getRepository(SupportedArea::class)->findBy(['price_tier' => array(null, $pt)]);
$areas_set = [];
foreach ($areas as $area)
{
$areas_set[$area->getID()] = $area->getName();
}
return [
'areas' => $areas_set
];
}
protected function setQueryFilters($datatable, QueryBuilder $query)
{
if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) {
$query->where('q.name LIKE :filter')
->setParameter('filter', '%' . $datatable['query']['data-rows-search'] . '%');
}
}
}

View file

@ -13,6 +13,11 @@ use Catalyst\ApiBundle\Component\Response as APIResponse;
use App\Ramcar\APIResult; use App\Ramcar\APIResult;
use App\Entity\Vehicle; use App\Entity\Vehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use Catalyst\AuthBundle\Service\ACLGenerator as ACLGenerator; use Catalyst\AuthBundle\Service\ACLGenerator as ACLGenerator;
@ -25,7 +30,7 @@ class BatteryController extends ApiController
$this->acl_gen = $acl_gen; $this->acl_gen = $acl_gen;
} }
public function getCompatibleBatteries(Request $req, $vid, EntityManagerInterface $em) public function getCompatibleBatteries(Request $req, $vid, EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->denyAccessUnlessGranted('tapi_battery_compatible.list', null, 'No access.'); $this->denyAccessUnlessGranted('tapi_battery_compatible.list', null, 'No access.');
@ -43,13 +48,44 @@ class BatteryController extends ApiController
return new APIResponse(false, $message); return new APIResponse(false, $message);
} }
// get location from request
$lng = $req->request->get('longitude', '');
$lat = $req->request->get('latitude', '');
$batts = $vehicle->getActiveBatteries();
$pt_id = 0;
if ((!(empty($lng))) && (!(empty($lat))))
{
// get the price tier
$coordinates = new Point($lng, $lat);
$pt_id = $pt_manager->getPriceTier($coordinates);
}
// batteries // batteries
$batt_list = []; $batt_list = [];
// $batts = $vehicle->getBatteries();
$batts = $vehicle->getActiveBatteries();
foreach ($batts as $batt) foreach ($batts as $batt)
{ {
// TODO: Add warranty_tnv to battery information // TODO: Add warranty_tnv to battery information
// check if customer location is in a price tier location
if ($pt_id == 0)
$price = $batt->getSellingPrice();
else
{
// get item type for battery
$item_type = $em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
if ($item_type == null)
$price = $batt->getSellingPrice();
else
{
$item_type_id = $item_type->getID();
$batt_id = $batt->getID();
// find the item price given price tier id and battery id
$price = $pt_manager->getItemPrice($pt_id, $item_type_id, $batt_id);
}
}
$batt_list[] = [ $batt_list[] = [
'id' => $batt->getID(), 'id' => $batt->getID(),
'mfg_id' => $batt->getManufacturer()->getID(), 'mfg_id' => $batt->getManufacturer()->getID(),
@ -58,7 +94,7 @@ class BatteryController extends ApiController
'model_name' => $batt->getModel()->getName(), 'model_name' => $batt->getModel()->getName(),
'size_id' => $batt->getSize()->getID(), 'size_id' => $batt->getSize()->getID(),
'size_name' => $batt->getSize()->getName(), 'size_name' => $batt->getSize()->getName(),
'price' => $batt->getSellingPrice(), 'price' => $price,
'wty_private' => $batt->getWarrantyPrivate(), 'wty_private' => $batt->getWarrantyPrivate(),
'wty_commercial' => $batt->getWarrantyCommercial(), 'wty_commercial' => $batt->getWarrantyCommercial(),
'image_url' => $this->getBatteryImageURL($req, $batt), 'image_url' => $this->getBatteryImageURL($req, $batt),

View file

@ -46,6 +46,7 @@ use App\Service\RiderTracker;
use App\Service\PromoLogger; use App\Service\PromoLogger;
use App\Service\MapTools; use App\Service\MapTools;
use App\Service\JobOrderManager; use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use App\Entity\JobOrder; use App\Entity\JobOrder;
use App\Entity\CustomerVehicle; use App\Entity\CustomerVehicle;
@ -79,7 +80,8 @@ class JobOrderController extends ApiController
FCMSender $fcmclient, FCMSender $fcmclient,
RiderAssignmentHandlerInterface $rah, PromoLogger $promo_logger, RiderAssignmentHandlerInterface $rah, PromoLogger $promo_logger,
HubSelector $hub_select, HubDistributor $hub_dist, HubFilterLogger $hub_filter_logger, HubSelector $hub_select, HubDistributor $hub_dist, HubFilterLogger $hub_filter_logger,
HubFilteringGeoChecker $hub_geofence, EntityManagerInterface $em, JobOrderManager $jo_manager) HubFilteringGeoChecker $hub_geofence, EntityManagerInterface $em, JobOrderManager $jo_manager,
PriceTierManager $pt_manager)
{ {
$this->denyAccessUnlessGranted('tapi_jo.request', null, 'No access.'); $this->denyAccessUnlessGranted('tapi_jo.request', null, 'No access.');
@ -165,7 +167,17 @@ class JobOrderController extends ApiController
// set JO source // set JO source
$icrit->setSource(TransactionOrigin::THIRD_PARTY); $icrit->setSource(TransactionOrigin::THIRD_PARTY);
$icrit->addEntry($data['batt'], $data['trade_in_type'], 1); // set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// add the actual battery item first
$icrit->addEntry($data['batt'], null, 1);
// if we have a trade in, add it as well, assuming trade in battery == battery purchased
if (!empty($data['trade_in_type'])) {
$icrit->addEntry($data['batt'], $data['trade_in_type'], 1);
}
// send to invoice generator // send to invoice generator
$invoice = $ic->generateInvoice($icrit); $invoice = $ic->generateInvoice($icrit);

View file

@ -0,0 +1,86 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="aggregated_rider_rating", indexes={
* @ORM\Index(name="rider_id_idx", columns={"rider_id"}),
* })
*/
class AggregatedRiderRating
{
// unique id
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
// rider id, loose association
/**
* @ORM\Column(type="integer", nullable=true)
*/
protected $rider_id;
// average rating of rider
/**
* @ORM\Column(type="float")
*/
protected $aggregate_rating;
// number of job orders
/**
* @ORM\Column(type="integer")
*/
protected $aggregate_count;
public function __construct()
{
$this->aggregate_rating = 0;
$this->aggregate_count = 0;
}
public function getID()
{
return $this->id;
}
public function setRiderId($rider_id)
{
$this->rider_id = $rider_id;
return $this;
}
public function getRiderId()
{
return $this->rider_id;
}
public function setAggregateRating($aggregate_rating)
{
$this->aggregate_rating = $aggregate_rating;
return $this;
}
public function getAggregateRating()
{
return $this->aggregate_rating;
}
public function setAggregateCount($aggregate_count)
{
$this->aggregate_count = $aggregate_count;
return $this;
}
public function getAggregateCount()
{
return $this->aggregate_count;
}
}

97
src/Entity/ItemPrice.php Normal file
View file

@ -0,0 +1,97 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="item_price")
*/
class ItemPrice
{
// unique id
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\ManyToOne(targetEntity="PriceTier", inversedBy="item_prices")
* @ORM\JoinColumn(name="price_tier_id", referencedColumnName="id")
*/
protected $price_tier;
// item type
/**
* @ORM\ManyToOne(targetEntity="ItemType", inversedBy="items")
* @ORM\JoinColumn(name="item_type_id", referencedColumnName="id")
*/
protected $item_type;
// could be battery id or service offering id, loosely coupled
/**
* @ORM\Column(type="integer")
*/
protected $item_id;
// current price
// NOTE: we need to move the decimal point two places to the left to get actual value
// we want to avoid floating point problems
/**
* @ORM\Column(type="integer")
*/
protected $price;
public function getID()
{
return $this->id;
}
public function setPriceTier(PriceTier $price_tier)
{
$this->price_tier = $price_tier;
return $this;
}
public function getPriceTier()
{
return $this->price_tier;
}
public function setItemType(ItemType $item_type)
{
$this->item_type = $item_type;
return $this;
}
public function getItemType()
{
return $this->item_type;
}
public function setItemID($item_id)
{
$this->item_id = $item_id;
return $this;
}
public function getItemID()
{
return $this->item_id;
}
public function setPrice($price)
{
$this->price = $price;
return $this;
}
public function getPrice()
{
return $this->price;
}
}

80
src/Entity/ItemType.php Normal file
View file

@ -0,0 +1,80 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
* @ORM\Table(name="item_type", indexes={
* @ORM\Index(name="item_type_idx", columns={"code"})
* })
*/
class ItemType
{
// unique id
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="string", length=80)
* @Assert\NotBlank()
*/
protected $name;
/**
* @ORM\Column(type="string", length=80)
* @Assert\NotBlank()
*/
protected $code;
// items under an item type
/**
* @ORM\OneToMany(targetEntity="ItemPrice", mappedBy="item_type")
*/
protected $items;
public function __construct()
{
$this->code = '';
}
public function getID()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function setCode($code)
{
$this->code = $code;
return $this;
}
public function getCode()
{
return $this->code;
}
public function getItems()
{
return $this->items;
}
}

View file

@ -21,7 +21,7 @@ class MotoliteEvent
protected $id; protected $id;
/** /**
* @ORM\Column(type="string", length=80) * @ORM\Column(type="string", length=255)
* @Assert\NotBlank() * @Assert\NotBlank()
*/ */
protected $name; protected $name;

88
src/Entity/PriceTier.php Normal file
View file

@ -0,0 +1,88 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity
* @ORM\Table(name="price_tier")
*/
class PriceTier
{
// unique id
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
// name of price tier
/**
* @ORM\Column(type="string", length=80)
*/
protected $name;
// supported areas under price tier
/**
* @ORM\OneToMany(targetEntity="SupportedArea", mappedBy="price_tier");
*/
protected $supported_areas;
// items under a price tier
/**
* @ORM\OneToMany(targetEntity="ItemPrice", mappedBy="price_tier")
*/
protected $item_prices;
public function __construct()
{
$this->supported_areas = new ArrayCollection();
$this->items = new ArrayCollection();
}
public function getID()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function getSupportedAreaObjects()
{
return $this->supported_areas;
}
public function getSupportedAreas()
{
$str_supported_areas = [];
foreach ($this->supported_areas as $supported_area)
$str_supported_areas[] = $supported_area->getID();
return $str_supported_areas;
}
public function clearSupportedAreas()
{
$this->supported_areas->clear();
return $this;
}
public function getItemPrices()
{
return $this->item_prices;
}
}

View file

@ -241,15 +241,9 @@ class Rider
return $this->image_file; return $this->image_file;
} }
public function updateRatingAverage() public function updateRatingAverage($rating)
{ {
$total = 0; $this->setCurrentRating($rating);
foreach ($this->ratings as $rating) {
$total += $rating->getRating();
}
$this->setCurrentRating(round($total / $this->ratings->count(), 2));
} }
public function setCurrentRating($rating) public function setCurrentRating($rating)

View file

@ -39,9 +39,17 @@ class SupportedArea
*/ */
protected $coverage_area; protected $coverage_area;
/**
* @ORM\ManyToOne(targetEntity="PriceTier", inversedBy="supported_areas")
* @ORM\JoinColumn(name="price_tier_id", referencedColumnName="id", nullable=true)
*/
protected $price_tier;
public function __construct() public function __construct()
{ {
$this->date_create = new DateTime(); $this->date_create = new DateTime();
$this->price_tier = null;
} }
public function getID() public function getID()
@ -82,5 +90,16 @@ class SupportedArea
{ {
return $this->coverage_area; return $this->coverage_area;
} }
public function setPriceTier(PriceTier $price_tier = null)
{
$this->price_tier = $price_tier;
return $this;
}
public function getPriceTier()
{
return $this->price_tier;
}
} }

View file

@ -66,7 +66,7 @@ class GatewayTransactionListener
} }
// flag on api as paid // flag on api as paid
$result = $this->ic->tagApplicationPaid($obj->getID()); $result = $this->ic->tagApplicationPaid($obj->getExtTransactionId());
if (!$result['success'] || $result['response']['transaction_code'] !== 'GR004') { if (!$result['success'] || $result['response']['transaction_code'] !== 'GR004') {
error_log("INSURANCE MARK AS PAID FAILED FOR " . $obj->getID() . ": " . $result['error']['message']); error_log("INSURANCE MARK AS PAID FAILED FOR " . $obj->getID() . ": " . $result['error']['message']);
} }

View file

@ -11,14 +11,19 @@ use App\Ramcar\TradeInType;
use App\Entity\Battery; use App\Entity\Battery;
use App\Entity\ServiceOffering; use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class BatteryReplacementWarranty implements InvoiceRuleInterface class BatteryReplacementWarranty implements InvoiceRuleInterface
{ {
protected $em; protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->pt_manager = $pt_manager;
} }
public function getID() public function getID()
@ -29,6 +34,7 @@ class BatteryReplacementWarranty implements InvoiceRuleInterface
public function compute($criteria, &$total) public function compute($criteria, &$total)
{ {
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = []; $items = [];
if ($stype == $this->getID()) if ($stype == $this->getID())
@ -40,7 +46,14 @@ class BatteryReplacementWarranty implements InvoiceRuleInterface
{ {
$batt = $entry['battery']; $batt = $entry['battery'];
$qty = 1; $qty = 1;
$price = $this->getServiceTypeFee();
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id);
if ($pt_price == null)
$price = $this->getServiceTypeFee();
else
$price = $pt_price;
$items[] = [ $items[] = [
'service_type' => $this->getID(), 'service_type' => $this->getID(),
@ -117,6 +130,34 @@ class BatteryReplacementWarranty implements InvoiceRuleInterface
return null; return null;
} }
protected function getPriceTierItemPrice($pt_id)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'battery_replacement_warranty_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getTitle($battery) protected function getTitle($battery)
{ {
$title = $battery->getModel()->getName() . ' ' . $battery->getSize()->getName() . ' - Service Unit'; $title = $battery->getModel()->getName() . ' ' . $battery->getSize()->getName() . ' - Service Unit';

View file

@ -10,14 +10,19 @@ use App\Ramcar\TradeInType;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
use App\Entity\Battery; use App\Entity\Battery;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class BatterySales implements InvoiceRuleInterface class BatterySales implements InvoiceRuleInterface
{ {
protected $em; protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->pt_manager = $pt_manager;
} }
public function getID() public function getID()
@ -28,6 +33,7 @@ class BatterySales implements InvoiceRuleInterface
public function compute($criteria, &$total) public function compute($criteria, &$total)
{ {
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$pt = $criteria->getPriceTier();
$items = []; $items = [];
if ($stype == $this->getID()) if ($stype == $this->getID())
@ -36,19 +42,28 @@ class BatterySales implements InvoiceRuleInterface
$entries = $criteria->getEntries(); $entries = $criteria->getEntries();
foreach($entries as $entry) foreach($entries as $entry)
{ {
$batt = $entry['battery'];
$qty = $entry['qty']; $qty = $entry['qty'];
$trade_in = null; $trade_in = null;
// check if entry is for trade in
if (isset($entry['trade_in'])) if (isset($entry['trade_in']))
$trade_in = $entry['trade_in']; $trade_in = $entry['trade_in'];
$size = $batt->getSize(); // entry is a battery purchase
if ($trade_in == null) if ($trade_in == null)
{ {
// battery purchase // safe to get entry with battery key since CRM and apps
$price = $batt->getSellingPrice(); // will set this for a battery purchase and trade_in will
// will not be set
$batt = $entry['battery'];
// check if price tier has item price for battery
$pt_price = $this->getPriceTierItemPrice($pt, $batt);
if ($pt_price == null)
$price = $batt->getSellingPrice();
else
$price = $pt_price;
$items[] = [ $items[] = [
'service_type' => $this->getID(), 'service_type' => $this->getID(),
@ -114,6 +129,25 @@ class BatterySales implements InvoiceRuleInterface
return null; return null;
} }
protected function getPriceTierItemPrice($pt_id, $batt)
{
// price tier is default
if ($pt_id == 0)
return null;
// find the item type battery
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
if ($item_type == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $batt->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getTitle($battery) protected function getTitle($battery)
{ {
$title = $battery->getModel()->getName() . ' ' . $battery->getSize()->getName(); $title = $battery->getModel()->getName() . ' ' . $battery->getSize()->getName();

View file

@ -11,14 +11,19 @@ use App\Ramcar\ServiceType;
use App\Entity\ServiceOffering; use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle; use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class Fuel implements InvoiceRuleInterface class Fuel implements InvoiceRuleInterface
{ {
protected $em; protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->pt_manager = $pt_manager;
} }
public function getID() public function getID()
@ -29,6 +34,7 @@ class Fuel implements InvoiceRuleInterface
public function compute($criteria, &$total) public function compute($criteria, &$total)
{ {
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = []; $items = [];
@ -36,7 +42,13 @@ class Fuel implements InvoiceRuleInterface
{ {
$cv = $criteria->getCustomerVehicle(); $cv = $criteria->getCustomerVehicle();
$fee = $this->getServiceTypeFee($cv); // check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id, $cv);
if ($pt_price == null)
$service_price = $this->getServiceTypeFee($cv);
else
$service_price = $pt_price;
$ftype = $cv->getFuelType(); $ftype = $cv->getFuelType();
@ -46,10 +58,10 @@ class Fuel implements InvoiceRuleInterface
'service_type' => $this->getID(), 'service_type' => $this->getID(),
'qty' => $qty, 'qty' => $qty,
'title' => $this->getServiceTitle($ftype), 'title' => $this->getServiceTitle($ftype),
'price' => $fee, 'price' => $service_price,
]; ];
$qty_fee = bcmul($qty, $fee, 2); $qty_fee = bcmul($qty, $service_price, 2);
$total_price = $qty_fee; $total_price = $qty_fee;
switch ($ftype) switch ($ftype)
@ -57,7 +69,15 @@ class Fuel implements InvoiceRuleInterface
case FuelType::GAS: case FuelType::GAS:
case FuelType::DIESEL: case FuelType::DIESEL:
$qty = 1; $qty = 1;
$price = $this->getFuelFee($ftype);
// check if price tier has item price for fuel type
$pt_price = $this->getPriceTierFuelItemPrice($pt_id, $ftype);
if ($pt_price == null)
$price = $this->getFuelFee($ftype);
else
$price = $pt_price;
$items[] = [ $items[] = [
'service_type' => $this->getID(), 'service_type' => $this->getID(),
'qty' => $qty, 'qty' => $qty,
@ -138,6 +158,70 @@ class Fuel implements InvoiceRuleInterface
return null; return null;
} }
protected function getPriceTierItemPrice($pt_id, CustomerVehicle $cv)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
// check if customer vehicle has a motolite battery
// if yes, set the code to the motolite user service fee
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_service_fee';
else
$code = 'fuel_service_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getPriceTierFuelItemPrice($pt_id, $fuel_type)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = '';
if ($fuel_type == FuelType::GAS)
$code = 'fuel_gas_fee';
if ($fuel_type == FuelType::DIESEL)
$code = 'fuel_diesel_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getTitle($fuel_type) protected function getTitle($fuel_type)
{ {
$title = '4L - ' . ucfirst($fuel_type); $title = '4L - ' . ucfirst($fuel_type);

View file

@ -8,16 +8,21 @@ use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering; use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle; use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Ramcar\TransactionOrigin; use App\Ramcar\TransactionOrigin;
use App\Service\PriceTierManager;
class Jumpstart implements InvoiceRuleInterface class Jumpstart implements InvoiceRuleInterface
{ {
protected $em; protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->pt_manager = $pt_manager;
} }
public function getID() public function getID()
@ -29,13 +34,21 @@ class Jumpstart implements InvoiceRuleInterface
{ {
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$source = $criteria->getSource(); $source = $criteria->getSource();
$pt_id = $criteria->getPriceTier();
$items = []; $items = [];
if ($stype == $this->getID()) if ($stype == $this->getID())
{ {
$cv = $criteria->getCustomerVehicle(); $cv = $criteria->getCustomerVehicle();
$fee = $this->getServiceTypeFee($source, $cv);
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id, $source, $cv);
if ($pt_price == null)
$price = $this->getServiceTypeFee($source, $cv);
else
$price = $pt_price;
// add the service fee to items // add the service fee to items
$qty = 1; $qty = 1;
@ -43,10 +56,10 @@ class Jumpstart implements InvoiceRuleInterface
'service_type' => $this->getID(), 'service_type' => $this->getID(),
'qty' => $qty, 'qty' => $qty,
'title' => $this->getServiceTitle(), 'title' => $this->getServiceTitle(),
'price' => $fee, 'price' => $price,
]; ];
$qty_price = bcmul($fee, $qty, 2); $qty_price = bcmul($price, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2); $total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
} }
@ -86,6 +99,45 @@ class Jumpstart implements InvoiceRuleInterface
return null; return null;
} }
protected function getPriceTierItemPrice($pt_id, $source, $cv)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
// check the source of JO
// (1) if from app, service fee is 0 if motolite user. jumpstart fee for app if non-motolite user.
// (2) any other source, jumpstart fees are charged whether motolite user or not
if ($source == TransactionOrigin::MOBILE_APP)
{
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_service_fee';
else
$code = 'jumpstart_fee_mobile_app';
}
else
$code = 'jumpstart_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle() protected function getServiceTitle()
{ {
$title = 'Service - Troubleshooting fee'; $title = 'Service - Troubleshooting fee';

View file

@ -7,14 +7,19 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface; use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering; use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class JumpstartWarranty implements InvoiceRuleInterface class JumpstartWarranty implements InvoiceRuleInterface
{ {
protected $em; protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->pt_manager = $pt_manager;
} }
public function getID() public function getID()
@ -25,12 +30,19 @@ class JumpstartWarranty implements InvoiceRuleInterface
public function compute($criteria, &$total) public function compute($criteria, &$total)
{ {
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = []; $items = [];
if ($stype == $this->getID()) if ($stype == $this->getID())
{ {
$fee = $this->getServiceTypeFee(); // check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id);
if ($pt_price == null)
$price = $this->getServiceTypeFee();
else
$price = $pt_price;
// add the service fee to items // add the service fee to items
$qty = 1; $qty = 1;
@ -38,10 +50,10 @@ class JumpstartWarranty implements InvoiceRuleInterface
'service_type' => $this->getID(), 'service_type' => $this->getID(),
'qty' => $qty, 'qty' => $qty,
'title' => $this->getServiceTitle(), 'title' => $this->getServiceTitle(),
'price' => $fee, 'price' => $price,
]; ];
$qty_price = bcmul($fee, $qty, 2); $qty_price = bcmul($price, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2); $total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
} }
@ -72,6 +84,33 @@ class JumpstartWarranty implements InvoiceRuleInterface
return null; return null;
} }
protected function getPriceTierItemPrice($pt_id)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'jumpstart_warranty_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle() protected function getServiceTitle()
{ {
$title = 'Service - Troubleshooting fee'; $title = 'Service - Troubleshooting fee';

View file

@ -10,14 +10,19 @@ use App\Ramcar\ServiceType;
use App\Entity\ServiceOffering; use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle; use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class Overheat implements InvoiceRuleInterface class Overheat implements InvoiceRuleInterface
{ {
protected $em; protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->pt_manager = $pt_manager;
} }
public function getID() public function getID()
@ -29,13 +34,22 @@ class Overheat implements InvoiceRuleInterface
{ {
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$has_coolant = $criteria->hasCoolant(); $has_coolant = $criteria->hasCoolant();
$pt_id = $criteria->getPriceTier();
$items = []; $items = [];
if ($stype == $this->getID()) if ($stype == $this->getID())
{ {
$cv = $criteria->getCustomerVehicle(); $cv = $criteria->getCustomerVehicle();
$fee = $this->getServiceTypeFee($cv);
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id, $cv);
if ($pt_price == null)
$price = $this->getServiceTypeFee($cv);
else
$price = $pt_price;
// add the service fee to items // add the service fee to items
$qty = 1; $qty = 1;
@ -43,10 +57,10 @@ class Overheat implements InvoiceRuleInterface
'service_type' => $this->getID(), 'service_type' => $this->getID(),
'qty' => $qty, 'qty' => $qty,
'title' => $this->getServiceTitle(), 'title' => $this->getServiceTitle(),
'price' => $fee, 'price' => $price,
]; ];
$qty_fee = bcmul($qty, $fee, 2); $qty_fee = bcmul($qty, $price, 2);
$total_price = $qty_fee; $total_price = $qty_fee;
if ($has_coolant) if ($has_coolant)
@ -94,7 +108,7 @@ class Overheat implements InvoiceRuleInterface
// find the service fee using the code // find the service fee using the code
// if we can't find the fee, return 0 // if we can't find the fee, return 0
$fee = $this->em->getRepository(ServiceOffering::class)->findOneBy($code); $fee = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
if ($fee == null) if ($fee == null)
return 0; return 0;
@ -112,6 +126,39 @@ class Overheat implements InvoiceRuleInterface
return null; return null;
} }
protected function getPriceTierItemPrice($pt_id, CustomerVehicle $cv)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'overheat_fee';
// check if customer vehicle has a motolite battery
// if yes, set the code to the motolite user service fee
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_service_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle() protected function getServiceTitle()
{ {
$title = 'Service - ' . ServiceType::getName(ServiceType::OVERHEAT_ASSISTANCE); $title = 'Service - ' . ServiceType::getName(ServiceType::OVERHEAT_ASSISTANCE);

View file

@ -7,14 +7,19 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface; use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering; use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class PostRecharged implements InvoiceRuleInterface class PostRecharged implements InvoiceRuleInterface
{ {
protected $em; protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->pt_manager = $pt_manager;
} }
public function getID() public function getID()
@ -25,22 +30,29 @@ class PostRecharged implements InvoiceRuleInterface
public function compute($criteria, &$total) public function compute($criteria, &$total)
{ {
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = []; $items = [];
if ($stype == $this->getID()) if ($stype == $this->getID())
{ {
$fee = $this->getServiceTypeFee(); // check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id);
if ($pt_price == null)
$price = $this->getServiceTypeFee();
else
$price = $pt_price;
$qty = 1; $qty = 1;
$items[] = [ $items[] = [
'service_type' => $this->getID(), 'service_type' => $this->getID(),
'qty' => $qty, 'qty' => $qty,
'title' => $this->getServiceTitle(), 'title' => $this->getServiceTitle(),
'price' => $fee, 'price' => $price,
]; ];
$qty_price = bcmul($fee, $qty, 2); $qty_price = bcmul($price, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2); $total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
} }
@ -72,6 +84,33 @@ class PostRecharged implements InvoiceRuleInterface
return null; return null;
} }
protected function getPriceTierItemPrice($pt_id)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'post_recharged_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle() protected function getServiceTitle()
{ {
$title = 'Recharge fee'; $title = 'Recharge fee';

View file

@ -7,14 +7,19 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface; use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering; use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class PostReplacement implements InvoiceRuleInterface class PostReplacement implements InvoiceRuleInterface
{ {
protected $em; protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->pt_manager = $pt_manager;
} }
public function getID() public function getID()
@ -25,22 +30,29 @@ class PostReplacement implements InvoiceRuleInterface
public function compute($criteria, &$total) public function compute($criteria, &$total)
{ {
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = []; $items = [];
if ($stype == $this->getID()) if ($stype == $this->getID())
{ {
$fee = $this->getServiceTypeFee(); // check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id);
if ($pt_price == null)
$price = $this->getServiceTypeFee();
else
$price = $pt_price;
$qty = 1; $qty = 1;
$items[] = [ $items[] = [
'service_type' => $this->getID(), 'service_type' => $this->getID(),
'qty' => $qty, 'qty' => $qty,
'title' => $this->getServiceTitle(), 'title' => $this->getServiceTitle(),
'price' => $fee, 'price' => $price,
]; ];
$qty_price = bcmul($fee, $qty, 2); $qty_price = bcmul($price, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2); $total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
} }
@ -71,6 +83,33 @@ class PostReplacement implements InvoiceRuleInterface
return null; return null;
} }
protected function getPriceTierItemPrice($pt_id)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'post_replacement_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle() protected function getServiceTitle()
{ {
$title = 'Battery replacement'; $title = 'Battery replacement';

View file

@ -9,14 +9,19 @@ use App\InvoiceRuleInterface;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
use App\Entity\ServiceOffering; use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class Tax implements InvoiceRuleInterface class Tax implements InvoiceRuleInterface
{ {
protected $em; protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->pt_manager = $pt_manager;
} }
public function getID() public function getID()
@ -40,6 +45,7 @@ class Tax implements InvoiceRuleInterface
// compute tax per item if service type is battery sales // compute tax per item if service type is battery sales
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$pt = $criteria->getPriceTier();
if ($stype == ServiceType::BATTERY_REPLACEMENT_NEW) if ($stype == ServiceType::BATTERY_REPLACEMENT_NEW)
{ {
@ -58,7 +64,13 @@ class Tax implements InvoiceRuleInterface
$battery = $entry['battery']; $battery = $entry['battery'];
$qty = $entry['qty']; $qty = $entry['qty'];
$price = $battery->getSellingPrice(); // check if price tier has item price for battery
$pt_price = $this->getPriceTierItemPrice($pt, $battery);
if ($pt_price == null)
$price = $battery->getSellingPrice();
else
$price = $pt_price;
$vat = $this->getTaxAmount($price, $tax_rate); $vat = $this->getTaxAmount($price, $tax_rate);
@ -96,6 +108,25 @@ class Tax implements InvoiceRuleInterface
return null; return null;
} }
protected function getPriceTierItemPrice($pt_id, $batt)
{
// price tier is default
if ($pt_id == 0)
return null;
// find the item type battery
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
if ($item_type == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $batt->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getTaxAmount($price, $tax_rate) protected function getTaxAmount($price, $tax_rate)
{ {
$vat_ex_price = $this->getTaxExclusivePrice($price, $tax_rate); $vat_ex_price = $this->getTaxExclusivePrice($price, $tax_rate);

View file

@ -8,14 +8,19 @@ use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering; use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle; use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class TireRepair implements InvoiceRuleInterface class TireRepair implements InvoiceRuleInterface
{ {
protected $em; protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->pt_manager = $pt_manager;
} }
public function getID() public function getID()
@ -26,13 +31,21 @@ class TireRepair implements InvoiceRuleInterface
public function compute($criteria, &$total) public function compute($criteria, &$total)
{ {
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = []; $items = [];
if ($stype == $this->getID()) if ($stype == $this->getID())
{ {
$cv = $criteria->getCustomerVehicle(); $cv = $criteria->getCustomerVehicle();
$fee = $this->getServiceTypeFee($cv);
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id, $cv);
if ($pt_price == null)
$price = $this->getServiceTypeFee($cv);
else
$price = $pt_price;
// add the service fee to items // add the service fee to items
$qty = 1; $qty = 1;
@ -40,10 +53,10 @@ class TireRepair implements InvoiceRuleInterface
'service_type' => $this->getID(), 'service_type' => $this->getID(),
'qty' => $qty, 'qty' => $qty,
'title' => $this->getServiceTitle(), 'title' => $this->getServiceTitle(),
'price' => $fee, 'price' => $price,
]; ];
$qty_price = bcmul($fee, $qty, 2); $qty_price = bcmul($price, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2); $total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
} }
@ -79,6 +92,39 @@ class TireRepair implements InvoiceRuleInterface
return null; return null;
} }
protected function getPriceTierItemPrice($pt_id, CustomerVehicle $cv)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'tire_repair_fee';
// check if customer vehicle has a motolite battery
// if yes, set the code to the motolite user service fee
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_service_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle() protected function getServiceTitle()
{ {
$title = 'Service - Flat Tire'; $title = 'Service - Flat Tire';

View file

@ -21,7 +21,6 @@ class TradeIn implements InvoiceRuleInterface
$entries = $criteria->getEntries(); $entries = $criteria->getEntries();
foreach($entries as $entry) foreach($entries as $entry)
{ {
$batt = $entry['battery'];
$qty = $entry['qty']; $qty = $entry['qty'];
$trade_in_type = null; $trade_in_type = null;
@ -30,7 +29,18 @@ class TradeIn implements InvoiceRuleInterface
if ($trade_in_type != null) if ($trade_in_type != null)
{ {
$ti_rate = $this->getTradeInRate($batt, $trade_in_type); // at this point, entry is a trade in
// need to check if battery (coming from CRM) is set
// or battery_size is set (coming from rider app)
if (isset($entry['battery']))
{
$battery = $entry['battery'];
$batt_size = $battery->getSize();
}
else
$batt_size = $entry['battery_size'];
$ti_rate = $this->getTradeInRate($batt_size, $trade_in_type);
$qty_ti = bcmul($ti_rate, $qty, 2); $qty_ti = bcmul($ti_rate, $qty, 2);
@ -41,7 +51,7 @@ class TradeIn implements InvoiceRuleInterface
$items[] = [ $items[] = [
'qty' => $qty, 'qty' => $qty,
'title' => $this->getTitle($batt, $trade_in_type), 'title' => $this->getTitle($batt_size, $trade_in_type),
'price' => $price, 'price' => $price,
]; ];
} }
@ -60,10 +70,8 @@ class TradeIn implements InvoiceRuleInterface
return null; return null;
} }
protected function getTradeInRate($battery, $trade_in_type) protected function getTradeInRate($size, $trade_in_type)
{ {
$size = $battery->getSize();
switch ($trade_in_type) switch ($trade_in_type)
{ {
case TradeInType::MOTOLITE: case TradeInType::MOTOLITE:
@ -77,9 +85,9 @@ class TradeIn implements InvoiceRuleInterface
return 0; return 0;
} }
protected function getTitle($battery, $trade_in_type) protected function getTitle($battery_size, $trade_in_type)
{ {
$title = 'Trade-in ' . TradeInType::getName($trade_in_type) . ' ' . $battery->getSize()->getName() . ' battery'; $title = 'Trade-in ' . TradeInType::getName($trade_in_type) . ' ' . $battery_size->getName() . ' battery';
return $title; return $title;
} }

View file

@ -0,0 +1,18 @@
<?php
namespace App\Ramcar;
class InsuranceBodyType extends NameValue
{
const SEDAN = 'sedan';
const SUV = 'suv';
const TRUCK = 'truck';
const MOTORCYCLE = 'motorcycle';
const COLLECTION = [
'SEDAN' => 'Sedan',
'SUV' => 'SUV',
'TRUCK' => 'Truck',
'MOTORCYCLE' => 'Motorcycle',
];
}

View file

@ -17,6 +17,7 @@ class InvoiceCriteria
protected $service_charges; protected $service_charges;
protected $flag_taxable; protected $flag_taxable;
protected $source; // use Ramcar's TransactionOrigin protected $source; // use Ramcar's TransactionOrigin
protected $price_tier;
// entries are battery and trade-in combos // entries are battery and trade-in combos
protected $entries; protected $entries;
@ -32,6 +33,7 @@ class InvoiceCriteria
$this->service_charges = []; $this->service_charges = [];
$this->flag_taxable = false; $this->flag_taxable = false;
$this->source = ''; $this->source = '';
$this->price_tier = 0; // set to default
} }
public function setServiceType($stype) public function setServiceType($stype)
@ -108,6 +110,17 @@ class InvoiceCriteria
$this->entries[] = $entry; $this->entries[] = $entry;
} }
public function addTradeInEntry($battery_size, $trade_in, $qty)
{
$entry = [
'battery_size' => $battery_size,
'trade_in' => $trade_in,
'qty' => $qty
];
$this->entries[] = $entry;
}
public function getEntries() public function getEntries()
{ {
return $this->entries; return $this->entries;
@ -179,4 +192,14 @@ class InvoiceCriteria
return $this->source; return $this->source;
} }
public function setPriceTier($price_tier)
{
$this->price_tier = $price_tier;
return $this;
}
public function getPriceTier()
{
return $this->price_tier;
}
} }

View file

@ -17,13 +17,17 @@ class TransactionOrigin extends NameValue
const YOKOHAMA_TWITTER = 'yokohama_twitter'; const YOKOHAMA_TWITTER = 'yokohama_twitter';
const YOKOHAMA_INSTAGRAM = 'yokohama_instagram'; const YOKOHAMA_INSTAGRAM = 'yokohama_instagram';
const YOKOHAMA_CAROUSELL = 'yokohama_carousell'; const YOKOHAMA_CAROUSELL = 'yokohama_carousell';
const HOTLINE_CEBU = 'hotline_cebu';
const FACEBOOK_CEBU = 'facebook_cebu';
// TODO: for now, resq also gets the walk-in option // TODO: for now, resq also gets the walk-in option
// resq also gets new YOKOHAMA options // resq also gets new YOKOHAMA options
const COLLECTION = [ const COLLECTION = [
'call' => 'Hotline', 'call' => 'Hotline Manila',
'hotline_cebu' => 'Hotline Cebu',
'online' => 'Online', 'online' => 'Online',
'facebook' => 'Facebook', 'facebook' => 'Facebook Manila',
'facebook_cebu' => 'Facebook Cebu',
'vip' => 'VIP', 'vip' => 'VIP',
'mobile_app' => 'Mobile App', 'mobile_app' => 'Mobile App',
'walk_in' => 'Walk-in', 'walk_in' => 'Walk-in',
@ -33,6 +37,6 @@ class TransactionOrigin extends NameValue
'yokohama_op_facebook' => 'Yokohama OP Facebook', 'yokohama_op_facebook' => 'Yokohama OP Facebook',
'yokohama_twitter' => 'Yokohama Twitter', 'yokohama_twitter' => 'Yokohama Twitter',
'yokohama_instagram' => 'Yokohama Instagram', 'yokohama_instagram' => 'Yokohama Instagram',
'yokohama_carousell' => 'Yokohama Carousell', 'yokohama_carousell' => 'Yokohama Carousell'
]; ];
} }

View file

@ -91,7 +91,7 @@ class InsuranceConnector
return base64_encode($this->username . ":" . $this->password); return base64_encode($this->username . ":" . $this->password);
} }
protected function doRequest($url, $method, $body = []) protected function doRequest($url, $method, $request_body = [])
{ {
$client = new Client(); $client = new Client();
$headers = [ $headers = [
@ -102,7 +102,7 @@ class InsuranceConnector
try { try {
$response = $client->request($method, $this->base_url . '/' . $url, [ $response = $client->request($method, $this->base_url . '/' . $url, [
'json' => $body, 'json' => $request_body,
'headers' => $headers, 'headers' => $headers,
]); ]);
} catch (RequestException $e) { } catch (RequestException $e) {
@ -111,6 +111,11 @@ class InsuranceConnector
error_log("Insurance API Error: " . $error['message']); error_log("Insurance API Error: " . $error['message']);
error_log(Psr7\Message::toString($e->getRequest())); error_log(Psr7\Message::toString($e->getRequest()));
error_log($e->getResponse()->getBody()->getContents()); error_log($e->getResponse()->getBody()->getContents());
error_log("Insurance Creds: " . $this->username . ", " . $this->password);
error_log("Insurance Hash: " . $this->generateHash());
// log this error
$this->log($url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error');
if ($e->hasResponse()) { if ($e->hasResponse()) {
$error['response'] = Psr7\Message::toString($e->getResponse()); $error['response'] = Psr7\Message::toString($e->getResponse());
@ -122,11 +127,32 @@ class InsuranceConnector
]; ];
} }
error_log(print_r(json_decode($response->getBody(), true), true)); $result_body = $response->getBody();
// log response
$this->log($url, json_encode($request_body), $result_body);
return [ return [
'success' => true, 'success' => true,
'response' => json_decode($response->getBody(), true) 'response' => json_decode($result_body, true),
]; ];
} }
// TODO: make this more elegant
public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api')
{
$filename = '/../../var/log/insurance_' . $type . '.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("\r\n", [
$date,
$title,
"REQUEST:\r\n" . $request_body,
"RESPONSE:\r\n" . $result_body,
"\r\n----------------------------------------\r\n\r\n",
]);
@file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
}
} }

View file

@ -134,7 +134,7 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
} }
// generate invoice criteria // generate invoice criteria
public function generateInvoiceCriteria($jo, $discount, $invoice_items, $source = null, &$error_array) public function generateInvoiceCriteria($jo, $discount, $invoice_items, $price_tier = null, $source = null, &$error_array)
{ {
$em = $this->em; $em = $this->em;

View file

@ -144,7 +144,7 @@ class ResqInvoiceGenerator implements InvoiceGeneratorInterface
} }
// generate invoice criteria // generate invoice criteria
public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source = null, &$error_array) public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, $price_tier = null, $source = null, &$error_array)
{ {
$em = $this->em; $em = $this->em;

View file

@ -4,6 +4,7 @@ namespace App\Service;
use App\Entity\Invoice; use App\Entity\Invoice;
use App\Entity\JobOrder; use App\Entity\JobOrder;
use App\Entity\PriceTier;
use App\Ramcar\InvoiceCriteria; use App\Ramcar\InvoiceCriteria;
@ -13,7 +14,7 @@ interface InvoiceGeneratorInterface
public function generateInvoice(InvoiceCriteria $criteria); public function generateInvoice(InvoiceCriteria $criteria);
// generate invoice criteria // generate invoice criteria
public function generateInvoiceCriteria(JobOrder $jo, int $promo_id, array $invoice_items, $source, array &$error_array); public function generateInvoiceCriteria(JobOrder $jo, int $promo_id, array $invoice_items, $source, PriceTier $price_tier, array &$error_array);
// prepare draft for invoice // prepare draft for invoice
public function generateDraftInvoice(InvoiceCriteria $criteria, int $promo_id, array $service_charges, array $items); public function generateDraftInvoice(InvoiceCriteria $criteria, int $promo_id, array $service_charges, array $items);

View file

@ -10,6 +10,7 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRule; use App\InvoiceRule;
use App\Service\InvoiceGeneratorInterface; use App\Service\InvoiceGeneratorInterface;
use App\Service\PriceTierManager;
use App\Ramcar\InvoiceCriteria; use App\Ramcar\InvoiceCriteria;
use App\Ramcar\InvoiceStatus; use App\Ramcar\InvoiceStatus;
@ -28,12 +29,14 @@ class InvoiceManager implements InvoiceGeneratorInterface
protected $em; protected $em;
protected $validator; protected $validator;
protected $available_rules; protected $available_rules;
protected $pt_manager;
public function __construct(EntityManagerInterface $em, Security $security, ValidatorInterface $validator) public function __construct(EntityManagerInterface $em, Security $security, ValidatorInterface $validator, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->security = $security; $this->security = $security;
$this->validator = $validator; $this->validator = $validator;
$this->pt_manager = $pt_manager;
$this->available_rules = $this->getAvailableRules(); $this->available_rules = $this->getAvailableRules();
} }
@ -42,28 +45,29 @@ class InvoiceManager implements InvoiceGeneratorInterface
{ {
// TODO: get list of invoice rules from .env or a json file? // TODO: get list of invoice rules from .env or a json file?
return [ return [
new InvoiceRule\BatterySales($this->em), new InvoiceRule\BatterySales($this->em, $this->pt_manager),
new InvoiceRule\BatteryReplacementWarranty($this->em), new InvoiceRule\BatteryReplacementWarranty($this->em, $this->pt_manager),
new InvoiceRule\Jumpstart($this->em), new InvoiceRule\Jumpstart($this->em, $this->pt_manager),
new InvoiceRule\JumpstartWarranty($this->em), new InvoiceRule\JumpstartWarranty($this->em, $this->pt_manager),
new InvoiceRule\PostRecharged($this->em), new InvoiceRule\PostRecharged($this->em, $this->pt_manager),
new InvoiceRule\PostReplacement($this->em), new InvoiceRule\PostReplacement($this->em, $this->pt_manager),
new InvoiceRule\Overheat($this->em), new InvoiceRule\Overheat($this->em, $this->pt_manager),
new InvoiceRule\Fuel($this->em), new InvoiceRule\Fuel($this->em, $this->pt_manager),
new InvoiceRule\TireRepair($this->em), new InvoiceRule\TireRepair($this->em, $this->pt_manager),
new InvoiceRule\DiscountType($this->em), new InvoiceRule\DiscountType($this->em),
new InvoiceRule\TradeIn(), new InvoiceRule\TradeIn(),
new InvoiceRule\Tax($this->em), new InvoiceRule\Tax($this->em, $this->pt_manager),
]; ];
} }
// this is called when JO is submitted // this is called when JO is submitted
public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, &$error_array) public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, $price_tier, &$error_array)
{ {
// instantiate the invoice criteria // instantiate the invoice criteria
$criteria = new InvoiceCriteria(); $criteria = new InvoiceCriteria();
$criteria->setServiceType($jo->getServiceType()) $criteria->setServiceType($jo->getServiceType())
->setCustomerVehicle($jo->getCustomerVehicle()); ->setCustomerVehicle($jo->getCustomerVehicle())
->setPriceTier($price_tier);
// set if taxable // set if taxable
// NOTE: ideally, this should be a parameter when calling generateInvoiceCriteria. But that // NOTE: ideally, this should be a parameter when calling generateInvoiceCriteria. But that

View file

@ -69,6 +69,7 @@ use App\Service\HubSelector;
use App\Service\HubDistributor; use App\Service\HubDistributor;
use App\Service\HubFilteringGeoChecker; use App\Service\HubFilteringGeoChecker;
use App\Service\JobOrderManager; use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use CrEOF\Spatial\PHP\Types\Geometry\Point; use CrEOF\Spatial\PHP\Types\Geometry\Point;
@ -96,6 +97,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
protected $cust_distance_limit; protected $cust_distance_limit;
protected $hub_filter_enable; protected $hub_filter_enable;
protected $jo_manager; protected $jo_manager;
protected $pt_manager;
protected $template_hash; protected $template_hash;
@ -104,7 +106,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
TranslatorInterface $translator, RiderAssignmentHandlerInterface $rah, TranslatorInterface $translator, RiderAssignmentHandlerInterface $rah,
string $country_code, WarrantyHandler $wh, RisingTideGateway $rt, string $country_code, WarrantyHandler $wh, RisingTideGateway $rt,
PromoLogger $promo_logger, HubDistributor $hub_dist, HubFilteringGeoChecker $hub_geofence, PromoLogger $promo_logger, HubDistributor $hub_dist, HubFilteringGeoChecker $hub_geofence,
string $cust_distance_limit, string $hub_filter_enabled, JobOrderManager $jo_manager) string $cust_distance_limit, string $hub_filter_enabled, JobOrderManager $jo_manager, PriceTierManager $pt_manager)
{ {
$this->em = $em; $this->em = $em;
$this->ic = $ic; $this->ic = $ic;
@ -121,6 +123,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$this->cust_distance_limit = $cust_distance_limit; $this->cust_distance_limit = $cust_distance_limit;
$this->hub_filter_enabled = $hub_filter_enabled; $this->hub_filter_enabled = $hub_filter_enabled;
$this->jo_manager = $jo_manager; $this->jo_manager = $jo_manager;
$this->pt_manager = $pt_manager;
$this->loadTemplates(); $this->loadTemplates();
} }
@ -585,7 +588,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
{ {
$source = $jo->getSource(); $source = $jo->getSource();
$this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, $error_array); // get the price tier according to location.
$price_tier = $this->pt_manager->getPriceTier($jo->getCoordinates());
$this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, $price_tier, $error_array);
} }
// validate // validate
@ -817,7 +822,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
{ {
$source = $obj->getSource(); $source = $obj->getSource();
$this->ic->generateInvoiceCriteria($obj, $promo_id, $invoice_items, $source, $error_array); // get the price tier according to location.
$price_tier = $this->pt_manager->getPriceTier($obj->getCoordinates());
$this->ic->generateInvoiceCriteria($obj, $promo_id, $invoice_items, $source, $price_tier, $error_array);
} }
// validate // validate
@ -2014,6 +2021,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// NOTE: for resq2 app // NOTE: for resq2 app
$mclientv2->sendEvent($obj, $payload); $mclientv2->sendEvent($obj, $payload);
$mclientv2->sendRiderEvent($obj, $payload);
$fcmclient->sendJoEvent($obj, "jo_fcm_title_driver_assigned", "jo_fcm_body_driver_assigned"); $fcmclient->sendJoEvent($obj, "jo_fcm_title_driver_assigned", "jo_fcm_body_driver_assigned");
} }
@ -2165,7 +2173,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// NOTE: this is CMB code but for compilation purposes we need to add this // NOTE: this is CMB code but for compilation purposes we need to add this
$source = $jo->getSource(); $source = $jo->getSource();
$this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, $error_array); // get the price tier according to location.
$price_tier = $this->pt_manager->getPriceTier($jo->getCoordinates());
$this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, $price_tier, $error_array);
} }
// validate // validate
@ -2910,6 +2920,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$params['status_cancelled'] = JOStatus::CANCELLED; $params['status_cancelled'] = JOStatus::CANCELLED;
$params['hubs'] = []; $params['hubs'] = [];
$branch_codes = [];
// format duration and distance into friendly time // format duration and distance into friendly time
foreach ($hubs as $hub) { foreach ($hubs as $hub) {
// duration // duration
@ -4252,6 +4263,10 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
if ($rejection->getReason() == JORejectionReason::ADMINISTRATIVE) if ($rejection->getReason() == JORejectionReason::ADMINISTRATIVE)
return null; return null;
// check if reason is discount
if ($rejection->getReason() == JORejectionReason::DISCOUNT)
return null;
// sms content // sms content
// Job Order # - can get from jo // Job Order # - can get from jo
// Order Date and Time - get from jo // Order Date and Time - get from jo

View file

@ -68,4 +68,31 @@ class MQTTClientApiv2
// error_log('sent to ' . $channel); // error_log('sent to ' . $channel);
} }
} }
public function sendRiderEvent(JobOrder $job_order, $payload)
{
// check if a rider is available
$rider = $job_order->getRider();
if ($rider == null)
return;
/*
// NOTE: this is for the old rider app
// check if rider has sessions
$sessions = $rider->getSessions();
if (count($sessions) == 0)
return;
// send to every rider session
foreach ($sessions as $sess)
{
$sess_id = $sess->getID();
$channel = self::RIDER_PREFIX . $sess_id;
$this->publish($channel, json_encode($payload));
}
*/
// NOTE: this is for the new rider app
$this->publish('rider/' . $rider->getID() . '/delivery', json_encode($payload));
}
} }

View file

@ -79,7 +79,7 @@ class PayMongoConnector
return base64_encode($this->secret_key); return base64_encode($this->secret_key);
} }
protected function doRequest($url, $method, $body = []) protected function doRequest($url, $method, $request_body = [])
{ {
$client = new Client(); $client = new Client();
$headers = [ $headers = [
@ -90,14 +90,14 @@ class PayMongoConnector
try { try {
$response = $client->request($method, $this->base_url . '/' . $url, [ $response = $client->request($method, $this->base_url . '/' . $url, [
'json' => $body, 'json' => $request_body,
'headers' => $headers, 'headers' => $headers,
]); ]);
} catch (RequestException $e) { } catch (RequestException $e) {
$error = ['message' => $e->getMessage()]; $error = ['message' => $e->getMessage()];
ob_start(); ob_start();
var_dump($body); //var_dump($request_body);
$varres = ob_get_clean(); $varres = ob_get_clean();
error_log($varres); error_log($varres);
@ -107,6 +107,9 @@ class PayMongoConnector
error_log("PayMongo API Error: " . $error['message']); error_log("PayMongo API Error: " . $error['message']);
error_log(Psr7\Message::toString($e->getRequest())); error_log(Psr7\Message::toString($e->getRequest()));
// log this error
$this->log($url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error');
if ($e->hasResponse()) { if ($e->hasResponse()) {
$error['response'] = Psr7\Message::toString($e->getResponse()); $error['response'] = Psr7\Message::toString($e->getResponse());
} }
@ -117,9 +120,32 @@ class PayMongoConnector
]; ];
} }
$result_body = $response->getBody();
// log response
$this->log($url, json_encode($request_body), $result_body);
return [ return [
'success' => true, 'success' => true,
'response' => json_decode($response->getBody(), true) 'response' => json_decode($response->getBody(), true)
]; ];
} }
// TODO: make this more elegant
public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api')
{
$filename = '/../../var/log/paymongo_' . $type . '.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("\r\n", [
$date,
$title,
"REQUEST:\r\n" . $request_body,
"RESPONSE:\r\n" . $result_body,
"\r\n----------------------------------------\r\n\r\n",
]);
@file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
}
} }

View file

@ -0,0 +1,80 @@
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Entity\PriceTier;
class PriceTierManager
{
protected $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getItemPrice($pt_id, $item_type_id, $item_id)
{
// find the item price, given the price tier, battery id, and item type (battery)
$db_conn = $this->em->getConnection();
$ip_sql = 'SELECT ip.price AS price
FROM item_price ip
WHERE ip.price_tier_id = :pt_id
AND ip.item_type_id = :it_id
AND ip.item_id = :item_id';
$ip_stmt = $db_conn->prepare($ip_sql);
$ip_stmt->bindValue('pt_id', $pt_id);
$ip_stmt->bindValue('it_id', $item_type_id);
$ip_stmt->bindValue('item_id', $item_id);
$ip_result = $ip_stmt->executeQuery();
// results found
$actual_price = null;
// go through rows
while ($row = $ip_result->fetchAssociative())
{
// get the price
$price = $row['price'];
// actual price
$actual_price = number_format($price / 100, 2, '.', '');
}
return $actual_price;
}
public function getPriceTier(Point $coordinates)
{
$price_tier_id = 0;
if ($coordinates != null)
{
$long = $coordinates->getLongitude();
$lat = $coordinates->getLatitude();
// get location's price tier, 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)
{
$price_tier = $area->getPriceTier();
if ($price_tier != null)
$price_tier_id = $price_tier->getID();
}
}
return $price_tier_id;
}
}

View file

@ -0,0 +1,164 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Item Pricing</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-12">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-2">
<label>Item Prices for </label>
</div>
<div class="col-md-3">
<div class="m-input-icon m-input-icon--left">
<div class="input-group">
<select class="form-control m-input" id="price-tier-select" name="price_tier_list">
<option value="0">Default Price Tier</option>
{% for price_tier in sets.price_tiers %}
<option value="{{ price_tier.getID }}">{{ price_tier.getName }} </option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="col-md-3">
<div class="m-input-icon m-input-icon--left">
<div class="input-group">
<select class="form-control m-input" id="item-type-select" name="item_type_list">
{% for item_type in sets.item_types %}
<option value="{{ item_type.getID }}">{{ item_type.getName }} </option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right" method="post" action="{{ url('item_pricing_update') }}">
<input id="price-tier-id" type="hidden" name="price_tier_id" value="0">
<input id="item-type-id" type="hidden" name="item_type_id" value="{{ default_item_type_id }}">
<div style="padding-left: 25px; padding-right: 25px;">
<table class="table">
<thead>
<tr>
<th style="width: 100px">ID</th>
<th>Name</th>
<th hidden> Item Type ID </th>
<th>Item Type</th>
<th style="width: 180px">Price</th>
</tr>
</thead>
<tbody id="table-body">
{% for id, item in items.items %}
<tr>
<td>{{ id }}</td>
<td>{{ item.name }} </td>
<td hidden> {{ item.item_type_id }} </td>
<td>{{ item.item_type }} </td>
<td class="py-1">
<input name="price[{{ id }}]" class="form-control ca-filter" type="number" value="{{ item.price }}" step="0.01">
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="">
<input type="submit" class="btn btn-primary" value="Update Price">
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
initialize();
function initialize() {
init_price_tier_dropdown();
init_item_type_dropdown();
}
function init_price_tier_dropdown() {
var pt_dropdown = document.getElementById('price-tier-select');
var it_dropdown = document.getElementById('item-type-select');
pt_dropdown.addEventListener('change', function(e) {
var it_type = it_dropdown.value;
load_prices(e.target.value, it_type);
});
}
function init_item_type_dropdown() {
var it_dropdown = document.getElementById('item-type-select');
var pt_dropdown = document.getElementById('price-tier-select');
it_dropdown.addEventListener('change', function(e) {
var pt_type = pt_dropdown.value;
load_prices(pt_type, e.target.value);
});
}
function load_prices(price_tier_id, item_type_id) {
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
// process response
if (this.readyState == 4 && this.status == 200) {
// update form
update_table(JSON.parse(req.responseText));
var pt_field = document.getElementById('price-tier-id');
pt_field.value = price_tier_id;
var it_field = document.getElementById('item-type-id');
it_field.value = item_type_id;
} else {
// console.log('could not load tier prices');
}
}
var url_pattern = '{{ url('item_pricing_prices', {'pt_id': '--id--', 'it_id': '--it-id--'}) }}';
var url = url_pattern.replace('--id--', price_tier_id).replace('--it-id--', item_type_id);
console.log(url);
req.open('GET', url, true);
req.send();
}
function update_table(data) {
console.log(data);
var item_html = '';
for (var i in data.items) {
var item = data.items[i];
// console.log(item);
item_html += '<tr>';
item_html += '<td>' + item.id + '</td>';
item_html += '<td>' + item.name + '</td>';
item_html += '<td hidden>' + item.item_type_id + '</td>';
item_html += '<td>' + item.item_type + '</td>';
item_html += '<td class="py-1">';
item_html += '<input name="price[' + item.id + ']" class="form-control ca-filter" type="number" value="' + item.price + '" step="0.01">';
item_html += '</td>';
item_html += '</tr>';
}
var table_body = document.getElementById('table-body');
table_body.innerHTML = item_html;
}
</script>
{% endblock %}

View file

@ -0,0 +1,142 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Item Types</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-6">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
<div class="m-portlet__head-title">
<span class="m-portlet__head-icon">
<i class="la la-industry"></i>
</span>
<h3 class="m-portlet__head-text">
{% if mode == 'update' %}
Edit Item Type
<small>{{ obj.getName() }}</small>
{% else %}
New Item Type
{% endif %}
</h3>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right m-form--group-seperator-dashed" method="post" action="{{ mode == 'update' ? url('item_type_update_submit', {'id': obj.getId()}) : url('item_type_add_submit') }}">
<div class="m-portlet__body">
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="name">
Name:
</label>
<div class="col-lg-9">
<input type="text" name="name" class="form-control m-input" value="{{ obj.getName() }}">
<div class="form-control-feedback hide" data-field="name"></div>
</div>
</div>
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="code">
Code:
</label>
<div class="col-lg-9">
<input type="text" name="code" class="form-control m-input" value="{{ obj.getCode() }}">
<div class="form-control-feedback hide" data-field="code"></div>
</div>
</div>
</div>
<div class="m-portlet__foot m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-success">Submit</button>
<a href="{{ url('item_type_list') }}" class="btn btn-secondary">Back</a>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#row-form").submit(function(e) {
var form = $(this);
e.preventDefault();
$.ajax({
method: "POST",
url: form.prop('action'),
data: form.serialize()
}).done(function(response) {
// remove all error classes
removeErrors();
swal({
title: 'Done!',
text: 'Your changes have been saved!',
type: 'success',
onClose: function() {
window.location.href = "{{ url('item_type_list') }}";
}
});
}).fail(function(response) {
if (response.status == 422) {
var errors = response.responseJSON.errors;
var firstfield = false;
// remove all error classes first
removeErrors();
// display errors contextually
$.each(errors, function(field, msg) {
var formfield = $("[name='" + field + "']");
var label = $("label[data-field='" + field + "']");
var msgbox = $(".form-control-feedback[data-field='" + field + "']");
// add error classes to bad fields
formfield.addClass('form-control-danger');
label.addClass('has-danger');
msgbox.html(msg).addClass('has-danger').removeClass('hide');
// check if this field comes first in DOM
var domfield = formfield.get(0);
if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) {
firstfield = domfield;
}
});
// focus on first bad field
firstfield.focus();
// scroll to above that field to make it visible
$('html, body').animate({
scrollTop: $(firstfield).offset().top - 200
}, 100);
}
});
});
// remove all error classes
function removeErrors() {
$(".form-control-danger").removeClass('form-control-danger');
$("[data-field]").removeClass('has-danger');
$(".form-control-feedback[data-field]").addClass('hide');
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,146 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">
Item Types
</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-8 order-2 order-xl-1">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<input type="text" class="form-control m-input m-input--solid" placeholder="Search..." id="data-rows-search">
<span class="m-input-icon__icon m-input-icon__icon--left">
<span><i class="la la-search"></i></span>
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-4 order-1 order-xl-2 m--align-right">
<a href="{{ url('item_type_add_form') }}" class="btn btn-focus m-btn m-btn--custom m-btn--icon m-btn--air m-btn--pill">
<span>
<i class="la la-industry"></i>
<span>New Item Type</span>
</span>
</a>
<div class="m-separator m-separator--dashed d-xl-none"></div>
</div>
</div>
</div>
<!--begin: Datatable -->
<div id="data-rows"></div>
<!--end: Datatable -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
var options = {
data: {
type: 'remote',
source: {
read: {
url: '{{ url("item_type_rows") }}',
method: 'POST'
}
},
saveState: {
cookie: false,
webstorage: false
},
pageSize: 10,
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
layout: {
scroll: true
},
columns: [
{
field: 'id',
title: 'ID',
width: 30
},
{
field: 'name',
title: 'Name'
},
{
field: 'Actions',
width: 110,
title: 'Actions',
sortable: false,
overflow: 'visible',
template: function (row, index, datatable) {
var actions = '';
if (row.meta.update_url != '') {
actions += '<a href="' + row.meta.update_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-edit" data-id="' + row.name + '" title="Edit"><i class="la la-edit"></i></a>';
}
if (row.meta.delete_url != '') {
actions += '<a href="' + row.meta.delete_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-danger m-btn--icon m-btn--icon-only m-btn--pill btn-delete" data-id="' + row.name + '" title="Delete"><i class="la la-trash"></i></a>';
}
return actions;
},
}
],
search: {
onEnter: false,
input: $('#data-rows-search'),
delay: 400
}
};
var table = $("#data-rows").mDatatable(options);
$(document).on('click', '.btn-delete', function(e) {
var url = $(this).prop('href');
var id = $(this).data('id');
var btn = $(this);
e.preventDefault();
swal({
title: 'Confirmation',
html: 'Are you sure you want to delete <strong>' + id + '</strong>?',
type: 'warning',
showCancelButton: true
}).then((result) => {
if (result.value) {
$.ajax({
method: "DELETE",
url: url
}).done(function(response) {
table.row(btn.parents('tr')).remove();
table.reload();
});
}
});
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,177 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Items</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-6">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
<div class="m-portlet__head-title">
<span class="m-portlet__head-icon">
<i class="la la-industry"></i>
</span>
<h3 class="m-portlet__head-text">
{% if mode == 'update' %}
Edit Item
<small>{{ obj.getName() }}</small>
{% else %}
New Item
{% endif %}
</h3>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right m-form--group-seperator-dashed" method="post" action="{{ mode == 'update' ? url('item_type_update_submit', {'id': obj.getId()}) : url('item_add_submit') }}">
<div class="m-portlet__body">
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="item_type">
Item Type:
</label>
<div class="col-lg-9">
<select class="form-control m-input" id="item-type" name="item_type">
{% for id, label in sets.item_types %}
{% if obj.getItemType %}
<option value="{{ id }}"{{ obj.getItemType.getID == id ? ' selected' }}>{{ label }}</option>
{% else %}
<option value="{{ id }}">{{ label }}</option>
{% endif %}
{% endfor %}
</select>
<div class="form-control-feedback hide" data-field="item_type"></div>
</div>
</div>
<div class="form-group m-form__group row no-border
{% if obj.getItemType %}
{% if obj.getItemType.getCode is not same as ('battery') %}
hide
{% endif %}
{% else %}
hide
{% endif %}
" id="battery-row">
<label class="col-lg-3 col-form-label" data-field="battery">
Battery:
</label>
<div class="col-lg-9">
<select class="form-control m-input" id="item-type" name="battery">
{% for id, label in sets.batteries %}
<option value="{{ id }}"{{ obj.getItemID == id ? ' selected' }}>{{ label }}</option>
{% endfor %}
</select>
<div class="form-control-feedback hide" data-field="battery"></div>
</div>
</div>
</div>
<div class="m-portlet__foot m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-success">Submit</button>
<a href="{{ url('item_list') }}" class="btn btn-secondary">Back</a>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#row-form").submit(function(e) {
var form = $(this);
e.preventDefault();
$.ajax({
method: "POST",
url: form.prop('action'),
data: form.serialize()
}).done(function(response) {
// remove all error classes
removeErrors();
swal({
title: 'Done!',
text: 'Your changes have been saved!',
type: 'success',
onClose: function() {
window.location.href = "{{ url('item_list') }}";
}
});
}).fail(function(response) {
if (response.status == 422) {
var errors = response.responseJSON.errors;
var firstfield = false;
// remove all error classes first
removeErrors();
// display errors contextually
$.each(errors, function(field, msg) {
var formfield = $("[name='" + field + "']");
var label = $("label[data-field='" + field + "']");
var msgbox = $(".form-control-feedback[data-field='" + field + "']");
// add error classes to bad fields
formfield.addClass('form-control-danger');
label.addClass('has-danger');
msgbox.html(msg).addClass('has-danger').removeClass('hide');
// check if this field comes first in DOM
var domfield = formfield.get(0);
if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) {
firstfield = domfield;
}
});
// focus on first bad field
firstfield.focus();
// scroll to above that field to make it visible
$('html, body').animate({
scrollTop: $(firstfield).offset().top - 200
}, 100);
}
});
});
// remove all error classes
function removeErrors() {
$(".form-control-danger").removeClass('form-control-danger');
$("[data-field]").removeClass('has-danger');
$(".form-control-feedback[data-field]").addClass('hide');
}
});
$('#item-type').change(function(e) {
console.log('item type change ' + e.target.value);
if (e.target.value === '1') {
// display battery row
$('#battery-row').removeClass("hide");
// hide service offering rows
} else {
// display service offering row
// hide battery row
$('#battery-row').addClass("hide");
}
})
</script>
{% endblock %}

View file

@ -0,0 +1,146 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">
Items
</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-8 order-2 order-xl-1">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<input type="text" class="form-control m-input m-input--solid" placeholder="Search..." id="data-rows-search">
<span class="m-input-icon__icon m-input-icon__icon--left">
<span><i class="la la-search"></i></span>
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-4 order-1 order-xl-2 m--align-right">
<a href="{{ url('item_add_form') }}" class="btn btn-focus m-btn m-btn--custom m-btn--icon m-btn--air m-btn--pill">
<span>
<i class="la la-industry"></i>
<span>New Item</span>
</span>
</a>
<div class="m-separator m-separator--dashed d-xl-none"></div>
</div>
</div>
</div>
<!--begin: Datatable -->
<div id="data-rows"></div>
<!--end: Datatable -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
var options = {
data: {
type: 'remote',
source: {
read: {
url: '{{ url("item_rows") }}',
method: 'POST'
}
},
saveState: {
cookie: false,
webstorage: false
},
pageSize: 10,
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
layout: {
scroll: true
},
columns: [
{
field: 'id',
title: 'ID',
width: 30
},
{
field: 'name',
title: 'Name'
},
{
field: 'Actions',
width: 110,
title: 'Actions',
sortable: false,
overflow: 'visible',
template: function (row, index, datatable) {
var actions = '';
if (row.meta.update_url != '') {
actions += '<a href="' + row.meta.update_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-edit" data-id="' + row.name + '" title="Edit"><i class="la la-edit"></i></a>';
}
if (row.meta.delete_url != '') {
actions += '<a href="' + row.meta.delete_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-danger m-btn--icon m-btn--icon-only m-btn--pill btn-delete" data-id="' + row.name + '" title="Delete"><i class="la la-trash"></i></a>';
}
return actions;
},
}
],
search: {
onEnter: false,
input: $('#data-rows-search'),
delay: 400
}
};
var table = $("#data-rows").mDatatable(options);
$(document).on('click', '.btn-delete', function(e) {
var url = $(this).prop('href');
var id = $(this).data('id');
var btn = $(this);
e.preventDefault();
swal({
title: 'Confirmation',
html: 'Are you sure you want to delete <strong>' + id + '</strong>?',
type: 'warning',
showCancelButton: true
}).then((result) => {
if (result.value) {
$.ajax({
method: "DELETE",
url: url
}).done(function(response) {
table.row(btn.parents('tr')).remove();
table.reload();
});
}
});
});
});
</script>
{% endblock %}

View file

@ -1231,6 +1231,8 @@ $(function() {
function selectPoint(lat, lng) function selectPoint(lat, lng)
{ {
// check if point is in coverage area // check if point is in coverage area
// commenting out the geofence call for CRM
/*
$.ajax({ $.ajax({
method: "GET", method: "GET",
url: "{{ url('jo_geofence') }}", url: "{{ url('jo_geofence') }}",
@ -1248,7 +1250,7 @@ $(function() {
type: 'warning', type: 'warning',
}); });
} }
}); }); */
// clear markers // clear markers
markerLayerGroup.clearLayers(); markerLayerGroup.clearLayers();
@ -1761,6 +1763,8 @@ $(function() {
var table = $("#invoice-table tbody"); var table = $("#invoice-table tbody");
var stype = $("#service_type").val(); var stype = $("#service_type").val();
var cvid = $("#customer-vehicle").val(); var cvid = $("#customer-vehicle").val();
var lng = $("#map_lng").val();
var lat = $("#map_lat").val();
console.log(JSON.stringify(invoiceItems)); console.log(JSON.stringify(invoiceItems));
@ -1772,7 +1776,9 @@ $(function() {
'stype': stype, 'stype': stype,
'items': invoiceItems, 'items': invoiceItems,
'promo': promo, 'promo': promo,
'cvid': cvid 'cvid': cvid,
'coord_lng': lng,
'coord_lat': lat,
} }
}).done(function(response) { }).done(function(response) {
// mark as invoice changed // mark as invoice changed

View file

@ -635,6 +635,8 @@ $(function() {
function selectPoint(lat, lng) function selectPoint(lat, lng)
{ {
// check if point is in coverage area // check if point is in coverage area
// commenting out the geofence call for CRM
/*
$.ajax({ $.ajax({
method: "GET", method: "GET",
url: "{{ url('jo_geofence') }}", url: "{{ url('jo_geofence') }}",
@ -652,7 +654,7 @@ $(function() {
type: 'warning', type: 'warning',
}); });
} }
}); }); */
// clear markers // clear markers
markerLayerGroup.clearLayers(); markerLayerGroup.clearLayers();

View file

@ -0,0 +1,154 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Price Tiers</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-6">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
<div class="m-portlet__head-title">
<span class="m-portlet__head-icon">
<i class="la la-industry"></i>
</span>
<h3 class="m-portlet__head-text">
{% if mode == 'update' %}
Edit Price Tier
<small>{{ obj.getName() }}</small>
{% else %}
New Price Tier
{% endif %}
</h3>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right m-form--group-seperator-dashed" method="post" action="{{ mode == 'update' ? url('price_tier_update_submit', {'id': obj.getId()}) : url('price_tier_add_submit') }}">
<div class="m-portlet__body">
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="name">
Name:
</label>
<div class="col-lg-9">
<input type="text" name="name" class="form-control m-input" value="{{ obj.getName() }}">
<div class="form-control-feedback hide" data-field="name"></div>
</div>
</div>
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="areas">
Coverage Area:
</label>
<div class="col-lg-9">
{% if sets.areas is empty %}
No available supported areas.
{% else %}
<div class="m-checkbox-list">
{% for id, label in sets.areas %}
<label class="m-checkbox">
<input type="checkbox" name="areas[]" value="{{ id }}"{{ id in obj.getSupportedAreas() ? ' checked' : '' }}>
{{ label }}
<span></span>
</label>
{% endfor %}
</div>
{% endif %}
<div class="form-control-feedback hide" data-field="areas"></div>
</div>
</div>
</div>
<div class="m-portlet__foot m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-success">Submit</button>
<a href="{{ url('price_tier_list') }}" class="btn btn-secondary">Back</a>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#row-form").submit(function(e) {
var form = $(this);
e.preventDefault();
$.ajax({
method: "POST",
url: form.prop('action'),
data: form.serialize()
}).done(function(response) {
// remove all error classes
removeErrors();
swal({
title: 'Done!',
text: 'Your changes have been saved!',
type: 'success',
onClose: function() {
window.location.href = "{{ url('price_tier_list') }}";
}
});
}).fail(function(response) {
if (response.status == 422) {
var errors = response.responseJSON.errors;
var firstfield = false;
// remove all error classes first
removeErrors();
// display errors contextually
$.each(errors, function(field, msg) {
var formfield = $("[name='" + field + "']");
var label = $("label[data-field='" + field + "']");
var msgbox = $(".form-control-feedback[data-field='" + field + "']");
// add error classes to bad fields
formfield.addClass('form-control-danger');
label.addClass('has-danger');
msgbox.html(msg).addClass('has-danger').removeClass('hide');
// check if this field comes first in DOM
var domfield = formfield.get(0);
if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) {
firstfield = domfield;
}
});
// focus on first bad field
firstfield.focus();
// scroll to above that field to make it visible
$('html, body').animate({
scrollTop: $(firstfield).offset().top - 200
}, 100);
}
});
});
// remove all error classes
function removeErrors() {
$(".form-control-danger").removeClass('form-control-danger');
$("[data-field]").removeClass('has-danger');
$(".form-control-feedback[data-field]").addClass('hide');
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,146 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">
Price Tiers
</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-8 order-2 order-xl-1">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<input type="text" class="form-control m-input m-input--solid" placeholder="Search..." id="data-rows-search">
<span class="m-input-icon__icon m-input-icon__icon--left">
<span><i class="la la-search"></i></span>
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-4 order-1 order-xl-2 m--align-right">
<a href="{{ url('price_tier_add_form') }}" class="btn btn-focus m-btn m-btn--custom m-btn--icon m-btn--air m-btn--pill">
<span>
<i class="la la-industry"></i>
<span>New Price Tier</span>
</span>
</a>
<div class="m-separator m-separator--dashed d-xl-none"></div>
</div>
</div>
</div>
<!--begin: Datatable -->
<div id="data-rows"></div>
<!--end: Datatable -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
var options = {
data: {
type: 'remote',
source: {
read: {
url: '{{ url("price_tier_rows") }}',
method: 'POST'
}
},
saveState: {
cookie: false,
webstorage: false
},
pageSize: 10,
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
layout: {
scroll: true
},
columns: [
{
field: 'id',
title: 'ID',
width: 30
},
{
field: 'name',
title: 'Name'
},
{
field: 'Actions',
width: 110,
title: 'Actions',
sortable: false,
overflow: 'visible',
template: function (row, index, datatable) {
var actions = '';
if (row.meta.update_url != '') {
actions += '<a href="' + row.meta.update_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-edit" data-id="' + row.name + '" title="Edit"><i class="la la-edit"></i></a>';
}
if (row.meta.delete_url != '') {
actions += '<a href="' + row.meta.delete_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-danger m-btn--icon m-btn--icon-only m-btn--pill btn-delete" data-id="' + row.name + '" title="Delete"><i class="la la-trash"></i></a>';
}
return actions;
},
}
],
search: {
onEnter: false,
input: $('#data-rows-search'),
delay: 400
}
};
var table = $("#data-rows").mDatatable(options);
$(document).on('click', '.btn-delete', function(e) {
var url = $(this).prop('href');
var id = $(this).data('id');
var btn = $(this);
e.preventDefault();
swal({
title: 'Confirmation',
html: 'Are you sure you want to delete <strong>' + id + '</strong>?',
type: 'warning',
showCancelButton: true
}).then((result) => {
if (result.value) {
$.ajax({
method: "DELETE",
url: url
}).done(function(response) {
table.row(btn.parents('tr')).remove();
table.reload();
});
}
});
});
});
</script>
{% endblock %}

View file

@ -233,13 +233,14 @@ $(function() {
var batteryIds = []; var batteryIds = [];
var battMfgModelSize = [] var battMfgModelSize = []
{% for batt in obj.getActiveBatteries %} {% for batt in obj.getBatteries %}
trow = { trow = {
id: "{{ batt.getID }}", id: "{{ batt.getID }}",
manufacturer: "{{ batt.getManufacturer.getName|default('') }} ", manufacturer: "{{ batt.getManufacturer.getName|default('') }} ",
model: "{{ batt.getModel.getName|default('') }}", model: "{{ batt.getModel.getName|default('') }}",
size: "{{ batt.getSize.getName|default('') }}", size: "{{ batt.getSize.getName|default('') }}",
sell_price: "{{ batt.getSellingPrice }}", sell_price: "{{ batt.getSellingPrice }}",
flag_active: {{ batt.isActive ? "true" : "false" }},
}; };
battRows.push(trow); battRows.push(trow);
@ -360,6 +361,21 @@ $(function() {
title: 'Model', title: 'Model',
width: 150 width: 150
}, },
{
field: 'flag_active',
title: 'Active',
template: function (row, index, datatable) {
var tag = '';
if (row.flag_active === true) {
tag = '<span class="m-badge m-badge--success m-badge--wide">Yes</span>';
} else {
tag = '<span class="m-badge m-badge--danger m-badge--wide">No</span>';
}
return tag;
},
},
{ {
field: 'sell_price', field: 'sell_price',
title: 'Price' title: 'Price'

View file

@ -159,6 +159,7 @@ menu.database.subtickettypes: 'Sub Ticket Types'
menu.database.emergencytypes: 'Emergency Types' menu.database.emergencytypes: 'Emergency Types'
menu.database.ownershiptypes: 'Ownership Types' menu.database.ownershiptypes: 'Ownership Types'
menu.database.serviceofferings: 'Service Offerings' menu.database.serviceofferings: 'Service Offerings'
menu.database.itemtypes: 'Item Types'
# fcm jo status updates # fcm jo status updates
jo_fcm_title_outlet_assign: 'Looking for riders' jo_fcm_title_outlet_assign: 'Looking for riders'

View file

@ -0,0 +1,53 @@
-- MySQL dump 10.19 Distrib 10.3.39-MariaDB, for Linux (x86_64)
--
-- Host: localhost Database: resq
-- ------------------------------------------------------
-- Server version 10.3.39-MariaDB
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `item_type`
--
DROP TABLE IF EXISTS `item_type`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `item_type` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(80) NOT NULL,
`code` varchar(80) NOT NULL,
PRIMARY KEY (`id`),
KEY `item_type_idx` (`code`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `item_type`
--
LOCK TABLES `item_type` WRITE;
/*!40000 ALTER TABLE `item_type` DISABLE KEYS */;
INSERT INTO `item_type` VALUES (1,'Battery','battery'),(2,'Service Offering','service_offering');
/*!40000 ALTER TABLE `item_type` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2024-01-28 20:59:44