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(); // 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); 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(); if ($user != null) { $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); } } 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; } public 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); } 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(); $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) { $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); } 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; } }