Merge branch '744-new-invoice-service' into '746-resq-2-0-final'

Resolve "New invoice service"

See merge request jankstudio/resq!860
This commit is contained in:
Ramon Gutierrez 2023-07-10 12:48:30 +00:00
commit f26d6284f3
28 changed files with 3651 additions and 6 deletions

View file

@ -115,7 +115,10 @@ services:
App\Service\InvoiceGenerator\ResqInvoiceGenerator: ~
# invoice generator interface
App\Service\InvoiceGeneratorInterface: "@App\\Service\\InvoiceGenerator\\ResqInvoiceGenerator"
App\Service\InvoiceGeneratorInterface: "@App\\Service\\InvoiceManager"
# invoice manager
App\Service\InvoiceManager: ~
# job order generator
App\Service\JobOrderHandler\ResqJobOrderHandler:

File diff suppressed because it is too large Load diff

View file

@ -2865,6 +2865,9 @@ class APIController extends Controller implements LoggedController
$icrit = new InvoiceCriteria();
$icrit->setServiceType($stype);
// set taxable
$icrit->setIsTaxable(true);
// check promo
$promo_id = $req->request->get('promo_id');
if (!empty($promo_id))

View file

@ -1201,6 +1201,7 @@ class RiderAppController extends APIController
$crit->setServiceType($stype_id);
$crit->setCustomerVehicle($cv);
$crit->setHasCoolant($jo->hasCoolant());
$crit->setIsTaxable();
if ($promo != null)
$crit->addPromo($promo);

View file

@ -94,6 +94,9 @@ class InvoiceController extends ApiController
$icrit->addEntry($batt, $trade_in, 1);
// set if taxable
$icrit->setIsTaxable();
// send to invoice generator
$invoice = $ic->generateInvoice($icrit);

View file

@ -658,6 +658,9 @@ class JobOrderController extends ApiController
$icrit->addBattery($batt);
*/
// NOTE: trade in is currently not supported. Would it be better
// if we remove trade-in as a required parameter? Or just leave it be
// and simply not process it?
// check trade-in
// only allow motolite, other, none
switch ($trade_in) {
@ -672,6 +675,9 @@ class JobOrderController extends ApiController
$icrit->addEntry($batt, $trade_in, 1);
// set taxable
$icrit->setIsTaxable();
// send to invoice generator
$invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice);
@ -1074,6 +1080,9 @@ class JobOrderController extends ApiController
break;
}
// set taxable
$icrit->setIsTaxable();
$icrit->addEntry($batt, $trade_in, 1);
// send to invoice generator

View file

@ -737,7 +737,8 @@ class JobOrderController extends Controller
// instantiate invoice criteria
$criteria = new InvoiceCriteria();
$criteria->setServiceType($stype)
->setCustomerVehicle($cv);
->setCustomerVehicle($cv)
->setIsTaxable();
/*

View file

@ -157,6 +157,9 @@ class JobOrderController extends APIController
$icrit->setCustomerVehicle($data['customer_vehicle']);
// set taxable
$icrit->setIsTaxable();
$icrit->addEntry($data['batt'], $data['trade_in_type'], 1);
// send to invoice generator

View file

@ -0,0 +1,120 @@
<?php
namespace App\InvoiceRule;
use App\InvoiceRuleInterface;
use Doctrine\ORM\EntityManagerInterface;
use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType;
use App\Entity\Battery;
class BatteryReplacementWarranty implements InvoiceRuleInterface
{
protected $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getID()
{
return 'battery_warranty';
}
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$items = [];
if ($stype == $this->getID())
{
// get the entries
$entries = $criteria->getEntries();
foreach($entries as $entry)
{
$batt = $entry['battery'];
$qty = 1;
$price = $this->getServiceTypeFee();
$items[] = [
'service_type' => $this->getID(),
'battery' => $batt,
'qty' => $qty,
'title' => $this->getTitle($batt),
'price' => $price,
];
$qty_price = bcmul($price, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
}
return $items;
}
public function getServiceTypeFee()
{
// TODO: we need to to put this somewhere like in .env
// so that if any chanages are to be made, we just edit the file
// instead of the code
return 0;
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
// check service type. Only battery sales and battery warranty should have invoice items. Since this is the
// battery replacement warranty rule, we only check for battery replacement warranty.
$stype = $criteria->getServiceType();
if ($stype != ServiceType::BATTERY_REPLACEMENT_WARRANTY)
return null;
// return error if there's a problem, false otherwise
if (!empty($invoice_items))
{
// check if this is a valid battery
foreach ($invoice_items as $item)
{
$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;
// 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 getTitle($battery)
{
$title = $battery->getModel()->getName() . ' ' . $battery->getSize()->getName() . ' - Service Unit';
return $title;
}
}

View file

@ -0,0 +1,123 @@
<?php
namespace App\InvoiceRule;
use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface;
use App\Ramcar\TradeInType;
use App\Ramcar\ServiceType;
use App\Entity\Battery;
class BatterySales implements InvoiceRuleInterface
{
protected $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getID()
{
return 'battery_new';
}
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$items = [];
if ($stype == $this->getID())
{
// get the entries
$entries = $criteria->getEntries();
foreach($entries as $entry)
{
$batt = $entry['battery'];
$qty = $entry['qty'];
$trade_in = null;
if (isset($entry['trade_in']))
$trade_in = $entry['trade_in'];
$size = $batt->getSize();
if ($trade_in == null)
{
// battery purchase
$price = $batt->getSellingPrice();
$items[] = [
'service_type' => $this->getID(),
'battery' => $batt,
'qty' => $qty,
'title' => $this->getTitle($batt),
'price' => $price,
];
$qty_price = bcmul($price, $qty, 2);
$total['sell_price'] = bcadd($total['sell_price'], $qty_price, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
}
}
return $items;
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
// check service type. Only battery sales and battery warranty should have invoice items. Since this is the battery sales
// rule, we only check for battery sales.
$stype = $criteria->getServiceType();
if ($stype != ServiceType::BATTERY_REPLACEMENT_NEW)
return null;
// return error if there's a problem, false otherwise
if (!empty($invoice_items))
{
// check if this is a valid battery
foreach ($invoice_items as $item)
{
$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;
// 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 getTitle($battery)
{
$title = $battery->getModel()->getName() . ' ' . $battery->getSize()->getName();
return $title;
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace App\InvoiceRule;
use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface;
use App\Ramcar\ServiceType;
use App\Ramcar\DiscountApply;
use App\Entity\Promo;
class DiscountType implements InvoiceRuleInterface
{
protected $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getID()
{
return 'discount';
}
public function compute($criteria, &$total)
{
$items = [];
$promos = $criteria->getPromos();
if (empty($promos))
return $items;
// 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 = bcmul($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);
$num1 = bcdiv(1.5, 0.7, 9);
$num1_rate = bcmul($num1, $rate, 9);
$multiplier = bcsub(1, $num1_rate, 9);
$discount = bcmul($total['sell_price'], $multiplier, 2);
break;
}
// if discount is higher than 0, add to items
if ($discount > 0)
{
$qty = 1;
$price = bcmul(-1, $discount, 2);
$items[] = [
'promo' => $promo,
'title' => $this->getTitle(),
'qty' => $qty,
'price' => $price,
];
}
$total['discount'] = $discount;
$total['total_price'] = bcsub($total['total_price'], $discount, 2);
return $items;
}
public function validatePromo($criteria, $promo_id)
{
// return error if there's a problem, false otherwise
// check service type
$stype = $criteria->getServiceType();
// discount/promo only applies for battery sales
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 validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getTitle()
{
$title = 'Promo discount';
return $title;
}
}

133
src/InvoiceRule/Fuel.php Normal file
View file

@ -0,0 +1,133 @@
<?php
namespace App\InvoiceRule;
use App\InvoiceRuleInterface;
use App\Ramcar\FuelType;
use App\Ramcar\ServiceType;
class Fuel implements InvoiceRuleInterface
{
public function getID()
{
return 'fuel';
}
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$items = [];
if ($stype == $this->getID())
{
// check if customer vehicle has a motolite battery
$cv = $criteria->getCustomerVehicle();
if ($cv->hasMotoliteBattery())
$fee = 0;
else
$fee = $this->getServiceTypeFee();
$ftype = $cv->getFuelType();
// add the service fee to items
$qty = 1;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle($ftype),
'price' => $fee,
];
$qty_fee = bcmul($qty, $fee, 2);
$total_price = $qty_fee;
switch ($ftype)
{
case FuelType::GAS:
case FuelType::DIESEL:
$qty = 1;
$price = $this->getFuelFee($ftype);
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getTitle($ftype),
'price' => $price,
];
$qty_price = bcmul($price, $qty, 2);
$total_price = bcadd($total_price, $qty_price, 2);
break;
default:
$qty = 1;
$price = 0;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getTitle('Unknown'),
'price' => $price,
];
$qty_price = bcmul($price, $qty, 2);
$total_price = bcadd($total_price, $qty_price, 2);
break;
}
$total['total_price'] = bcadd($total['total_price'], $total_price, 2);
}
return $items;
}
public function getServiceTypeFee()
{
// TODO: we need to to put this somewhere like in .env
// so that if any changes are to be made, we just edit the file
// instead of the code
return 300;
}
public function getFuelFee($fuel_type)
{
// TODO: we need to to put this somewhere like in .env
// so that if any changes are to be made, we just edit the file
// instead of the code
if ($fuel_type == FuelType::GAS)
{
// gas fuel fee
return 320;
}
else
{
// diesel fuel fee
return 340;
}
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getTitle($fuel_type)
{
$title = '4L - ' . ucfirst($fuel_type);
return $title;
}
protected function getServiceTitle($fuel_type)
{
$title = 'Service - ' . ServiceType::getName(ServiceType::EMERGENCY_REFUEL);
return $title;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace App\InvoiceRule;
use App\InvoiceRuleInterface;
class Jumpstart implements InvoiceRuleInterface
{
public function getID()
{
return 'jumpstart_troubleshoot';
}
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$items = [];
if ($stype == $this->getID())
{
$fee = $this->getServiceTypeFee();
// add the service fee to items
$qty = 1;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
];
$qty_price = bcmul($fee, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
return $items;
}
public function getServiceTypeFee()
{
// TODO: we need to to put this somewhere like in .env
// so that if any chanages are to be made, we just edit the file
// instead of the code
return 150;
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getServiceTitle()
{
$title = 'Service - Troubleshooting fee';
return $title;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace App\InvoiceRule;
use App\InvoiceRuleInterface;
class JumpstartWarranty implements InvoiceRuleInterface
{
public function getID()
{
return 'jumpstart_warranty';
}
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$items = [];
if ($stype == $this->getID())
{
$fee = $this->getServiceTypeFee();
// add the service fee to items
$qty = 1;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
];
$qty_price = bcmul($fee, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
return $items;
}
public function getServiceTypeFee()
{
// TODO: we need to to put this somewhere like in .env
// so that if any chanages are to be made, we just edit the file
// instead of the code
return 0;
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getServiceTitle()
{
$title = 'Service - Troubleshooting fee';
return $title;
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace App\InvoiceRule;
use App\InvoiceRuleInterface;
use App\Ramcar\ServiceType;
class Overheat implements InvoiceRuleInterface
{
public function getID()
{
return 'overheat';
}
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$has_coolant = $criteria->hasCoolant();
$items = [];
if ($stype == $this->getID())
{
// check if customer vehicle has a motolite battery
$cv = $criteria->getCustomerVehicle();
if ($cv->hasMotoliteBattery())
$fee = 0;
else
$fee = $this->getServiceTypeFee();
// add the service fee to items
$qty = 1;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
];
$qty_fee = bcmul($qty, $fee, 2);
$total_price = $qty_fee;
if ($has_coolant)
{
$coolant_fee = $this->getCoolantFee();
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceCoolantTitle(),
'price' => $coolant_fee,
];
$qty_price = bcmul($coolant_fee, $qty, 2);
$total_price = bcadd($total_price, $qty_price, 2);
}
$total['total_price'] = bcadd($total['total_price'], $total_price, 2);
}
return $items;
}
public function getServiceTypeFee()
{
// TODO: we need to to put this somewhere like in .env
// so that if any chanages are to be made, we just edit the file
// instead of the code
return 300;
}
public function getCoolantFee()
{
// TODO: we need to to put this somewhere like in .env
// so that if any chanages are to be made, we just edit the file
// instead of the code
return 1600;
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getServiceTitle()
{
$title = 'Service - ' . ServiceType::getName(ServiceType::OVERHEAT_ASSISTANCE);
return $title;
}
protected function getServiceCoolantTitle()
{
$title = '4L Coolant';
return $title;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace App\InvoiceRule;
use App\InvoiceRuleInterface;
class PostRecharged implements InvoiceRuleInterface
{
public function getID()
{
return 'post_recharged';
}
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$items = [];
if ($stype == $this->getID())
{
$fee = $this->getServiceTypeFee();
$qty = 1;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
];
$qty_price = bcmul($fee, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
return $items;
}
public function getServiceTypeFee()
{
// TODO: we need to to put this somewhere like in .env
// so that if any chanages are to be made, we just edit the file
// instead of the code
return 300;
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getServiceTitle()
{
$title = 'Recharge fee';
return $title;
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\InvoiceRule;
use App\InvoiceRuleInterface;
class PostReplacement implements InvoiceRuleInterface
{
public function getID()
{
return 'post_replacement';
}
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$items = [];
if ($stype == $this->getID())
{
$fee = $this->getServiceTypeFee();
$qty = 1;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
];
$qty_price = bcmul($fee, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
return $items;
}
public function getServiceTypeFee()
{
// TODO: we need to to put this somewhere like in .env
// so that if any chanages are to be made, we just edit the file
// instead of the code
return 0;
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getServiceTitle()
{
$title = 'Battery replacement';
return $title;
}
}

110
src/InvoiceRule/Tax.php Normal file
View file

@ -0,0 +1,110 @@
<?php
namespace App\InvoiceRule;
use App\InvoiceRuleInterface;
use App\Ramcar\ServiceType;
class Tax implements InvoiceRuleInterface
{
public function getID()
{
return 'tax';
}
public function compute($criteria, &$total)
{
// check if taxable
if (!$criteria->isTaxable())
{
// nothing to compute
return [];
}
$tax_rate = $this->getTaxRate();
$is_battery_sales = false;
$total_price = 0;
// compute tax per item if service type is battery sales
$stype = $criteria->getServiceType();
if ($stype == ServiceType::BATTERY_REPLACEMENT_NEW)
{
// get the entries
$entries = $criteria->getEntries();
foreach($entries as $entry)
{
// check if entry is trade-in
if (isset($entry['trade_in']))
{
// continue to next entry
continue;
}
$battery = $entry['battery'];
$qty = $entry['qty'];
$price = $battery->getSellingPrice();
$vat = $this->getTaxAmount($price, $tax_rate);
$qty_price = bcmul($price, $qty, 2);
$qty_vat = bcmul($vat, $qty, 2);
$price_minus_vat = bcsub($price, $vat, 2);
$qty_price_minus_vat = bcmul($price_minus_vat, $qty, 2);
$total['vat'] = bcadd($total['vat'], $qty_vat, 2);
$total['vat_ex_price'] = bcadd($total['vat_ex_price'], $qty_price_minus_vat, 2);
}
}
else
{
// compute VAT after adding all item costs, if service type is not battery sales
$total_price = $total['total_price'];
$vat_ex_price = $this->getTaxExclusivePrice($total_price, $tax_rate);
$vat = bcsub($total_price, $vat_ex_price, 2);
$total['vat_ex_price'] = $vat_ex_price;
$total['vat'] = $vat;
}
return [];
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getTaxAmount($price, $tax_rate)
{
$vat_ex_price = $this->getTaxExclusivePrice($price, $tax_rate);
$tax_amount = bcsub($price, $vat_ex_price, 2);
return $tax_amount;
}
protected function getTaxExclusivePrice($price, $tax_rate)
{
$tax_ex_price = bcdiv($price, bcadd(1, $tax_rate, 2), 2);
return $tax_ex_price;
}
protected function getTaxRate()
{
// TODO: we need to to put this somewhere like in .env
// so that if any chanages are to be made, we just edit the file
// instead of the code
return 0.12;
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace App\InvoiceRule;
use App\InvoiceRuleInterface;
class TireRepair implements InvoiceRuleInterface
{
public function getID()
{
return 'tire';
}
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$items = [];
if ($stype == $this->getID())
{
// check if customer vehicle has a motolite battery
$cv = $criteria->getCustomerVehicle();
if ($cv->hasMotoliteBattery())
$fee = 0;
else
$fee = $this->getServiceTypeFee();
// add the service fee to items
$qty = 1;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
];
$qty_price = bcmul($fee, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
return $items;
}
protected function getServiceTypeFee()
{
// TODO: we need to to put this somewhere like in .env
// so that if any chanages are to be made, we just edit the file
// instead of the code
return 300;
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getServiceTitle()
{
$title = 'Service - Flat Tire';
return $title;
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace App\InvoiceRule;
use App\InvoiceRuleInterface;
use App\Ramcar\TradeInType;
class TradeIn implements InvoiceRuleInterface
{
public function getID()
{
return 'trade-in';
}
public function compute($criteria, &$total)
{
$items = [];
// get the entries
$entries = $criteria->getEntries();
foreach($entries as $entry)
{
$batt = $entry['battery'];
$qty = $entry['qty'];
$trade_in_type = null;
if (isset($entry['trade_in']))
$trade_in_type = $entry['trade_in'];
if ($trade_in_type != null)
{
$ti_rate = $this->getTradeInRate($batt, $trade_in_type);
$qty_ti = bcmul($ti_rate, $qty, 2);
$total['ti_rate'] = bcadd($total['ti_rate'], $qty_ti, 2);
$total['total_price'] = bcsub($total['total_price'], $qty_ti, 2);
$price = bcmul($ti_rate, -1, 2);
$items[] = [
'qty' => $qty,
'title' => $this->getTitle($batt, $trade_in_type),
'price' => $price,
];
}
}
return $items;
}
public function validatePromo($criteria, $promo_id)
{
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getTradeInRate($battery, $trade_in_type)
{
$size = $battery->getSize();
switch ($trade_in_type)
{
case TradeInType::MOTOLITE:
return $size->getTIPriceMotolite();
case TradeInType::PREMIUM:
return $size->getTIPricePremium();
case TradeInType::OTHER:
return $size->getTIPriceOther();
}
return 0;
}
protected function getTitle($battery, $trade_in_type)
{
$title = 'Trade-in ' . TradeInType::getName($trade_in_type) . ' ' . $battery->getSize()->getName() . ' battery';
return $title;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App;
interface InvoiceRuleInterface
{
// validate promo
public function validatePromo($criteria, $promo_id);
// validate invoice items
public function validateInvoiceItems($criteria, $invoice_items);
// compute
// this returns an array of items or empty array
public function compute($criteria, &$total);
}

View file

@ -15,6 +15,7 @@ class InvoiceCriteria
protected $flag_coolant;
protected $discount;
protected $service_charges;
protected $flag_taxable;
// entries are battery and trade-in combos
protected $entries;
@ -28,6 +29,7 @@ class InvoiceCriteria
$this->flag_coolant = false;
$this->discount = 0;
$this->service_charges = [];
$this->flag_taxable = false;
}
public function setServiceType($stype)
@ -153,4 +155,15 @@ class InvoiceCriteria
return $this->service_charges;
}
public function setIsTaxable($flag = true)
{
$this->flag_taxable = $flag;
return $this;
}
public function isTaxable()
{
return $this->flag_taxable;
}
}

View file

@ -0,0 +1,285 @@
<?php
namespace App\Service;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRule;
use App\Service\InvoiceGeneratorInterface;
use App\Ramcar\InvoiceCriteria;
use App\Ramcar\InvoiceStatus;
use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType;
use App\Entity\Invoice;
use App\Entity\InvoiceItem;
use App\Entity\User;
use App\Entity\Battery;
use App\Entity\Promo;
class InvoiceManager implements InvoiceGeneratorInterface
{
private $security;
protected $em;
protected $validator;
protected $available_rules;
public function __construct(EntityManagerInterface $em, Security $security, ValidatorInterface $validator)
{
$this->em = $em;
$this->security = $security;
$this->validator = $validator;
$this->available_rules = $this->getAvailableRules();
}
public function getAvailableRules()
{
// 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(),
new InvoiceRule\JumpstartWarranty(),
new InvoiceRule\PostRecharged(),
new InvoiceRule\PostReplacement(),
new InvoiceRule\Overheat(),
new InvoiceRule\Fuel(),
new InvoiceRule\TireRepair(),
new InvoiceRule\DiscountType($this->em),
new InvoiceRule\TradeIn(),
new InvoiceRule\Tax(),
];
}
// this is called when JO is submitted
public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, &$error_array)
{
// instantiate the invoice criteria
$criteria = new InvoiceCriteria();
$criteria->setServiceType($jo->getServiceType())
->setCustomerVehicle($jo->getCustomerVehicle());
// set if taxable
// NOTE: ideally, this should be a parameter when calling generateInvoiceCriteria. But that
// would mean adding it as a parameter to the call, impacting all calls
$criteria->setIsTaxable();
foreach ($this->available_rules as $avail_rule)
{
$ierror = $avail_rule->validatePromo($criteria, $promo_id);
// break out of loop when error found
if ($ierror)
break;
}
if (!$ierror && !empty($invoice_items))
{
// validate the invoice items (batteries and trade ins)
foreach ($this->available_rules as $avail_rule)
{
$ierror = $avail_rule->validateInvoiceItems($criteria, $invoice_items);
// break out of loop when error found
if ($ierror)
break;
}
}
if ($ierror)
{
$error_array['invoice'] = $ierror;
}
else
{
// generate the invoice
$invoice = $this->generateInvoice($criteria);
// validate
$ierrors = $this->validator->validate($invoice);
// 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
$this->em->remove($old_invoice);
$this->em->flush();
}
// add invoice to JO
$jo->setInvoice($invoice);
$this->em->persist($invoice);
}
}
// this is called by JobOrderController when JS script generateInvoice is called
public function generateDraftInvoice($criteria, $promo_id, $service_charges, $items)
{
foreach ($this->available_rules as $avail_rule)
{
$ierror = $avail_rule->validatePromo($criteria, $promo_id);
// break out of loop when error found
if ($ierror)
break;
}
if (!$ierror && !empty($items))
{
// validate the invoice items (batteries and trade ins)
foreach ($this->available_rules as $avail_rule)
{
$ierror = $avail_rule->validateInvoiceItems($criteria, $items);
// break out of loop when error found
if ($ierror)
break;
}
}
return $ierror;
}
// called by the following:
// (1) JobOrderController when JS script generateInvoice is called
// (2) APIController from newRequestJobOrder
// (3) generateInvoiceCriteria
// (4) RiderAPIHandler's changeService
// (5) TAPI's JobOrderController
public function generateInvoice($criteria)
{
// no need to validate since generateDraftInvoice was called before this was called
// generate the invoice and from APIController, the fields were validated
$invoice_data = $this->compute($criteria);
$invoice = $this->createInvoice($invoice_data);
$invoice_items = $invoice->getItems();
return $invoice;
}
public function compute($criteria)
{
// initialize
$total = [
'sell_price' => 0.0,
'vat' => 0.0,
'vat_ex_price' => 0.0,
'ti_rate' => 0.0,
'total_price' => 0.0,
'discount' => 0.0,
];
// get what is in criteria
$stype = $criteria->getServiceType();
$entries = $criteria->getEntries();
$promos = $criteria->getPromos();
$is_taxable = $criteria->isTaxable();
$invoice_items = [];
$data = [];
foreach ($this->available_rules as $rule)
{
$items = $rule->compute($criteria, $total);
$promo = null;
if (count($items) > 0)
{
foreach ($items as $item)
{
$title = $item['title'];
$quantity = $item['qty'];
$price = $item['price'];
$battery = null;
if (isset($item['battery']))
$battery = $item['battery'];
$promo = null;
if (isset($item['promo']))
$promo = $item['promo'];
$invoice_items[] = [
'title' => $title,
'quantity' => $quantity,
'price' => $price,
'battery' => $battery,
'promo' => $promo,
];
}
}
}
// also need to return the total and the promo
$data[] = [
'promo' => $promo,
'invoice_items' => $invoice_items,
'total' => $total,
];
return $data;
}
protected function createInvoice($invoice_data)
{
$invoice = new Invoice();
// get current user
$user = $this->security->getUser();
// check if user is User or APIUser
if ($user instanceof User)
{
$invoice->setCreatedBy($user);
}
foreach ($invoice_data as $data)
{
$invoice_items = $data['invoice_items'];
$total = $data['total'];
// check if promo is set
if (isset($data['promo']))
$promo = $data['promo'];
foreach ($invoice_items as $item)
{
$invoice_item = new InvoiceItem();
$invoice_item->setInvoice($invoice)
->setTitle($item['title'])
->setQuantity($item['quantity'])
->setPrice($item['price']);
if ($item['battery'] != null)
$invoice_item->setBattery($item['battery']);
$invoice->addItem($invoice_item);
}
$invoice->setTotalPrice($total['total_price'])
->setVATExclusivePrice($total['vat_ex_price'])
->setVAT($total['vat'])
->setDiscount($total['discount'])
->setTradeIn($total['ti_rate'])
->setStatus(InvoiceStatus::DRAFT);
}
return $invoice;
}
}

View file

@ -29,12 +29,13 @@ use App\Entity\CustomerTag;
use App\Entity\EmergencyType;
use App\Entity\OwnershipType;
use App\Entity\CustomerLocation;
use App\Entity\Battery;
use App\Ramcar\InvoiceCriteria;
use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType;
use App\Ramcar\JOEventType;
use App\Ramcar\JOStatus;
use App\Ramcar\InvoiceCriteria;
use App\Ramcar\WarrantyClass;
use App\Ramcar\DiscountApply;
use App\Ramcar\ModeOfPayment;
@ -3487,8 +3488,19 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// db loaded
$params['bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll();
$params['trade_in_bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll();
$params['promos'] = $em->getRepository(Promo::class)->findAll();
// list of batteries for trade-in
$ti_batteries = $em->getRepository(Battery::class)->findAll();
$trade_in_batteries = [];
foreach ($ti_batteries as $ti_battery)
{
$battery_name = $ti_battery->getModel()->getName() . ' ' . $ti_battery->getSize()->getName();
$trade_in_batteries[$ti_battery->getID()] = $battery_name;
}
$params['trade_in_batteries'] = $trade_in_batteries;
// list of emergency types
$e_types = $em->getRepository(EmergencyType::class)->findBy([], ['name' => 'ASC']);
$emergency_types = [];

View file

@ -866,6 +866,9 @@ class ResqRiderAPIHandler implements RiderAPIHandlerInterface
$crit->setCustomerVehicle($cv);
$crit->setHasCoolant($jo->hasCoolant());
// set istaxable
$crit->setIsTaxable();
if ($promo != null)
$crit->addPromo($promo);

View file

@ -0,0 +1,46 @@
<div class="form-group m-form__group row">
<div class="col-lg-1 hide">
<label for="invoice-trade-in-bmfg">Manufacturer</label>
<select class="form-control m-input" id="invoice-trade-in-bmfg">
{% for manufacturer in trade_in_bmfgs %}
<option value="{{ manufacturer.getID() }}">{{ manufacturer.getName() }}</option>
{% endfor %}
</select>
</div>
<div class="col-lg-3">
<label for="invoice-trade-in-battery">Battery For Trade In</label>
<select class="form-control m-input" id="invoice-trade-in-battery" data-value="">
<option value=""></option>
{% for id, battery_name in trade_in_batteries %}
<option value="{{ id }}">{{ battery_name }}</option>
{% endfor %}
</select>
</div>
<div class="col-lg-2">
<label for="invoice-trade-in-type">Trade In</label>
<select class="form-control m-input" name="invoice_trade_in_type" id="invoice-trade-in-type">
<option value="">None</option>
{% for key, type in trade_in_types %}
<option value="{{ key }}">{{ type }}</option>
{% endfor %}
</select>
</div>
<div class="col-lg-2">
<label data-field="no_trade_in_reason">No Trade In Reason</label>
<select class="form-control m-input" id="no-trade-in-reason" name="no_trade_in_reason">
<option value="">Select reason</option>
{% for key, class in no_trade_in_reasons %}
<option value="{{ key }}"{{ obj.getNoTradeInReason == key ? ' selected' }}>{{ class }}</option>
{% endfor %}
</select>
<div class="form-control-feedback hide" data-field="no_trade_in_reason"></div>
</div>
<div class="col-lg-1">
<label for="invoice-trade-in-quantity">Quantity</label>
<input type="text" id="invoice-trade-in-quantity" class="form-control m-input text-right" value="1">
</div>
<div class="col-lg-3">
<div><label>&nbsp;</label></div>
<button type="button" class="btn btn-primary" id="btn-add-trade-in-to-invoice">Add</button>
</div>
</div>

View file

@ -0,0 +1,17 @@
// add trade in battery to invoice
$('#btn-add-trade-in-to-invoice').click(function() {
var bmfg = $("#invoice-trade-in-bmfg").val();
var battery = $("#invoice-trade-in-battery").val();
var tradeIn = $("#invoice-trade-in-type").val();
var qty = $("#invoice-trade-in-quantity").val();
// add to invoice array
invoiceItems.push({
battery: battery,
quantity: qty,
trade_in: tradeIn,
});
// regenerate the invoice
generateInvoice();
});

View file

@ -715,6 +715,7 @@
<option value="">Select a vehicle and manufacturer first</option>
</select>
</div>
<!--
<div class="col-lg-2">
<label for="invoice-trade-in-type">Trade In</label>
<select class="form-control m-input" name="invoice_trade_in_type" id="invoice-trade-in-type">
@ -734,6 +735,7 @@
</select>
<div class="form-control-feedback hide" data-field="no_trade_in_reason"></div>
</div>
-->
<div class="col-lg-1">
<label for="invoice-quantity">Quantity</label>
@ -748,6 +750,7 @@
<button type="button" class="btn btn-danger" id="btn-reset-invoice">Reset</button>
</div>
</div>
{% include('invoice/trade_in.html.twig') %}
{% endif %}
</div>
@ -1190,7 +1193,6 @@
<script src="/assets/vendors/custom/gmaps/gmaps.js" type="text/javascript"></script>
<script>
// location search autocomplete
var input = document.getElementById('m_gmap_address');
@ -1686,11 +1688,13 @@ $(function() {
var invoiceItems = [];
{% include 'invoice/trade_in.js.twig' %}
// add to invoice
$("#btn-add-to-invoice").click(function() {
var bmfg = $("#invoice-bmfg").val();
var battery = $("#invoice-battery").val();
var tradeIn = $("#invoice-trade-in-type").val();
// var tradeIn = $("#invoice-trade-in-type").val();
var qty = $("#invoice-quantity").val();
if (!bmfg || !battery || !qty) {
@ -1714,10 +1718,12 @@ $(function() {
}
// add to invoice array
// TODO: need to figure out how to push battery and tradein separately
// right now, battery and trade in is one entry. Need to separate that into two entries
invoiceItems.push({
battery: battery,
quantity: qty,
trade_in: tradeIn,
trade_in: '',
});
// regenerate the invoice
@ -1752,6 +1758,8 @@ $(function() {
var stype = $("#service_type").val();
var cvid = $("#customer-vehicle").val();
console.log(JSON.stringify(invoiceItems));
// generate invoice values
$.ajax({
method: "POST",