Work on product costing (broadband) and reporting
This commit is contained in:
parent
f8d998d935
commit
910edfd89f
13
app/Http/Controllers/Wholesale/ReportController.php
Normal file
13
app/Http/Controllers/Wholesale/ReportController.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Wholesale;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class ReportController extends Controller
|
||||||
|
{
|
||||||
|
public function products()
|
||||||
|
{
|
||||||
|
return view('a/product/report');
|
||||||
|
}
|
||||||
|
}
|
28
app/Interfaces/ProductSupplier.php
Normal file
28
app/Interfaces/ProductSupplier.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Interfaces;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
interface ProductSupplier {
|
||||||
|
/**
|
||||||
|
* Return the traffic inclusion with the service
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function allowance(): Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the traffic inclusion as a string
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function allowance_string(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the product cost
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public function getCostAttribute(): float;
|
||||||
|
}
|
10
app/Models/AdslSupplier.php
Normal file
10
app/Models/AdslSupplier.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class AdslSupplier extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'ab_adsl_supplier';
|
||||||
|
}
|
@ -3,11 +3,139 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class AdslSupplierPlan extends Model
|
class AdslSupplierPlan extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'ab_adsl_supplier_plan';
|
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()
|
public function getNameAttribute()
|
||||||
{
|
{
|
||||||
return $this->speed;
|
return $this->speed;
|
||||||
|
@ -7,6 +7,7 @@ use Illuminate\Support\Arr;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
use App\Traits\NextKey;
|
use App\Traits\NextKey;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class Product extends Model
|
class Product extends Model
|
||||||
{
|
{
|
||||||
@ -132,7 +133,12 @@ class Product extends Model
|
|||||||
|
|
||||||
public function getPriceArrayAttribute()
|
public function getPriceArrayAttribute()
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
return unserialize($this->attributes['price_group']);
|
return unserialize($this->attributes['price_group']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::debug('Problem with Price array in product ',['pid'=>$this->id]);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPriceTypeAttribute()
|
public function getPriceTypeAttribute()
|
||||||
|
@ -2,30 +2,65 @@
|
|||||||
|
|
||||||
namespace App\Models\Product;
|
namespace App\Models\Product;
|
||||||
|
|
||||||
use App\Traits\NextKey;
|
use Illuminate\Support\Collection;
|
||||||
use App\Models\AdslSupplierPlan;
|
|
||||||
|
|
||||||
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;
|
use NextKey;
|
||||||
|
|
||||||
const RECORD_ID = 'adsl_plan';
|
const RECORD_ID = 'adsl_plan';
|
||||||
|
|
||||||
protected $table = 'ab_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()
|
public function product()
|
||||||
{
|
{
|
||||||
return $this->hasOne(AdslSupplierPlan::class,'id','adsl_supplier_plan_id');
|
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)
|
public function __get($key)
|
||||||
{
|
{
|
||||||
switch($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'];
|
|
||||||
case 'speed':
|
case 'speed':
|
||||||
return $this->product->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
|
// If we dont have a specific key, we'll resolve it normally
|
||||||
return parent::__get($key);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,13 +2,34 @@
|
|||||||
|
|
||||||
namespace App\Models\Product;
|
namespace App\Models\Product;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
use App\Interfaces\ProductSupplier;
|
||||||
|
use App\Models\Base\ProductType;
|
||||||
use App\Traits\NextKey;
|
use App\Traits\NextKey;
|
||||||
|
|
||||||
class SSL extends \App\Models\Base\ProductType
|
class SSL extends ProductType implements ProductSupplier
|
||||||
{
|
{
|
||||||
use NextKey;
|
use NextKey;
|
||||||
|
|
||||||
const RECORD_ID = 'ssl';
|
const RECORD_ID = 'ssl';
|
||||||
|
|
||||||
protected $table = 'ab_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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -301,6 +301,22 @@ class Service extends Model
|
|||||||
return $this->addTax(is_null($this->price) ? $this->product->price($this->recur_schedule) : $this->price);
|
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
|
* Return the service billing period
|
||||||
*
|
*
|
||||||
|
@ -27,5 +27,9 @@ class AuthServiceProvider extends ServiceProvider
|
|||||||
$this->registerPolicies();
|
$this->registerPolicies();
|
||||||
Passport::routes();
|
Passport::routes();
|
||||||
// Passport::enableImplicitGrant();
|
// Passport::enableImplicitGrant();
|
||||||
|
|
||||||
|
Gate::define('wholesaler', function ($user) {
|
||||||
|
return $user->isWholesaler();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
86
database/factories/AdslPlanFactory.php
Normal file
86
database/factories/AdslPlanFactory.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Faker\Generator as Faker;
|
||||||
|
|
||||||
|
$factory->define(App\Models\Product\Adsl::class, function (Faker $faker) {
|
||||||
|
return [
|
||||||
|
'id'=>1,
|
||||||
|
'contract_term'=>12,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$factory->afterMaking(App\Models\Product\Adsl::class, function ($adsl,$faker) {
|
||||||
|
$product = factory(App\Models\AdslSupplierPlan::class)->make();
|
||||||
|
$adsl->setRelation('product',$product);
|
||||||
|
$adsl->adsl_supplier_plan_id = $product->id;
|
||||||
|
});
|
||||||
|
|
||||||
|
$factory->state(App\Models\Product\Adsl::class,'unlimit',[
|
||||||
|
'base_down_peak'=>NULL,
|
||||||
|
'base_up_peak'=>NULL,
|
||||||
|
'base_down_offpeak'=>NULL,
|
||||||
|
'base_up_offpeak'=>NULL,
|
||||||
|
'extra_charged'=>NULL,
|
||||||
|
'extra_shaped'=>NULL,
|
||||||
|
'extra_down_peak'=>NULL,
|
||||||
|
'extra_up_peak'=>NULL,
|
||||||
|
'extra_down_offpeak'=>NULL,
|
||||||
|
'extra_up_offpeak'=>NULL,
|
||||||
|
'metric'=>1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$factory->state(App\Models\Product\Adsl::class,'140/0/0/0',[
|
||||||
|
'base_down_peak'=>140,
|
||||||
|
'base_up_peak'=>0,
|
||||||
|
'base_down_offpeak'=>0,
|
||||||
|
'base_up_offpeak'=>0,
|
||||||
|
'extra_charged'=>NULL,
|
||||||
|
'extra_shaped'=>NULL,
|
||||||
|
'extra_down_peak'=>1,
|
||||||
|
'extra_up_peak'=>NULL,
|
||||||
|
'extra_down_offpeak'=>NULL,
|
||||||
|
'extra_up_offpeak'=>NULL,
|
||||||
|
'metric'=>1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$factory->state(App\Models\Product\Adsl::class,'70/-/0/-',[
|
||||||
|
'base_down_peak'=>70,
|
||||||
|
'base_up_peak'=>NULL,
|
||||||
|
'base_down_offpeak'=>0,
|
||||||
|
'base_up_offpeak'=>NULL,
|
||||||
|
'extra_charged'=>NULL,
|
||||||
|
'extra_shaped'=>NULL,
|
||||||
|
'extra_down_peak'=>1,
|
||||||
|
'extra_up_peak'=>NULL,
|
||||||
|
'extra_down_offpeak'=>NULL,
|
||||||
|
'extra_up_offpeak'=>NULL,
|
||||||
|
'metric'=>1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$factory->state(App\Models\Product\Adsl::class,'100/0/40/0',[
|
||||||
|
'base_down_peak'=>100,
|
||||||
|
'base_up_peak'=>0,
|
||||||
|
'base_down_offpeak'=>40,
|
||||||
|
'base_up_offpeak'=>0,
|
||||||
|
'extra_charged'=>NULL,
|
||||||
|
'extra_shaped'=>NULL,
|
||||||
|
'extra_down_peak'=>0,
|
||||||
|
'extra_up_peak'=>NULL,
|
||||||
|
'extra_down_offpeak'=>0,
|
||||||
|
'extra_up_offpeak'=>NULL,
|
||||||
|
'metric'=>1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$factory->state(App\Models\Product\Adsl::class,'50/-/20/-',[
|
||||||
|
'base_down_peak'=>50,
|
||||||
|
'base_up_peak'=>NULL,
|
||||||
|
'base_down_offpeak'=>20,
|
||||||
|
'base_up_offpeak'=>NULL,
|
||||||
|
'extra_charged'=>NULL,
|
||||||
|
'extra_shaped'=>NULL,
|
||||||
|
'extra_down_peak'=>0,
|
||||||
|
'extra_up_peak'=>NULL,
|
||||||
|
'extra_down_offpeak'=>0,
|
||||||
|
'extra_up_offpeak'=>NULL,
|
||||||
|
'metric'=>1,
|
||||||
|
]);
|
10
database/factories/AdslSupplierPlanFactory.php
Normal file
10
database/factories/AdslSupplierPlanFactory.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Faker\Generator as Faker;
|
||||||
|
|
||||||
|
$factory->define(App\Models\AdslSupplierPlan::class, function (Faker $faker) {
|
||||||
|
return [
|
||||||
|
'id'=>1,
|
||||||
|
'contract_term'=>12,
|
||||||
|
];
|
||||||
|
});
|
@ -27,3 +27,38 @@ $factory->state(App\Models\Product::class,'strict',[
|
|||||||
$factory->state(App\Models\Product::class,'notstrict',[
|
$factory->state(App\Models\Product::class,'notstrict',[
|
||||||
'price_recurr_strict' => 0,
|
'price_recurr_strict' => 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$factory->afterMakingState(App\Models\Product::class,'broadband-unlimit',function ($product,$faker) {
|
||||||
|
$type = factory(App\Models\Product\Adsl::class)->state('unlimit')->make();
|
||||||
|
$product->setRelation('type',$type);
|
||||||
|
$product->prod_plugin_data = $type->id;
|
||||||
|
$product->model = 'App\Models\Product\Adsl';
|
||||||
|
});
|
||||||
|
|
||||||
|
$factory->afterMakingState(App\Models\Product::class,'broadband-140/0/0/0',function ($product,$faker) {
|
||||||
|
$type = factory(App\Models\Product\Adsl::class)->state('140/0/0/0')->make();
|
||||||
|
$product->setRelation('type',$type);
|
||||||
|
$product->prod_plugin_data = $type->id;
|
||||||
|
$product->model = 'App\Models\Product\Adsl';
|
||||||
|
});
|
||||||
|
|
||||||
|
$factory->afterMakingState(App\Models\Product::class,'broadband-70/-/0/-',function ($product,$faker) {
|
||||||
|
$type = factory(App\Models\Product\Adsl::class)->state('70/-/0/-')->make();
|
||||||
|
$product->setRelation('type',$type);
|
||||||
|
$product->prod_plugin_data = $type->id;
|
||||||
|
$product->model = 'App\Models\Product\Adsl';
|
||||||
|
});
|
||||||
|
|
||||||
|
$factory->afterMakingState(App\Models\Product::class,'broadband-100/0/40/0',function ($product,$faker) {
|
||||||
|
$type = factory(App\Models\Product\Adsl::class)->state('100/0/40/0')->make();
|
||||||
|
$product->setRelation('type',$type);
|
||||||
|
$product->prod_plugin_data = $type->id;
|
||||||
|
$product->model = 'App\Models\Product\Adsl';
|
||||||
|
});
|
||||||
|
|
||||||
|
$factory->afterMakingState(App\Models\Product::class,'broadband-50/-/20/-',function ($product,$faker) {
|
||||||
|
$type = factory(App\Models\Product\Adsl::class)->state('50/-/20/-')->make();
|
||||||
|
$product->setRelation('type',$type);
|
||||||
|
$product->prod_plugin_data = $type->id;
|
||||||
|
$product->model = 'App\Models\Product\Adsl';
|
||||||
|
});
|
106
resources/theme/backend/adminlte/a/product/report.blade.php
Normal file
106
resources/theme/backend/adminlte/a/product/report.blade.php
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
@extends('adminlte::layouts.app')
|
||||||
|
|
||||||
|
@section('htmlheader_title')
|
||||||
|
Product List
|
||||||
|
@endsection
|
||||||
|
@section('page_title')
|
||||||
|
Product List
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('contentheader_title')
|
||||||
|
Product List
|
||||||
|
@endsection
|
||||||
|
@section('contentheader_description')
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('main-content')
|
||||||
|
<div class="col-md-12">
|
||||||
|
<table class="table table-sm table-striped" id="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Service</th>
|
||||||
|
<th>Product</th>
|
||||||
|
<th>Monthly</th>
|
||||||
|
<th>Cost</th>
|
||||||
|
<th>Traffic</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
@foreach (\App\Models\Service::active()->get() as $o)
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ url('u/service',[$o->id]) }}">{{ $o->id }}</a></td>
|
||||||
|
<td>{{ $o->sname }}</td>
|
||||||
|
<td>{{ $o->product->name }}</td>
|
||||||
|
<td>{{ number_format($o->billing_monthly_price,2) }}</td>
|
||||||
|
<td>{{ $o->product->type ? number_format($o->product->type->cost,2) : 'NO TYPE' }}</td>
|
||||||
|
<td>{{ $o->product->type ? $o->product->type->allowance_string() : '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('page-scripts')
|
||||||
|
@css('//cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css','datatables-css')
|
||||||
|
@js('//cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js','datatables-js')
|
||||||
|
@css('//cdn.datatables.net/responsive/2.2.1/css/responsive.dataTables.min.css','datatables-responsive-css')
|
||||||
|
@js('//cdn.datatables.net/responsive/2.2.1/js/dataTables.responsive.min.js','datatables-responsive-js')
|
||||||
|
@css('//cdn.datatables.net/rowgroup/1.1.0/css/rowGroup.dataTables.min.css','datatables-rowgroup-css')
|
||||||
|
@js('//cdn.datatables.net/rowgroup/1.1.0/js/dataTables.rowGroup.min.js','datatables-rowgroup-js')
|
||||||
|
@css('//cdn.datatables.net/buttons/1.5.6/css/buttons.dataTables.min.css','datatables-button-css')
|
||||||
|
@js('//cdn.datatables.net/buttons/1.5.6/js/dataTables.buttons.min.js','datatables-button-js')
|
||||||
|
@css('//cdn.datatables.net/fixedheader/3.1.5/css/fixedHeader.dataTables.min.css','datatables-fixed-css')
|
||||||
|
@js('//cdn.datatables.net/fixedheader/3.1.5/js/dataTables.fixedHeader.min.js','datatables-fixed-js')
|
||||||
|
@css('/plugin/dataTables/dataTables.bootstrap4.css','datatables-bootstrap4-css')
|
||||||
|
@js('/plugin/dataTables/dataTables.bootstrap4.js','datatables-bootstrap4-js')
|
||||||
|
@css('/plugin/dataTables/dataTables.bootstrap4.css','datatables-bootstrap4-css')
|
||||||
|
@js('//cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js','jszip')
|
||||||
|
@js('//cdn.datatables.net/buttons/1.5.6/js/buttons.html5.min.js','datatables-buttons-html5')
|
||||||
|
|
||||||
|
<style>
|
||||||
|
tr.odd td:first-child,
|
||||||
|
tr.even td:first-child {
|
||||||
|
padding-left: 3em;
|
||||||
|
}
|
||||||
|
table.dataTable tr.dtrg-group.dtrg-level-1 td {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
color: #4c110f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#table').DataTable({
|
||||||
|
//oSearch: { sSearch: searchString ? decodeURIComponent(searchString) : '' },
|
||||||
|
aLengthMenu: [
|
||||||
|
[25, 50, 100, 200, -1],
|
||||||
|
[25, 50, 100, 200, "All"]
|
||||||
|
],
|
||||||
|
paging: true,
|
||||||
|
pageLength: 25,
|
||||||
|
lengthChange: true,
|
||||||
|
searching: true,
|
||||||
|
ordering: true,
|
||||||
|
info: true,
|
||||||
|
autoWidth: false,
|
||||||
|
fixedHeader: true,
|
||||||
|
order: [
|
||||||
|
[2,'asc'],
|
||||||
|
[1,'asc'],
|
||||||
|
],
|
||||||
|
rowGroup: {
|
||||||
|
dataSrc: [2],
|
||||||
|
},
|
||||||
|
columnDefs: [
|
||||||
|
{
|
||||||
|
targets: [2],
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@append
|
@ -0,0 +1,28 @@
|
|||||||
|
<table class="table table-sm">
|
||||||
|
<tr>
|
||||||
|
<th>Supplier</th><td>{{ $o->product->type->supplier->name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Supplier Product</th><td>#{{ $o->product_id }}: {{ $o->product->type->product->product_id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<!-- @todo Tax shouldnt be hard coded -->
|
||||||
|
<th>Supplier Setup</th><td>${{ number_format($o->product->type->product->setup_cost*1.1,2) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Supplier Cost</th><td>${{ number_format($o->product->type->cost,2) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Supplier Contract</th><td>{{ $o->product->type->product->contract_term }} months</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<!-- @todo Tax shouldnt be hard coded -->
|
||||||
|
<th>Supplier Min Cost</th><td>${{ number_format((($x=$o->product->type->product)->setup_cost+$x->base_cost*$x->contract_term)*1.1,2) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Price</th><td>${{ number_format($o->billing_monthly_price,2) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Markup</th><td>{{ number_format(($o->billing_monthly_price/$o->product->type->cost-1)*100,2) }}%</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
@ -26,16 +26,19 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-dark d-flex p-0">
|
<div class="card-header bg-dark d-flex p-0">
|
||||||
<span class="p-3"><i class="fa fa-bars"></i></span>
|
<span class="p-3"><i class="fa fa-bars"></i></span>
|
||||||
<ul class="nav nav-pills p-2">
|
<ul class="nav nav-pills p-2 w-100">
|
||||||
{{--
|
{{--
|
||||||
<li class="nav-item"><a class="nav-link active" href="#product" data-toggle="tab">Product</a></li>
|
<li class="nav-item"><a class="nav-link active" href="#product" data-toggle="tab">Product</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#traffic" data-toggle="tab">Traffic</a></li>
|
<li class="nav-item"><a class="nav-link" href="#traffic" data-toggle="tab">Traffic</a></li>
|
||||||
--}}
|
--}}
|
||||||
<li class="nav-item active"><a class="nav-link" href="#invoice_next" data-toggle="tab">Next Invoice</a></li>
|
<li class="nav-item active"><a class="nav-link" href="#pending_items" data-toggle="tab">Pending Items</a></li>
|
||||||
{{--
|
{{--
|
||||||
<li class="nav-item"><a class="nav-link" href="#invoices" data-toggle="tab">Invoices</a></li>
|
<li class="nav-item"><a class="nav-link" href="#invoices" data-toggle="tab">Invoices</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#emails" data-toggle="tab">Emails</a></li>
|
<li class="nav-item"><a class="nav-link" href="#emails" data-toggle="tab">Emails</a></li>
|
||||||
--}}
|
--}}
|
||||||
|
@can('wholesaler')
|
||||||
|
<li class="nav-item ml-auto"><a class="nav-link" href="#internal" data-toggle="tab">Internal</a></li>
|
||||||
|
@endcan
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@can('update',$o)
|
@can('update',$o)
|
||||||
@ -64,7 +67,7 @@
|
|||||||
<div class="tab-pane fade" id="product" role="tabpanel">
|
<div class="tab-pane fade" id="product" role="tabpanel">
|
||||||
Product.
|
Product.
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade show active" id="invoice_next" role="tabpanel">
|
<div class="tab-pane fade show active" id="pending_items" role="tabpanel">
|
||||||
@include('common.service.widget.invoice')
|
@include('common.service.widget.invoice')
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="invoices" role="tabpanel">
|
<div class="tab-pane fade" id="invoices" role="tabpanel">
|
||||||
@ -73,6 +76,11 @@
|
|||||||
<div class="tab-pane fade" id="emails" role="tabpanel">
|
<div class="tab-pane fade" id="emails" role="tabpanel">
|
||||||
Email.
|
Email.
|
||||||
</div>
|
</div>
|
||||||
|
@can('wholesaler')
|
||||||
|
<div class="tab-pane fade" id="internal" role="tabpanel">
|
||||||
|
@include('a.service.widget.internal')
|
||||||
|
</div>
|
||||||
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- /.card -->
|
<!-- /.card -->
|
||||||
|
@ -36,16 +36,21 @@
|
|||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
<!-- @todo -->
|
<!-- @todo -->
|
||||||
@if(FALSE)
|
@if($o->technology)
|
||||||
<tr>
|
<tr>
|
||||||
<th>Speed</th>
|
<th>Technology</th>
|
||||||
<td>{{ 'xxx/YY' }} Mbps</td>
|
<td>{{ $o->technology }}</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Traffic</th>
|
|
||||||
<td>{{ 'xxx' }} GB (YY GB used month)</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
<tr>
|
||||||
|
<th>Speed</th>
|
||||||
|
<td>{{ $o->service->product->type->product->speed }} Mbps</td>
|
||||||
|
</tr>
|
||||||
|
<!-- @todo -->
|
||||||
|
<tr>
|
||||||
|
<th>Traffic</th>
|
||||||
|
<td>{{ $o->service->product->type->allowance_string() }} GB @if(FALSE)(YY GB used month)@endif</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>IP Address</th>
|
<th>IP Address</th>
|
||||||
<td>{{ $o->ipaddress ?: 'Dynamic' }}</td>
|
<td>{{ $o->ipaddress ?: 'Dynamic' }}</td>
|
||||||
|
@ -27,3 +27,21 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@can('wholesaler')
|
||||||
|
<li class="nav-header">ADMIN</li>
|
||||||
|
|
||||||
|
<li class="nav-item has-treeview @if(preg_match('#^a/report/(products)#',request()->path()))menu-open @else menu-closed @endif">
|
||||||
|
<a href="#" class="nav-link @if(preg_match('#^a/report/(products)#',request()->path())) active @endif">
|
||||||
|
<i class="nav-icon fa fa-list"></i> <p>REPORT<i class="fa fa-angle-left right"></i></p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul class="nav nav-treeview">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ url('a/report/products') }}" class="nav-link @if(preg_match('#^a/report/products$#',request()->path()))active @endif">
|
||||||
|
<i class="nav-icon fa fa-shopping-cart"></i> <p>Products</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
@endcan
|
@ -30,6 +30,7 @@ Route::group(['middleware'=>['theme:adminlte-be','auth','role:wholesaler'],'pref
|
|||||||
Route::post('setup','AdminHomeController@setup_update');
|
Route::post('setup','AdminHomeController@setup_update');
|
||||||
Route::get('service/{o}','AdminHomeController@service');
|
Route::get('service/{o}','AdminHomeController@service');
|
||||||
Route::post('service/{o}','AdminHomeController@service_update');
|
Route::post('service/{o}','AdminHomeController@service_update');
|
||||||
|
Route::get('report/products','Wholesale\ReportController@products');
|
||||||
|
|
||||||
//Route::get('accounting/connect','AccountingController@connect');
|
//Route::get('accounting/connect','AccountingController@connect');
|
||||||
});
|
});
|
||||||
|
76
tests/Feature/ProductAdslTest.php
Normal file
76
tests/Feature/ProductAdslTest.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Foundation\Testing\WithFaker;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\Service;
|
||||||
|
|
||||||
|
class ProductAdslTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testTraffic()
|
||||||
|
{
|
||||||
|
// Test ADSL/NBN Traffic Calculations
|
||||||
|
$traffic = [
|
||||||
|
'base_down_peak'=>50,
|
||||||
|
'base_up_peak'=>50,
|
||||||
|
'base_down_offpeak'=>20,
|
||||||
|
'base_up_offpeak'=>20,
|
||||||
|
];
|
||||||
|
|
||||||
|
// A:Unlimited
|
||||||
|
$o = factory(Service::class)->states('month')->make();
|
||||||
|
$o->setRelation('product',factory(Product::class)->states('broadband-unlimit')->make());
|
||||||
|
$x=$o->product->type->allowance($traffic);
|
||||||
|
$this->assertEquals(0,count($x),'A:Traffic is unlimited');
|
||||||
|
|
||||||
|
// ** Extra Traffic Charged **
|
||||||
|
// B:140GB All Traffic Counted (uploads and download - no peak periods)
|
||||||
|
$o = factory(Service::class)->states('month')->make();
|
||||||
|
$o->setRelation('product',factory(Product::class)->states('broadband-140/0/0/0')->make());
|
||||||
|
$x = $o->product->type->allowance($traffic);
|
||||||
|
$this->assertEquals(1,count($x),'B:Traffic is 140GB');
|
||||||
|
$this->assertArrayHasKey('base_down_peak',$x,'B:Traffic has base_down_peak key for 140GB');
|
||||||
|
$this->assertEquals(0,Arr::get($x,'base_down_peak'),'B:Traffic base_down_peak equals 140GB');
|
||||||
|
|
||||||
|
// C:70GB Download - Uploads Not Counted
|
||||||
|
$o = factory(Service::class)->states('month')->make();
|
||||||
|
$o->setRelation('product',factory(Product::class)->states('broadband-70/-/0/-')->make());
|
||||||
|
$x=$o->product->type->allowance($traffic);
|
||||||
|
$this->assertEquals(1,count($x),'C:Traffic is 70GB');
|
||||||
|
$this->assertArrayHasKey('base_down_peak',$x,'C:Traffic has base_down_peak key for 70GB');
|
||||||
|
$this->assertEquals(0,Arr::get($x,'base_down_peak'),'C:Traffic base_down_peak equals 70GB');
|
||||||
|
|
||||||
|
// D:100GB Peak / 40GB OffPeak - No Free Uploads
|
||||||
|
$o = factory(Service::class)->states('month')->make();
|
||||||
|
$o->setRelation('product',factory(Product::class)->states('broadband-100/0/40/0')->make());
|
||||||
|
$x=$o->product->type->allowance($traffic);
|
||||||
|
$this->assertEquals(2,count($x),'D:Traffic is 140/40GB');
|
||||||
|
$this->assertArrayHasKey('base_down_peak',$x,'D:Traffic has base_down_peak key for 100GB');
|
||||||
|
$this->assertArrayHasKey('base_down_offpeak',$x,'D:Traffic has base_down_offpeak key for 40GB');
|
||||||
|
$this->assertEquals(0,Arr::get($x,'base_down_peak'),'D:Traffic base_down_peak equals 100GB');
|
||||||
|
$this->assertEquals(0,Arr::get($x,'base_down_offpeak'),'D:Traffic base_down_offpeak equals 40GB');
|
||||||
|
|
||||||
|
// 50GB Peak / 20GB OffPeak - Uploads Not Counted
|
||||||
|
$o = factory(Service::class)->states('month')->make();
|
||||||
|
$o->setRelation('product',factory(Product::class)->states('broadband-50/-/20/-')->make());
|
||||||
|
$x=$o->product->type->allowance($traffic);
|
||||||
|
$this->assertEquals(2,count($x),'E:Traffic is 50/20GB');
|
||||||
|
$this->assertArrayHasKey('base_down_peak',$x,'E:Traffic has base_down_peak key for 50GB');
|
||||||
|
$this->assertArrayHasKey('base_down_offpeak',$x,'E:Traffic has base_down_offpeak key for 20GB');
|
||||||
|
$this->assertEquals(0,Arr::get($x,'base_down_peak'),'E:Traffic base_down_peak equals 50GB');
|
||||||
|
$this->assertEquals(0,Arr::get($x,'base_down_offpeak'),'E:Traffic base_down_offpeak equals 20GB');
|
||||||
|
|
||||||
|
// ** Extra Traffic NOT Charged - Service Shaped **
|
||||||
|
// 100GB All Traffic (uploads and download - no peak periods)
|
||||||
|
|
||||||
|
// 100GB Download - Uploads Not Counted
|
||||||
|
|
||||||
|
// 100GB Peak / 200GB OffPeak - No Free Uploads
|
||||||
|
|
||||||
|
// 100GB Peak / 200GB OffPeak - Uploads Not Counted
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user