diff --git a/.gitignore b/.gitignore index cf3b012e..2e46e65c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ *.swp /public/warranty_uploads/* +.vscode +*__pycache__ +/public/assets/images/insurance-premiums.png \ No newline at end of file diff --git a/config/packages/catalyst_auth.yaml b/config/packages/catalyst_auth.yaml index dfaf31e0..9707fc93 100644 --- a/config/packages/catalyst_auth.yaml +++ b/config/packages/catalyst_auth.yaml @@ -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: diff --git a/config/packages/catalyst_menu.yaml b/config/packages/catalyst_menu.yaml index deb8c43f..7972a414 100644 --- a/config/packages/catalyst_menu.yaml +++ b/config/packages/catalyst_menu.yaml @@ -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 diff --git a/config/routes/apiv2.yaml b/config/routes/apiv2.yaml index 9f183530..dc603c9f 100644 --- a/config/routes/apiv2.yaml +++ b/config/routes/apiv2.yaml @@ -303,3 +303,13 @@ apiv2_insurance_application_create: path: /apiv2/insurance/application controller: App\Controller\CustomerAppAPI\InsuranceController::createApplication methods: [POST] + +apiv2_insurance_premiums_banner: + path: /apiv2/insurance/premiums_banner + controller: App\Controller\CustomerAppAPI\InsuranceController::getPremiumsBanner + methods: [GET] + +apiv2_insurance_body_types: + path: /apiv2/insurance/body_types + controller: App\Controller\CustomerAppAPI\InsuranceController::getBodyTypes + methods: [GET] \ No newline at end of file diff --git a/config/routes/capi_rider.yaml b/config/routes/capi_rider.yaml index 6ef2e92a..798da1da 100644 --- a/config/routes/capi_rider.yaml +++ b/config/routes/capi_rider.yaml @@ -94,3 +94,24 @@ capi_rider_jo_start: path: /rider_api/start controller: App\Controller\CAPI\RiderAppController::startJobOrder methods: [POST] + +# trade-ins +capi_rider_battery_sizes: + path: /rider_api/battery_sizes + controller: App\Controller\CAPI\RiderAppController::getBatterySizes + methods: [GET] + +capi_rider_trade_in_types: + path: /rider_api/trade_in_types + controller: App\Controller\CAPI\RiderAppController::getTradeInTypes + methods: [GET] + +capi_rider_battery_info: + path: /rider_api/battery/{serial} + controller: App\Controller\CAPI\RiderAppController::getBatteryInfo + methods: [GET] + +capi_rider_update_jo: + path: /rider_api/job_order/update + controller: App\Controller\CAPI\RiderAppController::updateJobOrder + methods: [POST] diff --git a/config/routes/item_pricing.yaml b/config/routes/item_pricing.yaml new file mode 100644 index 00000000..a557a1ec --- /dev/null +++ b/config/routes/item_pricing.yaml @@ -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] diff --git a/config/routes/item_type.yaml b/config/routes/item_type.yaml new file mode 100644 index 00000000..adaa8dee --- /dev/null +++ b/config/routes/item_type.yaml @@ -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] diff --git a/config/routes/price_tier.yaml b/config/routes/price_tier.yaml new file mode 100644 index 00000000..397858d9 --- /dev/null +++ b/config/routes/price_tier.yaml @@ -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] diff --git a/config/routes/tapi.yaml b/config/routes/tapi.yaml index afa2f084..5bf1a503 100644 --- a/config/routes/tapi.yaml +++ b/config/routes/tapi.yaml @@ -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: diff --git a/config/services.yaml b/config/services.yaml index b19ecabc..00085811 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -15,6 +15,7 @@ parameters: api_version: "%env(API_VERSION)%" android_app_version: "%env(ANDROID_APP_VERSION)%" ios_app_version: "%env(IOS_APP_VERSION)%" + insurance_premiums_banner_url: "%env(INSURANCE_PREMIUMS_BANNER_URL)%" services: # default configuration for services in *this* file @@ -310,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" diff --git a/src/Command/GetJobOrderArchiveDataCommand.php b/src/Command/GetJobOrderArchiveDataCommand.php new file mode 100644 index 00000000..a70c62dc --- /dev/null +++ b/src/Command/GetJobOrderArchiveDataCommand.php @@ -0,0 +1,941 @@ +em = $em; + $this->project_dir = $kernel->getProjectDir(); + $this->filesystem = $filesystem; + + parent::__construct(); + } + + protected function configure() + { + $this->setName('joborder:archive') + ->setDescription('Get job order data to archive.') + ->setHelp('Get job order data to archive.') + ->addArgument('year', InputArgument::REQUIRED, 'year'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $current_datetime = new DateTime('now'); + + error_log('Archive start time ' . $current_datetime->format('Y-m-d H:i')); + + // get year to archive + $year = $input->getArgument('year'); + + // get the data to archive for the following tables: + // (1) job_order + // (2) invoice (has jo foreign key) + // (3) invoice_item (has invoice foreign key) + // (4) ticket (has jo foreign key) + // (5) jo_rejection (has jo foreign key) + // (6) rider_rating (has jo foreign key) + + // create the archive tables + $this->createJobOrderArchiveTables($year); + $this->createInvoiceArchiveTable($year); + $this->createInvoiceItemArchiveTable($year); + $this->createTicketArchiveTable($year); + $this->createJORejectionArchiveTable($year); + $this->createRiderRatingArchiveTable($year); + $this->createJOEventArchiveTable($year); + + $db = $this->em->getConnection(); + + // set the pdo connection to use unbuffered query so we don't run out of memory + // when processing the job order related tables + $db->getWrappedConnection()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + + $query_sql = 'SELECT * + FROM job_order + WHERE YEAR(date_create) = :year + ORDER BY date_create'; + + $query_stmt = $db->prepare($query_sql); + $query_stmt->bindValue('year', $year, PDO::PARAM_STR); + + $callback = [$this, 'getRelatedArchiveData']; + + $this->getArchiveData($query_stmt, $callback, 'job_order', $year); + + $current_datetime = new DateTime('now'); + + error_log('Archive end time ' . $current_datetime->format('Y-m-d H:i')); + + return 0; + } + + protected function getArchiveData($stmt, $callbackJO, $jo_tname, $year) + { + $results = $stmt->executeQuery(); + + $related_tables = ['jo_event', 'invoice', 'ticket', 'jo_rejection', 'rider_rating']; + + // delete the related data files + foreach ($related_tables as $tname) + { + $this->deleteDataFiles($tname, $year); + } + + // special since this is not directly related to JO but to invoice + $this->deleteDataFiles('invoice_item', $year); + + $archive_files = []; + + $jo_id_list = []; + $ii_id_list = []; + + while ($row = $results->fetchAssociative()) + { + $jo_data['job_order'][$row['id']] = $this->createJobOrderArchiveData($row); + + // add jo id to jo_id_list + $jo_id_list[] = $row['id']; + } + + // write the array into the file + $file = $this->createDataFileRelatedArchiveData($jo_data, $jo_tname, $year, 'w'); + + if ($file != null) + { + $archive_files[$jo_tname] = $file; + } + + // error_log('jo_data total ' . count($jo_data['job_order'])); + // error_log('jo id list total ' . count($jo_id_list)); + + unset($jo_data); + + // load the job order archive file for job order into the database + $this->loadArchiveFiles($archive_files, $year); + + unset($archive_files[$jo_tname]); + + // get all related data for job order + foreach ($jo_id_list as $jo_id) + { + // foreach job order id we got from the first query, we get the JO related + // data for that id from jo_event, invoice, ticket, jo_rejection, rider_rating + if (is_callable($callbackJO)) + { + foreach ($related_tables as $table_name) + { + switch ($table_name) { + case 'jo_event': + case 'invoice': + case 'ticket': + $query_sql = 'SELECT * FROM ' . $table_name . ' WHERE job_order_id = :id'; + break; + case 'jo_rejection': + case 'rider_rating': + $query_sql = 'SELECT * FROM ' . $table_name . ' WHERE jo_id = :id'; + break; + default: + break; + } + + $db = $this->em->getConnection(); + $db->getWrappedConnection()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + + $query_stmt = $db->prepare($query_sql); + $query_stmt->bindValue('id', $jo_id); + + $files = call_user_func($callbackJO, $row, $query_stmt, $table_name, $year); + + foreach ($files as $key =>$file) + { + if ($file != null) + $archive_files[$key] = $file; + } + } + } + } + + $this->loadArchiveFiles($archive_files, $year); + + error_log('Done loading files into database...'); + + $jo_id_array = array_chunk($jo_id_list, 15000); + + unset($jo_id_list); + + foreach($jo_id_array as $key => $jo_ids) + { + // update rider's active_jo_id and current_jo_id to null + // so we can delete the old job orders (foreign key constraint) + $this->updateRiderActiveJobOrders($jo_ids); + $this->updateRiderCurrentJobOrders($jo_ids); + + $this->deleteData($jo_ids, $related_tables); + } + } + + protected function getRelatedArchiveData($row, $query_stmt, $table_name, $year) + { + $results = $query_stmt->executeQuery(); + + $data = []; + $files = []; + $invoice_id_list = []; + + while ($q_row = $results->fetchAssociative()) + { + // check if table name is invoice because we need to get + // all invoice items for a specific invoice too + if ($table_name == 'invoice') + { + // add invoice id to list + $invoice_id_list[] = $q_row['id']; + } + + $fields = []; + + foreach ($q_row as $key => $value) + { + $cleaned_value = $this->cleanData(($value) ?? '\N'); + $fields[] = $cleaned_value; + } + + $data[$table_name][$q_row['id']] = $fields; + } + + // get the invoice items for archiving + $ii_id_list = []; + foreach ($invoice_id_list as $i_id) + { + $ii_file = $this->getInvoiceItemArchiveData($i_id, $year); + + $files['invoice_item'] = $ii_file; + } + + // write the array into the file + $file = $this->createDataFileRelatedArchiveData($data, $table_name, $year, 'a'); + + unset($data); + + if ($file != null) + $files[$table_name] = $file; + + return $files; + } + + protected function getInvoiceItemArchiveData($id, $year) + { + $db = $this->em->getConnection(); + $db->getWrappedConnection()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + + $query_sql = 'SELECT * FROM invoice_item WHERE invoice_id = :id'; + + $query_stmt = $db->prepare($query_sql); + $query_stmt->bindValue('id', $id); + + $results = $query_stmt->executeQuery(); + + $ii_data = []; + $ii_id_list = []; + while ($ii_row = $results->fetchAssociative()) + { + $id = $ii_row['id']; + $invoice_id = $ii_row['invoice_id']; + $title = $ii_row['title']; + $qty = $ii_row['qty']; + $price = $ii_row['price']; + $battery_id = $ii_row['battery_id'] ?? '\N'; + + $ii_data['invoice_item'][$id] = [ + $id, + $invoice_id, + $title, + $qty, + $price, + $battery_id + ]; + + $ii_id_list[] = $id; + } + + $file = $this->createDataFileRelatedArchiveData($ii_data, 'invoice_item', $year, 'a'); + + unset($ii_data); + + // special case, delete the invoice items already + // so that we don't have to query for the invoice items to delete + if (count($ii_id_list) > 0) + $this->deleteInvoiceItems($ii_id_list); + + unset($ii_id_list); + + return $file; + } + + protected function deleteDataFiles($tname, $year) + { + // cache directory + $cache_dir = __DIR__ . '/../../var/cache'; + + $file = $cache_dir . '/' . $tname . '_archive_' . $year .'.tab'; + + if (file_exists($file)) + unlink($file); + } + + protected function createDataFileRelatedArchiveData($archive_data, $table_name, $year, $option) + { + if (isset($archive_data[$table_name])) + { + $adata = $archive_data[$table_name]; + + // cache directory + $cache_dir = __DIR__ . '/../../var/cache'; + + $file = $cache_dir . '/' . $table_name . '_archive_'. $year . '.tab'; + // error_log('opening file for archive - ' . $file); + + $fp = fopen($file, $option); + if ($fp === false) + { + error_log('could not open file for load data infile - ' . $file); + } + else + { + foreach ($adata as $key => $data) + { + $line = implode('|', $data) . "\r\n"; + fwrite($fp, $line); + } + } + + fclose($fp); + + return $file; + } + + return null; + } + + protected function createJobOrderArchiveTables($year) { + // form the archive table name _archive_ + $archive_table_name = 'job_order_archive_' . $year; + + // create the table if it doesn't exist + $db = $this->em->getConnection(); + + // TODO: What if table already exists? + $create_sql = 'CREATE TABLE IF NOT EXISTS `' . $archive_table_name . '` ( + `id` int(11) NOT NULL, + `customer_id` int(11) DEFAULT NULL, + `cvehicle_id` int(11) DEFAULT NULL, + `rider_id` int(11) DEFAULT NULL, + `date_create` datetime NOT NULL, + `date_schedule` datetime NOT NULL, + `date_fulfill` datetime DEFAULT NULL, + `coordinates` point NOT NULL COMMENT \'(DC2Type:point)\', + `flag_advance` tinyint(1) NOT NULL, + `service_type` varchar(25) COLLATE utf8_unicode_ci NOT NULL, + `source` varchar(30) COLLATE utf8_unicode_ci NOT NULL, + `date_cancel` datetime DEFAULT NULL, + `status` varchar(15) COLLATE utf8_unicode_ci NOT NULL, + `delivery_instructions` longtext COLLATE utf8_unicode_ci DEFAULT NULL, + `delivery_address` longtext COLLATE utf8_unicode_ci NOT NULL, + `create_user_id` int(11) DEFAULT NULL, + `assign_user_id` int(11) DEFAULT NULL, + `date_assign` datetime DEFAULT NULL, + `warranty_class` varchar(25) COLLATE utf8_unicode_ci NOT NULL, + `process_user_id` int(11) DEFAULT NULL, + `hub_id` int(11) DEFAULT NULL, + `cancel_reason` varchar(200) COLLATE utf8_unicode_ci DEFAULT NULL, + `ref_jo_id` int(11) DEFAULT NULL, + `tier1_notes` longtext COLLATE utf8_unicode_ci DEFAULT NULL, + `tier2_notes` longtext COLLATE utf8_unicode_ci DEFAULT NULL, + `mode_of_payment` varchar(50) COLLATE utf8_unicode_ci NOT NULL, + `or_name` varchar(80) COLLATE utf8_unicode_ci NOT NULL, + `landmark` longtext COLLATE utf8_unicode_ci NOT NULL, + `promo_detail` varchar(80) COLLATE utf8_unicode_ci NOT NULL, + `or_num` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, + `trade_in_type` varchar(25) COLLATE utf8_unicode_ci DEFAULT NULL, + `flag_rider_rating` tinyint(1) DEFAULT NULL, + `flag_coolant` tinyint(1) NOT NULL, + `facilitated_hub_id` int(11) DEFAULT NULL, + `facilitated_type` varchar(8) COLLATE utf8_unicode_ci DEFAULT NULL, + `coord_long` decimal(11,8) NOT NULL, + `coord_lat` decimal(11,8) NOT NULL, + `priority` int(11) NOT NULL DEFAULT 0, + `meta` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `status_autoassign` varchar(30) COLLATE utf8_unicode_ci DEFAULT NULL, + `first_name` varchar(80) COLLATE utf8_unicode_ci NOT NULL, + `last_name` varchar(80) COLLATE utf8_unicode_ci NOT NULL, + `plate_number` varchar(100) COLLATE utf8_unicode_ci NOT NULL, + `phone_mobile` varchar(30) COLLATE utf8_unicode_ci NOT NULL, + `no_trade_in_reason` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, + `will_wait` varchar(30) COLLATE utf8_unicode_ci NOT NULL, + `reason_not_waiting` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, + `not_waiting_notes` longtext COLLATE utf8_unicode_ci DEFAULT NULL, + `delivery_status` varchar(30) COLLATE utf8_unicode_ci DEFAULT NULL, + `emergency_type_id` int(11) DEFAULT NULL, + `ownership_type_id` int(11) DEFAULT NULL, + `cust_location_id` int(11) DEFAULT NULL, + `source_of_awareness` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, + `remarks` longtext COLLATE utf8_unicode_ci DEFAULT NULL, + `initial_concern` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, + `initial_concern_notes` longtext COLLATE utf8_unicode_ci DEFAULT NULL, + `gender` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, + `caller_classification` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, + `inventory_count` smallint(6) NOT NULL, PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci'; + + $create_stmt = $db->prepare($create_sql); + + $result = $create_stmt->execute(); + + return $archive_table_name; + } + + protected function createInvoiceArchiveTable($year) + { + // form the archive table name _archive_ + $archive_table_name = 'invoice_archive_' . $year; + + // create the table if it doesn't exist + $db = $this->em->getConnection(); + + // TODO: What if table already exists? + $create_sql = 'CREATE TABLE IF NOT EXISTS `' . $archive_table_name . '` ( + `id` int(11) NOT NULL, + `user_id` int(11) DEFAULT NULL, + `job_order_id` int(11) DEFAULT NULL, + `date_create` datetime NOT NULL, + `date_paid` datetime DEFAULT NULL, + `date_cancel` datetime DEFAULT NULL, + `discount` decimal(9,2) NOT NULL, + `trade_in` decimal(9,2) NOT NULL, + `vat` decimal(9,2) NOT NULL, + `vat_exclusive_price` decimal(9,2) NOT NULL, + `total_price` decimal(9,2) NOT NULL, + `status` varchar(40) COLLATE utf8_unicode_ci NOT NULL, + `promo_id` int(11) DEFAULT NULL, + `used_customer_tag_id` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci'; + + $create_stmt = $db->prepare($create_sql); + + $result = $create_stmt->execute(); + } + + protected function createInvoiceItemArchiveTable($year) + { + // form the archive table name _archive_ + $archive_table_name = 'invoice_item_archive_' . $year; + + // create the table if it doesn't exist + $db = $this->em->getConnection(); + + // TODO: What if table already exists? + $create_sql = 'CREATE TABLE IF NOT EXISTS `' . $archive_table_name . '` ( + `id` int(11) NOT NULL, + `invoice_id` int(11) DEFAULT NULL, + `title` varchar(80) COLLATE utf8_unicode_ci NOT NULL, + `qty` smallint(6) NOT NULL, + `price` decimal(9,2) NOT NULL, + `battery_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci'; + + $create_stmt = $db->prepare($create_sql); + + $result = $create_stmt->execute(); + } + + protected function createTicketArchiveTable($year) + { + // form the archive table name _archive_ + $archive_table_name = 'ticket_archive_' . $year; + + // create the table if it doesn't exist + $db = $this->em->getConnection(); + + // TODO: What if table already exists? + $create_sql = 'CREATE TABLE IF NOT EXISTS `' . $archive_table_name . '` ( + `id` int(11) NOT NULL, + `user_id` int(11) DEFAULT NULL, + `customer_id` int(11) DEFAULT NULL, + `date_create` datetime NOT NULL, + `status` varchar(15) COLLATE utf8_unicode_ci NOT NULL, + `ticket_type` varchar(15) COLLATE utf8_unicode_ci DEFAULT NULL, + `other_ticket_type` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, + `first_name` varchar(80) COLLATE utf8_unicode_ci NOT NULL, + `last_name` varchar(80) COLLATE utf8_unicode_ci NOT NULL, + `contact_num` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL, + `details` longtext COLLATE utf8_unicode_ci DEFAULT NULL, + `job_order_id` int(11) DEFAULT NULL, + `plate_number` varchar(10) COLLATE utf8_unicode_ci DEFAULT NULL, + `ticket_type_id` int(11) DEFAULT NULL, + `subticket_type_id` int(11) DEFAULT NULL, + `source_of_awareness` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, + `remarks` longtext COLLATE utf8_unicode_ci DEFAULT NULL, + `other_description` longtext COLLATE utf8_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci'; + + $create_stmt = $db->prepare($create_sql); + + $result = $create_stmt->execute(); + } + + protected function createJORejectionArchiveTable($year) + { + // form the archive table name _archive_ + $archive_table_name = 'jo_rejection_archive_' . $year; + + // create the table if it doesn't exist + $db = $this->em->getConnection(); + + // TODO: What if table already exists? + $create_sql = 'CREATE TABLE IF NOT EXISTS `' . $archive_table_name . '` ( + `id` int(11) NOT NULL, + `user_id` int(11) DEFAULT NULL, + `hub_id` int(11) DEFAULT NULL, + `jo_id` int(11) DEFAULT NULL, + `date_create` datetime NOT NULL, + `reason` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `remarks` longtext COLLATE utf8_unicode_ci DEFAULT NULL, + `contact_person` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci'; + + $create_stmt = $db->prepare($create_sql); + + $result = $create_stmt->execute(); + } + + protected function createRiderRatingArchiveTable($year) + { + // form the archive table name _archive_ + $archive_table_name = 'rider_rating_archive_' . $year; + + // create the table if it doesn't exist + $db = $this->em->getConnection(); + + // TODO: What if table already exists? + $create_sql = 'CREATE TABLE IF NOT EXISTS `' . $archive_table_name . '` ( + `id` int(11) NOT NULL, + `rider_id` int(11) DEFAULT NULL, + `customer_id` int(11) DEFAULT NULL, + `jo_id` int(11) DEFAULT NULL, + `date_create` datetime NOT NULL, + `rating` int(11) NOT NULL, + `comment` longtext COLLATE utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci'; + + $create_stmt = $db->prepare($create_sql); + + $result = $create_stmt->execute(); + } + + protected function createJOEventArchiveTable($year) + { + // form the archive table name _archive_ + $archive_table_name = 'jo_event_archive_' . $year; + + // create the table if it doesn't exist + $db = $this->em->getConnection(); + + // TODO: What if table already exists? + $create_sql = 'CREATE TABLE IF NOT EXISTS `' . $archive_table_name . '` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `create_user_id` int(11) DEFAULT NULL, + `job_order_id` int(11) DEFAULT NULL, + `date_create` datetime NOT NULL, + `date_happen` datetime NOT NULL, + `type_id` varchar(30) COLLATE utf8_unicode_ci NOT NULL, + `rider_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci'; + + $create_stmt = $db->prepare($create_sql); + + $result = $create_stmt->execute(); + } + + + protected function createJobOrderArchiveData($row) + { + // TODO: this could be shrunk further + + // get job order data + // check for nulls. check the ff fields since these can be null: date_fulfill, date_cancel, date_assign, create_user_id, + // assign_user_id, process_user_id, hub_id, rider_id, cancel_reason, ref_jo_id, or_num, trade_in_type, + // flag_rider_rating, facilitated_type, facilitated_hub_id, status_autoassign, reason_not_waiting, + // not_waiting_notes, no_trade_in_reason, delivery_status, source_of_awareness, remarks, initial_concern, + // initial_concern_notes, gender, caller_classifications, emergency_type_id, ownership_type_id, cust_location_id + + $id = $row['id']; + $cust_id = $row['customer_id']; + $cv_id = $row['cvehicle_id']; + + $rider_id = $row['rider_id'] ?? '\N'; + + $date_create = $row['date_create']; + $date_schedule = $row['date_schedule']; + + $date_fulfill = $row['date_fulfill'] ?? '\N'; + + $flag_advance = $row['flag_advance']; + $service_type = $row['service_type']; + $source = $row['source']; + + $date_cancel = $row['date_cancel'] ?? '\N'; + + $status = $row['status']; + + $del_instructions = $this->cleanData($row['delivery_instructions']); + $del_address = $this->cleanData($row['delivery_address']); + + $create_user_id = $row['create_user_id'] ?? '\N'; + $assign_user_id = $row['assign_user_id'] ?? '\N'; + $date_assign = $row['date_assign'] ?? '\N'; + + $warr_class = $row['warranty_class']; + + $process_user_id = $row['process_user_id'] ?? '\N'; + $hub_id = $row['hub_id'] ?? '\N'; + $cancel_reason = $row['cancel_reason'] ?? '\N'; + $ref_jo_id = $row['ref_jo_id'] ?? '\N'; + + $tier1_notes = $this->cleanData($row['tier1_notes']); + $tier2_notes = $this->cleanData($row['tier2_notes']); + + $mode_of_payment = $row['mode_of_payment']; + $or_name = $row['or_name']; + + $landmark = $this->cleanData($row['landmark']); + $promo_details = $row['promo_detail']; + + $or_num = $row['or_num'] ?? '\N'; + $trade_in_type = $row['trade_in_type'] ?? '\N'; + $flag_rider_rating = $row['flag_rider_rating'] ?? '\N'; + + $flag_coolant = $row['flag_coolant']; + + $fac_hub_id = $row['facilitated_hub_id'] ?? '\N'; + $fac_type = $row['facilitated_type'] ?? '\N'; + + $coord_long = $row['coord_long']; + $coord_lat = $row['coord_lat']; + + // coordinates needs special handling since it's a spatial column + $geo_coordinates = 'POINT(' . $coord_lat . ' ' . $coord_long .')'; + + $priority = $row['priority']; + $meta = $row['meta']; + + $status_autoassign = $row['status_autoassign'] ?? '\N'; + + $first_name = $row['first_name']; + $last_name = $row['last_name']; + $plate_number = $row['plate_number']; + $phone_mobile = $row['phone_mobile']; + + $no_trade_in_reason = $row['no_trade_in_reason'] ?? '\N'; + + $will_wait = $row['will_wait']; + + $reason_not_waiting = $row['reason_not_waiting'] ?? '\N'; + $not_waiting_notes = $this->cleanData($row['not_waiting_notes']) ?? '\N'; + $del_status = $row['delivery_status'] ?? '\N'; + $emergency_type_id = $row['emergency_type_id'] ?? '\N'; + $owner_type_id = $row['ownership_type_id'] ?? '\N'; + $cust_location_id = $row['cust_location_id'] ?? '\N'; + $source_of_awareness = $row['source_of_awareness'] ?? '\N'; + $remarks = $this->cleanData($row['remarks']) ?? '\N'; + $initial_concern = $row['initial_concern'] ?? '\N'; + $initial_concern_notes = $this->cleanData($row['initial_concern_notes']) ?? '\N'; + $gender = $row['gender'] ?? '\N'; + $caller_class = $row['caller_classification'] ?? '\N'; + + $inv_count = $row['inventory_count']; + + // create the array for the file + $data = [ + $id, + $cust_id, + $cv_id, + $rider_id, + $date_create, + $date_schedule, + $date_fulfill, + $geo_coordinates, + $flag_advance, + $service_type, + $source, + $date_cancel, + $status, + $del_instructions, + $del_address, + $create_user_id, + $assign_user_id, + $date_assign, + $warr_class, + $process_user_id, + $hub_id, + $cancel_reason, + $ref_jo_id, + $tier1_notes, + $tier2_notes, + $mode_of_payment, + $or_name, + $landmark, + $promo_details, + $or_num, + $trade_in_type, + $flag_rider_rating, + $flag_coolant, + $fac_hub_id, + $fac_type, + $coord_long, + $coord_lat, + $priority, + $meta, + $status_autoassign, + $first_name, + $last_name, + $plate_number, + $phone_mobile, + $no_trade_in_reason, + $will_wait, + $reason_not_waiting, + $not_waiting_notes, + $del_status, + $emergency_type_id, + $owner_type_id, + $cust_location_id, + $source_of_awareness, + $remarks, + $initial_concern, + $initial_concern_notes, + $gender, + $caller_class, + $inv_count + ]; + + return $data; + } + + protected function loadArchiveFiles($archive_files, $year) + { + foreach ($archive_files as $tname => $file) + { + $archive_tname = $tname . '_archive_' . $year; + + if ($tname == 'job_order') + { + // load statement for job order + $load_stmt = 'LOAD DATA LOCAL INFILE \'' . $file . '\' INTO TABLE ' . $archive_tname . ' + CHARACTER SET UTF8 + FIELDS TERMINATED BY \'|\' + ESCAPED BY \'\b\' + LINES TERMINATED BY \'\\r\\n\' + (id, customer_id, cvehicle_id, rider_id, date_create, + date_schedule, date_fulfill, @coordinates, flag_advance, service_type, + source, date_cancel, status, delivery_instructions, delivery_address, + create_user_id, assign_user_id, date_assign, warranty_class, process_user_id, + hub_id, cancel_reason, ref_jo_id, tier1_notes, tier2_notes, + mode_of_payment, or_name, landmark, promo_detail, or_num, + trade_in_type, flag_rider_rating, flag_coolant, facilitated_hub_id, facilitated_type, + coord_long, coord_lat, priority, meta, status_autoassign, + first_name, last_name, plate_number, phone_mobile, no_trade_in_reason, + will_wait, reason_not_waiting, not_waiting_notes, delivery_status, emergency_type_id, + ownership_type_id, cust_location_id, source_of_awareness, remarks, initial_concern, + initial_concern_notes, gender, caller_classification, inventory_count) + SET coordinates=ST_GeomFromText(@geo_coordinates)'; + } + if ($tname == 'jo_event') + { + $load_stmt = 'LOAD DATA LOCAL INFILE \'' . $file . '\' INTO TABLE ' . $archive_tname . ' + CHARACTER SET UTF8 + FIELDS TERMINATED BY \'|\' + ESCAPED BY \'\b\' + LINES TERMINATED BY \'\\r\\n\' + (id, create_user_id, job_order_id, date_create, date_happen, type_id, rider_id)'; + } + if ($tname == 'jo_rejection') + { + $load_stmt = 'LOAD DATA LOCAL INFILE \'' . $file . '\' INTO TABLE ' . $archive_tname . ' + CHARACTER SET UTF8 + FIELDS TERMINATED BY \'|\' + ESCAPED BY \'\b\' + LINES TERMINATED BY \'\\r\\n\' + (id, user_id, hub_id, jo_id, date_create, reason, remarks, contact_person)'; + } + if ($tname == 'invoice') + { + $load_stmt = 'LOAD DATA LOCAL INFILE \'' . $file . '\' INTO TABLE ' . $archive_tname . ' + CHARACTER SET UTF8 + FIELDS TERMINATED BY \'|\' + ESCAPED BY \'\b\' + LINES TERMINATED BY \'\\r\\n\' + (id, user_id, job_order_id, date_create, date_paid, discount, trade_in, vat, vat_exclusive_price, + total_price, status, promo_id, used_customer_tag_id)'; + } + if ($tname == 'rider_rating') + { + $load_stmt = 'LOAD DATA LOCAL INFILE \'' . $file . '\' INTO TABLE ' . $archive_tname . ' + CHARACTER SET UTF8 + FIELDS TERMINATED BY \'|\' + ESCAPED BY \'\b\' + LINES TERMINATED BY \'\\r\\n\' + (id, rider_id, customer_id, jo_id, date_create, rating, comment)'; + } + if ($tname == 'ticket') + { + $load_stmt = 'LOAD DATA LOCAL INFILE \'' . $file . '\' INTO TABLE ' . $archive_tname . ' + CHARACTER SET UTF8 + FIELDS TERMINATED BY \'|\' + ESCAPED BY \'\b\' + LINES TERMINATED BY \'\\r\\n\' + (id, user_id, customer_id, date_create, status, ticket_type, other_ticket_type, first_name, last_name, + contact_num, details, job_order_id, plate_number, ticket_type_id, subticket_type_id, + source_of_awareness, remarks, other_description)'; + } + if ($tname == 'invoice_item') + { + $load_stmt = 'LOAD DATA LOCAL INFILE \'' . $file . '\' INTO TABLE ' . $archive_tname . ' + CHARACTER SET UTF8 + FIELDS TERMINATED BY \'|\' + ESCAPED BY \'\b\' + LINES TERMINATED BY \'\\r\\n\' + (id, invoice_id, title, qty, price, battery_id)'; + } + + // call load data infile + $this->loadDataFileForArchiveData($load_stmt); + } + } + + protected function deleteInvoiceItems($ii_id_list) + { + $db = $this->em->getConnection(); + + // delete the invoice items first + $ii_ids = str_repeat('?,', count($ii_id_list) - 1) . '?'; + + $ii_del_sql = 'DELETE FROM invoice_item WHERE id IN (' . $ii_ids . ')'; + $ii_stmt = $db->prepare($ii_del_sql); + + $ii_stmt->execute($ii_id_list); + } + + protected function deleteData($jo_id_list, $related_tables) + { + $db = $this->em->getConnection(); + + // delete from invoice, jo_rejection, rider_rating, ticket, and jo_event + $jo_ids = str_repeat('?,', count($jo_id_list) - 1) . '?'; + + foreach ($related_tables as $table_name) + { + switch ($table_name) { + case 'jo_event': + case 'invoice': + case 'ticket': + $related_del_sql = 'DELETE FROM ' . $table_name . ' WHERE job_order_id IN ('. $jo_ids . ')'; + break; + case 'jo_rejection': + case 'rider_rating': + $related_del_sql = 'DELETE FROM ' . $table_name . ' WHERE jo_id IN (' . $jo_ids. ')'; + break; + default: + break; + } + + $related_stmt = $db->prepare($related_del_sql); + + $related_stmt->execute($jo_id_list); + } + + // delete from job order last + $jo_del_sql = 'DELETE FROM job_order WHERE id IN (' . $jo_ids . ')'; + $jo_stmt = $db->prepare($jo_del_sql); + + $jo_stmt->execute($jo_id_list); + } + + protected function updateRiderActiveJobOrders($jo_id_list) + { + $db = $this->em->getConnection(); + $db->getWrappedConnection()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + + $jo_ids = str_repeat('?,', count($jo_id_list) - 1) . '?'; + + $update_active_sql = 'UPDATE rider SET active_jo_id = NULL WHERE active_jo_id IN (' . $jo_ids . ')'; + $update_active_stmt = $db->prepare($update_active_sql); + + // error_log('Updating active rider job orders...'); + + $update_active_stmt->execute($jo_id_list); + + unset($update_active_stmt); + } + + protected function updateRiderCurrentJobOrders($jo_id_list) + { + $db = $this->em->getConnection(); + $db->getWrappedConnection()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + + $jo_ids = str_repeat('?,', count($jo_id_list) - 1) . '?'; + + $update_curr_sql = 'UPDATE rider SET current_jo_id = NULL WHERE current_jo_id IN (' . $jo_ids . ')'; + $update_curr_stmt = $db->prepare($update_curr_sql); + + // error_log('Updating current rider job orders...'); + + $update_curr_stmt->execute($jo_id_list); + + unset($update_curr_stmt); + } + + protected function loadDataFileForArchiveData($load_stmt) + { + $conn = $this->em->getConnection(); + $conn->getWrappedConnection()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + + $stmt = $conn->prepare($load_stmt); + + $result = $stmt->execute(); + + if (!$result) + error_log('Failed loading data.'); + + // TODO: delete file? + } + + protected function cleanData($text) + { + $clean_text = ''; + + // replace the new lines with whitespace + $clean_text = preg_replace("/[\n\r]/", ' ', $text); + + return $clean_text; + + } + +} diff --git a/src/Command/SetJobOrderReferenceJOIdCommand.php b/src/Command/SetJobOrderReferenceJOIdCommand.php new file mode 100644 index 00000000..20ee236a --- /dev/null +++ b/src/Command/SetJobOrderReferenceJOIdCommand.php @@ -0,0 +1,53 @@ +em = $em; + + parent::__construct(); + } + + protected function configure() + { + $this->setName('joborder:setreferencejoid') + ->setDescription('Set job order reference jo id for existing job orders.') + ->setHelp('Set job order reference jo id for existing job orders. Decoupling job order from reference job order.'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + // get the job orders where ref_jo is not null + $query = $this->em->createQuery('SELECT jo FROM App\Entity\JobOrder jo WHERE jo.ref_jo IS NOT NULL'); + + $jos = $query->getResult(); + + foreach ($jos as $jo) + { + $ref_jo_id = $jo->getReferenceJO()->getID(); + + error_log('Setting reference jo id ' . $ref_jo_id . ' for job order ' . $jo->getID()); + + $jo->setReferenceJOId($ref_jo_id); + + // set the ref_jo to null to decouple ref jo and jo + $jo->setReferenceJO(null); + } + + $this->em->flush(); + + return 0; + } +} diff --git a/src/Controller/APIController.php b/src/Controller/APIController.php index 16a9cdf5..0eb148c5 100644 --- a/src/Controller/APIController.php +++ b/src/Controller/APIController.php @@ -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)) diff --git a/src/Controller/CAPI/RiderAppController.php b/src/Controller/CAPI/RiderAppController.php index d29f6a04..694ffdd0 100644 --- a/src/Controller/CAPI/RiderAppController.php +++ b/src/Controller/CAPI/RiderAppController.php @@ -23,7 +23,9 @@ use App\Entity\BatterySize; use App\Entity\RiderAPISession; use App\Entity\User; use App\Entity\ApiUser as APIUser; - +use App\Entity\JobOrder; +use App\Entity\SAPBattery; +use App\Entity\WarrantySerial; use App\Service\RedisClientProvider; use App\Service\RiderCache; use App\Service\MQTTClient; @@ -34,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; @@ -286,8 +289,9 @@ class RiderAppController extends ApiController // do we have a job order? // $jo = $rider->getActiveJobOrder(); + // NOTE: we do not include job orders that have been cancelled $jo = $rider->getCurrentJobOrder(); - if ($jo == null) + if ($jo == null || $jo->getStatus() == JOStatus::CANCELLED) { $data = [ 'job_order' => null @@ -408,6 +412,9 @@ class RiderAppController extends ApiController if (!empty($msg)) return new APIResponse(false, $msg); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // TODO: refactor this into a jo handler class, so we don't have to repeat for control center // set jo status to in transit @@ -458,6 +465,9 @@ class RiderAppController extends ApiController // TODO: this is a workaround for requeue, because rider app gets stuck in accept / decline screen return new APIResponse(true, $msg); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // requeue it, instead of cancelling it $jo->requeue(); @@ -516,6 +526,9 @@ class RiderAppController extends ApiController // get rider's current job order $jo = $rider->getCurrentJobOrder(); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // set delivery status $jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB); @@ -556,6 +569,9 @@ class RiderAppController extends ApiController // get rider's current job order $jo = $rider->getCurrentJobOrder(); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // set delivery status $jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB_PRE_JO); @@ -596,6 +612,9 @@ class RiderAppController extends ApiController // get rider's current job order $jo = $rider->getCurrentJobOrder(); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // set delivery status $jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB_PRE_JO); @@ -636,6 +655,9 @@ class RiderAppController extends ApiController // get rider's current job order $jo = $rider->getCurrentJobOrder(); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // set delivery status $jo->setDeliveryStatus(DeliveryStatus::RIDER_START); @@ -677,6 +699,9 @@ class RiderAppController extends ApiController // set jo status to in progress $jo->setStatus(JOStatus::IN_PROGRESS); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // set delivery status $jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE); @@ -735,6 +760,9 @@ class RiderAppController extends ApiController // get rider's current job order $jo = $rider->getCurrentJobOrder(); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // set delivery status $jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB); @@ -758,6 +786,54 @@ class RiderAppController extends ApiController return new APIResponse(true, 'Rider arrive at hub.', $data); } + public function getBatterySizes(Request $req, EntityManagerInterface $em) + { + // get capi user + $capi_user = $this->getUser(); + if ($capi_user == null) + return new APIResponse(false, 'User not found.'); + + // get rider id from capi user metadata + $rider = $this->getRiderFromCAPI($capi_user, $em); + if ($rider == null) + return new APIResponse(false, 'No rider found.'); + + // get sizes + $qb = $em->getRepository(BatterySize::class) + ->createQueryBuilder('bs'); + + $sizes = $qb->select('bs.id, bs.name') + ->orderBy('bs.name', 'asc') + ->getQuery() + ->getResult(); + + // response + return new APIResponse(true, '', [ + 'sizes' => $sizes, + ]); + } + + public function getTradeInTypes(Request $req, EntityManagerInterface $em) + { + // get capi user + $capi_user = $this->getUser(); + if ($capi_user == null) + return new APIResponse(false, 'User not found.'); + + // get rider id from capi user metadata + $rider = $this->getRiderFromCAPI($capi_user, $em); + if ($rider == null) + return new APIResponse(false, 'No rider found.'); + + // get trade-in types + $types = TradeInType::getCollection(); + + // response + return new APIResponse(true, '', [ + 'types' => $types, + ]); + } + public function payment(Request $req, EntityManagerInterface $em, JobOrderHandlerInterface $jo_handler, RisingTideGateway $rt, WarrantyHandler $wh, MQTTClient $mclient, MQTTClientApiv2 $mclientv2, FCMSender $fcmclient, TranslatorInterface $translator) { @@ -777,6 +853,20 @@ class RiderAppController extends ApiController if (!empty($msg)) return new APIResponse(false, $msg); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + + // need to check if service type is battery sales + // if so, serial is a required parameter + $serial = $req->request->get('serial', ''); + if ($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) + { + /* + if (empty($serial)) + return new APIResponse(false, 'Missing parameter(s): serial'); + */ + } + // set invoice to paid $jo->getInvoice()->setStatus(InvoiceStatus::PAID); @@ -828,7 +918,6 @@ class RiderAppController extends ApiController // create warranty if($jo_handler->checkIfNewBattery($jo)) { - $serial = null; $warranty_class = $jo->getWarrantyClass(); $first_name = $jo->getCustomer()->getFirstName(); $last_name = $jo->getCustomer()->getLastName(); @@ -912,6 +1001,9 @@ class RiderAppController extends ApiController // get rider's current job order $jo = $rider->getCurrentJobOrder(); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // set delivery status $jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB_POST_JO); @@ -953,6 +1045,9 @@ class RiderAppController extends ApiController // get rider's current job order $jo = $rider->getCurrentJobOrder(); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // set delivery status $jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB_POST_JO); @@ -1099,7 +1194,167 @@ class RiderAppController extends ApiController return new APIResponse(true, 'Batteries found.', $data); } - public function changeService(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic) + public function getBatteryInfo(Request $req, $serial, EntityManagerInterface $em) + { + if (empty($serial)) + { + return new APIResponse(false, 'Missing parameter(s): serial'); + } + + // get capi user + $capi_user = $this->getUser(); + if ($capi_user == null) + return new APIResponse(false, 'User not found.'); + + // get rider id from capi user metadata + $rider = $this->getRiderFromCAPI($capi_user, $em); + if ($rider == null) + return new APIResponse(false, 'No rider found.'); + + // find battery given serial/sap_code and flag_active is true + $serial = $em->getRepository(WarrantySerial::class)->find($serial); + + if (empty($serial)) { + return new APIResponse(false, 'Warranty serial number not found.'); + } + + $sap_battery = $em->getRepository(SAPBattery::class)->find($serial->getSKU()); + + if (empty($sap_battery)) { + return new APIResponse(false, 'No battery info found.'); + } + + $battery = [ + 'id' => $sap_battery->getID(), + 'brand' => $sap_battery->getBrand()->getName(), + 'size' => $sap_battery->getSize()->getName(), + 'size_id' => $sap_battery->getSize()->getID(), + 'trade_in_type' => TradeInType::MOTOLITE, + 'container_size' => $sap_battery->getContainerSize()->getName(), + ]; + + return new APIResponse(true, 'Battery info found.', [ + 'battery' => $battery, + ]); + } + + public function updateJobOrder(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager) + { + $items = json_decode(file_get_contents('php://input'), true); + + // get job order id + if (!isset($items['jo_id'])) + return new APIResponse(false, 'Missing parameter(s): jo_id'); + + // validate jo_id + $jo_id = $items['jo_id']; + if (empty($jo_id) || $jo_id == null) + return new APIResponse(false, 'Missing parameter(s): jo_id'); + + // get capi user + $capi_user = $this->getUser(); + if ($capi_user == null) + return new APIResponse(false, 'User not found.'); + + // get rider id from capi user metadata + $rider = $this->getRiderFromCAPI($capi_user, $em); + if ($rider == null) + return new APIResponse(false, 'No rider found.'); + + // get the job order + $jo = $em->getRepository(JobOrder::class)->find($jo_id); + + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + + // check if we have trade in items + $ti_items = []; + if (isset($items['trade_in_items'])) + { + // validate the trade in items first + $ti_items = $items['trade_in_items']; + $msg = $this->validateTradeInItems($em, $ti_items); + if (!empty($msg)) + return new APIResponse(false, $msg); + } + + // get the service type + if (!isset($items['stype_id'])) + return new APIResponse(false, 'Missing parameter(s): stype_id'); + + // validate service type + $stype_id = $items['stype_id']; + if (!ServiceType::validate($stype_id)) + return new APIResponse(false, 'Invalid service type - ' . $stype_id); + + // save service type + $jo->setServiceType($stype_id); + + // validate promo if any. Promo not required + $promo = null; + if (isset($items['promo_id'])) + { + $promo_id = $items['promo_id']; + $promo = $em->getRepository(Promo::class)->find($promo_id); + if ($promo == null) + return new APIResponse(false, 'Invalid promo id - ' . $promo_id); + } + + // get other parameters, if any: has motolite battery, has warranty doc, with coolant, payment method + if (isset($items['flag_motolite_battery'])) + { + // get customer vehicle from jo + $cv = $jo->getCustomerVehicle(); + $has_motolite = $items['flag_motolite_battery']; + if ($has_motolite == 'true') + $cv->setHasMotoliteBattery(true); + else + $cv->setHasMotoliteBattery(false); + + $em->persist($cv); + + } + if (isset($items['flag_warranty_doc'])) + { + // TODO: what do we do? + } + if (isset($items['flag_coolant'])) + { + $has_coolant = $items['flag_coolant']; + if ($has_coolant == 'true') + $jo->setHasCoolant(true); + else + $jo->setHasCoolant(false); + + } + if (isset($items['mode_of_payment'])) + { + $payment_method = $items['payment_method']; + if (!ModeOfPayment::validate($payment_method)) + $payment_method = ModeOfPayment::CASH; + $jo->setModeOfPayment($payment_method); + } + + // get capi user + $capi_user = $this->getUser(); + if ($capi_user == null) + return new APIResponse(false, 'User not found.'); + + // get rider id from capi user metadata + $rider = $this->getRiderFromCAPI($capi_user, $em); + if ($rider == null) + return new APIResponse(false, 'No rider found.'); + + // need to get the existing invoice items using jo id and invoice id + $existing_ii = $this->getInvoiceItems($em, $jo); + + $this->generateUpdatedInvoice($em, $ic, $jo, $existing_ii, $ti_items, $promo, $pt_manager); + + $data = []; + return new APIResponse(true, 'Job order updated.', $data); + } + + public function changeService(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager) { // $this->debugRequest($req); @@ -1120,6 +1375,9 @@ class RiderAppController extends ApiController if (!empty($msg)) return new APIResponse(false, $msg); + // check if JO can be modified first + $this->checkJOProgressionAllowed($jo, $rider); + // check service type $stype_id = $req->request->get('stype_id'); if (!ServiceType::validate($stype_id)) @@ -1203,6 +1461,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); @@ -1241,6 +1503,164 @@ class RiderAppController extends ApiController return new APIResponse(true, 'Job order service changed.', $data); } + protected function generateUpdatedInvoice(EntityManagerInterface $em, InvoiceGeneratorInterface $ic, JobOrder $jo, $existing_ii, $trade_in_items, $promo, PriceTierManager $pt_manager) + { + // get the service type + $stype = $jo->getServiceType(); + + // get the source + $source = $jo->getSource(); + + // get the customer vehicle + $cv = $jo->getCustomerVehicle(); + + // get coolant if any + $flag_coolant = $jo->hasCoolant(); + + // check if new promo is null + if ($promo == null) + { + // promo not updated from app so check existing invoice + // get the promo id from existing invoice item + $promo_id = $existing_ii['promo_id']; + if ($promo_id == null) + $promo = null; + else + $promo = $em->getRepository(Promo::class)->find($promo_id); + } + + // populate Invoice Criteria + $icrit = new InvoiceCriteria(); + $icrit->setServiceType($stype) + ->setCustomerVehicle($cv) + ->setSource($source) + ->setHasCoolant($flag_coolant) + ->setIsTaxable(); + + // set price tier + $pt_id = $pt_manager->getPriceTier($jo->getCoordinates()); + $icrit->setPriceTier($pt_id); + + // at this point, all information should be valid + // assuming JO information is already valid since this + // is in the system already + // add promo if any to criteria + if ($promo != null) + $icrit->addPromo($promo); + + // get the battery purchased from existing invoice items + // add the batteries ordered to criteria + $ii_items = $existing_ii['invoice_items']; + foreach ($ii_items as $ii_item) + { + $batt_id = $ii_item['batt_id']; + $qty = $ii_item['qty']; + + $battery = $em->getRepository(Battery::class)->find($batt_id); + + $icrit->addEntry($battery, null, $qty); + } + + // add the trade in items to the criteria + foreach ($trade_in_items as $ti_item) + { + $batt_size_id = $ti_item['battery_size_id']; + $qty = $ti_item['qty']; + $trade_in_type = $ti_item['trade_in_type']; + + $batt_size = $em->getRepository(BatterySize::class)->find($batt_size_id); + + $icrit->addTradeInEntry($batt_size, $trade_in_type, $qty); + } + + // call generateInvoice + $invoice = $ic->generateInvoice($icrit); + + // remove previous invoice + $old_invoice = $jo->getInvoice(); + $em->remove($old_invoice); + $em->flush(); + + // save new invoice + $jo->setInvoice($invoice); + $em->persist($invoice); + + // log event? + $event = new JOEvent(); + $event->setDateHappen(new DateTime()) + ->setTypeID(JOEventType::RIDER_EDIT) + ->setJobOrder($jo) + ->setRider($jo->getRider()); + $em->persist($event); + + $em->flush(); + } + + protected function getInvoiceItems(EntityManagerInterface $em, JobOrder $jo) + { + $jo_id = $jo->getID(); + $conn = $em->getConnection(); + + // need to get the ordered battery id and quantity from invoice item + // and the promo from invoice + $query_sql = 'SELECT ii.battery_id AS battery_id, ii.qty AS qty, i.promo_id AS promo_id + FROM invoice_item ii, invoice i + WHERE ii.invoice_id = i.id + AND i.job_order_id = :jo_id + AND ii.battery_id IS NOT NULL'; + + $query_stmt = $conn->prepare($query_sql); + $query_stmt->bindValue('jo_id', $jo_id); + + $results = $query_stmt->executeQuery(); + + $promo_id = null; + $invoice_items = []; + while ($row = $results->fetchAssociative()) + { + $promo_id = $row['promo_id']; + $invoice_items[] = [ + 'batt_id' => $row['battery_id'], + 'qty' => $row['qty'], + 'trade_in' => '' + ]; + } + + $data = [ + 'promo_id' => $promo_id, + 'invoice_items' => $invoice_items + ]; + + return $data; + } + + protected function validateTradeInItems(EntityManagerInterface $em, $ti_items) + { + $msg = ''; + foreach ($ti_items as $ti_item) + { + $bs_id = $ti_item['battery_size_id']; + $ti_type = $ti_item['trade_in_type']; + + // validate the battery size id + $batt_size = $em->getRepository(BatterySize::class)->find($bs_id); + if ($batt_size == null) + { + $msg = 'Invalid battery size for trade in: ' . $bs_id; + return $msg; + } + + // validate the trade in type + if (!TradeInType::validate($ti_type)) + { + $msg = 'Invalid trade in type: ' . $ti_type; + return $msg; + } + } + + return $msg; + } + protected function getCAPIUser($id, EntityManagerInterface $em) { $capi_user = $em->getRepository(APIUser::class)->find($id); @@ -1320,6 +1740,24 @@ class RiderAppController extends ApiController return $msg; } + protected function checkJOProgressionAllowed(JobOrder $jo, $rider) + { + // TODO: add more statuses to block if needed, hence. this is a failsafe in case MQTT is not working. + switch ($jo->getStatus()) + { + case JOStatus::CANCELLED: + // if this is the rider's current JO, set to null + if ($rider->getCurrentJobOrder() === $jo) { + $rider->setCurrentJobOrder(); + } + + return new APIResponse(false, 'Job order can no longer be modified.'); + break; + default: + return true; + } + } + protected function debugRequest(Request $req) { $all = $req->request->all(); diff --git a/src/Controller/CustomerAppAPI/ApiController.php b/src/Controller/CustomerAppAPI/ApiController.php index 6a25eb80..fdc0ec59 100644 --- a/src/Controller/CustomerAppAPI/ApiController.php +++ b/src/Controller/CustomerAppAPI/ApiController.php @@ -162,6 +162,6 @@ class ApiController extends BaseApiController protected function getGeoErrorMessage() { - return 'Oops! Our service is limited to some areas in Metro Manila, Laguna, Cavite, Pampanga and Baguio only. We will update you as soon as we are able to cover your area'; + return 'Our services are currently limited to some areas in Metro Manila, Baguio, Batangas, Laguna, Cavite, Pampanga, and Palawan. We will update you as soon as we are available in your area. Thank you for understanding. Keep safe!'; } } diff --git a/src/Controller/CustomerAppAPI/InsuranceController.php b/src/Controller/CustomerAppAPI/InsuranceController.php index a3c5624a..271ebdb8 100644 --- a/src/Controller/CustomerAppAPI/InsuranceController.php +++ b/src/Controller/CustomerAppAPI/InsuranceController.php @@ -18,6 +18,7 @@ use App\Ramcar\InsuranceApplicationStatus; use App\Ramcar\InsuranceMVType; use App\Ramcar\InsuranceClientType; use App\Ramcar\TransactionStatus; +use App\Ramcar\InsuranceBodyType; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use DateTime; @@ -293,6 +294,45 @@ class InsuranceController extends ApiController ]); } + public function getPremiumsBanner(Request $req) + { + // validate params + $validity = $this->validateRequest($req); + + if (!$validity['is_valid']) { + return new ApiResponse(false, $validity['error']); + } + + return new ApiResponse(true, '', [ + 'url' => $this->getParameter('insurance_premiums_banner_url'), + ]); + } + + public function getBodyTypes(Request $req) + { + // validate params + $validity = $this->validateRequest($req); + + if (!$validity['is_valid']) { + return new ApiResponse(false, $validity['error']); + } + + $bt_collection = InsuranceBodyType::getCollection(); + $body_types = []; + + // NOTE: formatting it this way to match how insurance third party API returns their own stuff, so it's all handled one way on the app + foreach ($bt_collection as $bt_key => $bt_name) { + $body_types[] = [ + 'id' => $bt_key, + 'name' => $bt_name, + ]; + } + + return new ApiResponse(true, '', [ + 'body_types' => $body_types, + ]); + } + protected function getLineType($mv_type_id, $vehicle_use_type, $is_public = false) { $line = ''; diff --git a/src/Controller/CustomerAppAPI/InvoiceController.php b/src/Controller/CustomerAppAPI/InvoiceController.php index a5c3a8b8..2e9779eb 100644 --- a/src/Controller/CustomerAppAPI/InvoiceController.php +++ b/src/Controller/CustomerAppAPI/InvoiceController.php @@ -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; + } } diff --git a/src/Controller/CustomerAppAPI/JobOrderController.php b/src/Controller/CustomerAppAPI/JobOrderController.php index 143f2a2e..fa5a9a85 100644 --- a/src/Controller/CustomerAppAPI/JobOrderController.php +++ b/src/Controller/CustomerAppAPI/JobOrderController.php @@ -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); diff --git a/src/Controller/CustomerAppAPI/VehicleController.php b/src/Controller/CustomerAppAPI/VehicleController.php index bc1e29c2..c8fd15e9 100644 --- a/src/Controller/CustomerAppAPI/VehicleController.php +++ b/src/Controller/CustomerAppAPI/VehicleController.php @@ -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->getBatteries(); 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), diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index 97744d27..cf3f9967 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Ramcar\InsuranceApplicationStatus; use App\Service\FCMSender; +use App\Service\InsuranceConnector; use App\Entity\InsuranceApplication; use Doctrine\ORM\EntityManagerInterface; @@ -15,11 +16,13 @@ use DateTime; class InsuranceController extends Controller { + protected $ic; protected $em; protected $fcmclient; - public function __construct(EntityManagerInterface $em, FCMSender $fcmclient) + public function __construct(InsuranceConnector $ic, EntityManagerInterface $em, FCMSender $fcmclient) { + $this->ic = $ic; $this->em = $em; $this->fcmclient = $fcmclient; } @@ -28,17 +31,8 @@ class InsuranceController extends Controller { $payload = $req->request->all(); - // DEBUG - @file_put_contents(__DIR__ . '/../../var/log/insurance.log', print_r($payload, true) . "\r\n----------------------------------------\r\n\r\n", FILE_APPEND); - error_log(print_r($payload, true)); - - /* - return $this->json([ - 'success' => true, - ]); - */ - - // END DEBUG + // log this callback + $this->ic->log('CALLBACK', "[]", json_encode($payload), 'callback'); // if no transaction code given, silently fail if (empty($payload['transaction_code'])) { diff --git a/src/Controller/ItemPricingController.php b/src/Controller/ItemPricingController.php new file mode 100644 index 00000000..3129f362 --- /dev/null +++ b/src/Controller/ItemPricingController.php @@ -0,0 +1,269 @@ +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, + ]; + } +} diff --git a/src/Controller/ItemTypeController.php b/src/Controller/ItemTypeController.php new file mode 100644 index 00000000..c7c1b110 --- /dev/null +++ b/src/Controller/ItemTypeController.php @@ -0,0 +1,251 @@ +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'] . '%'); + } + } +} diff --git a/src/Controller/JobOrderController.php b/src/Controller/JobOrderController.php index 91921a0f..17c62a45 100644 --- a/src/Controller/JobOrderController.php +++ b/src/Controller/JobOrderController.php @@ -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 diff --git a/src/Controller/PayMongoController.php b/src/Controller/PayMongoController.php index 02f75928..21cd864c 100644 --- a/src/Controller/PayMongoController.php +++ b/src/Controller/PayMongoController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Entity\GatewayTransaction; use App\Ramcar\TransactionStatus; +use App\Service\PayMongoConnector; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; @@ -12,10 +13,12 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller; class PayMongoController extends Controller { + protected $pm; protected $em; - public function __construct(EntityManagerInterface $em) + public function __construct(PayMongoConnector $pm, EntityManagerInterface $em) { + $this->pm = $pm; $this->em = $em; } @@ -23,16 +26,8 @@ class PayMongoController extends Controller { $payload = json_decode($req->getContent(), true); - // DEBUG - @file_put_contents(__DIR__ . '/../../var/log/paymongo.log', print_r($payload, true) . "\r\n----------------------------------------\r\n\r\n", FILE_APPEND); - - /* - return $this->json([ - 'success' => true, - ]); - */ - - // END DEBUG + // log this callback + $this->pm->log('CALLBACK', "[]", $req->getContent(), 'callback'); // if no event type given, silently fail if (empty($payload['data'])) { diff --git a/src/Controller/PriceTierController.php b/src/Controller/PriceTierController.php new file mode 100644 index 00000000..b44a6bb8 --- /dev/null +++ b/src/Controller/PriceTierController.php @@ -0,0 +1,355 @@ +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'] . '%'); + } + } +} diff --git a/src/Controller/TAPI/BatteryController.php b/src/Controller/TAPI/BatteryController.php index 0705a3ab..0d38dd5f 100644 --- a/src/Controller/TAPI/BatteryController.php +++ b/src/Controller/TAPI/BatteryController.php @@ -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), diff --git a/src/Controller/TAPI/JobOrderController.php b/src/Controller/TAPI/JobOrderController.php index f7541b50..ae4d5a61 100644 --- a/src/Controller/TAPI/JobOrderController.php +++ b/src/Controller/TAPI/JobOrderController.php @@ -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); diff --git a/src/Entity/ItemPrice.php b/src/Entity/ItemPrice.php new file mode 100644 index 00000000..48130468 --- /dev/null +++ b/src/Entity/ItemPrice.php @@ -0,0 +1,97 @@ +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; + } +} diff --git a/src/Entity/ItemType.php b/src/Entity/ItemType.php new file mode 100644 index 00000000..8c24e65c --- /dev/null +++ b/src/Entity/ItemType.php @@ -0,0 +1,80 @@ +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; + } +} diff --git a/src/Entity/JobOrder.php b/src/Entity/JobOrder.php index 885bc295..9229eed9 100644 --- a/src/Entity/JobOrder.php +++ b/src/Entity/JobOrder.php @@ -441,6 +441,12 @@ class JobOrder */ protected $flag_cust_new; + // reference JO id. We are now decoupling job order from itself + /** + * @ORM\Column(type="integer", nullable=true) + */ + protected $reference_jo_id; + public function __construct() { $this->date_create = new DateTime(); @@ -792,7 +798,7 @@ class JobOrder return $this->tickets; } - public function setReferenceJO(JobOrder $ref_jo) + public function setReferenceJO(JobOrder $ref_jo = null) { $this->ref_jo = $ref_jo; return $this; @@ -1256,4 +1262,15 @@ class JobOrder return $this->flag_cust_new; } + public function setReferenceJOId($reference_jo_id) + { + $this->reference_jo_id = $reference_jo_id; + return $this; + } + + public function getReferenceJOId() + { + return $this->reference_jo_id; + } + } diff --git a/src/Entity/MotoliteEvent.php b/src/Entity/MotoliteEvent.php index ccdb367b..4c8df366 100644 --- a/src/Entity/MotoliteEvent.php +++ b/src/Entity/MotoliteEvent.php @@ -21,7 +21,7 @@ class MotoliteEvent protected $id; /** - * @ORM\Column(type="string", length=80) + * @ORM\Column(type="string", length=255) * @Assert\NotBlank() */ protected $name; diff --git a/src/Entity/PriceTier.php b/src/Entity/PriceTier.php new file mode 100644 index 00000000..1e2599a5 --- /dev/null +++ b/src/Entity/PriceTier.php @@ -0,0 +1,88 @@ +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; + } + +} diff --git a/src/Entity/SupportedArea.php b/src/Entity/SupportedArea.php index 0f70c39e..0e176f6a 100644 --- a/src/Entity/SupportedArea.php +++ b/src/Entity/SupportedArea.php @@ -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; + } } diff --git a/src/EntityListener/GatewayTransactionListener.php b/src/EntityListener/GatewayTransactionListener.php index 745fc5f9..c9007aaf 100644 --- a/src/EntityListener/GatewayTransactionListener.php +++ b/src/EntityListener/GatewayTransactionListener.php @@ -66,7 +66,7 @@ class GatewayTransactionListener } // flag on api as paid - $result = $this->ic->tagApplicationPaid($obj->getID()); + $result = $this->ic->tagApplicationPaid($obj->getExtTransactionId()); if (!$result['success'] || $result['response']['transaction_code'] !== 'GR004') { error_log("INSURANCE MARK AS PAID FAILED FOR " . $obj->getID() . ": " . $result['error']['message']); } diff --git a/src/InvoiceRule/BatteryReplacementWarranty.php b/src/InvoiceRule/BatteryReplacementWarranty.php index ee1497b5..46ba5e8a 100644 --- a/src/InvoiceRule/BatteryReplacementWarranty.php +++ b/src/InvoiceRule/BatteryReplacementWarranty.php @@ -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'; diff --git a/src/InvoiceRule/BatterySales.php b/src/InvoiceRule/BatterySales.php index 45b060d1..f3b66c11 100644 --- a/src/InvoiceRule/BatterySales.php +++ b/src/InvoiceRule/BatterySales.php @@ -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()) @@ -36,19 +42,28 @@ class BatterySales implements InvoiceRuleInterface $entries = $criteria->getEntries(); foreach($entries as $entry) { - $batt = $entry['battery']; $qty = $entry['qty']; $trade_in = null; + // check if entry is for trade in if (isset($entry['trade_in'])) $trade_in = $entry['trade_in']; - $size = $batt->getSize(); - + // entry is a battery purchase if ($trade_in == null) { - // battery purchase - $price = $batt->getSellingPrice(); + // safe to get entry with battery key since CRM and apps + // will set this for a battery purchase and trade_in will + // will not be set + $batt = $entry['battery']; + + // check if price tier has item price for battery + $pt_price = $this->getPriceTierItemPrice($pt, $batt); + + if ($pt_price == null) + $price = $batt->getSellingPrice(); + else + $price = $pt_price; $items[] = [ 'service_type' => $this->getID(), @@ -114,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(); diff --git a/src/InvoiceRule/Fuel.php b/src/InvoiceRule/Fuel.php index f843e1c0..e4f365b9 100644 --- a/src/InvoiceRule/Fuel.php +++ b/src/InvoiceRule/Fuel.php @@ -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); diff --git a/src/InvoiceRule/Jumpstart.php b/src/InvoiceRule/Jumpstart.php index dce41d99..d2e89b0a 100644 --- a/src/InvoiceRule/Jumpstart.php +++ b/src/InvoiceRule/Jumpstart.php @@ -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'; diff --git a/src/InvoiceRule/JumpstartWarranty.php b/src/InvoiceRule/JumpstartWarranty.php index 9423da44..4c6ac387 100644 --- a/src/InvoiceRule/JumpstartWarranty.php +++ b/src/InvoiceRule/JumpstartWarranty.php @@ -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'; diff --git a/src/InvoiceRule/Overheat.php b/src/InvoiceRule/Overheat.php index 4c06bddb..6ed7bedb 100644 --- a/src/InvoiceRule/Overheat.php +++ b/src/InvoiceRule/Overheat.php @@ -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) @@ -94,7 +108,7 @@ class Overheat implements InvoiceRuleInterface // find the service fee using the code // if we can't find the fee, return 0 - $fee = $this->em->getRepository(ServiceOffering::class)->findOneBy($code); + $fee = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]); if ($fee == null) return 0; @@ -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); diff --git a/src/InvoiceRule/PostRecharged.php b/src/InvoiceRule/PostRecharged.php index 808f2340..b0b20995 100644 --- a/src/InvoiceRule/PostRecharged.php +++ b/src/InvoiceRule/PostRecharged.php @@ -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'; diff --git a/src/InvoiceRule/PostReplacement.php b/src/InvoiceRule/PostReplacement.php index aba6d9aa..774b61de 100644 --- a/src/InvoiceRule/PostReplacement.php +++ b/src/InvoiceRule/PostReplacement.php @@ -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'; diff --git a/src/InvoiceRule/Tax.php b/src/InvoiceRule/Tax.php index b1e6a600..50834e44 100644 --- a/src/InvoiceRule/Tax.php +++ b/src/InvoiceRule/Tax.php @@ -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); diff --git a/src/InvoiceRule/TireRepair.php b/src/InvoiceRule/TireRepair.php index 755c11bd..96d3c525 100644 --- a/src/InvoiceRule/TireRepair.php +++ b/src/InvoiceRule/TireRepair.php @@ -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'; diff --git a/src/InvoiceRule/TradeIn.php b/src/InvoiceRule/TradeIn.php index 8e3c6063..224bf098 100644 --- a/src/InvoiceRule/TradeIn.php +++ b/src/InvoiceRule/TradeIn.php @@ -21,7 +21,6 @@ class TradeIn implements InvoiceRuleInterface $entries = $criteria->getEntries(); foreach($entries as $entry) { - $batt = $entry['battery']; $qty = $entry['qty']; $trade_in_type = null; @@ -30,7 +29,18 @@ class TradeIn implements InvoiceRuleInterface if ($trade_in_type != null) { - $ti_rate = $this->getTradeInRate($batt, $trade_in_type); + // at this point, entry is a trade in + // need to check if battery (coming from CRM) is set + // or battery_size is set (coming from rider app) + if (isset($entry['battery'])) + { + $battery = $entry['battery']; + $batt_size = $battery->getSize(); + } + else + $batt_size = $entry['battery_size']; + + $ti_rate = $this->getTradeInRate($batt_size, $trade_in_type); $qty_ti = bcmul($ti_rate, $qty, 2); @@ -41,7 +51,7 @@ class TradeIn implements InvoiceRuleInterface $items[] = [ 'qty' => $qty, - 'title' => $this->getTitle($batt, $trade_in_type), + 'title' => $this->getTitle($batt_size, $trade_in_type), 'price' => $price, ]; } @@ -60,10 +70,8 @@ class TradeIn implements InvoiceRuleInterface return null; } - protected function getTradeInRate($battery, $trade_in_type) + protected function getTradeInRate($size, $trade_in_type) { - $size = $battery->getSize(); - switch ($trade_in_type) { case TradeInType::MOTOLITE: @@ -77,9 +85,9 @@ class TradeIn implements InvoiceRuleInterface return 0; } - protected function getTitle($battery, $trade_in_type) + protected function getTitle($battery_size, $trade_in_type) { - $title = 'Trade-in ' . TradeInType::getName($trade_in_type) . ' ' . $battery->getSize()->getName() . ' battery'; + $title = 'Trade-in ' . TradeInType::getName($trade_in_type) . ' ' . $battery_size->getName() . ' battery'; return $title; } diff --git a/src/Ramcar/InsuranceBodyType.php b/src/Ramcar/InsuranceBodyType.php new file mode 100644 index 00000000..2abffcdc --- /dev/null +++ b/src/Ramcar/InsuranceBodyType.php @@ -0,0 +1,18 @@ + 'Sedan', + 'SUV' => 'SUV', + 'TRUCK' => 'Truck', + 'MOTORCYCLE' => 'Motorcycle', + ]; +} diff --git a/src/Ramcar/InvoiceCriteria.php b/src/Ramcar/InvoiceCriteria.php index b27395da..3c1f907f 100644 --- a/src/Ramcar/InvoiceCriteria.php +++ b/src/Ramcar/InvoiceCriteria.php @@ -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) @@ -108,6 +110,17 @@ class InvoiceCriteria $this->entries[] = $entry; } + public function addTradeInEntry($battery_size, $trade_in, $qty) + { + $entry = [ + 'battery_size' => $battery_size, + 'trade_in' => $trade_in, + 'qty' => $qty + ]; + + $this->entries[] = $entry; + } + public function getEntries() { return $this->entries; @@ -179,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; + } } diff --git a/src/Ramcar/TransactionOrigin.php b/src/Ramcar/TransactionOrigin.php index a204b35f..7f459e96 100644 --- a/src/Ramcar/TransactionOrigin.php +++ b/src/Ramcar/TransactionOrigin.php @@ -17,13 +17,17 @@ class TransactionOrigin extends NameValue const YOKOHAMA_TWITTER = 'yokohama_twitter'; const YOKOHAMA_INSTAGRAM = 'yokohama_instagram'; const YOKOHAMA_CAROUSELL = 'yokohama_carousell'; + const HOTLINE_CEBU = 'hotline_cebu'; + const FACEBOOK_CEBU = 'facebook_cebu'; // TODO: for now, resq also gets the walk-in option // resq also gets new YOKOHAMA options const COLLECTION = [ - 'call' => 'Hotline', + 'call' => 'Hotline Manila', + 'hotline_cebu' => 'Hotline Cebu', 'online' => 'Online', - 'facebook' => 'Facebook', + 'facebook' => 'Facebook Manila', + 'facebook_cebu' => 'Facebook Cebu', 'vip' => 'VIP', 'mobile_app' => 'Mobile App', 'walk_in' => 'Walk-in', @@ -33,6 +37,6 @@ class TransactionOrigin extends NameValue 'yokohama_op_facebook' => 'Yokohama OP Facebook', 'yokohama_twitter' => 'Yokohama Twitter', 'yokohama_instagram' => 'Yokohama Instagram', - 'yokohama_carousell' => 'Yokohama Carousell', + 'yokohama_carousell' => 'Yokohama Carousell' ]; } diff --git a/src/Service/InsuranceConnector.php b/src/Service/InsuranceConnector.php index b56415c0..9ad10847 100644 --- a/src/Service/InsuranceConnector.php +++ b/src/Service/InsuranceConnector.php @@ -91,7 +91,7 @@ class InsuranceConnector return base64_encode($this->username . ":" . $this->password); } - protected function doRequest($url, $method, $body = []) + protected function doRequest($url, $method, $request_body = []) { $client = new Client(); $headers = [ @@ -102,7 +102,7 @@ class InsuranceConnector try { $response = $client->request($method, $this->base_url . '/' . $url, [ - 'json' => $body, + 'json' => $request_body, 'headers' => $headers, ]); } catch (RequestException $e) { @@ -111,6 +111,11 @@ class InsuranceConnector error_log("Insurance API Error: " . $error['message']); error_log(Psr7\Message::toString($e->getRequest())); error_log($e->getResponse()->getBody()->getContents()); + error_log("Insurance Creds: " . $this->username . ", " . $this->password); + error_log("Insurance Hash: " . $this->generateHash()); + + // log this error + $this->log($url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error'); if ($e->hasResponse()) { $error['response'] = Psr7\Message::toString($e->getResponse()); @@ -122,11 +127,32 @@ class InsuranceConnector ]; } - error_log(print_r(json_decode($response->getBody(), true), true)); + $result_body = $response->getBody(); + + // log response + $this->log($url, json_encode($request_body), $result_body); return [ 'success' => true, - 'response' => json_decode($response->getBody(), true) + 'response' => json_decode($result_body, true), ]; } + + // TODO: make this more elegant + public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api') + { + $filename = '/../../var/log/insurance_' . $type . '.log'; + $date = date("Y-m-d H:i:s"); + + // build log entry + $entry = implode("\r\n", [ + $date, + $title, + "REQUEST:\r\n" . $request_body, + "RESPONSE:\r\n" . $result_body, + "\r\n----------------------------------------\r\n\r\n", + ]); + + @file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND); + } } diff --git a/src/Service/InvoiceGenerator/CMBInvoiceGenerator.php b/src/Service/InvoiceGenerator/CMBInvoiceGenerator.php index 5dc86f8d..c1f40509 100644 --- a/src/Service/InvoiceGenerator/CMBInvoiceGenerator.php +++ b/src/Service/InvoiceGenerator/CMBInvoiceGenerator.php @@ -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; diff --git a/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php b/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php index 3809133b..efe0a9de 100644 --- a/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php +++ b/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php @@ -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; diff --git a/src/Service/InvoiceGeneratorInterface.php b/src/Service/InvoiceGeneratorInterface.php index e2cb2cc3..4c46c38f 100644 --- a/src/Service/InvoiceGeneratorInterface.php +++ b/src/Service/InvoiceGeneratorInterface.php @@ -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); diff --git a/src/Service/InvoiceManager.php b/src/Service/InvoiceManager.php index 65fc3a43..8f655224 100644 --- a/src/Service/InvoiceManager.php +++ b/src/Service/InvoiceManager.php @@ -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 diff --git a/src/Service/JobOrderHandler/ResqJobOrderHandler.php b/src/Service/JobOrderHandler/ResqJobOrderHandler.php index 361c0b22..f5e07aaf 100644 --- a/src/Service/JobOrderHandler/ResqJobOrderHandler.php +++ b/src/Service/JobOrderHandler/ResqJobOrderHandler.php @@ -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(); } @@ -548,7 +551,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface if (empty($ref_jo)) { $error_array['ref_jo'] = 'Invalid reference job order specified.'; } else { - $jo->setReferenceJO($ref_jo); + $jo->setReferenceJOId($ref_jo->getID()); } } @@ -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 @@ -2014,6 +2021,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface // NOTE: for resq2 app $mclientv2->sendEvent($obj, $payload); + $mclientv2->sendRiderEvent($obj, $payload); $fcmclient->sendJoEvent($obj, "jo_fcm_title_driver_assigned", "jo_fcm_body_driver_assigned"); } @@ -2150,7 +2158,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface if (empty($ref_jo)) { $error_array['ref_jo'] = 'Invalid reference job order specified.'; } else { - $jo->setReferenceJO($ref_jo); + $jo->setReferenceJOId($ref_jo->getID()); } } @@ -2165,7 +2173,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 @@ -2910,6 +2920,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface $params['status_cancelled'] = JOStatus::CANCELLED; $params['hubs'] = []; + $branch_codes = []; // format duration and distance into friendly time foreach ($hubs as $hub) { // duration @@ -4252,6 +4263,10 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface if ($rejection->getReason() == JORejectionReason::ADMINISTRATIVE) return null; + // check if reason is discount + if ($rejection->getReason() == JORejectionReason::DISCOUNT) + return null; + // sms content // Job Order # - can get from jo // Order Date and Time - get from jo diff --git a/src/Service/MQTTClientApiv2.php b/src/Service/MQTTClientApiv2.php index 42b85648..f819be53 100644 --- a/src/Service/MQTTClientApiv2.php +++ b/src/Service/MQTTClientApiv2.php @@ -68,4 +68,31 @@ class MQTTClientApiv2 // error_log('sent to ' . $channel); } } + + public function sendRiderEvent(JobOrder $job_order, $payload) + { + // check if a rider is available + $rider = $job_order->getRider(); + if ($rider == null) + return; + + /* + // NOTE: this is for the old rider app + // check if rider has sessions + $sessions = $rider->getSessions(); + if (count($sessions) == 0) + return; + + // send to every rider session + foreach ($sessions as $sess) + { + $sess_id = $sess->getID(); + $channel = self::RIDER_PREFIX . $sess_id; + $this->publish($channel, json_encode($payload)); + } + */ + + // NOTE: this is for the new rider app + $this->publish('rider/' . $rider->getID() . '/delivery', json_encode($payload)); + } } diff --git a/src/Service/PayMongoConnector.php b/src/Service/PayMongoConnector.php index ad2750a2..c34a94e7 100644 --- a/src/Service/PayMongoConnector.php +++ b/src/Service/PayMongoConnector.php @@ -79,7 +79,7 @@ class PayMongoConnector return base64_encode($this->secret_key); } - protected function doRequest($url, $method, $body = []) + protected function doRequest($url, $method, $request_body = []) { $client = new Client(); $headers = [ @@ -90,14 +90,14 @@ class PayMongoConnector try { $response = $client->request($method, $this->base_url . '/' . $url, [ - 'json' => $body, + 'json' => $request_body, 'headers' => $headers, ]); } catch (RequestException $e) { $error = ['message' => $e->getMessage()]; ob_start(); - var_dump($body); + //var_dump($request_body); $varres = ob_get_clean(); error_log($varres); @@ -107,6 +107,9 @@ class PayMongoConnector error_log("PayMongo API Error: " . $error['message']); error_log(Psr7\Message::toString($e->getRequest())); + // log this error + $this->log($url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error'); + if ($e->hasResponse()) { $error['response'] = Psr7\Message::toString($e->getResponse()); } @@ -117,9 +120,32 @@ class PayMongoConnector ]; } + $result_body = $response->getBody(); + + // log response + $this->log($url, json_encode($request_body), $result_body); + return [ 'success' => true, 'response' => json_decode($response->getBody(), true) ]; } + + // TODO: make this more elegant + public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api') + { + $filename = '/../../var/log/paymongo_' . $type . '.log'; + $date = date("Y-m-d H:i:s"); + + // build log entry + $entry = implode("\r\n", [ + $date, + $title, + "REQUEST:\r\n" . $request_body, + "RESPONSE:\r\n" . $result_body, + "\r\n----------------------------------------\r\n\r\n", + ]); + + @file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND); + } } diff --git a/src/Service/PriceTierManager.php b/src/Service/PriceTierManager.php new file mode 100644 index 00000000..62d657f4 --- /dev/null +++ b/src/Service/PriceTierManager.php @@ -0,0 +1,80 @@ +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; + } +} diff --git a/templates/item-pricing/form.html.twig b/templates/item-pricing/form.html.twig new file mode 100644 index 00000000..d6099390 --- /dev/null +++ b/templates/item-pricing/form.html.twig @@ -0,0 +1,164 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +
+
+
+

Item Pricing

+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + +
+ + + + + + + + + + + + {% for id, item in items.items %} + + + + + + + + {% endfor %} + +
IDNameItem TypePrice
{{ id }}{{ item.name }} {{ item.item_type }} + +
+
+ +
+
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/item-type/form.html.twig b/templates/item-type/form.html.twig new file mode 100644 index 00000000..97d0d9ec --- /dev/null +++ b/templates/item-type/form.html.twig @@ -0,0 +1,142 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +
+
+
+

Item Types

+
+
+
+ +
+ +
+
+
+
+
+
+ + + +

+ {% if mode == 'update' %} + Edit Item Type + {{ obj.getName() }} + {% else %} + New Item Type + {% endif %} +

+
+
+
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+
+ + Back +
+
+
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/item-type/list.html.twig b/templates/item-type/list.html.twig new file mode 100644 index 00000000..cc11e81d --- /dev/null +++ b/templates/item-type/list.html.twig @@ -0,0 +1,146 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +
+
+
+

+ Item Types +

+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+ + + + +
+
+
+
+ +
+
+ +
+ +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/item/form.html.twig b/templates/item/form.html.twig new file mode 100644 index 00000000..f7a76ce1 --- /dev/null +++ b/templates/item/form.html.twig @@ -0,0 +1,177 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +
+
+
+

Items

+
+
+
+ +
+ +
+
+
+
+
+
+ + + +

+ {% if mode == 'update' %} + Edit Item + {{ obj.getName() }} + {% else %} + New Item + {% endif %} +

+
+
+
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+
+ + Back +
+
+
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/item/list.html.twig b/templates/item/list.html.twig new file mode 100644 index 00000000..b3a36a61 --- /dev/null +++ b/templates/item/list.html.twig @@ -0,0 +1,146 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +
+
+
+

+ Items +

+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+ + + + +
+
+
+
+ +
+
+ +
+ +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/job-order/form.html.twig b/templates/job-order/form.html.twig index ca68e922..edbd97a0 100644 --- a/templates/job-order/form.html.twig +++ b/templates/job-order/form.html.twig @@ -1231,6 +1231,8 @@ $(function() { function selectPoint(lat, lng) { // check if point is in coverage area + // commenting out the geofence call for CRM + /* $.ajax({ method: "GET", url: "{{ url('jo_geofence') }}", @@ -1248,7 +1250,7 @@ $(function() { type: 'warning', }); } - }); + }); */ // clear markers markerLayerGroup.clearLayers(); @@ -1761,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)); @@ -1772,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 diff --git a/templates/job-order/form.view.html.twig b/templates/job-order/form.view.html.twig index cc463f11..33d6fc5b 100644 --- a/templates/job-order/form.view.html.twig +++ b/templates/job-order/form.view.html.twig @@ -635,6 +635,8 @@ $(function() { function selectPoint(lat, lng) { // check if point is in coverage area + // commenting out the geofence call for CRM + /* $.ajax({ method: "GET", url: "{{ url('jo_geofence') }}", @@ -652,7 +654,7 @@ $(function() { type: 'warning', }); } - }); + }); */ // clear markers markerLayerGroup.clearLayers(); diff --git a/templates/price-tier/form.html.twig b/templates/price-tier/form.html.twig new file mode 100644 index 00000000..0056cdc8 --- /dev/null +++ b/templates/price-tier/form.html.twig @@ -0,0 +1,154 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +
+
+
+

Price Tiers

+
+
+
+ +
+ +
+
+
+
+
+
+ + + +

+ {% if mode == 'update' %} + Edit Price Tier + {{ obj.getName() }} + {% else %} + New Price Tier + {% endif %} +

+
+
+
+
+
+
+ +
+ + +
+
+
+ +
+ {% if sets.areas is empty %} + No available supported areas. + {% else %} +
+ {% for id, label in sets.areas %} + + {% endfor %} +
+ {% endif %} + +
+
+
+
+
+
+
+ + Back +
+
+
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/price-tier/list.html.twig b/templates/price-tier/list.html.twig new file mode 100644 index 00000000..e97eed40 --- /dev/null +++ b/templates/price-tier/list.html.twig @@ -0,0 +1,146 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +
+
+
+

+ Price Tiers +

+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+ + + + +
+
+
+
+ +
+
+ +
+ +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/vehicle/form.html.twig b/templates/vehicle/form.html.twig index 99ecbc0f..e09e275c 100644 --- a/templates/vehicle/form.html.twig +++ b/templates/vehicle/form.html.twig @@ -233,13 +233,14 @@ $(function() { var batteryIds = []; var battMfgModelSize = [] - {% for batt in obj.getActiveBatteries %} + {% for batt in obj.getBatteries %} trow = { id: "{{ batt.getID }}", manufacturer: "{{ batt.getManufacturer.getName|default('') }} ", model: "{{ batt.getModel.getName|default('') }}", size: "{{ batt.getSize.getName|default('') }}", sell_price: "{{ batt.getSellingPrice }}", + flag_active: {{ batt.isActive ? "true" : "false" }}, }; battRows.push(trow); @@ -360,6 +361,21 @@ $(function() { title: 'Model', width: 150 }, + { + field: 'flag_active', + title: 'Active', + template: function (row, index, datatable) { + var tag = ''; + + if (row.flag_active === true) { + tag = 'Yes'; + } else { + tag = 'No'; + } + + return tag; + }, + }, { field: 'sell_price', title: 'Price' diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index 7278212d..8387663e 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -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' diff --git a/utils/item_types/item_types.sql b/utils/item_types/item_types.sql new file mode 100644 index 00000000..7c5aeeba --- /dev/null +++ b/utils/item_types/item_types.sql @@ -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