security = $security; $this->em = $em; $this->validator = $validator; } public function generateInvoice(InvoiceCriteria $criteria) { // initialize $invoice = new Invoice(); $total = [ 'sell_price' => 0.0, 'vat' => 0.0, 'vat_ex_price' => 0.0, 'ti_rate' => 0.0, 'total_price' => 0.0, 'discount' => 0.0, ]; $stype = $criteria->getServiceType(); $cv = $criteria->getCustomerVehicle(); $has_coolant = $criteria->hasCoolant(); $cust_tag_info = []; if ($stype == ServiceType::BATTERY_REPLACEMENT_NEW) { // check if criteria has entries $entries = $criteria->getEntries(); if (!empty($entries)) $cust_tag_info = $this->getCustomerTagInfo($cv); } // error_log($stype); switch ($stype) { case ServiceType::JUMPSTART_TROUBLESHOOT: $this->processJumpstart($total, $invoice); break; case ServiceType::JUMPSTART_WARRANTY: $this->processJumpstartWarranty($total, $invoice); case ServiceType::BATTERY_REPLACEMENT_NEW: $this->processEntries($total, $criteria, $invoice); /* $this->processBatteries($total, $criteria, $invoice); $this->processTradeIns($total, $criteria, $invoice); */ $this->processDiscount($total, $criteria, $invoice, $cust_tag_info); break; case ServiceType::BATTERY_REPLACEMENT_WARRANTY: $this->processWarranty($total, $criteria, $invoice); break; case ServiceType::POST_RECHARGED: $this->processRecharge($total, $invoice); break; case ServiceType::POST_REPLACEMENT: $this->processReplacement($total, $invoice); break; case ServiceType::TIRE_REPAIR: $this->processTireRepair($total, $invoice, $cv); // $this->processOtherServices($total, $invoice, $stype); break; case ServiceType::OVERHEAT_ASSISTANCE: $this->processOverheat($total, $invoice, $cv, $has_coolant); break; case ServiceType::EMERGENCY_REFUEL: error_log('processing refuel'); $ftype = $criteria->getCustomerVehicle()->getFuelType(); $this->processRefuel($total, $invoice, $cv); break; } // TODO: check if any promo is applied // apply discounts $promos = $criteria->getPromos(); // get current user $user = $this->security->getUser(); // check if user is User or APIUser //if ($user != null) if ($user instanceof User) { $invoice->setCreatedBy($user); } $invoice->setTotalPrice($total['total_price']) ->setVATExclusivePrice($total['vat_ex_price']) ->setVAT($total['vat']) ->setDiscount($total['discount']) ->setTradeIn($total['ti_rate']) ->setStatus(InvoiceStatus::DRAFT); // dump //Debug::dump($invoice, 1); return $invoice; } // generate invoice criteria public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, &$error_array) { $em = $this->em; // instantiate the invoice criteria $criteria = new InvoiceCriteria(); $criteria->setServiceType($jo->getServiceType()) ->setCustomerVehicle($jo->getCustomerVehicle()); $ierror = $this->invoicePromo($criteria, $promo_id); if (!$ierror && !empty($invoice_items)) { // check for trade-in so we can mark it for mobile app foreach ($invoice_items as $item) { // get first trade-in if (!empty($item['trade_in'])) { $jo->getTradeInType($item['trade_in']); break; } } $ierror = $this->invoiceBatteries($criteria, $invoice_items); } if ($ierror) { $error_array['invoice'] = $ierror; } else { // generate the invoice $iobj = $this->generateInvoice($criteria); // validate $ierrors = $this->validator->validate($iobj); // add errors to list foreach ($ierrors as $error) { $error_array[$error->getPropertyPath()] = $error->getMessage(); } // check if invoice already exists for JO $old_invoice = $jo->getInvoice(); if ($old_invoice != null) { // remove old invoice $em->remove($old_invoice); $em->flush(); } // add invoice to JO $jo->setInvoice($iobj); $em->persist($iobj); } } // prepare draft for invoice public function generateDraftInvoice($criteria, $promo_id, $service_charges, $items) { $ierror = $this->invoicePromo($criteria, $promo_id); if (!$ierror) { $ierror = $this->invoiceBatteries($criteria, $items); } return $ierror; } protected function getTaxAmount($price) { $vat_ex_price = $this->getTaxExclusivePrice($price); return $price - $vat_ex_price; // return round($vat_ex_price * self::TAX_RATE, 2); } protected function getTaxExclusivePrice($price) { return round($price / (1 + self::TAX_RATE), 2); } protected function getTradeInRate($ti) { $size = $ti['size']; $trade_in = $ti['trade_in']; if ($trade_in == null) return 0; switch ($trade_in) { case TradeInType::MOTOLITE: return $size->getTIPriceMotolite(); case TradeInType::PREMIUM: return $size->getTIPricePremium(); case TradeInType::OTHER: return $size->getTIPriceOther(); } return 0; } protected function invoicePromo(InvoiceCriteria $criteria, $promo_id) { // return error if there's a problem, false otherwise // check service type $stype = $criteria->getServiceType(); if ($stype != ServiceType::BATTERY_REPLACEMENT_NEW) return null; if (empty($promo_id)) { return false; } // check if this is a valid promo $promo = $this->em->getRepository(Promo::class)->find($promo_id); if (empty($promo)) return 'Invalid promo specified.'; $criteria->addPromo($promo); return false; } public function invoiceBatteries(InvoiceCriteria $criteria, $items) { // check service type $stype = $criteria->getServiceType(); if ($stype != ServiceType::BATTERY_REPLACEMENT_NEW && $stype != ServiceType::BATTERY_REPLACEMENT_WARRANTY) return null; // return error if there's a problem, false otherwise if (!empty($items)) { foreach ($items as $item) { // check if this is a valid battery $battery = $this->em->getRepository(Battery::class)->find($item['battery']); if (empty($battery)) { $error = 'Invalid battery specified.'; return $error; } // quantity $qty = $item['quantity']; if ($qty < 1) continue; /* // add to criteria $criteria->addBattery($battery, $qty); */ // if this is a trade in, add trade in if (!empty($item['trade_in']) && TradeInType::validate($item['trade_in'])) $trade_in = $item['trade_in']; else $trade_in = null; $criteria->addEntry($battery, $trade_in, $qty); } } return null; } protected function processEntries(&$total, InvoiceCriteria $criteria, Invoice $invoice) { // error_log('processing entries...'); $entries = $criteria->getEntries(); $con_batts = []; $con_tis = []; foreach ($entries as $entry) { $batt = $entry['battery']; $qty = $entry['qty']; $trade_in = $entry['trade_in']; $size = $batt->getSize(); // consolidate batteries $batt_id = $batt->getID(); if (!isset($con_batts[$batt_id])) $con_batts[$batt->getID()] = [ 'batt' => $batt, 'qty' => 0 ]; $con_batts[$batt_id]['qty']++; // no trade-in if ($trade_in == null) continue; // consolidate trade-ins $ti_key = $size->getID() . '|' . $trade_in; if (!isset($con_tis[$ti_key])) $con_tis[$ti_key] = [ 'size' => $size, 'trade_in' => $trade_in, 'qty' => 0 ]; $con_tis[$ti_key]['qty']++; } $this->processBatteries($total, $con_batts, $invoice); $this->processTradeIns($total, $con_tis, $invoice); $this->processVAT($total); } protected function processBatteries(&$total, $con_batts, Invoice $invoice) { // process batteries foreach ($con_batts as $con_data) { $batt = $con_data['batt']; $qty = $con_data['qty']; $sell_price = $batt->getSellingPrice(); // comment out the getting of tax amount // $vat = $this->getTaxAmount($sell_price); // $vat_ex_price = $this->getTaxExclusivePrice($sell_price); $total['sell_price'] += $sell_price * $qty; // $total['vat'] += $vat * $qty; // $total['vat_ex_price'] += ($sell_price - $vat) * $qty; $total['total_price'] += $sell_price * $qty; // add item $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle($batt->getModel()->getName() . ' ' . $batt->getSize()->getName()) ->setQuantity($qty) ->setPrice($sell_price) ->setBattery($batt); $invoice->addItem($item); } } protected function processTradeIns(&$total, $con_tis, Invoice $invoice) { foreach ($con_tis as $ti) { $qty = $ti['qty']; $ti_rate = $this->getTradeInRate($ti); $total['ti_rate'] += $ti_rate * $qty; $total['total_price'] -= $ti_rate * $qty; // add item $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle('Trade-in ' . TradeInType::getName($ti['trade_in']) . ' ' . $ti['size']->getName() . ' battery') ->setQuantity($qty) ->setPrice($ti_rate * -1); $invoice->addItem($item); } } protected function processDiscount(&$total, InvoiceCriteria $criteria, Invoice $invoice, $cust_tag_info) { if (empty($cust_tag_info)) { //error_log('empty cust tag'); $promos = $criteria->getPromos(); if (count($promos) < 1) return; // NOTE: only get first promo because only one is applicable anyway $promo = $promos[0]; $rate = $promo->getDiscountRate(); $apply_to = $promo->getDiscountApply(); switch ($apply_to) { case DiscountApply::SRP: $discount = round($total['sell_price'] * $rate, 2); break; case DiscountApply::OPL: // $discount = round($total['sell_price'] * 0.6 / 0.7 * $rate, 2); $discount = round($total['sell_price'] * (1 - 1.5 / 0.7 * $rate), 2); break; } // if discount is higher than 0, display in invoice if ($discount > 0) { $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle('Promo discount') ->setQuantity(1) ->setPrice(-1 * $discount); $invoice->addItem($item); } $total['discount'] = $discount; $total['total_price'] -= $discount; // process $invoice->setPromo($promo); } else { // since only one promo can only be used, we prioritize the tag promos // TODO: need to test this for multiple tags $total_discount_amount = 0; $total_amount = $total['total_price']; foreach ($cust_tag_info as $ct_info) { // check discount type $discount_type = ''; $discount_value = 0; $discount_amount = 0; $discounted_total = 0; if (isset($ct_info['discount_type'])) $discount_type = $ct_info['discount_type']; if (isset($ct_info['discount_value'])) { $discount_value = $ct_info['discount_value']; } if ($discount_type == 'percent') { $discount = round(($discount_value / 100), 2); $discount_amount = $total_amount * $discount; $discounted_total = $total_amount - $discount_amount; } else { // assume fixed amount for this $discount_amount = $discount_value; $discounted_total = $total_amount - $discount; } $total_discount_amount += $discount_amount; $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle($ct_info['invoice_display']) ->setQuantity(1) ->setPrice(-1 * $total_discount_amount); $invoice->addItem($item); $invoice->setUsedCustomerTagId($ct_info['id']); } $total['discount'] = $total_discount_amount; $total['total_price'] -= $total_discount_amount; } } protected function processJumpstart(&$total, $invoice) { // add troubleshooting fee $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle('Troubleshooting fee') ->setQuantity(1) ->setPrice(self::TROUBLESHOOTING_FEE); $invoice->addItem($item); $total['sell_price'] = self::TROUBLESHOOTING_FEE; $total['vat_ex_price'] = self::TROUBLESHOOTING_FEE; $total['total_price'] = self::TROUBLESHOOTING_FEE; } protected function processJumpstartWarranty(&$total, $invoice) { $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle('Troubleshooting fee') ->setQuantity(1) ->setPrice(0.00); $invoice->addItem($item); } protected function processRecharge(&$total, $invoice) { // add recharge fee $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle('Recharge fee') ->setQuantity(1) ->setPrice(self::RECHARGE_FEE); $invoice->addItem($item); $total['sell_price'] = self::RECHARGE_FEE; $total['vat_ex_price'] = self::RECHARGE_FEE; $total['total_price'] = self::RECHARGE_FEE; } protected function processReplacement(&$total, $invoice) { // add recharge fee $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle('Battery replacement') ->setQuantity(1) ->setPrice(self::BATT_REPLACEMENT_FEE); $invoice->addItem($item); } protected function processWarranty(&$total, InvoiceCriteria $criteria, $invoice) { // error_log('processing warranty'); $entries = $criteria->getEntries(); foreach ($entries as $entry) { $batt = $entry['battery']; $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle($batt->getModel()->getName() . ' ' . $batt->getSize()->getName() . ' - Service Unit') ->setQuantity(1) ->setPrice(self::WARRANTY_FEE) ->setBattery($batt); $invoice->addItem($item); } } protected function processOtherServices(&$total, $invoice, $stype) { $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle('Service - ' . ServiceType::getName($stype)) ->setQuantity(1) ->setPrice(self::OTHER_SERVICES_FEE); $invoice->addItem($item); $total['total_price'] = 200.00; } protected function processOverheat(&$total, $invoice, $cv, $has_coolant) { // free if they have a motolite battery if ($cv->hasMotoliteBattery()) $fee = 0; else $fee = self::SERVICE_FEE; $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle('Service - Overheat Assistance') ->setQuantity(1) ->setPrice($fee); $invoice->addItem($item); $total_price = $fee; if ($has_coolant) { $coolant = new InvoiceItem(); $coolant->setInvoice($invoice) ->setTitle('4L Coolant') ->setQuantity(1) ->setPrice(self::COOLANT_FEE); $invoice->addItem($coolant); $total_price += self::COOLANT_FEE; //$total_price += 1600; } $vat_ex_price = $this->getTaxExclusivePrice($total_price); $vat = $total_price - $vat_ex_price; $total['total_price'] = $total_price; $total['vat_ex_price'] = $vat_ex_price; $total['vat'] = $vat; } protected function processTireRepair(&$total, $invoice, $cv) { // free if they have a motolite battery if ($cv->hasMotoliteBattery()) $fee = 0; else $fee = self::SERVICE_FEE; $item = new InvoiceItem(); $item->setInvoice($invoice) ->setTitle('Service - Flat Tire') ->setQuantity(1) ->setPrice($fee); $invoice->addItem($item); $total_price = $fee; $vat_ex_price = $this->getTaxExclusivePrice($total_price); $vat = $total_price - $vat_ex_price; $total['total_price'] = $total_price; $total['vat_ex_price'] = $vat_ex_price; $total['vat'] = $vat; } protected function processRefuel(&$total, $invoice, $cv) { // free if they have a motolite battery if ($cv->hasMotoliteBattery()) $fee = 0; else $fee = self::SERVICE_FEE; $ftype = $cv->getFuelType(); $item = new InvoiceItem(); // service charge $item->setInvoice($invoice) ->setTitle('Service - ' . ServiceType::getName(ServiceType::EMERGENCY_REFUEL)) ->setQuantity(1) ->setPrice($fee); $invoice->addItem($item); $total_price = $fee; // $total['total_price'] = 200.00; $gas_price = self::REFUEL_FEE_GAS; $diesel_price = self::REFUEL_FEE_DIESEL; $fuel = new InvoiceItem(); //error_log('fuel type - ' . $ftype); switch ($ftype) { case FuelType::GAS: $fuel->setInvoice($invoice) ->setTitle('4L Fuel - Gas') ->setQuantity(1) ->setPrice($gas_price); $invoice->addItem($fuel); $total_price += $gas_price; break; case FuelType::DIESEL: $fuel->setInvoice($invoice) ->setTitle('4L Fuel - Diesel') ->setQuantity(1) ->setPrice($diesel_price); $total_price += $diesel_price; $invoice->addItem($fuel); break; default: // NOTE: should never get to this point $fuel->setInvoice($invoice) ->setTitle('Fuel - Unknown') ->setQuantity(1) ->setPrice(0); $total_price += 0.00; $invoice->addItem($fuel); break; } $vat_ex_price = $this->getTaxExclusivePrice($total_price); $vat = $total_price - $vat_ex_price; $total['total_price'] = $total_price; $total['vat_ex_price'] = $vat_ex_price; $total['vat'] = $vat; } public function processCriteria(InvoiceCriteria $criteria) { // initialize $invoice = new Invoice(); $total = [ 'sell_price' => 0.0, 'vat' => 0.0, 'vat_ex_price' => 0.0, 'ti_rate' => 0.0, 'total_price' => 0.0, 'discount' => 0.0, ]; $stype = $criteria->getServiceType(); $cv = $criteria->getCustomerVehicle(); $has_coolant = $criteria->hasCoolant(); // error_log($stype); $cust_tag_info = []; switch ($stype) { case ServiceType::JUMPSTART_TROUBLESHOOT: $this->processJumpstart($total, $invoice); break; case ServiceType::JUMPSTART_WARRANTY: $this->processJumpstartWarranty($total, $invoice); case ServiceType::BATTERY_REPLACEMENT_NEW: $this->processEntries($total, $criteria, $invoice); /* $this->processBatteries($total, $criteria, $invoice); $this->processTradeIns($total, $criteria, $invoice); */ $this->processDiscount($total, $criteria, $invoice, $cust_tag_info); break; case ServiceType::BATTERY_REPLACEMENT_WARRANTY: $this->processWarranty($total, $criteria, $invoice); break; case ServiceType::POST_RECHARGED: $this->processRecharge($total, $invoice); break; case ServiceType::POST_REPLACEMENT: $this->processReplacement($total, $invoice); break; case ServiceType::TIRE_REPAIR: $this->processTireRepair($total, $invoice, $cv); // $this->processOtherServices($total, $invoice, $stype); break; case ServiceType::OVERHEAT_ASSISTANCE: $this->processOverheat($total, $invoice, $cv, $has_coolant); break; case ServiceType::EMERGENCY_REFUEL: //error_log('processing refuel'); $ftype = $criteria->getCustomerVehicle()->getFuelType(); $this->processRefuel($total, $invoice, $cv); break; } // TODO: check if any promo is applied // apply discounts $promos = $criteria->getPromos(); $invoice->setTotalPrice($total['total_price']) ->setVATExclusivePrice($total['vat_ex_price']) ->setVAT($total['vat']) ->setDiscount($total['discount']) ->setTradeIn($total['ti_rate']); // dump //Debug::dump($invoice, 1); return $invoice; } protected function getCustomerTagInfo($cv) { $cust_tag_info = []; // get customer and customer tags using customer vehicle $customer = $cv->getCustomer(); $customer_tags = $customer->getCustomerTagObjects(); if (!empty($customer_tags)) { foreach ($customer_tags as $customer_tag) { // TODO: can we keep the tag ids hardcoded? // check tag details // get first tag found $cust_tag_type = $customer_tag->getTagDetails('type'); // TODO: might have to make this statement be more generic? if (($cust_tag_type != null) && ($cust_tag_type == 'one-time-discount')) { $cust_tag_info[] = [ 'id' => $customer_tag->getID(), 'type' => $cust_tag_type, 'discount_type' => $customer_tag->getTagDetails('discount_type'), 'discount_amount' => $customer_tag->getTagDetails('discount_amount'), 'discount_value' => $customer_tag->getTagDetails('discount_value'), 'invoice_display' => $customer_tag->getTagDetails('invoice_display'), ]; break; } } } return $cust_tag_info; } protected function processVAT(&$total) { // total trade in amount to deduct from SRP before we compute VAT $total_ti_amt = 0; // need to check if there are trade-ins if (isset($total['ti_rate'])) $total_ti_amt = $total['ti_rate']; // compute the vat for battery // get the total selling price. That should be SRP. // we need to deduct the trade-ins before computing VAT $total_srp = $total['sell_price']; $total_price_minus_ti = bcsub($total_srp, $total_ti_amt, 2); $total_vat = $this->getTaxAmount($total_price_minus_ti); $total_vat_ex_price = bcsub($total_price_minus_ti, $total_vat, 2); $total['vat'] = $total_vat; $total['vat_ex_price'] = $total_vat_ex_price; } }