Work on product costing (broadband) and reporting

This commit is contained in:
Deon George
2020-02-18 22:35:20 +11:00
parent f8d998d935
commit 910edfd89f
19 changed files with 762 additions and 45 deletions

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AdslSupplier extends Model
{
protected $table = 'ab_adsl_supplier';
}

View File

@@ -3,11 +3,139 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class AdslSupplierPlan extends Model
{
protected $table = 'ab_adsl_supplier_plan';
/**
* Determine how traffic is counted for Broadband links.
*
* Configuration allows for traffic to fit into the following categories:
* + down_peak, when not NULL, traffic is included to value of this metric, extra traffic is charged per extra_peak
* + down_offpeak, when not NULL, traffic is included to the value of metric, extra traffic is charged per extra_offpeak
*
* If:
* + UPLOADS are charged and there are no PEAK/OFFPEAK periods (therefore all
* traffic is charged), the allowance will be shown as 1 metric - TRAFFIC.
* + UPLOADS are charged and there are PEAK/OFFPEAK periods the allowance
* will be shown as 2 metrics - PEAK/OFFPEAK.
* + UPLOADS are NOT charged and there are no PEAK/OFFPEAK periods the allowance
* will be shown as 1 metrics - TRAFFIC.
* + UPLOADS are NOT charged and there are PEAK/OFFPEAK periods the allowance
* will be shown as 2 metrics - PEAK/OFFPEAK.
*
* Thus:
* (x = up/down, Y=peak/offpeak)
*
* + If base_x_Y is NULL, all Y traffic is FREE (ignore respective extra_x_Y setting).
* + If base_x_Y is a number, all Y traffic is FREE up to the number (evaluate extra_x_Y setting).
* + If extra_x_Y is a number, charge this amount for traffic over base_x_Y.
*
* + If extra_down_peak is NULL this is invalid, treat base_down_peak as NULL
* + If extra_down_offpeak is NULL add traffic_down_offpeak to traffic_down_peak
* + If extra_up_peak is NULL add traffic_up_peak to traffic_down_peak
* + If extra_up_offpeak is NULL add traffic_up_offpeak to traffic_down_offpeak
*
* @param array $config The configuration of the link, if NULL assume the supplieres configuration
* @param array $data The traffic used on this link, determine whats left or over
* @param bool $format @deprecate
* @param bool $over @deprecate
* @param bool $ceil Round the numbers to integers
* @return array|string
*/
public function allowance(Collection $config=NULL,array $data=[],$ceil=TRUE) {
// Map the table fields, with the extra fields
$map = collect([
'base_up_offpeak'=>'extra_up_offpeak',
'base_down_offpeak'=>'extra_down_offpeak',
'base_up_peak'=>'extra_up_peak',
'base_down_peak'=>'extra_down_peak',
]);
// Map the NULL relationships - and where traffic gets applied if NULL
$merge = collect([
'extra_up_offpeak'=>'base_down_offpeak',
'extra_down_offpeak'=>'base_down_peak',
'extra_up_peak'=>'base_down_peak',
'extra_down_peak'=>'base_down_peak',
]);
if (is_null($config))
$config = collect($config);
// If config is null, use the configuration from this Model
if (! $config->count()) {
// Base Config
foreach ($map->keys() as $k) {
$config->put($k,$this->{$k});
}
// Excess Config
foreach ($map->values() as $k) {
$config->put($k,$this->{$k});
}
// Shaped or Charge
$config->put('shaped',$this->extra_shaped);
$config->put('charged',$this->extra_charged);
// Metric - used to round down data in $data.
$config->put('metric',$this->metric);
}
$result = collect();
// If data is empty, we'll report on allowance, otherwise we'll report on consumption
$report = $data ? FALSE : TRUE;
// Work out if we charge each period
foreach ($map as $k => $v) {
// Anything NULL is not counted
if (is_null($config->get($k)))
continue;
$x = $report ? $config->get($k) : ($config->get($k)-Arr::get($data,$k,0));
if ($ceil)
$x = (int)ceil($x);
// Non-NULL entries are counted as is
if (! is_null($config->get($v))) {
// Existing value for this item to be added
$value = $result->has($k) ? $result->get($k) : 0;
$result->put($k,$value+$x);
// NULL entries are merged into another key
} else {
// New Key for this item
$key = $merge->get($v);
// Existing value for this item to be added
$value = $result->has($key) ? $result->get($key) : 0;
// Any value in the existing key, add it too.
if ($k !== $key AND $result->has($k)) {
$value += $result->get($k);
$result->forget($k);
}
$result->put($key,$value+$x);
}
}
if ($config->has('metric') AND $config->get('metric'))
$result->transform(function($item) use ($config,$ceil) {
return $ceil
? (int)ceil($item/$config->get('metric'))
: $item/$config->get('metric');
});
return $result;
}
public function getNameAttribute()
{
return $this->speed;

View File

@@ -7,6 +7,7 @@ use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use App\Traits\NextKey;
use Illuminate\Support\Facades\Log;
class Product extends Model
{
@@ -132,7 +133,12 @@ class Product extends Model
public function getPriceArrayAttribute()
{
return unserialize($this->attributes['price_group']);
try {
return unserialize($this->attributes['price_group']);
} catch (\Exception $e) {
Log::debug('Problem with Price array in product ',['pid'=>$this->id]);
return [];
}
}
public function getPriceTypeAttribute()

View File

@@ -2,30 +2,65 @@
namespace App\Models\Product;
use App\Traits\NextKey;
use App\Models\AdslSupplierPlan;
use Illuminate\Support\Collection;
class Adsl extends \App\Models\Base\ProductType
use App\Interfaces\ProductSupplier;
use App\Models\Base\ProductType;
use App\Models\AdslSupplier;
use App\Models\AdslSupplierPlan;
use App\Traits\NextKey;
class Adsl extends ProductType implements ProductSupplier
{
use NextKey;
const RECORD_ID = 'adsl_plan';
protected $table = 'ab_adsl_plan';
public static $map = [
'base_up_offpeak'=>'extra_up_offpeak',
'base_down_offpeak'=>'extra_down_offpeak',
'base_up_peak'=>'extra_up_peak',
'base_down_peak'=>'extra_down_peak',
];
/**
* Map upstream metrics into traffic allowance metrics
*
* @var array
*/
public static $metrics = [
'down_peak'=>'base_down_peak',
'down_offpeak'=>'base_down_offpeak',
'up_peak'=>'base_up_peak',
'up_offpeak'=>'base_up_offpeak',
'peer'=>'base_down_peak',
'internal'=>'base_down_offpeak',
];
/**
* The suppliers product
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function product()
{
return $this->hasOne(AdslSupplierPlan::class,'id','adsl_supplier_plan_id');
}
/**
* The supplier
*
* @return \Illuminate\Database\Eloquent\Relations\HasOneThrough
*/
public function supplier()
{
return $this->hasOneThrough(AdslSupplier::class,AdslSupplierPlan::class,'id','id','adsl_supplier_plan_id','supplier_id');
}
public function __get($key)
{
switch($key)
{
case 'base_down_peak':
return $this->attributes['base_down_peak']/$this->attributes['metric'];
case 'base_down_peak':
return $this->attributes['base_down_offpeak']/$this->attributes['metric'];
switch($key) {
case 'speed':
return $this->product->speed;
}
@@ -33,4 +68,87 @@ class Adsl extends \App\Models\Base\ProductType
// If we dont have a specific key, we'll resolve it normally
return parent::__get($key);
}
/** ATTRIBUTES **/
/**
* Calculate the allowance array or traffic used array
*
* @param array Traffic Used in each metric.
* @param bool $ceil Round the numbers to integers
* @return array|string
*/
public function allowance(array $data=[],bool $ceil=TRUE): Collection
{
$config = collect();
// Base Config
foreach (array_keys(static::$map) as $k) {
$config->put($k,$this->{$k});
}
// Excess Config
foreach (array_values(static::$map) as $k) {
$config->put($k,$this->{$k});
}
// Shaped or Charge
$config->put('shaped',$this->extra_shaped);
$config->put('charged',$this->extra_charged);
// Metric - used to round down data in $data.
$config->put('metric',$this->metric);
return $this->product->allowance($config,$data,$ceil);
}
/**
* Return the suppliers cost for this service
*
* @return float
*/
public function allowance_cost(): float
{
$result = 0;
foreach ($this->product->allowance(NULL,$this->allowance([])->toArray()) as $k=>$v) {
$result += -$v*$this->product->{static::$map[$k]};
}
return $result;
}
/**
* Render the allowance as a string
* eg: 50/100
*
* @return string
*/
public function allowance_string(): string
{
$result = '';
$data = $this->allowance();
foreach ([
'base_down_peak',
'base_up_peak',
'base_down_offpeak',
'base_up_offpeak',
] as $k)
{
if ($data->has($k)) {
if ($result)
$result .= '/';
$result .= $data->get($k);
}
}
return $result;
}
public function getCostAttribute(): float
{
// @todo Tax shouldnt be hard coded
return ($this->product->base_cost+$this->allowance_cost())*1.1;
}
}

View File

@@ -2,13 +2,34 @@
namespace App\Models\Product;
use Illuminate\Support\Collection;
use App\Interfaces\ProductSupplier;
use App\Models\Base\ProductType;
use App\Traits\NextKey;
class SSL extends \App\Models\Base\ProductType
class SSL extends ProductType implements ProductSupplier
{
use NextKey;
const RECORD_ID = 'ssl';
protected $table = 'ab_ssl';
public function allowance(): Collection
{
// N/A
return collect();
}
public function allowance_string(): string
{
// N/A
return '';
}
public function getCostAttribute(): float
{
// N/A
return 0;
}
}

View File

@@ -301,6 +301,22 @@ class Service extends Model
return $this->addTax(is_null($this->price) ? $this->product->price($this->recur_schedule) : $this->price);
}
public function getBillingMonthlyPriceAttribute(): float
{
$d = 0;
switch ($this->recur_schedule) {
case 0: $d = 12/52; break;
case 1: $d = 1; break;
case 2: $d = 3; break;
case 3: $d = 6; break;
case 4: $d = 12; break;
case 5: $d = 24; break;
case 6: $d = 36; break;
}
return number_format($this->getBillingPriceAttribute()/$d,2);
}
/**
* Return the service billing period
*