diff --git a/app/Http/Controllers/Wholesale/ReportController.php b/app/Http/Controllers/Wholesale/ReportController.php new file mode 100644 index 0000000..e7d80b7 --- /dev/null +++ b/app/Http/Controllers/Wholesale/ReportController.php @@ -0,0 +1,13 @@ +'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; diff --git a/app/Models/Product.php b/app/Models/Product.php index 70c6b0d..af6d3a6 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -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() diff --git a/app/Models/Product/Adsl.php b/app/Models/Product/Adsl.php index ce7fb99..3df3ed3 100644 --- a/app/Models/Product/Adsl.php +++ b/app/Models/Product/Adsl.php @@ -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; + } } \ No newline at end of file diff --git a/app/Models/Product/SSL.php b/app/Models/Product/SSL.php index f0968b4..4bc36b5 100644 --- a/app/Models/Product/SSL.php +++ b/app/Models/Product/SSL.php @@ -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; + } } \ No newline at end of file diff --git a/app/Models/Service.php b/app/Models/Service.php index dcbef10..1c56637 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -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 * diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 7527c26..39ea383 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -8,24 +8,28 @@ use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvid class AuthServiceProvider extends ServiceProvider { - /** - * The policy mappings for the application. - * - * @var array - */ - protected $policies = [ - 'App\Model' => 'App\Policies\ModelPolicy', - ]; + /** + * The policy mappings for the application. + * + * @var array + */ + protected $policies = [ + 'App\Model' => 'App\Policies\ModelPolicy', + ]; - /** - * Register any authentication / authorization services. - * - * @return void - */ - public function boot() - { - $this->registerPolicies(); - Passport::routes(); - // Passport::enableImplicitGrant(); - } -} + /** + * Register any authentication / authorization services. + * + * @return void + */ + public function boot() + { + $this->registerPolicies(); + Passport::routes(); + // Passport::enableImplicitGrant(); + + Gate::define('wholesaler', function ($user) { + return $user->isWholesaler(); + }); + } +} \ No newline at end of file diff --git a/database/factories/AdslPlanFactory.php b/database/factories/AdslPlanFactory.php new file mode 100644 index 0000000..2578a35 --- /dev/null +++ b/database/factories/AdslPlanFactory.php @@ -0,0 +1,86 @@ +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, +]); \ No newline at end of file diff --git a/database/factories/AdslSupplierPlanFactory.php b/database/factories/AdslSupplierPlanFactory.php new file mode 100644 index 0000000..4cfb7ad --- /dev/null +++ b/database/factories/AdslSupplierPlanFactory.php @@ -0,0 +1,10 @@ +define(App\Models\AdslSupplierPlan::class, function (Faker $faker) { + return [ + 'id'=>1, + 'contract_term'=>12, + ]; +}); \ No newline at end of file diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php index 5a6d984..3d11846 100644 --- a/database/factories/ProductFactory.php +++ b/database/factories/ProductFactory.php @@ -26,4 +26,39 @@ $factory->state(App\Models\Product::class,'strict',[ ]); $factory->state(App\Models\Product::class,'notstrict',[ 'price_recurr_strict' => 0, -]); \ No newline at end of file +]); + +$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'; +}); \ No newline at end of file diff --git a/resources/theme/backend/adminlte/a/product/report.blade.php b/resources/theme/backend/adminlte/a/product/report.blade.php new file mode 100644 index 0000000..11a4bff --- /dev/null +++ b/resources/theme/backend/adminlte/a/product/report.blade.php @@ -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') +
+ + + + + + + + + + + + + + @foreach (\App\Models\Service::active()->get() as $o) + + + + + + + + + @endforeach + +
IDServiceProductMonthlyCostTraffic
{{ $o->id }}{{ $o->sname }}{{ $o->product->name }}{{ number_format($o->billing_monthly_price,2) }}{{ $o->product->type ? number_format($o->product->type->cost,2) : 'NO TYPE' }}{{ $o->product->type ? $o->product->type->allowance_string() : '-' }}
+
+@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') + + + + +@append \ No newline at end of file diff --git a/resources/theme/backend/adminlte/a/service/widget/internal.blade.php b/resources/theme/backend/adminlte/a/service/widget/internal.blade.php new file mode 100644 index 0000000..6120612 --- /dev/null +++ b/resources/theme/backend/adminlte/a/service/widget/internal.blade.php @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Supplier{{ $o->product->type->supplier->name }}
Supplier Product#{{ $o->product_id }}: {{ $o->product->type->product->product_id }}
Supplier Setup${{ number_format($o->product->type->product->setup_cost*1.1,2) }}
Supplier Cost${{ number_format($o->product->type->cost,2) }}
Supplier Contract{{ $o->product->type->product->contract_term }} months
Supplier Min Cost${{ number_format((($x=$o->product->type->product)->setup_cost+$x->base_cost*$x->contract_term)*1.1,2) }}
Price${{ number_format($o->billing_monthly_price,2) }}
Markup{{ number_format(($o->billing_monthly_price/$o->product->type->cost-1)*100,2) }}%
\ No newline at end of file diff --git a/resources/theme/backend/adminlte/u/service.blade.php b/resources/theme/backend/adminlte/u/service.blade.php index cb3a94e..52367ca 100644 --- a/resources/theme/backend/adminlte/u/service.blade.php +++ b/resources/theme/backend/adminlte/u/service.blade.php @@ -26,16 +26,19 @@
- - \ No newline at end of file + + +@can('wholesaler') + + + +@endcan \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index c110a93..9179a8d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -30,6 +30,7 @@ Route::group(['middleware'=>['theme:adminlte-be','auth','role:wholesaler'],'pref Route::post('setup','AdminHomeController@setup_update'); Route::get('service/{o}','AdminHomeController@service'); Route::post('service/{o}','AdminHomeController@service_update'); + Route::get('report/products','Wholesale\ReportController@products'); //Route::get('accounting/connect','AccountingController@connect'); }); diff --git a/tests/Feature/ProductAdslTest.php b/tests/Feature/ProductAdslTest.php new file mode 100644 index 0000000..b875a7a --- /dev/null +++ b/tests/Feature/ProductAdslTest.php @@ -0,0 +1,76 @@ +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 + } +} \ No newline at end of file