resq/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php

845 lines
27 KiB
PHP

<?php
namespace App\Service\InvoiceGenerator;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Doctrine\ORM\EntityManagerInterface;
use App\Ramcar\InvoiceCriteria;
use App\Ramcar\InvoiceStatus;
use App\Ramcar\TradeInType;
use App\Ramcar\DiscountApply;
use App\Ramcar\ServiceType;
use App\Ramcar\FuelType;
use App\Entity\Invoice;
use App\Entity\InvoiceItem;
use App\Entity\User;
use App\Entity\Battery;
use App\Entity\Promo;
use App\Entity\Customer;
use App\Service\InvoiceGeneratorInterface;
use Doctrine\Common\Util\Debug;
class ResqInvoiceGenerator implements InvoiceGeneratorInterface
{
const TAX_RATE = 0.12;
const SERVICE_FEE = 300;
const RECHARGE_FEE = 300;
const TROUBLESHOOTING_FEE = 150;
const BATT_REPLACEMENT_FEE = 0;
const WARRANTY_FEE = 0;
const OTHER_SERVICES_FEE = 200;
const COOLANT_FEE = 1600;
const REFUEL_FEE_GAS = 320; // for 4 liters
const REFUEL_FEE_DIESEL = 340; // for 4 liters
private $security;
protected $em;
protected $validator;
// creates invoice based on the criteria sent
public function __construct(Security $security, EntityManagerInterface $em,
ValidatorInterface $validator)
{
$this->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();
$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;
}
// commenting this out since not used. Reverting back to the old VAT computation
/*
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;
}
*/
}