Merge branch '780-regional-pricing' into 783-rider-app-trade-in-support

# Conflicts:
#	src/Controller/CAPI/RiderAppController.php
#	src/InvoiceRule/BatterySales.php
This commit is contained in:
Ramon Gutierrez 2024-03-07 09:42:42 +08:00
commit a2cd86b48c
51 changed files with 3278 additions and 80 deletions

View file

@ -634,6 +634,45 @@ catalyst_auth:
- id: service_offering.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:
user_entity: "App\\Entity\\ApiUser"
acl_data:

View file

@ -177,7 +177,7 @@ catalyst_menu:
acl: support.menu
label: '[menu.support]'
icon: flaticon-support
order: 10
order: 11
- id: customer_list
acl: customer.list
label: '[menu.support.customers]'
@ -223,7 +223,7 @@ catalyst_menu:
acl: service.menu
label: '[menu.service]'
icon: flaticon-squares
order: 11
order: 12
- id: service_list
acl: service.list
label: '[menu.service.services]'
@ -233,7 +233,7 @@ catalyst_menu:
acl: partner.menu
label: '[menu.partner]'
icon: flaticon-network
order: 12
order: 13
- id: partner_list
acl: partner.list
label: '[menu.partner.partners]'
@ -247,7 +247,7 @@ catalyst_menu:
acl: motolite_event.menu
label: '[menu.motolite_event]'
icon: flaticon-event-calendar-symbol
order: 13
order: 14
- id: motolite_event_list
acl: motolite_event.list
label: '[menu.motolite_event.events]'
@ -257,7 +257,7 @@ catalyst_menu:
acl: analytics.menu
label: '[menu.analytics]'
icon: flaticon-graphic
order: 14
order: 15
- id: analytics_forecast_form
acl: analytics.forecast
label: '[menu.analytics.forecasting]'
@ -267,7 +267,7 @@ catalyst_menu:
acl: database.menu
label: '[menu.database]'
icon: fa fa-database
order: 15
order: 16
- id: ticket_type_list
acl: ticket_type.menu
label: '[menu.database.tickettypes]'
@ -288,3 +288,21 @@ catalyst_menu:
acl: service_offering.menu
label: '[menu.database.serviceofferings]'
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

@ -307,4 +307,9 @@ apiv2_insurance_application_create:
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

@ -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:
path: /tapi/vehicles/{vid}/compatible_batteries
controller: App\Controller\TAPI\BatteryController::getCompatibleBatteries
methods: [GET]
methods: [POST]
# promos
tapi_promo_list:

View file

@ -311,3 +311,8 @@ services:
arguments:
$server_key: "%env(FCM_SERVER_KEY)%"
$sender_id: "%env(FCM_SENDER_ID)%"
# price tier manager
App\Service\PriceTierManager:
arguments:
$em: "@doctrine.orm.entity_manager"

View file

@ -50,6 +50,7 @@ use App\Service\HubFilterLogger;
use App\Service\HubFilteringGeoChecker;
use App\Service\HashGenerator;
use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use App\Entity\MobileSession;
use App\Entity\Customer;
@ -2911,6 +2912,10 @@ class APIController extends Controller implements LoggedController
// old app doesn't have separate jumpstart
$icrit->setSource(TransactionOrigin::CALL);
// set price tier
$pt_id = $this->pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// check promo
$promo_id = $req->request->get('promo_id');
if (!empty($promo_id))

View file

@ -36,6 +36,7 @@ use App\Service\JobOrderHandlerInterface;
use App\Service\InvoiceGeneratorInterface;
use App\Service\RisingTideGateway;
use App\Service\RiderTracker;
use App\Service\PriceTierManager;
use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType;
@ -1282,7 +1283,7 @@ class RiderAppController extends ApiController
$payment_method = $items['payment_method'];
if (!ModeOfPayment::validate($payment_method))
$payment_method = ModeOfPayment::CASH;
$jo->setModeOfPayment($payemnt_method);
$jo->setModeOfPayment($payment_method);
}
// get capi user
@ -1304,7 +1305,7 @@ class RiderAppController extends ApiController
return new APIResponse(true, 'Job order updated.', $data);
}
public function changeService(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic)
public function changeService(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager)
{
// $this->debugRequest($req);
@ -1408,6 +1409,10 @@ class RiderAppController extends ApiController
$crit->setHasCoolant($jo->hasCoolant());
$crit->setIsTaxable();
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$crit->setPriceTier($pt_id);
if ($promo != null)
$crit->addPromo($promo);

View file

@ -307,6 +307,36 @@ class InsuranceController extends ApiController
]);
}
public function getBodyTypes(Request $req)
{
// validate params
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// TODO: if this changes often, make an entity and make it manageable on CRM
$body_types = [
[
'id' => 'SEDAN',
'name' => 'Sedan',
],
[
'id' => 'SUV',
'name' => 'SUV',
],
[
'id' => 'TRUCK',
'name' => 'Truck',
]
];
return new ApiResponse(true, '', [
'body_types' => $body_types,
]);
}
protected function getLineType($mv_type_id, $vehicle_use_type, $is_public = false)
{
$line = '';

View file

@ -4,18 +4,22 @@ namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request;
use Catalyst\ApiBundle\Component\Response as ApiResponse;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Service\InvoiceGeneratorInterface;
use App\Service\PriceTierManager;
use App\Ramcar\InvoiceCriteria;
use App\Ramcar\TradeInType;
use App\Ramcar\TransactionOrigin;
use App\Entity\CustomerVehicle;
use App\Entity\Promo;
use App\Entity\Battery;
use App\Entity\Customer;
use App\Entity\CustomerMetadata;
class InvoiceController extends ApiController
{
public function getEstimate(Request $req, InvoiceGeneratorInterface $ic)
public function getEstimate(Request $req, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager)
{
// $this->debugRequest($req);
@ -36,6 +40,18 @@ class InvoiceController extends ApiController
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
$icrit = new InvoiceCriteria();
$icrit->setServiceType($req->request->get('service_type'));
@ -113,6 +129,18 @@ class InvoiceController extends ApiController
// set JO source
$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
$invoice = $ic->generateInvoice($icrit);
@ -148,4 +176,28 @@ class InvoiceController extends ApiController
// response
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\HubFilteringGeoChecker;
use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use App\Ramcar\ServiceType;
use App\Ramcar\APIRiderStatus;
use App\Ramcar\InvoiceCriteria;
@ -484,7 +485,8 @@ class JobOrderController extends ApiController
HubDistributor $hub_dist,
HubFilterLogger $hub_filter_logger,
HubFilteringGeoChecker $hub_geofence,
JobOrderManager $jo_manager
JobOrderManager $jo_manager,
PriceTierManager $pt_manager
) {
// validate params
$validity = $this->validateRequest($req, [
@ -698,6 +700,10 @@ class JobOrderController extends ApiController
// set JO source
$icrit->setSource(TransactionOrigin::MOBILE_APP);
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// send to invoice generator
$invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice);
@ -970,7 +976,8 @@ class JobOrderController extends ApiController
HubDistributor $hub_dist,
HubFilterLogger $hub_filter_logger,
HubFilteringGeoChecker $hub_geofence,
JobOrderManager $jo_manager
JobOrderManager $jo_manager,
PriceTierManager $pt_manager
) {
// validate params
$validity = $this->validateRequest($req, [
@ -1127,6 +1134,10 @@ class JobOrderController extends ApiController
// set JO source
$icrit->setSource(TransactionOrigin::MOBILE_APP);
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// send to invoice generator
$invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice);

View file

@ -4,16 +4,19 @@ namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request;
use Catalyst\ApiBundle\Component\Response as ApiResponse;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Entity\CustomerVehicle;
use App\Entity\JobOrder;
use App\Entity\VehicleManufacturer;
use App\Entity\Vehicle;
use App\Entity\ItemType;
use App\Ramcar\JOStatus;
use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType;
use App\Ramcar\InsuranceApplicationStatus;
use App\Service\PayMongoConnector;
use App\Service\PriceTierManager;
use DateTime;
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
$validity = $this->validateRequest($req);
@ -252,11 +255,43 @@ class VehicleController extends ApiController
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
$batt_list = [];
$batts = $vehicle->getActiveBatteries();
foreach ($batts as $batt) {
// 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[] = [
'id' => $batt->getID(),
'mfg_id' => $batt->getManufacturer()->getID(),
@ -265,7 +300,7 @@ class VehicleController extends ApiController
'model_name' => $batt->getModel()->getName(),
'size_id' => $batt->getSize()->getID(),
'size_name' => $batt->getSize()->getName(),
'price' => $batt->getSellingPrice(),
'price' => $price,
'wty_private' => $batt->getWarrantyPrivate(),
'wty_commercial' => $batt->getWarrantyCommercial(),
'image_url' => $this->getBatteryImageURL($req, $batt),

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\MotivConnector;
use App\Service\PriceTierManager;
use App\Service\GeofenceTracker;
use Symfony\Component\HttpFoundation\Request;
@ -42,6 +43,8 @@ use Doctrine\ORM\EntityManagerInterface;
use Catalyst\MenuBundle\Annotation\Menu;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
class JobOrderController extends Controller
{
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 = false;
@ -752,6 +755,19 @@ class JobOrderController extends Controller
$cvid = $req->request->get('cvid');
$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();
// get customer vehicle
@ -767,8 +783,8 @@ class JobOrderController extends Controller
$criteria->setServiceType($stype)
->setCustomerVehicle($cv)
->setIsTaxable()
->setSource(TransactionOrigin::CALL);
->setSource(TransactionOrigin::CALL)
->setPriceTier($price_tier);
/*
// if it's a jumpstart or troubleshoot only, we know what to charge already

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\Entity\Vehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use Catalyst\AuthBundle\Service\ACLGenerator as ACLGenerator;
@ -25,7 +30,7 @@ class BatteryController extends ApiController
$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.');
@ -43,13 +48,44 @@ class BatteryController extends ApiController
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
$batt_list = [];
// $batts = $vehicle->getBatteries();
$batts = $vehicle->getActiveBatteries();
foreach ($batts as $batt)
{
// 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[] = [
'id' => $batt->getID(),
'mfg_id' => $batt->getManufacturer()->getID(),
@ -58,7 +94,7 @@ class BatteryController extends ApiController
'model_name' => $batt->getModel()->getName(),
'size_id' => $batt->getSize()->getID(),
'size_name' => $batt->getSize()->getName(),
'price' => $batt->getSellingPrice(),
'price' => $price,
'wty_private' => $batt->getWarrantyPrivate(),
'wty_commercial' => $batt->getWarrantyCommercial(),
'image_url' => $this->getBatteryImageURL($req, $batt),

View file

@ -46,6 +46,7 @@ use App\Service\RiderTracker;
use App\Service\PromoLogger;
use App\Service\MapTools;
use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use App\Entity\JobOrder;
use App\Entity\CustomerVehicle;
@ -79,7 +80,8 @@ class JobOrderController extends ApiController
FCMSender $fcmclient,
RiderAssignmentHandlerInterface $rah, PromoLogger $promo_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.');
@ -165,7 +167,17 @@ class JobOrderController extends ApiController
// set JO source
$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
$invoice = $ic->generateInvoice($icrit);

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;
}
}

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

@ -39,9 +39,17 @@ class SupportedArea
*/
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()
{
$this->date_create = new DateTime();
$this->price_tier = null;
}
public function getID()
@ -82,5 +90,16 @@ class SupportedArea
{
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

@ -11,14 +11,19 @@ use App\Ramcar\TradeInType;
use App\Entity\Battery;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class BatteryReplacementWarranty implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -29,6 +34,7 @@ class BatteryReplacementWarranty implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
@ -40,7 +46,14 @@ class BatteryReplacementWarranty implements InvoiceRuleInterface
{
$batt = $entry['battery'];
$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[] = [
'service_type' => $this->getID(),
@ -117,6 +130,34 @@ class BatteryReplacementWarranty implements InvoiceRuleInterface
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)
{
$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\Entity\Battery;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class BatterySales implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -28,6 +33,7 @@ class BatterySales implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
@ -51,7 +57,13 @@ class BatterySales implements InvoiceRuleInterface
// will not be set
$batt = $entry['battery'];
$price = $batt->getSellingPrice();
// 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[] = [
'service_type' => $this->getID(),
@ -117,6 +129,25 @@ class BatterySales implements InvoiceRuleInterface
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)
{
$title = $battery->getModel()->getName() . ' ' . $battery->getSize()->getName();

View file

@ -11,14 +11,19 @@ use App\Ramcar\ServiceType;
use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class Fuel implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -29,6 +34,7 @@ class Fuel implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
@ -36,7 +42,13 @@ class Fuel implements InvoiceRuleInterface
{
$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();
@ -46,10 +58,10 @@ class Fuel implements InvoiceRuleInterface
'service_type' => $this->getID(),
'qty' => $qty,
'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;
switch ($ftype)
@ -57,7 +69,15 @@ class Fuel implements InvoiceRuleInterface
case FuelType::GAS:
case FuelType::DIESEL:
$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[] = [
'service_type' => $this->getID(),
'qty' => $qty,
@ -138,6 +158,70 @@ class Fuel implements InvoiceRuleInterface
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)
{
$title = '4L - ' . ucfirst($fuel_type);

View file

@ -8,16 +8,21 @@ use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Ramcar\TransactionOrigin;
use App\Service\PriceTierManager;
class Jumpstart implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -29,13 +34,21 @@ class Jumpstart implements InvoiceRuleInterface
{
$stype = $criteria->getServiceType();
$source = $criteria->getSource();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
{
$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
$qty = 1;
@ -43,10 +56,10 @@ class Jumpstart implements InvoiceRuleInterface
'service_type' => $this->getID(),
'qty' => $qty,
'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);
}
@ -86,6 +99,45 @@ class Jumpstart implements InvoiceRuleInterface
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()
{
$title = 'Service - Troubleshooting fee';

View file

@ -7,14 +7,19 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class JumpstartWarranty implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -25,12 +30,19 @@ class JumpstartWarranty implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
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
$qty = 1;
@ -38,10 +50,10 @@ class JumpstartWarranty implements InvoiceRuleInterface
'service_type' => $this->getID(),
'qty' => $qty,
'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);
}
@ -72,6 +84,33 @@ class JumpstartWarranty implements InvoiceRuleInterface
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()
{
$title = 'Service - Troubleshooting fee';

View file

@ -10,14 +10,19 @@ use App\Ramcar\ServiceType;
use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class Overheat implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -29,13 +34,22 @@ class Overheat implements InvoiceRuleInterface
{
$stype = $criteria->getServiceType();
$has_coolant = $criteria->hasCoolant();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
{
$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
$qty = 1;
@ -43,10 +57,10 @@ class Overheat implements InvoiceRuleInterface
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
'price' => $price,
];
$qty_fee = bcmul($qty, $fee, 2);
$qty_fee = bcmul($qty, $price, 2);
$total_price = $qty_fee;
if ($has_coolant)
@ -112,6 +126,39 @@ class Overheat implements InvoiceRuleInterface
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()
{
$title = 'Service - ' . ServiceType::getName(ServiceType::OVERHEAT_ASSISTANCE);

View file

@ -7,14 +7,19 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class PostRecharged implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -25,22 +30,29 @@ class PostRecharged implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
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;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'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);
}
@ -72,6 +84,33 @@ class PostRecharged implements InvoiceRuleInterface
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()
{
$title = 'Recharge fee';

View file

@ -7,14 +7,19 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class PostReplacement implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -25,22 +30,29 @@ class PostReplacement implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
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;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'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);
}
@ -71,6 +83,33 @@ class PostReplacement implements InvoiceRuleInterface
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()
{
$title = 'Battery replacement';

View file

@ -9,14 +9,19 @@ use App\InvoiceRuleInterface;
use App\Ramcar\ServiceType;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class Tax implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -40,6 +45,7 @@ class Tax implements InvoiceRuleInterface
// compute tax per item if service type is battery sales
$stype = $criteria->getServiceType();
$pt = $criteria->getPriceTier();
if ($stype == ServiceType::BATTERY_REPLACEMENT_NEW)
{
@ -58,7 +64,13 @@ class Tax implements InvoiceRuleInterface
$battery = $entry['battery'];
$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);
@ -96,6 +108,25 @@ class Tax implements InvoiceRuleInterface
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)
{
$vat_ex_price = $this->getTaxExclusivePrice($price, $tax_rate);

View file

@ -8,14 +8,19 @@ use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class TireRepair implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -26,13 +31,21 @@ class TireRepair implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
{
$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
$qty = 1;
@ -40,10 +53,10 @@ class TireRepair implements InvoiceRuleInterface
'service_type' => $this->getID(),
'qty' => $qty,
'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);
}
@ -79,6 +92,39 @@ class TireRepair implements InvoiceRuleInterface
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()
{
$title = 'Service - Flat Tire';

View file

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

View file

@ -134,7 +134,7 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
}
// 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;

View file

@ -144,7 +144,7 @@ class ResqInvoiceGenerator implements InvoiceGeneratorInterface
}
// 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;

View file

@ -4,6 +4,7 @@ namespace App\Service;
use App\Entity\Invoice;
use App\Entity\JobOrder;
use App\Entity\PriceTier;
use App\Ramcar\InvoiceCriteria;
@ -13,7 +14,7 @@ interface InvoiceGeneratorInterface
public function generateInvoice(InvoiceCriteria $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
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\Service\InvoiceGeneratorInterface;
use App\Service\PriceTierManager;
use App\Ramcar\InvoiceCriteria;
use App\Ramcar\InvoiceStatus;
@ -28,12 +29,14 @@ class InvoiceManager implements InvoiceGeneratorInterface
protected $em;
protected $validator;
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->security = $security;
$this->validator = $validator;
$this->pt_manager = $pt_manager;
$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?
return [
new InvoiceRule\BatterySales($this->em),
new InvoiceRule\BatteryReplacementWarranty($this->em),
new InvoiceRule\Jumpstart($this->em),
new InvoiceRule\JumpstartWarranty($this->em),
new InvoiceRule\PostRecharged($this->em),
new InvoiceRule\PostReplacement($this->em),
new InvoiceRule\Overheat($this->em),
new InvoiceRule\Fuel($this->em),
new InvoiceRule\TireRepair($this->em),
new InvoiceRule\BatterySales($this->em, $this->pt_manager),
new InvoiceRule\BatteryReplacementWarranty($this->em, $this->pt_manager),
new InvoiceRule\Jumpstart($this->em, $this->pt_manager),
new InvoiceRule\JumpstartWarranty($this->em, $this->pt_manager),
new InvoiceRule\PostRecharged($this->em, $this->pt_manager),
new InvoiceRule\PostReplacement($this->em, $this->pt_manager),
new InvoiceRule\Overheat($this->em, $this->pt_manager),
new InvoiceRule\Fuel($this->em, $this->pt_manager),
new InvoiceRule\TireRepair($this->em, $this->pt_manager),
new InvoiceRule\DiscountType($this->em),
new InvoiceRule\TradeIn(),
new InvoiceRule\Tax($this->em),
new InvoiceRule\Tax($this->em, $this->pt_manager),
];
}
// 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
$criteria = new InvoiceCriteria();
$criteria->setServiceType($jo->getServiceType())
->setCustomerVehicle($jo->getCustomerVehicle());
->setCustomerVehicle($jo->getCustomerVehicle())
->setPriceTier($price_tier);
// set if taxable
// 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\HubFilteringGeoChecker;
use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
@ -96,6 +97,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
protected $cust_distance_limit;
protected $hub_filter_enable;
protected $jo_manager;
protected $pt_manager;
protected $template_hash;
@ -104,7 +106,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
TranslatorInterface $translator, RiderAssignmentHandlerInterface $rah,
string $country_code, WarrantyHandler $wh, RisingTideGateway $rt,
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->ic = $ic;
@ -121,6 +123,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$this->cust_distance_limit = $cust_distance_limit;
$this->hub_filter_enabled = $hub_filter_enabled;
$this->jo_manager = $jo_manager;
$this->pt_manager = $pt_manager;
$this->loadTemplates();
}
@ -585,7 +588,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
{
$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
@ -817,7 +822,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
{
$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
@ -2165,7 +2172,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// NOTE: this is CMB code but for compilation purposes we need to add this
$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

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

@ -1763,6 +1763,8 @@ $(function() {
var table = $("#invoice-table tbody");
var stype = $("#service_type").val();
var cvid = $("#customer-vehicle").val();
var lng = $("#map_lng").val();
var lat = $("#map_lat").val();
console.log(JSON.stringify(invoiceItems));
@ -1774,7 +1776,9 @@ $(function() {
'stype': stype,
'items': invoiceItems,
'promo': promo,
'cvid': cvid
'cvid': cvid,
'coord_lng': lng,
'coord_lat': lat,
}
}).done(function(response) {
// mark as invoice changed

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

@ -159,6 +159,7 @@ menu.database.subtickettypes: 'Sub Ticket Types'
menu.database.emergencytypes: 'Emergency Types'
menu.database.ownershiptypes: 'Ownership Types'
menu.database.serviceofferings: 'Service Offerings'
menu.database.itemtypes: 'Item Types'
# fcm jo status updates
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