Work on products, first completed broadband

This commit is contained in:
Deon George 2021-12-24 12:14:01 +11:00
parent 8f5293662e
commit 1e9f15b40f
62 changed files with 2139 additions and 894 deletions

View File

@ -3,10 +3,9 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Config;
use App\Models\Service;
use App\Models\{Service,Site};
class ServiceList extends Command
{
@ -15,10 +14,10 @@ class ServiceList extends Command
*
* @var string
*/
protected $signature = 'service:list '.
'{--a|active : Active Only}'.
'{--category= : Category}'.
'{--f|fix : Fix start_date}';
protected $signature = 'service:list'.
' {--i|inactive : Include Inactive}'.
' {--t|type= : Type}'.
' {--f|fix : Fix start_date}';
/**
* The console command description.
@ -27,16 +26,6 @@ class ServiceList extends Command
*/
protected $description = 'List all services';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
@ -44,13 +33,11 @@ class ServiceList extends Command
*/
public function handle()
{
DB::listen(function($query) {
Log::debug('- SQL',['sql'=>$query->sql,'binding'=>$query->bindings]);
});
$header = '|%13s|%-10s|%-35s|%-40s|%8s|%10s|%15s|%10s|%10s|%12s|%14s|';
$this->warn(sprintf('|%10s|%-6s|%-20s|%-50s|%8s|%14s|%10s|%10s|%10s|%10s|%10s|',
$this->warn(sprintf($header,
'ID',
'CAT',
'Type',
'Product',
'Name',
'active',
@ -60,13 +47,15 @@ class ServiceList extends Command
'stop date',
'connect date',
'first invoice'
));
));
foreach (Service::all() as $o) {
if ($this->option('active') AND ! $o->isActive())
foreach (Service::withoutGlobalScope(\App\Models\Scopes\SiteScope::class)->with(['site'])->cursor() as $o) {
if ((! $this->option('inactive')) AND ! $o->isActive())
continue;
if ($this->option('category') AND $o->product->category !== $this->option('category'))
Config::set('site',$o->site);
if ($this->option('type') AND ($o->product->getProductTypeAttribute() !== $this->option('type')))
continue;
$c = $o->invoice_items->filter(function($item) {return $item->item_type === 0; })->sortby('date_start')->first();
@ -76,10 +65,10 @@ class ServiceList extends Command
$o->save();
}
$this->info(sprintf('|%10s|%-6s|%-20s|%-50s|%8s|%14s|%10s|%10s|%10s|%10s|%10s|',
$this->info(sprintf($header,
$o->sid,
$o->product->category,
$o->product_name,
$o->product->getProductTypeAttribute(),
$o->product->getNameAttribute(),
$o->name_short,
$o->active ? 'active' : 'inactive',
$o->status,
@ -87,7 +76,7 @@ class ServiceList extends Command
$o->date_start ? $o->date_start->format('Y-m-d') : NULL,
$o->date_end ? $o->date_end->format('Y-m-d') : NULL,
($o->type AND $o->type->service_connect_date) ? $o->type->service_connect_date->format('Y-m-d') : NULL,
$c ? $c->date_start->format('Y-m-d') : NULL,
($c && $c->date_start) ? $c->date_start->format('Y-m-d') : NULL,
));
}
}

View File

@ -170,7 +170,7 @@ class AdminController extends Controller
*/
public function supplier()
{
return view('a.supplier');
return view('a.supplier.find');
}
/**
@ -223,7 +223,7 @@ class AdminController extends Controller
if (! $o->exists && $request->name)
$o = Supplier::where('name',$request->name)->with(['details'])->firstOrNew();
return view('a.supplierdetails')
return view('a.supplier.details')
->with('o',$o);
}

View File

@ -14,16 +14,19 @@ use App\Models\{Account,Product,Service,User};
class OrderController extends Controller
{
// @todo To check
public function __construct()
{
$this->middleware('auth');
}
// @todo To check
public function index()
{
return view('order.home');
}
// @todo To check
public function product_order(Product $o)
{
Theme::set('metronic-fe');
@ -31,6 +34,7 @@ class OrderController extends Controller
return view('order.widget.order',['o'=>$o]);
}
// @todo To check
public function product_info(Product $o)
{
Theme::set('metronic-fe');
@ -38,9 +42,10 @@ class OrderController extends Controller
return view('order.widget.info',['o'=>$o]);
}
// @todo To check
public function submit(Request $request)
{
Validator::make($request->all(),['product_id'=>'required|exists:ab_product,id'])
Validator::make($request->all(),['product_id'=>'required|exists:products,id'])
// Reseller
->sometimes('account_id','required|email',function($input) use ($request) {
return is_null($input->account_id) AND is_null($input->order_email_manual);
@ -59,7 +64,7 @@ class OrderController extends Controller
$po = Product::findOrFail($request->input('product_id'));
// Check we have the custom attributes for the product
$options = $po->orderValidation($request);
$order = $po->orderValidation($request);
if ($request->input('order_email_manual')) {
$uo = User::firstOrNew(['email'=>$request->input('order_email_manual')]);
@ -101,24 +106,24 @@ class OrderController extends Controller
$so->product_id = $request->input('product_id');
$so->order_status = 'ORDER-SUBMIT';
$so->orderby_id = Auth::id();
$so->model = get_class($options);
$so->model = get_class($order);
if ($options->order_info) {
$so->order_info = $options->order_info;
if ($order->order_info) {
$so->order_info = $order->order_info;
unset($options->order_info);
unset($order->order_info);
}
$so = $ao->services()->save($so);
if ($options instanceOf Model) {
$options->service_id = $so->id;
$options->save();
if ($order instanceOf Model) {
$order->service_id = $so->id;
$order->save();
}
Mail::to('help@graytech.net.au')
->queue((new OrderRequest($so,$request->input('options.notes','')))->onQueue('email')); //@todo Get email from DB.
->queue((new OrderRequest($so,$request->input('options.notes') ?: ''))->onQueue('email')); //@todo Get email from DB.
return view('order_received',['o'=>$so]);
}
}
}

View File

@ -66,7 +66,7 @@ class SearchController extends Controller
}
# Look for an ADSL/NBN Service
foreach (Adsl::Search($request->input('term'))
foreach (Service\Broadband::Search($request->input('term'))
->whereIN('account_id',$accounts)
->orderBy('service_number')
->limit(10)->get() as $o)

View File

@ -9,12 +9,12 @@ interface IDs
*
* @return mixed
*/
public function getLIDattribute(): string;
public function getLIDAttribute(): string;
/**
* Return the system ID of the item
*
* @return mixed
*/
public function getSIDattribute(): string;
public function getSIDAttribute(): string;
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Interfaces;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
interface ProductItem
{
/* RELATIONS */
/**
* Supplier that provides this offering
*
* @return BelongsTo
*/
//public function supplier_detail(): BelongsTo;
/**
* Available products created from this supplier offering
*
* @return BelongsToMany
*/
//public function types(): BelongsToMany;
/* ATTRIBUTES */
/**
* Return the billing interval base cost including tax
*
* @return float
*/
public function getBaseCostTaxableAttribute(): float;
/**
* Return the billing interval that the supplier charges
*
* @return string
*/
public function getBillingIntervalAttribute(): int;
/**
* The term that the supplier imposes on this service being connected
*
* @return int
*/
public function getContractTermAttribute(): int;
/**
* Suppliers offering name (short)
*
* @return string
*/
public function getNameAttribute(): string;
/**
* Suppliers offering name (long)
*
* @return string
*/
public function getNameLongAttribute(): string;
/**
* Return the setup cost including tax
*
* @return float
*/
public function getSetupCostTaxableAttribute(): float;
/**
* Return the type of offering this is.
*
* @return string
*/
public function getTypeAttribute();
}

View File

@ -4,6 +4,9 @@ namespace App\Interfaces;
use Illuminate\Support\Collection;
/**
* @deprecated - rename to productItem if still required
*/
interface ProductSupplier
{
/**
@ -20,6 +23,13 @@ interface ProductSupplier
*/
public function allowance_string(): string;
/**
* Return the contract term for this product when sold as a service
*
* @return int Months
*/
public function getContractTermAttribute(): int;
/**
* Return the product cost
*
@ -36,4 +46,11 @@ interface ProductSupplier
* @return mixed
*/
public function getSupplierAttribute();
/**
* Does this offering capture usage information
*
* @return bool
*/
public function hasUsage(): bool;
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Interfaces;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
interface SupplierItem
{
/* RELATIONS */
/**
* Supplier that provides this offering
*
* @return BelongsTo
*/
public function supplier_detail(): BelongsTo;
/**
* Available products created from this supplier offering
*
* @return BelongsToMany
*/
public function types(): BelongsToMany;
/* ATTRIBUTES */
/**
* Return the billing interval base cost including tax
*
* @return float
*/
public function getBaseCostTaxableAttribute(): float;
/**
* Return the billing interval that the supplier charges
*
* @return string
*/
public function getBillingIntervalAttribute(): int;
/**
* The term that the supplier imposes on this service being connected
*
* @return int
*/
public function getContractTermAttribute(): int;
/**
* The minimum cost of ordering this offering
*
* @return float
*/
public function getMinCostAttribute(): float;
/**
* The minimum cost of ordering this offering including taxes
*
* @return float
*/
public function getMinCostTaxableAttribute(): float;
/**
* Suppliers offering name (short)
*
* @return string
*/
public function getNameAttribute(): string;
/**
* Suppliers offering name (long)
*
* @return string
*/
public function getNameLongAttribute(): string;
/**
* Return the setup cost including tax
*
* @return float
*/
public function getSetupCostTaxableAttribute(): float;
/**
* Return the type of offering this is.
*
* @return string
*/
public function getTypeAttribute();
}

View File

@ -7,25 +7,22 @@ use Illuminate\Database\Eloquent\Model;
use Leenooks\Traits\ScopeActive;
use App\Interfaces\IDs;
use App\Traits\NextKey;
/**
* Class Account
* Service Accounts
*
* Attributes for accounts:
* + lid: : Local ID for account
* + sid: : System ID for account
* + name: : Account Name
* + lid : Local ID for account
* + sid : System ID for account
* + name : Account Name
* + taxes : Taxes Applicable to this account
*
* @package App\Models
*/
class Account extends Model implements IDs
{
use HasFactory,NextKey,ScopeActive;
const RECORD_ID = 'account';
public $incrementing = FALSE;
use HasFactory,ScopeActive;
const CREATED_AT = 'date_orig';
const UPDATED_AT = 'date_last';
@ -67,6 +64,11 @@ class Account extends Model implements IDs
return $this->belongsToMany(External\Integrations::class,'external_account',NULL,'external_integration_id');
}
public function group()
{
return $this->hasOneThrough(Group::class,AccountGroup::class,'account_id','id','id','group_id');
}
public function invoices()
{
return $this->hasMany(Invoice::class);
@ -89,6 +91,11 @@ class Account extends Model implements IDs
return $active ? $query->active() : $query;
}
public function taxes()
{
return $this->hasMany(Tax::class,'country_id','country_id');
}
public function user()
{
return $this->belongsTo(User::class);

View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AccountGroup extends Model
{
protected $table = 'ab_account_group';
public $timestamps = FALSE;
}

View File

@ -1,36 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\OrderServiceOptions;
class AdslPlan extends Model
{
use OrderServiceOptions;
protected $table = 'ab_adsl_plan';
protected $order_attributes = [
'options.address'=>[
'request'=>'options.address',
'key'=>'service_address',
'validation'=>'required|string:10|unique:ab_service__adsl,service_address',
'validation_message'=>'Address is a required field.',
],
'options.notes'=>[
'request'=>'options.notes',
'key'=>'order_info.notes',
'validation'=>'present',
'validation_message'=>'Special Instructions here.',
],
];
protected $order_model = Service\Adsl::class;
public function product()
{
return $this->hasOne(AdslSupplierPlan::class,'id','adsl_supplier_plan_id');
}
}

View File

@ -7,6 +7,9 @@ use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
/**
* @deprecated
*/
class AdslSupplier extends Model
{
protected $table = 'ab_adsl_supplier';

View File

@ -4,11 +4,9 @@ namespace App\Models\Base;
use Illuminate\Database\Eloquent\Model;
use App\Models\Product;
//@todo column prod_plugin_file should no longer be required
/**
* @deprecated
*/
abstract class ProductType extends Model
{
public $timestamps = FALSE;
public $dateFormat = 'U';
}

View File

@ -45,6 +45,46 @@ class Invoice extends Model implements IDs
protected $dates = ['date_orig','due_date'];
public $dateFormat = 'U';
/* Our available billing periods */
public const billing_periods = [
0 => [
'name' => 'Weekly',
'interval' => 0.25,
],
1 => [
'name' => 'Monthly',
'interval' => 1,
],
2 => [
'name' => 'Quarterly',
'interval' => 3,
],
3 => [
'name' => 'Semi-Annually',
'interval' => 6,
],
4 => [
'name' => 'Annually',
'interval' => 12,
],
5 => [
'name' => 'Two years',
'interval' => 24,
],
6 => [
'name' => 'Three Years',
'interval' => 36,
],
7 => [
'name' => 'Four Years',
'interval' => 48,
],
8 => [
'name' => 'Five Years',
'interval' => 60,
],
];
// Array of items that can be updated with PushNew
protected $pushable = ['items'];
@ -61,6 +101,58 @@ class Invoice extends Model implements IDs
private int $_total = 0;
private int $_total_tax = 0;
/* STATIC */
/**
* This works out what multiplier to use to change billing periods
*
* @param int $source
* @param int $target
* @return float
*/
public static function billing_change(int $source,int $target): float
{
return Arr::get(self::billing_periods,$target.'.interval')/Arr::get(self::billing_periods,$source.'.interval');
}
/**
* Return the name for the billing interval
*
* @param int $interval
* @return string
*/
public static function billing_name(int $interval): string
{
$interval = collect(self::billing_periods)->get($interval);
return Arr::get($interval,'name','Unknown');
}
/**
* Return the number of months in the billing interval
*
* @param int $interval
* @return int
*/
public static function billing_period(int $interval): int
{
$interval = collect(self::billing_periods)->get($interval);
return Arr::get($interval,'interval',0);
}
/**
* Given a contract in months, this will calculate the number of billing intervals required
*
* @param int $contract_term
* @param int $source
* @return int
*/
public static function billing_term(int $contract_term,int $source): int
{
return ceil(($contract_term ?: 1)/(Arr::get(self::billing_periods,$source.'.interval') ?: 1));
}
/* RELATIONS */
public function account()
@ -312,7 +404,7 @@ class Invoice extends Model implements IDs
$lo = $this->account->user->language;
return $return->sortBy(function ($item) use ($lo) {
return $item->name($lo);
return $item->name;
});
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\OrderServiceOptions;
class PlanVoip extends Model
{
use OrderServiceOptions;
protected $order_attributes = [
'options.phonenumber'=>[
'request'=>'options.phonenumber',
'key'=>'service_number',
'validation'=>'nullable|size:10|unique:ab_service__voip,service_number',
'validation_message'=>'Phone Number is a required field.',
],
'options.supplier'=>[
'request'=>'options.supplier',
'key'=>'order_info.supplier',
'validation'=>'required_with:options.phonenumber',
'validation_message'=>'Phone Supplier is a required field.',
],
'options.supplieraccnum'=>[
'request'=>'options.supplieraccnum',
'key'=>'order_info.supplieraccnum',
'validation'=>'required_with:options.phonenumber',
'validation_message'=>'Phone Supplier Account Number is a required field.',
],
'options.notes'=>[
'request'=>'options.notes',
'key'=>'order_info.notes',
'validation'=>'required_if:options.phonenumber,null',
'validation_message'=>'Special Instructions here.',
],
];
protected $order_model = Service\Voip::class;
}

View File

@ -5,48 +5,84 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Leenooks\Traits\ScopeActive;
use App\Interfaces\IDs;
use App\Traits\NextKey;
use App\Traits\{ProductDetails,SiteID};
/**
* Class Product
* Products that are available to sale, and appear on invoices
* Products that are available to sale, and appear on invoices.
*
* Products have one Type (Product/*), made of an Offering (Supplier/*) from a Supplier.
* Conversely, Suppliers provide Offerings (Supplier/*) which belong to a Type (Product/*) of a Product.
*
* Attributes for products:
* + lid : Local ID for product (part number)
* + supplied : Supplier product provided for this offering
* + supplier : Supplier for this offering
* + name : Brief Name for our product
* + name_short : Product ID for our Product
* + name_long : Long Name for our product
* + billing_interval : Default Billing Interval
* + billing_interval_string: Default Billing Interval in human-readable form
* + setup_charge : Charge to setup this product
* + setup_charge_taxable : Charge to setup this product including taxes
* + base_charge : Default billing amount
* + base_charge_taxable : Default billing amount including taxes
* + min_charge : Minimum cost taking into account billing interval and setup costs
* + min_charge_taxable : Minimum cost taking into account billing interval and setup costs including taxes
*
* Attributes for product types (type - Product/*)
* + name : Short Name for our Product
* + name_long : Long Name for our Product
* + description : Description of offering (Broadband=speed)
*
* Attributes for supplier's offerings (type->supplied - Supplier/*)
* + name : Short Name for suppliers offering
* + name_long : Long Name for suppliers offering
* + description : Description of offering (Broadband=speed)
*
* Product Pricing self::pricing is an array of:
* [
* timeperiod => [
* show => true|false (show this time period to the user for ordering)
* group => [ pricing/setup ]
* ]
* ]
*
* @todo doesnt appear that price_type is used - but could be used to have different offering types billed differently
* @package App\Models
*/
class Product extends Model implements IDs
{
use HasFactory,NextKey;
const RECORD_ID = 'product';
public $incrementing = FALSE;
const CREATED_AT = 'date_orig';
const UPDATED_AT = 'date_last';
public $dateFormat = 'U';
protected $table = 'ab_product';
use HasFactory,SiteID,ProductDetails,ScopeActive;
protected $casts = [
// @todo convert existing data to a json array
// 'price_group'=>'array',
'pricing'=>'collection',
];
protected $with = ['descriptions'];
/* RELATIONS */
public function descriptions()
/**
* Get the product name in the users language, and if the user isnt logged in, the sites language
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function description()
{
return $this->hasMany(ProductTranslate::class);
return $this->hasOne(ProductTranslate::class)
->where('language_id',(Auth::user() && Auth::user()->language_id) ? Auth::user()->language_id : config('site')->language_id);
}
/**
* Which services are configured with this product
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function services()
{
return $this->hasMany(Service::class);
@ -59,144 +95,203 @@ class Product extends Model implements IDs
*/
public function type()
{
return $this->morphTo(null,'model','prod_plugin_data');
return $this->morphTo(null,'model','model_id');
}
/* INTERFACES */
public function getLIDAttribute(): string
{
return sprintf('%04s',$this->id);
}
public function getSIDAttribute(): string
{
return sprintf('%02s-%s',$this->site_id,$this->getLIDattribute());
}
/* ATTRIBUTES */
/**
* Get the service category (from the product)
* The amount we invoice each time period for this service
*
* @return string
* @param int|NULL $timeperiod
* @param Group|NULL $go
* @return float
*/
public function getCategoryAttribute()
public function getBaseChargeAttribute(int $timeperiod=NULL,Group $go=NULL): float
{
return $this->prod_plugin_file ?: 'Other';
}
public function getContractTermAttribute()
{
switch ($this->prod_plugin_file) {
case 'ADSL': return $this->plugin()->contract_term;
// @todo Incorporate into DB
case 'VOIP': return 12;
// @todo Change this after contracts implemented.
default:
return 'TBA';
}
}
public function getDefaultBillingAttribute()
{
return Arr::get($this->PricePeriods(),$this->price_recurr_default);
}
public function getDefaultCostAttribute()
{
// @todo Integrate this into a Tax::class
return Arr::get($this->price_array,sprintf('%s.1.price_base',$this->price_recurr_default))*1.1;
}
private function getDefaultLanguage()
{
return config('site')->language;
}
public function getDescriptionAttribute()
{
// @todo If the user has selected a specific language.
return $this->description($this->getDefaultLanguage());
return $this->getCharge('base',$timeperiod,$go);
}
/**
* Product Local ID
* The amount we invoice each time period for this service, including taxes
*
* @return string
* @param int|null $timeperiod
* @param Group|null $go
* @param Collection|NULL $taxes
* @return float
*/
public function getLIDattribute(): string
public function getBaseChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float
{
return sprintf('%04s',$this->id);
}
public function getMinimumCostAttribute()
{
$table = [
0=>4,
1=>1,
2=>1/3,
3=>1/6,
4=>1/12,
5=>1/24,
6=>1/36,
7=>1/48,
8=>1/60,
];
return $this->setup_cost + ( $this->default_cost * Arr::get($table,$this->price_recurr_default) * $this->contract_term);
}
public function getNameAttribute(Language $lo=NULL)
{
if (is_null($lo))
$lo = $this->getDefaultLanguage();
return $this->descriptions->where('language_id',$lo->id)->first()->description_short;
}
public function getNameShortAttribute(Language $lo=NULL)
{
if (is_null($lo))
$lo = $this->getDefaultLanguage();
return $this->descriptions->where('language_id',$lo->id)->first()->name;
}
public function getProductTypeAttribute()
{
return $this->plugin()->product->name;
}
public function getPriceArrayAttribute()
{
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()
{
$table = [
0=>_('One-time Charge'),
1=>_('Recurring Membership/Subscription'),
2=>_('Trial for Membership/Subscription'),
];
}
public function getProductIdAttribute()
{
return sprintf('#%04s',$this->id);
}
public function getSetupCostAttribute()
{
// @todo Integrate this into a Tax::class
return Arr::get($this->price_array,sprintf('%s.1.price_setup',$this->price_recurr_default))*1.1;
return Tax::tax_calc($this->getBaseChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes);
}
/**
* Product System ID
* The base cost of this product at the appropriate billing interval
*
* @return float
*/
public function getBaseCostAttribute(): float
{
return round($this->type->supplied->base_cost*Invoice::billing_change($this->type->supplied->getBillingIntervalAttribute(),$this->getBillingIntervalAttribute()) ?: 0,2);
}
/**
* The base cost of this product at the appropriate billing interval including taxes
*
* @param Collection|NULL $taxes
* @return float
*/
public function getBaseCostTaxableAttribute(Collection $taxes=NULL): float
{
return Tax::tax_calc($this->getBaseCostAttribute(),$taxes ?: config('site')->taxes);;
}
/**
* Our default billing interval
* Its the max of what we define, or what the supplier bills us at
*
* @return int
*/
public function getBillingIntervalAttribute(): int
{
return max($this->price_recur_default,$this->type->supplied->getBillingIntervalAttribute());
}
/**
* How long must this product be purchased for as a service.
*
* @return int
*/
public function getContractTermAttribute(): int
{
return $this->type->getContractTermAttribute();
}
/**
* Get the minimum cost of this product
*
* @param int|null $timeperiod
* @param Group|null $go
* @return float
*/
public function getMinChargeAttribute(int $timeperiod=NULL,Group $go=NULL): float
{
return $this->getSetupChargeAttribute($timeperiod,$go)+$this->getBaseChargeAttribute($timeperiod,$go)*Invoice::billing_term($this->getContractTermAttribute(),$this->getBillingIntervalAttribute());
}
/**
* Get the minimum cost of this product with taxes
*
* @param int|null $timeperiod
* @param Group|null $go
* @param Collection|NULL $taxes
* @return float
*/
public function getMinChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float
{
return Tax::tax_calc($this->getMinChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes);
}
/**
* Our products short descriptive name
*
* @return string
*/
public function getSIDattribute(): string
public function getNameAttribute(): string
{
return sprintf('%02s-%s',$this->site_id,$this->getLIDattribute());
return $this->description ? $this->description->description_short : 'Unknown PRODUCT';
}
/**
* Our products PID
*
* @return string
*/
public function getNameShortAttribute(): string
{
return $this->description ? $this->description->name : 'Unknown PID';
}
/**
* This product full description
*
* @return string
*/
public function getNameLongAttribute(): string
{
return $this->description->description_full;
}
/**
* Get our product type
*
* @return string
*/
public function getProductTypeAttribute(): string
{
return ($this->type && $this->type->supplied) ? $this->type->supplied->getTypeAttribute() : 'Unknown';
}
/**
* The charge to setup this service
*
* @param int|null $timeperiod
* @param Group|null $go
* @return float
*/
public function getSetupChargeAttribute(int $timeperiod=NULL,Group $go=NULL): float
{
return $this->getCharge('setup',$timeperiod,$go);
}
/**
* The charge to setup this service including taxes
*
* @param int|null $timeperiod
* @param Group|null $go
* @param Collection|null $taxes
* @return float
*/
public function getSetupChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float
{
return Tax::tax_calc($this->getSetupChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes);
}
/**
* The charge to setup this service
*
* @return float
*/
public function getSetupCostAttribute(): float
{
return $this->type->supplied->setup_cost ?: 0;
}
/**
* The charge to setup this service
*
* @param Collection|null $taxes
* @return float
*/
public function getSetupCostTaxableAttribute(Collection $taxes=NULL): float
{
return Tax::tax_calc($this->getSetupCostAttribute(),$taxes ?: config('site')->taxes);;
}
/* METHODS */
/**
* Return if this product captures usage data
*
@ -204,81 +299,49 @@ class Product extends Model implements IDs
*/
public function hasUsage(): bool
{
// @todo This should be configured in the DB
return in_array($this->model, ['App\Models\Product\Adsl']);
}
public function scopeActive()
{
return $this->where('active',TRUE);
}
public function description(Language $lo=NULL)
{
if (is_null($lo))
$lo = $this->getDefaultLanguage();
return $this->descriptions->where('language_id',$lo->id)->first()->description_full;
}
public function orderValidation(Request $request)
{
return $this->plugin()->orderValidation($request);
}
private function plugin()
{
switch ($this->prod_plugin_file) {
case 'ADSL':
return AdslPlan::findOrFail($this->prod_plugin_data);
case 'VOIP':
return new PlanVoip;
}
return $this->type->supplied->hasUsage();
}
/**
* Get the price for this product based on the period being requested.
* Get a charge value from the pricing array
*
* If the price period doesnt exist, we'll take the default period (0) which should.
* @param string $type
* @param int|NULL $timeperiod
* @param Group|NULL $go
* @return float
*/
private function getCharge(string $type,int $timeperiod=NULL,Group $go=NULL): float
{
static $default = NULL;
if (! $go) {
if (is_null($default))
$default = Group::findOrFail(0); // All public users
$go = $default;
}
if (is_null($timeperiod))
$timeperiod = $this->getBillingIntervalAttribute();
// If the price doesnt exist for $go->id, use $go->id = 0 which is all users.
if (! $price=Arr::get($this->pricing,sprintf('%d.%d.%s',$timeperiod,$go->id,$type)))
$price = Arr::get($this->pricing,sprintf('%d.%d.%s',$timeperiod,0,$type));
// @todo - if price doesnt exist for the time period, reduce down to timeperiod 1 and multiply appropriately.
if (is_null($price))
abort(500,sprintf('Price is NULL, we need to find it timeperiod[%s] group[%s]',$timeperiod,$go->id));
return round($price,2);
}
/**
* When receiving an order, validate that we have all the required information for the product type
*
* @param int $period
* @param Request $request
* @return mixed
*/
public function price(int $period,string $key='price_base')
public function orderValidation(Request $request): ?Model
{
return Arr::get(
$this->price_array,
sprintf('%s.1.%s',$period,$key),
Arr::get($this->price_array,sprintf('%s.0.%s',$period,$key))
);
}
public function PricePeriods()
{
return [
0=>_('Weekly'),
1=>_('Monthly'),
2=>_('Quarterly'),
3=>_('Semi-Annually'),
4=>_('Annually'),
5=>_('Two years'),
6=>_('Three Years'),
7=>_('Four Years'),
8=>_('Five Years'),
];
}
/**
* Get the product name
*
* @param Language $lo
* @return string Product Name
*/
public function name(Language $lo=NULL)
{
if (is_null($lo))
$lo = $this->getDefaultLanguage();
return $this->descriptions->where('language_id',$lo->id)->first()->name;
return $this->type->orderValidation($request);
}
}

View File

@ -1,159 +0,0 @@
<?php
namespace App\Models\Product;
use Illuminate\Support\Collection;
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 'speed':
return $this->product->speed;
}
// 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;
}
public function getSupplierAttribute()
{
return $this->getRelationValue('supplier');
}
}

View File

@ -0,0 +1,178 @@
<?php
namespace App\Models\Product;
use Illuminate\Support\Collection;
use App\Interfaces\ProductSupplier;
use App\Models\Base\ProductType;
use App\Models\{Product,Supplier};
use App\Models\Service\Broadband as ServiceBroadband;
use App\Models\Supplier\Broadband as SupplierBroadband;
use App\Traits\{OrderServiceOptions,SiteID};
class Broadband extends ProductType implements ProductSupplier
{
use SiteID;
use OrderServiceOptions;
protected $table = 'product_broadband';
// Information required during the order process
private array $order_attributes = [
'options.address'=>[
'request'=>'options.address',
'key'=>'service_address',
'validation'=>'required|string:10|unique:ab_service__adsl,service_address',
'validation_message'=>'Address is a required field.',
],
'options.notes'=>[
'request'=>'options.notes',
'key'=>'order_info.notes',
'validation'=>'present',
'validation_message'=>'Special Instructions here.',
],
];
protected string $order_model = ServiceBroadband::class;
/* RELATIONS */
/**
* The product that sells this type
*
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
*/
public function product()
{
return $this->morphOne(Product::class, null,'model','model_id');
}
/**
* The offering supplied with this product
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function supplied()
{
return $this->hasOne(SupplierBroadband::class,'id','supplier_broadband_id');
}
/**
* The supplier
*
* @return \Illuminate\Database\Eloquent\Relations\HasOneThrough
*/
// @todo To check
public function supplier()
{
return $this->hasOneThrough(Supplier::class,SupplierBroadband::class,'id','id','adsl_supplier_plan_id','supplier_id');
}
/* INTERFACES */
/**
* 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();
foreach (array_keys(Supplier\Broadband::traffic_map) as $k => $v) {
// Base Config
$config->put($k,$this->{$k});
// Excess Config
$config->put($v,$this->{$v});
}
// 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->supplied->allowance($config,$data,$ceil);
}
/* ATTRIBUTES */
/**
* Return the suppliers cost for this service
*
* @return float
*/
// @todo To check
public function allowance_cost(): float
{
$result = 0;
foreach ($this->supplied->allowance(NULL,$this->allowance([])->toArray()) as $k=>$v) {
$result += -$v*$this->supplied->{Supplier\Broadband::traffic_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;
}
/**
* The product contract term is the highest of
* + This defined contract_term
* + The suppliers contract_term
*
* @return int
*/
public function getContractTermAttribute(): int
{
return max($this->attributes['contract_term'],$this->supplied->getContractTermAttribute());
}
public function getCostAttribute(): float
{
abort(500,'deprecated');
// @todo Tax shouldnt be hard coded
return ($this->supplied->base_cost+$this->allowance_cost())*1.1;
}
public function getSupplierAttribute()
{
abort(500,'deprecated');
return $this->getRelationValue('supplier');
}
public function hasUsage(): bool
{
return TRUE;
}
}

View File

@ -25,6 +25,11 @@ class Domain extends ProductType implements ProductSupplier
return '';
}
public function getContractTermAttribute(): int
{
return 12;
}
public function getCostAttribute(): float
{
// N/A
@ -35,4 +40,14 @@ class Domain extends ProductType implements ProductSupplier
{
return '';
}
public function getTypeAttribute()
{
return 'Domain Name';
}
public function hasUsage(): bool
{
return FALSE;
}
}

View File

@ -6,4 +6,18 @@ use App\Models\Base\ProductType;
class Generic extends ProductType
{
public function getContractTermAttribute(): int
{
return 0;
}
public function getTypeAttribute()
{
return 'Generic';
}
public function hasUsage(): bool
{
return FALSE;
}
}

View File

@ -9,4 +9,19 @@ class Host extends ProductType
{
use NextKey;
const RECORD_ID = '';
public function getContractTermAttribute(): int
{
return 12;
}
public function getTypeAttribute()
{
return 'Hosting';
}
public function hasUsage(): bool
{
return FALSE;
}
}

View File

@ -27,6 +27,11 @@ class SSL extends ProductType implements ProductSupplier
return '';
}
public function getContractTermAttribute(): int
{
return 12;
}
public function getCostAttribute(): float
{
// N/A
@ -51,4 +56,14 @@ class SSL extends ProductType implements ProductSupplier
return $o;
}
public function getTypeAttribute()
{
return 'SSL Certificate';
}
public function hasUsage(): bool
{
return FALSE;
}
}

View File

@ -4,9 +4,56 @@ namespace App\Models\Product;
use App\Models\Base\ProductType;
use App\Traits\NextKey;
use App\Traits\OrderServiceOptions;
class Voip extends ProductType
{
use NextKey;
const RECORD_ID = '';
use OrderServiceOptions;
protected $order_attributes = [
'options.phonenumber'=>[
'request'=>'options.phonenumber',
'key'=>'service_number',
'validation'=>'nullable|size:10|unique:ab_service__voip,service_number',
'validation_message'=>'Phone Number is a required field.',
],
'options.supplier'=>[
'request'=>'options.supplier',
'key'=>'order_info.supplier',
'validation'=>'required_with:options.phonenumber',
'validation_message'=>'Phone Supplier is a required field.',
],
'options.supplieraccnum'=>[
'request'=>'options.supplieraccnum',
'key'=>'order_info.supplieraccnum',
'validation'=>'required_with:options.phonenumber',
'validation_message'=>'Phone Supplier Account Number is a required field.',
],
'options.notes'=>[
'request'=>'options.notes',
'key'=>'order_info.notes',
'validation'=>'required_if:options.phonenumber,null',
'validation_message'=>'Special Instructions here.',
],
];
protected $order_model = \App\Models\Service\Voip::class;
public function getContractTermAttribute(): int
{
return 12;
}
public function getTypeAttribute()
{
return 'VOIP';
}
public function hasUsage(): bool
{
return TRUE;
}
}

View File

@ -18,14 +18,15 @@ use Leenooks\Carbon;
use Symfony\Component\HttpKernel\Exception\HttpException;
use App\Interfaces\IDs;
use App\Traits\NextKey;
/**
* Class Service
* Services that belong to an account
*
* Attributes for services:
* + billing_period : The period that this service is billed for by default
* + additional_cost : Pending additional charges for this service (excluding setup)
* + billing_cost : Charge for this service each invoice period
* + billing_interval : The period that this service is billed for by default
* + name : Service short name with service address
* + name_short : Service Product short name, eg: phone number, domain name, certificate CN
* + name_detail : Service Detail, eg: service_address
@ -33,9 +34,10 @@ use App\Traits\NextKey;
*
* @package App\Models
*/
// @todo All the methods/attributes in this file need to be checked.
class Service extends Model implements IDs
{
use NextKey,HasFactory;
use HasFactory;
const RECORD_ID = 'service';
public $incrementing = FALSE;
@ -85,6 +87,7 @@ class Service extends Model implements IDs
'status',
];
/*
protected $with = [
'account.language',
'charges',
@ -92,6 +95,7 @@ class Service extends Model implements IDs
'product',
'type',
];
*/
// @todo Change to self::INACTIVE_STATUS
private $inactive_status = [
@ -370,7 +374,6 @@ class Service extends Model implements IDs
* Product of the service
*
* @return BelongsTo
* @deprecated use type->product
*/
public function product()
{
@ -491,10 +494,10 @@ class Service extends Model implements IDs
public function getBillingPriceAttribute(): float
{
// @todo Temporary for services that dont have recur_schedule set.
if (is_null($this->recur_schedule) OR is_null($this->product->price($this->recur_schedule)))
if (is_null($this->recur_schedule) OR is_null($this->product->getBaseChargeAttribute($this->recur_schedule,$this->account->group)))
$this->price=0;
return $this->addTax(is_null($this->price) ? $this->product->price($this->recur_schedule) : $this->price);
return $this->addTax(is_null($this->price) ? $this->product->getBaseChargeAttribute($this->recur_schedule,$this->account->group) : $this->price);
}
public function getBillingMonthlyPriceAttribute(): float
@ -516,11 +519,32 @@ class Service extends Model implements IDs
/**
* Return the service billing period
*
* @return int
*/
public function getBillingIntervalAttribute(): int
{
return $this->recur_schedule ?: $this->product->getBillingIntervalAttribute();
}
/**
* Return a human friendly name for the billing interval
*
* @return string
*/
public function getBillingPeriodAttribute(): string
public function getBillingIntervalStringAttribute(): string
{
return Arr::get($this->product->PricePeriods(),$this->recur_schedule,'Unknown');
return Invoice::billing_name($this->getBillingIntervalAttribute());
}
/**
* This function will determine the minimum contract term for a service, which is the maximum of
* this::type->contract_term, or the product->type->contract_term();
*
* @return int
*/
public function getContractTermAttribute(): int
{
abort(500,'To implement (Dec 2021)');
}
/**
@ -561,42 +585,42 @@ class Service extends Model implements IDs
{
switch ($this->recur_schedule) {
// Weekly
case 0: $date = $this->product->price_recurr_strict
case 0: $date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfWeek()
: $this->getInvoiceNextAttribute()->addWeek()->subDay();
break;
// Monthly
case 1:
$date = $this->product->price_recurr_strict
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfMonth()
: $this->getInvoiceNextAttribute()->addMonth()->subDay();
break;
// Quarterly
case 2:
$date = $this->product->price_recurr_strict
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfQuarter()
: $this->getInvoiceNextAttribute()->addQuarter()->subDay();
break;
// Half Yearly
case 3:
$date = $this->product->price_recurr_strict
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfHalf()
: $this->getInvoiceNextAttribute()->addQuarter(2)->subDay();
break;
// Yearly
case 4:
$date = $this->product->price_recurr_strict
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfYear()
: $this->getInvoiceNextAttribute()->addYear()->subDay();
break;
// Two Yearly
case 5:
if (!$this->product->price_recurr_strict)
if (!$this->product->price_recur_strict)
$date = $this->getInvoiceNextAttribute()->addYear(2)->subDay();
else {
$date = $this->getInvoiceNextAttribute()->addYear(2)->subDay()->endOfYear();
@ -608,7 +632,7 @@ class Service extends Model implements IDs
break;
// Three Yearly
// NOTE: price_recurr_strict ignored
// NOTE: price_recur_strict ignored
case 6: $date = $this->getInvoiceNextAttribute()->addYear(3)->subDay(); break;
default: throw new Exception('Unknown recur_schedule');
@ -624,7 +648,7 @@ class Service extends Model implements IDs
public function getInvoiceNextQuantityAttribute()
{
// If we are not rounding to the first day of the cycle, then it is always a full cycle
if (! $this->product->price_recurr_strict)
if (! $this->product->price_recur_strict)
return 1;
$n = $this->invoice_next->diff($this->invoice_next_end)->days+1;
@ -759,11 +783,12 @@ class Service extends Model implements IDs
/**
* Get the Product's Category for this service
*
* @deprecated use product->category directly
* @deprecated use product->getProductTypeAttribute() directly
*/
public function getProductCategoryAttribute(): string
{
return $this->product->category;
abort(500,'deprecated');
return $this->product->getProductTypeAttribute();
}
/**
@ -774,6 +799,7 @@ class Service extends Model implements IDs
*/
public function getProductNameAttribute(): string
{
abort(500,'deprecated');
return $this->product->name($this->account->language);
}
@ -860,7 +886,7 @@ class Service extends Model implements IDs
public function getSTypeAttribute(): string
{
switch($this->product->model) {
case 'App\Models\Product\Adsl': return 'broadband';
case 'App\Models\Product\Broadband': return 'broadband';
default: return $this->type->type;
}
}
@ -918,6 +944,18 @@ class Service extends Model implements IDs
: $this->status;
}
/**
* Return the type of service is provided.
*
* @return string
*/
public function getServiceTypeAttribute(): string
{
// @todo This is temporary, while we clean the database.
return ($this->product->type && $this->product->type->supplied) ? $this->product->type->supplied->getTypeAttribute() : '** TBA **';
}
/**
* URL used by an admin to administer the record
*
@ -1275,7 +1313,7 @@ class Service extends Model implements IDs
// Connection charges are only charged once
if ((! $this->invoice_items->filter(function($item) { return $item->item_type==4; })->sum('total'))
AND ($this->isPending() OR is_null($this->invoice_to))
AND $this->product->price($this->recur_schedule,'price_setup'))
AND $this->product->getSetupChargeAttribute($this->recur_schedule,$this->account->group))
{
$o = new InvoiceItem;
@ -1283,7 +1321,7 @@ class Service extends Model implements IDs
$o->service_id = $this->id;
$o->product_id = $this->product_id;
$o->item_type = 4; // @todo change to const or something
$o->price_base = $this->product->price($this->recur_schedule,'price_setup'); // @todo change to a method in this class
$o->price_base = $this->product->getSetupChargeAttribute($this->recur_schedule,$this->account->group);
//$o->recurring_schedule = $this->recur_schedule;
$o->date_start = $this->invoice_next;
$o->date_stop = $this->invoice_next;
@ -1309,7 +1347,7 @@ class Service extends Model implements IDs
$o->product_id = $this->product_id;
$o->item_type = 0;
$o->price_base = is_null($this->price)
? (is_null($this->price_override) ? $this->product->price($this->recur_schedule) : $this->price_override)
? (is_null($this->price_override) ? $this->product->getBaseChargeAttribute($this->recur_schedule,$this->account->group) : $this->price_override)
: $this->price; // @todo change to a method in this class
$o->recurring_schedule = $this->recur_schedule;
$o->date_start = $this->invoice_next;
@ -1354,6 +1392,7 @@ class Service extends Model implements IDs
*/
private function ServicePlugin()
{
abort(500,'deprecated');
// @todo: All services should be linked to a product. This might require data cleaning for old services not linked to a product.
if (! is_object($this->product))
return NULL;

View File

@ -3,17 +3,15 @@
namespace App\Models\Service;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Leenooks\Carbon;
use App\Interfaces\{ServiceItem,ServiceUsage};
use App\Models\AdslSupplierPlan;
use App\Models\Base\ServiceType;
use App\Models\Supplier\Broadband as SupplierBroadband;
use App\Traits\NextKey;
class Adsl extends ServiceType implements ServiceItem,ServiceUsage
class Broadband extends ServiceType implements ServiceItem,ServiceUsage
{
private const LOGKEY = 'MSA';
@ -26,21 +24,7 @@ class Adsl extends ServiceType implements ServiceItem,ServiceUsage
];
protected $table = 'ab_service__adsl';
/** RELATIONSHIPS **/
/**
* The suppliers product
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function product()
{
return $this
->hasOne(AdslSupplierPlan::class,'id','adsl_supplier_plan_id')
->withDefault(function() {
$o = new AdslSupplierPlan;
});
}
/* RELATIONS */
/**
* The accounts that this user manages
@ -53,7 +37,7 @@ class Adsl extends ServiceType implements ServiceItem,ServiceUsage
return $this->hasMany(AdslTraffic::class,'ab_service_adsl_id');
}
/** SCOPES */
/* SCOPES */
/**
* Search for a record
@ -71,12 +55,13 @@ class Adsl extends ServiceType implements ServiceItem,ServiceUsage
->orWhere('ipaddress','like','%'.$term.'%');
}
/** ATTRIBUTES **/
/**
* @deprecated use $o->service_name;
* @return mixed|string
*/
/* ATTRIBUTES */
public function getNameAttribute()
{
return $this->service_number ?: $this->service_address;
@ -107,6 +92,8 @@ class Adsl extends ServiceType implements ServiceItem,ServiceUsage
return $this->service_number ?: $this->service_address;
}
/* METHODS */
/**
* Is this service currently in a contract
*
@ -117,6 +104,18 @@ class Adsl extends ServiceType implements ServiceItem,ServiceUsage
return $this->service_contract_date AND $this->service_contract_date->addMonths($this->contract_term)->isFuture();
}
/**
* Return the suppliers offering that this service is providing
*
* @return SupplierBroadband
*/
public function supplied(): SupplierBroadband
{
return $this->provided_adsl_plan_id
? SupplierBroadband::findOrFail($this->provided_adsl_plan_id)
: $this->service->product->type->supplied;
}
/**
* Return service usage data
*

View File

@ -67,7 +67,7 @@ class SSL extends ServiceType implements ServiceItem
{
return $this->cert
? Arr::get($this->crt_parse,'subject.CN')
: Arr::get(openssl_csr_get_subject($this->csr),'CN');
: Arr::get(openssl_csr_get_subject($this->csr),'CN','');
}
public function inContract(): bool

View File

@ -58,6 +58,11 @@ class Site extends Model
return $this->belongsTo(Language::class);
}
public function taxes()
{
return $this->hasMany(Tax::class,'country_id','country_id');
}
/* ATTRIBUTES */
/**

View File

@ -3,18 +3,93 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Leenooks\Traits\ScopeActive;
use App\Models\Supplier\{Broadband,Ethernet,HSPA};
class Supplier extends Model
{
use ScopeActive;
public $timestamps = FALSE;
/* The offerings we provide */
public const offering_types = [
'broadband' => [
'name' => 'Broadband',
'class' => Broadband::class,
],
'hspa' => [
'name' => 'Mobile Broadband',
'class' => HSPA::class,
],
'ethernet' => [
'name' => 'Ethernet Broadband',
'class' => Ethernet::class,
],
'domainname' => [
'name' => 'Domain Name',
//'class' => Domain::class,
],
'generic' => [
'name' => 'Generic',
//'class' => Generic::class,
],
'hosting' => [
'name' => 'Hosting',
//'class' => Host::class,
],
'voip' => [
'name' => 'VOIP Telephone',
//'class' => Voip::class,
],
];
/* RELATIONS */
public function detail()
{
return $this->hasOne(SupplierDetail::class);
}
/* METHODS */
/**
* Return the offerings that this supplier provides
*
* @return void
*/
public function offeringTypes(): Collection
{
$result = collect();
// See if we have any configurations
foreach (self::offering_types as $key => $type) {
if (! $class = Arr::get($type,'class'))
continue;
if (Arr::get($this->detail->connections,$key)) {
$result->put($key,(object)[
'type' => Arr::get($type,'name'),
'items' => (new $class)->where('supplier_detail_id',$this->detail->id),
]);
continue;
}
// See if we have any products defined
$o = new $class;
$o->where('supplier_detail_id',$this->detail->id);
if ($o->count())
$result->put($key,(object)[
'type' => Arr::get($type,'name'),
'items' => (new $class)->where('supplier_detail_id',$this->detail->id),
]);
}
return $result;
}
}

View File

@ -1,14 +1,112 @@
<?php
namespace App\Models;
namespace App\Models\Supplier;
use App\Traits\ProductDetails;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Leenooks\Traits\ScopeActive;
class AdslSupplierPlan extends Model
use App\Interfaces\SupplierItem;
use App\Models\{Invoice,Supplier,SupplierDetail,Tax};
use App\Models\Product\Broadband as ProductBroadband;
use App\Traits\SiteID;
class Broadband extends Model implements SupplierItem
{
protected $table = 'ab_adsl_supplier_plan';
use SiteID,ScopeActive,ProductDetails;
protected $casts = [
'offpeak_start' => 'datetime:H:i',
'offpeak_end' => 'datetime:H:i',
];
protected $table = 'supplier_broadband';
// Map the table fields, with the extra fields
public const traffic_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 the NULL relationships - and where traffic gets applied if NULL
public const traffic_merge = [
'extra_up_offpeak' => 'base_down_offpeak',
'extra_down_offpeak' => 'base_down_peak',
'extra_up_peak' => 'base_down_peak',
'extra_down_peak' => 'base_down_peak',
];
/* INTERFACES */
public function supplier_detail(): BelongsTo
{
return $this->belongsTo(SupplierDetail::class);
}
public function types(): BelongsToMany
{
return $this->belongsToMany(ProductBroadband::class,'supplier_broadband','id','id','id','supplier_broadband_id');
}
public function getBaseCostTaxableAttribute(): float
{
return Tax::tax_calc($this->attributes['base_cost'],config('site')->taxes);
}
public function getBillingIntervalAttribute(): int
{
return 1; // Monthly
}
/**
* This contract term is the highest of
* + The defined contract_term
* + The default months in a billing interval
*
* @return int
*/
public function getContractTermAttribute(): int
{
return max(Invoice::billing_period(self::getBillingIntervalAttribute()),Arr::get($this->attributes,'contract_term'));
}
public function getMinCostAttribute(): float
{
return $this->attributes['setup_cost']+$this->attributes['base_cost']*Invoice::billing_term($this->getContractTermAttribute(),$this->getBillingIntervalAttribute());
}
public function getMinCostTaxableAttribute(): float
{
return Tax::tax_calc($this->getMinCostAttribute(),config('site')->taxes);
}
public function getNameAttribute(): string
{
return $this->product_id ?: 'Supplier PID Unknown';
}
public function getNameLongAttribute(): string
{
return $this->product_desc ?: 'Supplier NAME Unknown';
}
public function getSetupCostTaxableAttribute(): float
{
return Tax::tax_calc($this->attributes['setup_cost'],config('site')->taxes);
}
public function getTypeAttribute(): string
{
return Arr::get(collect(Supplier::offering_types)->firstWhere('class',get_class($this)),'name','Unknown');
}
/* METHODS */
/**
* Determine how traffic is counted for Broadband links.
@ -17,7 +115,7 @@ class AdslSupplierPlan extends Model
* + 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:
* 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
@ -39,29 +137,15 @@ class AdslSupplierPlan extends Model
* + 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
* @param Collection|null $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 $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',
]);
public function allowance(Collection $config=NULL,array $data=[],bool $ceil=TRUE)
{
$map = collect(self::traffic_map);
$merge = collect(self::traffic_merge);
if (is_null($config))
$config = collect($config);
@ -69,14 +153,12 @@ class AdslSupplierPlan extends Model
// If config is null, use the configuration from this Model
if (! $config->count()) {
// Base Config
foreach ($map->keys() as $k) {
foreach ($map->keys() as $k)
$config->put($k,$this->{$k});
}
// Excess Config
foreach ($map->values() as $k) {
foreach ($map->values() as $k)
$config->put($k,$this->{$k});
}
// Shaped or Charge
$config->put('shaped',$this->extra_shaped);
@ -89,7 +171,7 @@ class AdslSupplierPlan extends Model
$result = collect();
// If data is empty, we'll report on allowance, otherwise we'll report on consumption
$report = $data ? FALSE : TRUE;
$report = ! $data;
// Work out if we charge each period
foreach ($map as $k => $v) {
@ -136,7 +218,12 @@ class AdslSupplierPlan extends Model
return $result;
}
public function getNameAttribute()
/**
* Return the Broadband Speed
*
* @return string
*/
public function speed(): string
{
return $this->speed;
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Models\Supplier;
class Ethernet extends Broadband
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Models\Supplier;
class HSPA extends Broadband
{
}

View File

@ -10,6 +10,8 @@ class SupplierDetail extends Model
{
use SiteID;
protected $casts = [ 'connections'=>'collection' ];
/* RELATIONS */
public function supplier()

View File

@ -2,6 +2,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
class Tax extends Model
@ -14,4 +15,30 @@ class Tax extends Model
{
return $this->belongsTo(Country::class);
}
/* METHODS */
/**
* Calculate Tax on a value
*
* @param float $value
* @param Collection $taxes
* @return void
*/
public static function tax_calc(?float $value,Collection $taxes): float
{
if (! $value)
$value = 0;
$tax = 0;
foreach ($taxes as $o) {
// Quick sanity check
if (! $o instanceof self)
abort(500,'Invalid object for tax calculation');
$tax += round($value*$o->rate,2);
}
return round($value+$tax,2);
}
}

View File

@ -495,7 +495,7 @@ class User extends Authenticatable
}
$result->load([
'product.descriptions',
'product.description',
'service.type',
]);

View File

@ -0,0 +1,21 @@
<?php
/**
* Consistent Details on Products
*/
namespace App\Traits;
use App\Models\Invoice;
trait ProductDetails
{
/**
* Return a human friendly name for the billing interval
*
* @return string
*/
public function getBillingIntervalStringAttribute(): string
{
return Invoice::billing_name(self::getBillingIntervalAttribute());
}
}

View File

@ -2,20 +2,20 @@
use Faker\Generator as Faker;
$factory->define(App\Models\Product\Adsl::class, function (Faker $faker) {
$factory->define(App\Models\Product\Broadband::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();
$factory->afterMaking(App\Models\Product\Broadband::class, function ($adsl,$faker) {
$product = factory(App\Models\Supplier\Broadband::class)->make();
$adsl->setRelation('product',$product);
$adsl->adsl_supplier_plan_id = $product->id;
});
$factory->state(App\Models\Product\Adsl::class,'unlimit',[
$factory->state(App\Models\Product\Broadband::class,'unlimit',[
'base_down_peak'=>NULL,
'base_up_peak'=>NULL,
'base_down_offpeak'=>NULL,
@ -29,7 +29,7 @@ $factory->state(App\Models\Product\Adsl::class,'unlimit',[
'metric'=>1,
]);
$factory->state(App\Models\Product\Adsl::class,'140/0/0/0',[
$factory->state(App\Models\Product\Broadband::class,'140/0/0/0',[
'base_down_peak'=>140,
'base_up_peak'=>0,
'base_down_offpeak'=>0,
@ -43,7 +43,7 @@ $factory->state(App\Models\Product\Adsl::class,'140/0/0/0',[
'metric'=>1,
]);
$factory->state(App\Models\Product\Adsl::class,'70/-/0/-',[
$factory->state(App\Models\Product\Broadband::class,'70/-/0/-',[
'base_down_peak'=>70,
'base_up_peak'=>NULL,
'base_down_offpeak'=>0,
@ -57,7 +57,7 @@ $factory->state(App\Models\Product\Adsl::class,'70/-/0/-',[
'metric'=>1,
]);
$factory->state(App\Models\Product\Adsl::class,'100/0/40/0',[
$factory->state(App\Models\Product\Broadband::class,'100/0/40/0',[
'base_down_peak'=>100,
'base_up_peak'=>0,
'base_down_offpeak'=>40,
@ -71,7 +71,7 @@ $factory->state(App\Models\Product\Adsl::class,'100/0/40/0',[
'metric'=>1,
]);
$factory->state(App\Models\Product\Adsl::class,'50/-/20/-',[
$factory->state(App\Models\Product\Broadband::class,'50/-/20/-',[
'base_down_peak'=>50,
'base_up_peak'=>NULL,
'base_down_offpeak'=>20,

View File

@ -2,7 +2,7 @@
use Faker\Generator as Faker;
$factory->define(App\Models\AdslSupplierPlan::class, function (Faker $faker) {
$factory->define(App\Models\Supplier\Broadband::class, function (Faker $faker) {
return [
'id'=>1,
'contract_term'=>12,

View File

@ -33,7 +33,6 @@ class ProductFactory extends Factory
'taxable' => TRUE,
'active' => TRUE,
// 'position'
// 'cart_multiple'
// 'group_avail'
// 'avail_category'
// 'price_type'
@ -50,7 +49,7 @@ class ProductFactory extends Factory
// 'price_recurr_weekday'
// 'price_recurr_strict'
// 'prod_plugin_file'
//'prod_plugin_data' => 1,
// 'prod_plugin_data' => 1,
// 'accounting'
// 'model' => 'App\Models\Product\Adsl',
];

View File

@ -10,9 +10,8 @@ use Illuminate\Support\Facades\Schema;
| ab_account_log |
| ab_account_memo |
| ab_account_oauth |
| ab_adsl_plan |
| ab_adsl_supplier |
| ab_adsl_supplier_plan |
| product_broadband |*DONE*
| supplier_broadband |*DONE*
| ab_affiliate |
| ab_asset |
| ab_asset_pool |
@ -48,7 +47,7 @@ use Illuminate\Support\Facades\Schema;
| ab_module_method_token |
| ab_oauth |
| ab_pivot_product_cat |
| ab_product |
| products |*PARTIAL* - make model/model_id NOT NULL
| ab_product_cat |
| ab_product_cat_translate |
| ab_product_translate |
@ -71,7 +70,7 @@ use Illuminate\Support\Facades\Schema;
| ab_task |
| ab_task_log |
| ab_voip_plan |
| accounts |*PARTIAL*
| accounts |*PARTIAL* - make timestamp columns, make date_expire timestamp
| charges |
| countries |*DONE*
| currencies |*DONE*
@ -98,7 +97,7 @@ use Illuminate\Support\Facades\Schema;
| supplier_details |*DONE*
| suppliers |*DONE*
| taxes |*DONE*
| users |*PARTIAL*
| users |*DONE*
*/
class IntUnsigned extends Migration
{
@ -322,7 +321,8 @@ class IntUnsigned extends Migration
DB::statement('ALTER TABLE users MODIFY country_id int unsigned NOT NULL,MODIFY language_id int unsigned NOT NULL,MODIFY currency_id int unsigned NOT NULL');
Schema::table('users', function (Blueprint $table) {
$table->foreign(['country_id','currency_id'])->references(['id','currency_id'])->on('countries');
$table->dropColumn(['currency_id']);
$table->foreign(['country_id'])->references(['id'])->on('countries');
$table->foreign(['parent_id','site_id'])->references(['id','site_id'])->on('users');
$table->foreign(['language_id'])->references(['id'])->on('languages');
});
@ -332,11 +332,11 @@ class IntUnsigned extends Migration
DB::statement('ALTER TABLE accounts MODIFY site_id int unsigned NOT NULL');
DB::statement('ALTER TABLE accounts MODIFY country_id int unsigned NOT NULL,MODIFY language_id int unsigned NOT NULL,MODIFY currency_id int unsigned NOT NULL,MODIFY rtm_id int unsigned DEFAULT NULL,MODIFY active tinyint(1) NOT NULL');
Schema::table('accounts', function (Blueprint $table) {
$table->dropColumn(['currency_id','language_id']);
$table->foreign(['site_id'])->references(['site_id'])->on('sites');
$table->index(['id','site_id']);
$table->foreign(['country_id','currency_id'])->references(['id','currency_id'])->on('countries');
$table->foreign(['country_id'])->references(['id'])->on('countries');
$table->foreign(['user_id','site_id'])->references(['id','site_id'])->on('users');
$table->foreign(['language_id'])->references(['id'])->on('languages');
$table->foreign(['rtm_id','site_id'])->references(['id','site_id'])->on('rtm');
});

View File

@ -0,0 +1,295 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class OptimizeProduct extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
/*
Schema::table('ab_product', function (Blueprint $table) {
$table->dropForeign(['site_id']);
$table->dropIndex(['id','site_id']);
$table->dropIndex('ab_product_site_id_foreign');
});
DB::statement('ALTER TABLE ab_product RENAME TO products');
Schema::table('products', function (Blueprint $table) {
$table->dropColumn(['cart_multiple']);
$table->index(['id','site_id']);
$table->foreign(['site_id'])->references(['id'])->on('sites');
$table->dateTime('created_at')->nullable()->after('id');
$table->dateTime('updated_at')->nullable()->after('created_at');
});
Schema::table('supplier_details', function (Blueprint $table) {
$table->jsonb('connections')->nullable();
});
foreach (\Illuminate\Support\Facades\DB::select('SELECT * FROM AB_ADSL_SUPPLIER') as $o) {
switch($o->name) {
case 'PeopleAgent':
$type = 'broadband';
$name = 'People Telecom';
break;
case 'iiNetADSL':
$type = 'broadband';
$name = 'iiNet';
break;
case 'ExetelVisp':
$type = 'broadband';
$name = 'Exetel';
break;
case 'ExetelHSPA':
$type = 'hspa';
$name = 'Exetel';
break;
case 'ExetelPE':
$type = 'ethernet';
$name = 'Exetel';
break;
default:
throw new Exception('Unknown Supplier: '.$o->name);
}
$so = \App\Models\Supplier::where('name',$name)->singleOrNew();
if (! $so->exists) {
$so->name = $name;
$so->address1 = '...';
$so->city = '...';
$so->state = '...';
$so->postcode = '...';
}
$so->active = $so->active ?: $o->active;
$so->save();
$sdo = \App\Models\SupplierDetail::withoutGlobalScope(\App\Models\Scopes\SiteScope::class)
->where('supplier_id',$so->id)
->where('site_id',$o->site_id)
->firstOrNew();
if (! $sdo->exists) {
$sdo->site_id = $o->site_id;
}
$connections = $sdo->connections ?: collect();
$connections->put($type,[
'user'=>$o->stats_username,
'pass'=>$o->stats_password,
'last'=>$o->stats_lastupdate,
'url'=>$o->stats_url,
]);
$sdo->connections = $connections;
$so->detail()->save($sdo);
};
Schema::table('ab_adsl_supplier_plan', function (Blueprint $table) {
$table->dropForeign(['site_id']);
$table->dropIndex(['id','site_id']);
$table->dropIndex('ab_adsl_supplier_plan_site_id_foreign');
});
DB::statement('ALTER TABLE ab_adsl_supplier_plan RENAME TO supplier_broadband');
DB::statement('ALTER TABLE supplier_broadband MODIFY product_id varchar(16) NOT NULL');
DB::statement('ALTER TABLE supplier_broadband MODIFY base_cost double NOT NULL');
DB::statement('ALTER TABLE supplier_broadband MODIFY active tinyint(1)');
DB::statement('ALTER TABLE supplier_broadband RENAME COLUMN supplier_id TO old_supplier_id');
DB::statement('ALTER TABLE supplier_broadband RENAME COLUMN offpeak_start TO old_offpeak_start');
DB::statement('ALTER TABLE supplier_broadband RENAME COLUMN offpeak_end TO old_offpeak_end');
Schema::table('supplier_broadband', function (Blueprint $table) {
$table->index(['id','site_id']);
$table->foreign(['site_id'])->references(['id'])->on('sites');
$table->dateTime('created_at')->nullable()->after('id');
$table->dateTime('updated_at')->nullable()->after('created_at');
$table->time('offpeak_start')->nullable()->after('old_offpeak_end');
$table->time('offpeak_end')->nullable()->after('offpeak_start');
});
Schema::table('supplier_broadband', function (Blueprint $table) {
$table->integer('supplier_detail_id')->unsigned()->nullable()->after('old_supplier_id');
$table->foreign(['supplier_detail_id','site_id'])->references(['id','site_id'])->on('supplier_details');
});
\Illuminate\Support\Facades\DB::select("UPDATE ab_service SET model='App\\\\Models\\\\Service\\\\Broadband' where model='App\\\\Models\\\\Service\\\\Adsl'");
\Illuminate\Support\Facades\DB::select("UPDATE products SET model='App\\\\Models\\\\Product\\\\Broadband' where model='App\\\\Models\\\\Product\\\\Adsl'");
// Convert to use the new supplier
foreach (\Illuminate\Support\Facades\DB::select('SELECT * FROM AB_ADSL_SUPPLIER') as $o) {
switch ($o->name) {
case 'PeopleAgent':
$so = \App\Models\Supplier::where('name','People Telecom')->singleOrFail();
break;
case 'iiNetADSL':
$so = \App\Models\Supplier::where('name','iiNet')->singleOrFail();
break;
case 'ExetelVisp':
case 'ExetelHSPA':
case 'ExetelPE':
$so = \App\Models\Supplier::where('name','Exetel')->singleOrFail();
break;
default:
throw new Exception('Unknown Supplier: ' . $o->name);
}
$sdo = \App\Models\SupplierDetail::withoutGlobalScope(\App\Models\Scopes\SiteScope::class)
->where('supplier_id',$so->id)
->where('site_id',$o->site_id)
->singleOrFail();
\Illuminate\Support\Facades\DB::select(sprintf("UPDATE supplier_broadband SET supplier_detail_id=%d where old_supplier_id=%d and site_id=%d",$sdo->id,$o->id,$sdo->site_id));
}
// Convert out dates
foreach (\App\Models\Supplier\Broadband::withoutGlobalScope(\App\Models\Scopes\SiteScope::class)->cursor() as $o) {
if ($o->date_orig)
$o->created_at = \Carbon\Carbon::createFromTimestamp($o->date_orig);
if ($o->date_last)
$o->updated_at = \Carbon\Carbon::createFromTimestamp($o->date_last);
if ($o->old_offpeak_start)
$o->offpeak_start = \Carbon\Carbon::createFromTimestamp($o->old_offpeak_start);
if ($o->old_offpeak_end)
$o->offpeak_end = \Carbon\Carbon::createFromTimestamp($o->old_offpeak_end);
$o->save();
}
Schema::table('supplier_broadband', function (Blueprint $table) {
$table->dropPrimary();
$table->primary(['id','site_id']);
$table->dropColumn(['date_orig','date_last','old_supplier_id','old_offpeak_start','old_offpeak_end']);
});
Schema::dropIfExists('ab_adsl_supplier');
DB::statement('ALTER TABLE supplier_broadband MODIFY extra_charged tinyint(1)');
DB::statement('ALTER TABLE supplier_broadband MODIFY extra_shaped tinyint(1)');
DB::statement('ALTER TABLE supplier_broadband MODIFY contract_term int unsigned');
Schema::table('ab_adsl_plan', function (Blueprint $table) {
$table->dropForeign(['site_id']);
$table->dropIndex(['id','site_id']);
$table->dropIndex('ab_adsl_plan_site_id_foreign');
});
DB::statement('ALTER TABLE ab_adsl_plan RENAME TO product_broadband');
DB::statement('ALTER TABLE product_broadband DROP PRIMARY KEY,ADD PRIMARY KEY (id,site_id)');
DB::statement('ALTER TABLE product_broadband MODIFY extra_charged tinyint(1)');
DB::statement('ALTER TABLE product_broadband MODIFY extra_shaped tinyint(1)');
DB::statement('ALTER TABLE product_broadband MODIFY contract_term int unsigned');
DB::statement('ALTER TABLE product_broadband RENAME COLUMN adsl_supplier_plan_id TO supplier_broadband_id');
DB::statement('ALTER TABLE product_broadband MODIFY supplier_broadband_id int unsigned');
Schema::table('product_broadband', function (Blueprint $table) {
$table->index(['id','site_id']);
$table->foreign(['site_id'])->references(['id'])->on('sites');
$table->foreign(['supplier_broadband_id','site_id'])->references(['id','site_id'])->on('supplier_broadband');
$table->dateTime('created_at')->nullable()->after('id');
$table->dateTime('updated_at')->nullable()->after('created_at');
});
// Convert product pricegroups
foreach (\App\Models\Product::withoutGlobalScope(\App\Models\Scopes\SiteScope::class)->cursor() as $po) {
if ($po->date_orig)
$po->created_at = \Carbon\Carbon::createFromTimestamp($po->date_orig);
if ($po->date_last)
$po->updated_at = \Carbon\Carbon::createFromTimestamp($po->date_last);
if (! ($po instanceof \Illuminate\Support\Collection) || ! $po->price_group->count()) {
$original = $po->getRawOriginal('price_group');
// serialized
if (preg_match('/^a:/',$original)) {
try {
$price_group = collect(unserialize(str_replace("\n","",$original)));
} catch (Exception $e) {
dd(['error'=>$e->getMessage(),'raw'=>$po->getRawOriginal('price_group')]);
}
} elseif (is_null($po->getRawOriginal('price_group'))) {
$price_group = collect();
} else {
try {
$price_group = unserialize(gzuncompress($po->getRawOriginal('price_group')));
} catch (Exception $e) {
dd(['error'=>$e->getMessage(),'raw'=>$po->getRawOriginal('price_group')]);
}
}
$new_price_group = collect();
// Remove any blank entries, or when base/setup = 0
foreach ($price_group as $group => $values) {
$build = collect();
foreach ($values as $key => $pricing) {
switch ($key) {
case 'show':
$build->put('show',(bool) $pricing);
break;
default:
// key is a time period
if ((! Arr::get($pricing,'price_base')) && (! Arr::get($pricing,'price_setup')))
break;
$build->put($key,[
'base'=>Arr::get($pricing,'price_base'),
'setup'=>Arr::get($pricing,'price_setup'),
]);
}
}
$new_price_group->put($group,$build);
}
$po->price_group = $new_price_group;
}
$po->save();
}
DB::statement('ALTER TABLE products MODIFY taxable tinyint(1),MODIFY active tinyint(1),MODIFY price_recurr_strict tinyint(1),MODIFY prod_plugin_data int unsigned');
DB::statement('ALTER TABLE products RENAME COLUMN price_group TO pricing');
DB::statement('ALTER TABLE products RENAME COLUMN price_recurr_default TO price_recur_default');
DB::statement('ALTER TABLE products RENAME COLUMN price_recurr_strict TO price_recur_strict');
DB::statement('ALTER TABLE products RENAME COLUMN prod_plugin_data TO model_id');
Schema::table('products', function (Blueprint $table) {
$table->dropColumn(['date_orig','date_last','group_avail','avail_category','price_recurr_day','price_recurr_weekday','prod_plugin_file']);
});
*/
abort(500,'here');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//abort(500,'Cant go back');
}
}

View File

@ -19,6 +19,12 @@ body {
box-shadow: 0 0 0 #fff;
background-color: #fff;
}
/* Fix select width */
.dataTables_wrapper .dataTables_length select {
width: 5em !important;
}
.dataTables_scrollHeadInner {
width: 100% !important;
}

View File

@ -1,32 +1,80 @@
<!-- $o = Service::class -->
<table class="table table-sm">
<thead>
<tr>
<th>&nbsp;</th>
<th>{{ $o->product->type->supplied->supplier_detail->supplier->name }}</th>
<th>Us</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<th>Supplier</th><td>{{ $o->product->type ? $o->product->type->supplier->name : 'Supplier Unknown' }}</td>
<th>Product</th>
<td>#{{ ($s=$o->product->type->supplied)->id }}: {{ $s->name }}</td>
<td>#{{ $o->product->id }}: {{ $o->product->name }}</td>
<td>{{ $s->type }}</td>
</tr>
<tr>
<th>Supplier Product</th><td>#{{ $o->product_id }}: {{ $o->product->type ? $o->product->type->product->product_id : 'Product Unknown' }}</td>
</tr>
@if($o->product->type)
<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>
@endif
<tr>
<th>Price</th><td>${{ number_format($o->billing_monthly_price,2) }} <small>(${{ number_format($o->billing_monthly_price*12,2) }} Annually)</small></td>
</tr>
@if($o->product->type AND $o->product->type->cost)
<tr>
<th>Markup</th><td>{{ number_format(($o->billing_monthly_price/$o->product->type->cost-1)*100,2) }}%</td>
</tr>
@endif
<tr>
<th>Setup</th>
<td>${{ number_format($a=\App\Models\Tax::tax_calc($s->setup_cost,$o->account->taxes),2) }}</td>
<td>${{ number_format($b=\App\Models\Tax::tax_calc($o->product->setup_charge,$o->account->taxes),2) }}</td>
<td>
@if ($a > $b)
<span class="badge bg-danger>">({{ number_format(($a-$b)/($b ?: 1)*100,1) }})%</span>
@else
<span class="badge">{{ number_format(($b-$a)/($a ?: 1)*100,1) }}%</span>
@endif
</td>
</tr>
<tr>
<th>Billed</th>
<td>{{ $s->billing_interval_string }}</td>
<td>{{ $o->product->billing_interval_string }}</td>
<td>&nbsp;</td>
</tr>
<tr>
<th>Billing Charge</th>
<td>${{ number_format($a=\App\Models\Tax::tax_calc($s->base_cost*\App\Models\Invoice::billing_change($s->billing_interval,$o->product->billing_interval),$o->account->taxes),2) }}</td>
<td>${{ number_format($b=\App\Models\Tax::tax_calc($o->product->getBaseChargeAttribute($o->billing_interval),$o->account->taxes),2) }}</td>
<td>
@if ($a > $b)
<span class="badge bg-danger>">({{ number_format(($a-$b)/($b ?: 1)*100,1) }})%</span>
@else
<span class="badge">{{ number_format(($b-$a)/($a ?: 1)*100,1) }}%</span>
@endif
</td>
</tr>
<tr>
<th>Monthly Cost</th>
<td>${{ number_format($a=\App\Models\Tax::tax_calc($s->base_cost*\App\Models\Invoice::billing_change($s->billing_interval,1),$o->account->taxes),2) }}</td>
<td>${{ number_format($b=\App\Models\Tax::tax_calc($o->product->getBaseChargeAttribute($o->billing_interval)*\App\Models\Invoice::billing_change($o->billing_interval,1),$o->account->taxes),2) }}</td>
<td>
@if ($a > $b)
<span class="badge bg-danger>">({{ number_format(($a-$b)/($b ?: 1)*100,1) }})%</span>
@else
<span class="badge">{{ number_format(($b-$a)/($a ?: 1)*100,1) }}%</span>
@endif
</td>
</tr>
<tr>
<th>Contract</th>
<td>{{ $s->contract_term }} months</td>
<td>{{ $o->product->contract_term }} months</td>
<td>&nbsp;</td>
</tr>
<tr>
<th>Min Cost</th>
<td>${{ number_format($a=\App\Models\Tax::tax_calc($s->min_cost,$o->account->taxes),2) }}</td>
<td>${{ number_format($b=\App\Models\Tax::tax_calc($o->product->getMinChargeAttribute($o->billing_interval),$o->account->taxes),2) }}</td>
<td>
@if ($a > $b)
<span class="badge bg-danger>">({{ number_format(($a-$b)/($b ?: 1)*100,1) }})%</span>
@else
<span class="badge">{{ number_format(($b-$a)/($a ?: 1)*100,1) }}%</span>
@endif
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,57 @@
@extends('adminlte::layouts.app')
@section('htmlheader_title')
{{ $o->name ?: 'New Supplier' }}
@endsection
@section('page_title')
{{ $o->name ?: 'New Supplier' }}
@endsection
@section('contentheader_title')
{{ $o->name ?: 'New Supplier' }}
@endsection
@section('contentheader_description')
@endsection
@section('main-content')
<div class="row">
<div class="col-12">
@include('adminlte::widget.status')
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-dark d-flex p-0">
<ul class="nav nav-pills w-100 p-2">
<li class="nav-item"><a class="nav-link active" href="#details" data-toggle="tab">Detail</a></li>
<li class="nav-item"><a class="nav-link" href="#products" data-toggle="tab">Products</a></li>
<li class="nav-item"><a class="nav-link" href="#offerings" data-toggle="tab">Offerings</a></li>
<li class="nav-item"><a class="nav-link" href="#connections" data-toggle="tab">Connections</a></li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade active show" id="details" role="tabpanel">
@include('a.supplier.widgets.detail')
</div>
<div class="tab-pane fade" id="products" role="tabpanel">
@include('a.supplier.widgets.products')
</div>
<div class="tab-pane fade" id="offerings" role="tabpanel">
@include('a.supplier.widgets.offerings')
</div>
<div class="tab-pane fade" id="connections" role="tabpanel">
@include('a.supplier.widgets.connections')
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,34 @@
<!-- $o = Supplier::class -->
<div class="row">
<div class="col-9">
<table class="table table-sm">
<thead>
<tr>
<th>Service Type</th>
<th>User Name</th>
<th>Password</th>
<th>URL</th>
<th>Last Connect</th>
<th class="text-right">Offerings</th>
</tr>
</thead>
<tbody>
@foreach ($o->offeringTypes() as $key => $offering)
<tr>
<th>{{ $offering->type }}</th>
@if(Arr::get($o->detail->connections,$key))
<td>{{ Arr::get($o->detail->connections,$key.'.user') }}</td>
<td>{{ Arr::get($o->detail->connections,$key.'.pass') }}</td>
<td>{{ Arr::get($o->detail->connections,$key.'.url') }}</td>
<td>{{ \Carbon\Carbon::createFromFormat('Y-m-d',Arr::get($o->detail->connections,$key.'.last'))->format('Y-m-d') }}</td>
<td class="text-right">{{ number_format($offering->items->count()) }}</td>
@else
<td colspan="5">&nbsp;</td>
@endif
</tr>
@endforeach
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,185 @@
<!-- $o = Supplier::class -->
<div class="row">
<div class="col-12">
<h3>Supplier Details</h3>
<hr>
@if(session()->has('success'))
<span class="ml-3 pt-0 pb-0 pr-1 pl-1 btn btn-outline-success"><small>{{ session()->get('success') }}</small></span>
@endif
<form class="g-0 needs-validation" method="POST" enctype="multipart/form-data" role="form">
@csrf
<div class="row">
<div class="col-4">
<div class="row">
<!-- Supplier Name -->
<div class="col-9">
<div class="form-group has-validation">
<label for="name">Supplier Name</label>
<input type="text" class="form-control form-control-border @error('name') is-invalid @enderror" id="name" name="name" placeholder="Supplier Name" value="{{ old('name',$o->name) }}" required>
<span class="invalid-feedback" role="alert">
@error('name')
{{ $message }}
@else
Supplier Name required.
@enderror
</span>
</div>
</div>
<!-- Supplier Name -->
<div class="col-3">
<div class="form-group">
<div class="custom-control custom-switch custom-switch-off-danger custom-switch-on-success">
<input type="checkbox" class="custom-control-input" id="active" name="active" {{ old('active',$o->active) ? 'checked' : '' }}>
<label class="custom-control-label" for="active">Active</label>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Address Lines -->
<div class="col-12">
<div class="form-group has-validation">
<label for="address1">Address Lines</label>
<input type="text" class="form-control form-control-border @error('address1') is-invalid @enderror" id="address1" name="address1" placeholder="Address1" value="{{ old('address1',$o->address1) }}" required>
<input type="text" class="form-control form-control-border" id="address2" name="address2" placeholder="Address2" value="{{ old('address2',$o->address2) }}">
<span class="invalid-feedback" role="alert">
@error('address1')
{{ $message }}
@else
Atleast 1 address line required.
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<!-- City -->
<div class="col-12">
<div class="form-group has-validation">
<label for="city">City</label>
<input type="text" class="form-control form-control-border @error('city') is-invalid @enderror" id="city" name="city" placeholder="City" value="{{ old('city',$o->city) }}">
<span class="invalid-feedback" role="alert">
@error('city')
{{ $message }}
@else
City is required.
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<!-- State -->
<div class="col-9">
<div class="form-group has-validation">
<label for="state">State</label>
<input type="text" class="form-control form-control-border @error('state') is-invalid @enderror" id="state" name="state" placeholder="State" value="{{ old('state',$o->state) }}">
<span class="invalid-feedback" role="alert">
@error('state')
{{ $message }}
@else
State is required.
@enderror
</span>
</div>
</div>
<!-- Postal Code -->
<div class="col-3">
<div class="form-group has-validation">
<label for="postcode">Postal Code</label>
<input type="text" class="form-control form-control-border @error('postcode') is-invalid @enderror" id="postcode" name="postcode" placeholder="Postal Code" value="{{ old('postcode',$o->postcode) }}">
<span class="invalid-feedback" role="alert">
@error('postcode')
{{ $message }}
@else
Postcode is required.
@enderror
</span>
</div>
</div>
</div>
</div>
<div class="offset-1 col-4">
<div class="row">
<!-- Accounts Email -->
<div class="col-12">
<div class="form-group has-validation">
<label for="accounts">Accounts Email</label>
<input type="accounts" class="form-control form-control-border @error('supplier_details.accounts') is-invalid @enderror" id="accounts" name="supplier_details[accounts]" placeholder="Accounts Email" value="{{ old('supplier_details.accounts',($o->detail ? $o->detail->accounts : '')) }}">
<span class="invalid-feedback" role="alert">
@error('supplier_details.accounts')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<!-- Support Email -->
<div class="col-12">
<div class="form-group has-validation">
<label for="support">Support Email</label>
<input type="support" class="form-control form-control-border @error('supplier_details.support') is-invalid @enderror" id="support" name="supplier_details[support]" placeholder="Support Email" value="{{ old('supplier_details.support',($o->detail ? $o->detail->support : '')) }}">
<span class="invalid-feedback" role="alert">
@error('supplier_details.support')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<!-- Payment Details -->
<div class="col-12">
<div class="form-group has-validation">
<label for="payments">Payment Details</label>
<input type="payments" class="form-control form-control-border @error('supplier_details.payments') is-invalid @enderror" id="payments" name="supplier_details[payments]" placeholder="Payment Details" value="{{ old('supplier_details.payments',($o->detail ? $o->detail->payments : '')) }}">
<span class="invalid-feedback" role="alert">
@error('supplier_details.payments')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Notes -->
<div class="col-12">
<div class="form-group has-validation">
<label for="notes">Notes</label>
<textarea class="form-control @error('supplier_details.notes') is-invalid @enderror" id="notes" name="supplier_details[notes]" placeholder="Notes...">{{ old('supplier_details.notes',($o->detail ? $o->detail->notes : '')) }}</textarea>
<span class="input-helper">Notes.</span>
<span class="invalid-feedback" role="alert">
@error('supplier_details.notes')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<!-- Buttons -->
<div class="col-12">
<a href="{{ url('/home') }}" class="btn btn-danger">Cancel</a>
@can('wholesaler')
<button type="submit" name="submit" class="btn btn-success mr-0 float-right">@if ($o->exists)Save @else Add @endif</button>
@endcan
</div>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,84 @@
<!-- $o = Supplier::class -->
<div class="row">
<div class="col-5 col-sm-3">
<div class="nav flex-column nav-tabs h-100" role="tablist" aria-orientation="vertical">
@foreach($o->offeringTypes() as $key => $offering)
<a class="nav-link @if($loop->first)active @endif" id="offering-{{ $key }}-tab" data-toggle="pill" href="#offering-{{ $key }}-profile" role="tab" aria-controls="offering-{{ $key }}-tab" aria-selected="true">{{ $offering->type }}</a>
@endforeach
</div>
</div>
<div class="col-7 col-sm-9">
<div class="tab-content">
@foreach($o->offeringTypes() as $key => $offering)
<div class="tab-pane text-left fade show @if($loop->first)active @endif" id="offering-{{ $key }}-profile" role="tabpanel" aria-labelledby="offering-{{ $key }}-tab">
<table class="table table-sm table-bordered w-100" id="offering-{{ $key }}-table">
<thead>
<tr>
<th colspan="8">Product</th>
<th colspan="2">Services</th>
</tr>
<tr>
<th>ID</th>
<th>Product ID</th>
<th>Product Name</th>
<th>Active</th>
<th class="text-right">Setup Cost</th>
<th class="text-right">Base Cost</th>
<th class="text-right">Types</th>
<th class="text-right">Products</th>
<th class="text-right">Sold</th>
<th class="text-right">Active</th>
</tr>
</thead>
<tbody>
@foreach($xx=$offering->items->with(['types.product.services'])->get() as $oo)
<tr>
<td>{{ $oo->id }}</td>
<td>{{ $oo->name }}</td>
<td>{{ $oo->name_long }}</td>
<td class="text-right">{{ $oo->active ? 'YES' : 'NO' }}</td>
<td class="text-right">{{ number_format($oo->setup_cost_taxable,2) }}</td>
<td class="text-right">{{ number_format($oo->base_cost_taxable,2) }}</td>
<td class="text-right">{{ number_format($oo->types->count()) }}</td>
<td class="text-right">{{ number_format($oo->types->pluck('product')->filter()->count()) }}</td>
<td class="text-right">{{ number_format(($x=$oo->types->pluck('product.services')->flatten()->filter())->count()) }}</td>
<td class="text-right">{{ number_format($x->where('active')->count()) }}</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<th colspan="3">TOTALS</th>
<td class="text-right" colspan="3">{{ $xx->where('active',TRUE)->count() }}</td>
<td class="text-right">{{ number_format(($x=$xx->pluck('types')->flatten()->filter())->count()) }}</td>
<td class="text-right">{{ number_format($x->pluck('product')->filter()->count()) }}</td>
<td class="text-right">{{ number_format(($xxx=$x->pluck('product.services')->flatten()->filter())->count()) }}</td>
<td class="text-right">{{ number_format($xxx->where('active')->count()) }}</td>
</tr>
</tfoot>
</table>
</div>
@endforeach
</div>
</div>
</div>
@section('page-scripts')
@css(datatables,bootstrap4)
@js(datatables,bootstrap4)
<script type="text/javascript">
$(document).ready(function() {
@foreach($o->offeringTypes() as $key => $offering)
$('#offering-{{ $key }}-table').DataTable();
$('#offering-{{ $key }}-table tbody').on('click','tr', function () {
$(this).toggleClass('selected');
});
@endforeach
});
</script>
@append

View File

@ -0,0 +1,88 @@
<!-- $o = Supplier::class -->
<div class="row">
<div class="col-5 col-sm-3">
<div class="nav flex-column nav-tabs h-100" role="tablist" aria-orientation="vertical">
@foreach($o->offeringTypes() as $key => $offering)
<a class="nav-link @if($loop->first)active @endif" id="products-{{ $key }}-tab" data-toggle="pill" href="#products-{{ $key }}-profile" role="tab" aria-controls="products-{{ $key }}-tab" aria-selected="true">{{ $offering->type }}</a>
@endforeach
</div>
</div>
<div class="col-7 col-sm-9">
<div class="tab-content">
@foreach($o->offeringTypes() as $key => $offering)
<div class="tab-pane text-left fade show @if($loop->first)active @endif" id="products-{{ $key }}-profile" role="tabpanel" aria-labelledby="products-{{ $key }}-tab">
<table class="table table-sm table-bordered w-100" id="products-{{ $key }}-table">
<thead>
<tr>
<th colspan="7">Product</th>
<th colspan="4">Services</th>
</tr>
<tr>
<th>ID</th>
<th>Product ID</th>
<th>Product Name</th>
<th>Active</th>
<th>Default Billing</th>
<th class="text-right">Setup Cost</th>
<th class="text-right">Base Cost</th>
<th class="text-right">Setup Charge</th>
<th class="text-right">Base Charge</th>
<th class="text-right">Sold</th>
<th class="text-right">Active</th>
</tr>
</thead>
<tbody>
@foreach($xx=$offering->items->with(['types.product.services','types.product.type.supplied','types.product.description'])->get() as $oo)
@foreach($oo->types->pluck('product')->filter() as $po)
<tr>
<td>{{ $po->lid }}</td>
<td>{{ $po->name_short }}</td>
<td>{{ $po->name }}</td>
<td class="text-right">{{ $po->active ? 'YES' : 'NO' }}</td>
<td class="text-right">{{ $po->billing_interval_string }}</td>
<td class="text-right">{{ number_format($po->setup_cost_taxable,2) }}</td>
<td class="text-right">{{ number_format($po->base_cost_taxable,2) }}</td>
<td class="text-right">{{ number_format($po->setup_charge_taxable,2) }}</td>
<td class="text-right">{{ number_format($po->base_charge_taxable,2) }}</td>
<td class="text-right">{{ number_format($po->services->count()) }}</td>
<td class="text-right">{{ number_format($po->services->where('active')->count()) }}</td>
</tr>
@endforeach
@endforeach
</tbody>
<tfoot>
<tr>
<th colspan="3">TOTALS</th>
<td class="text-right">{{ $xx->where('active',TRUE)->count() }}</td>
<th colspan="5">&nbsp;</th>
<td class="text-right">{{ number_format(($xxx=$xx->pluck('types')->flatten()->pluck('product.services')->flatten()->filter())->count()) }}</td>
<td class="text-right">{{ number_format($xxx->where('active')->count()) }}</td>
</tr>
</tfoot>
</table>
</div>
@endforeach
</div>
</div>
</div>
@section('page-scripts')
@css(datatables,bootstrap4)
@js(datatables,bootstrap4)
<script type="text/javascript">
$(document).ready(function() {
@foreach($o->offeringTypes() as $key => $offering)
$('#products-{{ $key }}-table').DataTable();
$('#products-{{ $key }}-table tbody').on('click','tr', function () {
$(this).toggleClass('selected');
});
@endforeach
});
</script>
@append

View File

@ -1,225 +0,0 @@
@extends('adminlte::layouts.app')
@section('htmlheader_title')
{{ $o->name ?: 'New Supplier' }}
@endsection
@section('page_title')
{{ $o->name ?: 'New Supplier' }}
@endsection
@section('contentheader_title')
{{ $o->name ?: 'New Supplier' }}
@endsection
@section('contentheader_description')
@endsection
@section('main-content')
<div class="row">
<div class="col-12">
@include('adminlte::widget.status')
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-dark d-flex p-0">
<ul class="nav nav-pills w-100 p-2">
<li class="nav-item"><a class="nav-link active" href="#details" data-toggle="tab">Details</a></li>
<li class="nav-item"><a class="nav-link " href="#products" data-toggle="tab">Products</a></li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade active show" id="details" role="tabpanel">
<h3>Supplier Details</h3>
<hr>
@if(session()->has('success'))
<span class="ml-3 pt-0 pb-0 pr-1 pl-1 btn btn-outline-success"><small>{{ session()->get('success') }}</small></span>
@endif
<form class="g-0 needs-validation" method="POST" enctype="multipart/form-data" role="form">
@csrf
<div class="row">
<div class="col-4">
<div class="row">
<!-- Supplier Name -->
<div class="col-9">
<div class="form-group has-validation">
<label for="name">Supplier Name</label>
<input type="text" class="form-control form-control-border @error('name') is-invalid @enderror" id="name" name="name" placeholder="Supplier Name" value="{{ old('name',$o->name) }}" required>
<span class="invalid-feedback" role="alert">
@error('name')
{{ $message }}
@else
Supplier Name required.
@enderror
</span>
</div>
</div>
<!-- Supplier Name -->
<div class="col-3">
<div class="form-group">
<div class="custom-control custom-switch custom-switch-off-danger custom-switch-on-success">
<input type="checkbox" class="custom-control-input" id="active" name="active" {{ old('active',$o->active) ? 'checked' : '' }}>
<label class="custom-control-label" for="active">Active</label>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Address Lines -->
<div class="col-12">
<div class="form-group has-validation">
<label for="address1">Address Lines</label>
<input type="text" class="form-control form-control-border @error('address1') is-invalid @enderror" id="address1" name="address1" placeholder="Address1" value="{{ old('address1',$o->address1) }}" required>
<input type="text" class="form-control form-control-border" id="address2" name="address2" placeholder="Address2" value="{{ old('address2',$o->address2) }}">
<span class="invalid-feedback" role="alert">
@error('address1')
{{ $message }}
@else
Atleast 1 address line required.
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<!-- City -->
<div class="col-12">
<div class="form-group has-validation">
<label for="city">City</label>
<input type="text" class="form-control form-control-border @error('city') is-invalid @enderror" id="city" name="city" placeholder="City" value="{{ old('city',$o->city) }}">
<span class="invalid-feedback" role="alert">
@error('city')
{{ $message }}
@else
City is required.
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<!-- State -->
<div class="col-9">
<div class="form-group has-validation">
<label for="state">State</label>
<input type="text" class="form-control form-control-border @error('state') is-invalid @enderror" id="state" name="state" placeholder="State" value="{{ old('state',$o->state) }}">
<span class="invalid-feedback" role="alert">
@error('state')
{{ $message }}
@else
State is required.
@enderror
</span>
</div>
</div>
<!-- Postal Code -->
<div class="col-3">
<div class="form-group has-validation">
<label for="postcode">Postal Code</label>
<input type="text" class="form-control form-control-border @error('postcode') is-invalid @enderror" id="postcode" name="postcode" placeholder="Postal Code" value="{{ old('postcode',$o->postcode) }}">
<span class="invalid-feedback" role="alert">
@error('postcode')
{{ $message }}
@else
Postcode is required.
@enderror
</span>
</div>
</div>
</div>
</div>
<div class="offset-1 col-4">
<div class="row">
<!-- Accounts Email -->
<div class="col-12">
<div class="form-group has-validation">
<label for="accounts">Accounts Email</label>
<input type="accounts" class="form-control form-control-border @error('supplier_details.accounts') is-invalid @enderror" id="accounts" name="supplier_details[accounts]" placeholder="Accounts Email" value="{{ old('supplier_details.accounts',($o->detail ? $o->detail->accounts : '')) }}">
<span class="invalid-feedback" role="alert">
@error('supplier_details.accounts')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<!-- Support Email -->
<div class="col-12">
<div class="form-group has-validation">
<label for="support">Support Email</label>
<input type="support" class="form-control form-control-border @error('supplier_details.support') is-invalid @enderror" id="support" name="supplier_details[support]" placeholder="Support Email" value="{{ old('supplier_details.support',($o->detail ? $o->detail->support : '')) }}">
<span class="invalid-feedback" role="alert">
@error('supplier_details.support')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<!-- Payment Details -->
<div class="col-12">
<div class="form-group has-validation">
<label for="payments">Payment Details</label>
<input type="payments" class="form-control form-control-border @error('supplier_details.payments') is-invalid @enderror" id="payments" name="supplier_details[payments]" placeholder="Payment Details" value="{{ old('supplier_details.payments',($o->detail ? $o->detail->payments : '')) }}">
<span class="invalid-feedback" role="alert">
@error('supplier_details.payments')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Notes -->
<div class="col-12">
<div class="form-group has-validation">
<label for="notes">Notes</label>
<textarea class="form-control @error('supplier_details.notes') is-invalid @enderror" id="notes" name="supplier_details[notes]" placeholder="Notes...">{{ old('supplier_details.notes',($o->detail ? $o->detail->notes : '')) }}</textarea>
<span class="input-helper">Notes.</span>
<span class="invalid-feedback" role="alert">
@error('supplier_details.notes')
{{ $message }}
@enderror
</span>
</div>
</div>
</div>
<div class="row">
<!-- Buttons -->
<div class="col-12">
<a href="{{ url('/home') }}" class="btn btn-danger">Cancel</a>
@can('wholesaler')
<button type="submit" name="submit" class="btn btn-success mr-0 float-right">@if ($o->exists)Save @else Add @endif</button>
@endcan
</div>
</div>
</form>
</div>
<div class="tab-pane fade" id="products" role="tabpanel">
Products.
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -102,7 +102,7 @@
<tr id="invoice-services">
<td>{{ $po->count }}</td>
<td>#{{ $po->lid }}</td>
<td colspan="2">{{ $po->name($o->account->user->language) }}</td>
<td colspan="2">{{ $po->name }}</td>
<td colspan="3" class="text-right">${{ number_format($o->items->filter(function($item) use ($po) {return $item->product_id == $po->id; })->sum('total'),$o->currency()->rounding) }}</td>
</tr>

View File

@ -42,7 +42,8 @@
<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>
<li class="nav-item ml-auto"><a class="nav-link" href="#internal" data-toggle="tab">Billing History</a></li>
<li class="nav-item"><a class="nav-link" href="#internal" data-toggle="tab">Internal</a></li>
<li class="nav-item"><a class="nav-link {{ session()->has('service_update') ? 'active' : '' }}" href="#update" data-toggle="tab">Update</a></li>
@endcan
</ul>

View File

@ -10,7 +10,7 @@
<thead>
<tr>
<th>ID</th>
<th>Category</th>
<th>Type</th>
<th>Service</th>
<th>Product</th>
<th>Next Invoice</th>
@ -21,9 +21,9 @@
@foreach ($o->services as $oo)
<tr>
<td><a href="{{ url('u/service',[$oo->id]) }}">{{ $oo->sid }}</a></td>
<td>{{ $oo->product_category }}</td>
<td>{{ $oo->service_type }}</td>
<td>{{ $oo->name_short }}</td>
<td>{{ $oo->product_name }}</td>
<td>{{ $oo->product->name }}</td>
<td>{{ $oo->external_billing ? '-' : $oo->next_invoice->format('Y-m-d') }}</td>
</tr>
@endforeach

View File

@ -7,7 +7,7 @@
@php
$po = $selected = NULL;
@endphp
@foreach (\App\Models\Product::active()->get()->filter(function($item) { return $item->type && (get_class($item->type) == 'App\Models\Product\Adsl'); })->sortBy('name') as $o)
@foreach (\App\Models\Product::active()->get()->filter(function($item) { return $item->type && (get_class($item->type) == 'App\Models\Product\Broadband'); })->sortBy('name') as $o)
@php
if ($o->id == old('product_id')) {
$selected = 'selected';

View File

@ -1,3 +1,4 @@
<!-- $o = Service\Broadband::class -->
<div class="card">
@if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg">
@ -44,7 +45,7 @@
@endif
<tr>
<th>Speed</th>
<td>{{ $o->service->product->type->product->speed }} Mbps</td>
<td>{{ $o->supplied()->speed() }} Mbps</td>
</tr>
<!-- @todo -->
<tr>

View File

@ -9,7 +9,7 @@
<div class="container">
<div class="col-12">
<h1>Order Service</h1>
@if ($errors->count()))<h4><span class="note-danger">There were errors with your order, please try again.</span></h4>@endif
@if ($errors->count())<h4><span class="note-danger">There were errors with your order, please try again.</span></h4>@endif
<div class="order-page" id="order-page">
<div class="row">
<div class="col-3">

View File

@ -1,8 +1,9 @@
@if(View::exists('order.widget.info.'.$o->category))
<!-- $o = Product::class [{{$o->product_type}}]-->
@if(View::exists('order.widget.info.'.strtolower($o->product_type)))
<div class="box box-primary">
<div class="box-body">
{{-- Return Category Requirements --}}
@include('order.widget.info.'.$o->category)
@include('order.widget.info.'.strtolower($o->product_type))
{{-- Return Supplier Requirements --}}
{{-- Return Product Requirements --}}

View File

@ -1,30 +0,0 @@
<div class="col-md-12">
<p>{!! $o->description !!}</p>
</div>
<table class="table table-condensed">
<tr>
<th>Type</th>
<td class="text-right">VOIP</td>
</tr>
<tr>
<th>Setup Charges</th>
<td class="text-right">${{ is_numeric($o->setup_cost) ? number_format($o->setup_cost,2) : $o->setup_cost }}</td>
</tr>
<tr>
<th>Cost</th>
<td class="text-right">${{ is_numeric($o->default_cost) ? number_format($o->default_cost,2) : $o->default_cost }}</td>
</tr>
<tr>
<th>Default Billing</th>
<td class="text-right">{{ is_numeric($o->default_billing) ? number_format($o->default_billing,2) : $o->default_billing }}</td>
</tr>
<tr>
<th>Contract Term</th>
<td class="text-right">{{ $o->contract_term }} mths</td>
</tr>
<tr>
<th>Minimum Costs</th>
<td class="text-right">${{ is_numeric($o->minimum_cost) ? number_format($o->minimum_cost,2) : $o->minimum_cost }}</td>
</tr>
</table>

View File

@ -1,3 +1,4 @@
<!-- $o = Product::class -->
<div class="col-md-12">
<p>{!! $o->description !!}</p>
</div>
@ -9,15 +10,15 @@
</tr>
<tr>
<th>Setup Charges <sup>*</sup></th>
<td class="text-right">${{ is_numeric($o->setup_cost) ? number_format($o->setup_cost,2) : $o->setup_cost }}</td>
<td class="text-right">${{ number_format($o->setup_charge_taxable,2) }}</td>
</tr>
<tr>
<th>Cost <sup>+</sup></th>
<td class="text-right">${{ is_numeric($o->default_cost) ? number_format($o->default_cost,2) : $o->default_cost }}</td>
<td class="text-right">${{ number_format($o->base_charge_taxable,2) }}</td>
</tr>
<tr>
<th>Default Billing</th>
<td class="text-right">{{ is_numeric($o->default_billing) ? number_format($o->default_billing,2) : $o->default_billing }}</td>
<td class="text-right">{{ $o->billing_interval_string }}</td>
</tr>
<tr>
<th>Contract Term</th>
@ -25,7 +26,7 @@
</tr>
<tr>
<th>Minimum Costs <sup>+*</sup></th>
<td class="text-right">${{ is_numeric($o->minimum_cost) ? number_format($o->minimum_cost,2) : $o->minimum_cost }}</td>
<td class="text-right">${{ number_format($o->min_charge_taxable,2) }}</td>
</tr>
<tfoot>

View File

@ -0,0 +1,31 @@
<!-- $o = Product::class -->
<div class="col-md-12">
<p>{!! $o->description !!}</p>
</div>
<table class="table table-condensed">
<tr>
<th>Type</th>
<td class="text-right">{{ $o->product_type }}</td>
</tr>
<tr>
<th>Setup Charges</th>
<td class="text-right">${{ number_format($o->setup_charge_taxable,2) }}</td>
</tr>
<tr>
<th>Cost</th>
<td class="text-right">${{ number_format($o->base_charge_taxable,2) }}</td>
</tr>
<tr>
<th>Default Billing</th>
<td class="text-right">{{ $o->billing_interval_string }}</td>
</tr>
<tr>
<th>Contract Term</th>
<td class="text-right">{{ $o->contract_term }} mths</td>
</tr>
<tr>
<th>Minimum Costs</th>
<td class="text-right">${{ number_format($o->min_charge_taxable,2) }}</td>
</tr>
</table>

View File

@ -1,4 +1,5 @@
@if(View::exists('order.widget.order.'.$o->category))
<!-- $o = Product::class -->
@if(View::exists('order.widget.order.'.strtolower($o->product_type)))
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Order Configuration</h3>
@ -6,7 +7,7 @@
<div class="box-body">
{{-- Return Category Requirements --}}
@include('order.widget.order.'.$o->category)
@include('order.widget.order.'.strtolower($o->product_type))
{{-- Return Supplier Requirements --}}
{{-- Return Product Requirements --}}