Compare commits

...

7 Commits

32 changed files with 382 additions and 130 deletions

View File

@ -10,32 +10,39 @@ use App\Models\{Site,Supplier,User};
class SupplierAccountSync extends Command class SupplierAccountSync extends Command
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'supplier:account:sync' protected $signature = 'supplier:account:sync'
.' {siteid : Site ID}'
.' {supplier : Supplier Name}' .' {supplier : Supplier Name}'
.' {--f|forceprod : Force Prod API on dev environment}'; .' {--f|forceprod : Force Prod API on dev environment}'
.' {--s|site : Site ID}';
/** /**
* The console command description. * The console command description.
* *
* @var string * @var string
*/ */
protected $description = 'Sync accounts with a supplier'; protected $description = 'Sync accounts with a supplier';
/** /**
* Execute the console command. * Execute the console command.
* *
* @return int * @return int
*/ * @note Suppliers are now associated with accounts, so this needs to be updated
public function handle() */
{ public function handle()
Config::set('site',Site::findOrFail($this->argument('siteid'))); {
$so = Supplier::where('name',$this->argument('supplier'))->singleOrFail(); Config::set(
'site',
$this->option('site')
? Site::findOrFail($this->option('site'))
: Site::where('url',config('app.url'))->sole()
);
$so = Supplier::where('name','ilike',strtolower($this->argument('supplier')))->sole();
foreach ($so->API($this->option('forceprod'))->getCustomers(['fetchall'=>true]) as $customer) { foreach ($so->API($this->option('forceprod'))->getCustomers(['fetchall'=>true]) as $customer) {
// Check if we have this customer already (by ID) // Check if we have this customer already (by ID)
@ -43,7 +50,6 @@ class SupplierAccountSync extends Command
$this->info(sprintf('User already linked (%s:%s)',$customer->id,$customer->email)); $this->info(sprintf('User already linked (%s:%s)',$customer->id,$customer->email));
} elseif ($x=User::where('email',$customer->email)->single()) { } elseif ($x=User::where('email',$customer->email)->single()) {
//dump($x->suppliers->first());
if ($x->suppliers->count()) { if ($x->suppliers->count()) {
$this->alert(sprintf('User [%d:%s] already linked to this supplier with ID (%s)',$customer->id,$customer->email,$x->suppliers->first()->pivot->id)); $this->alert(sprintf('User [%d:%s] already linked to this supplier with ID (%s)',$customer->id,$customer->email,$x->suppliers->first()->pivot->id));
@ -63,5 +69,7 @@ class SupplierAccountSync extends Command
$this->error(sprintf('User doesnt exist with email (%s:%s)',$customer->id,$customer->email)); $this->error(sprintf('User doesnt exist with email (%s:%s)',$customer->id,$customer->email));
} }
} }
}
return self::SUCCESS;
}
} }

View File

@ -3,9 +3,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\{Site,Supplier};
use App\Jobs\SupplierDomainSync as Job; use App\Jobs\SupplierDomainSync as Job;
use App\Models\{Site,Supplier};
class SupplierDomainSync extends Command class SupplierDomainSync extends Command
{ {
@ -15,9 +16,9 @@ class SupplierDomainSync extends Command
* @var string * @var string
*/ */
protected $signature = 'supplier:domain:sync' protected $signature = 'supplier:domain:sync'
.' {siteid : Site ID}' .' {supplier : Supplier Name}'
.' {supplier : Supplier Name}' .' {--f|forceprod : Force Prod API on dev environment}'
.' {--f|forceprod : Force Prod API on dev environment}'; .' {--s|site : Site ID}';
/** /**
* The console command description. * The console command description.
@ -33,8 +34,15 @@ class SupplierDomainSync extends Command
*/ */
public function handle() public function handle()
{ {
Config::set(
'site',
($x=$this->option('site')
? Site::findOrFail($this->option('site'))
: Site::where('url',config('app.url'))->sole())
);
$so = Supplier::where('name',$this->argument('supplier'))->singleOrFail(); $so = Supplier::where('name',$this->argument('supplier'))->singleOrFail();
Job::dispatchSync(Site::findOrFail($this->argument('siteid')),$so,$this->option('forceprod')); return Job::dispatchSync($x,$so,$this->option('forceprod'));
} }
} }

View File

@ -27,7 +27,7 @@ class SocialLoginController extends Controller
$openiduser = Socialite::with($provider)->user(); $openiduser = Socialite::with($provider)->user();
if (! $openiduser) if (! $openiduser)
return redirect('/home') return redirect('home')
->with('error','No user details obtained.'); ->with('error','No user details obtained.');
$oo = ProviderOauth::firstOrCreate(['name'=>$provider,'active'=>TRUE]); $oo = ProviderOauth::firstOrCreate(['name'=>$provider,'active'=>TRUE]);
@ -43,7 +43,7 @@ class SocialLoginController extends Controller
$user = User::where('email',$openiduser->email)->first(); $user = User::where('email',$openiduser->email)->first();
if ((! $user) || (! $user->active)) if ((! $user) || (! $user->active))
return redirect('/login') return redirect('login')
->with('error','Invalid account, or account inactive, please contact an admin.'); ->with('error','Invalid account, or account inactive, please contact an admin.');
return $this->link($provider,$aoo,$user); return $this->link($provider,$aoo,$user);
@ -54,7 +54,7 @@ class SocialLoginController extends Controller
// If there are too many users, then we have a problem // If there are too many users, then we have a problem
} elseif ($aoo->count() > 1) { } elseif ($aoo->count() > 1) {
return redirect('/login') return redirect('login')
->with('error','Seems you have multiple oauth IDs, please contact an admin.'); ->with('error','Seems you have multiple oauth IDs, please contact an admin.');
// User is using OAUTH for the first time. // User is using OAUTH for the first time.
@ -73,11 +73,11 @@ class SocialLoginController extends Controller
// If there are too many users, then we have a problem // If there are too many users, then we have a problem
} elseif ($uo->count() > 1) { } elseif ($uo->count() > 1) {
return redirect('/login') return redirect('login')
->with('error','Seems you have multiple accounts, please contact an admin.'); ->with('error','Seems you have multiple accounts, please contact an admin.');
} else { } else {
return redirect('/login') return redirect('login')
->with('error','Seems you dont have an account with that email, please contact an admin.'); ->with('error','Seems you dont have an account with that email, please contact an admin.');
} }
} }
@ -91,7 +91,7 @@ class SocialLoginController extends Controller
$openiduser = Socialite::with($provider)->user(); $openiduser = Socialite::with($provider)->user();
if (! $openiduser) if (! $openiduser)
return redirect('/home') return redirect('home')
->with('error','No user details obtained.'); ->with('error','No user details obtained.');
$po = ProviderOauth::where('name',$provider)->singleOrFail(); $po = ProviderOauth::where('name',$provider)->singleOrFail();
@ -137,12 +137,12 @@ class SocialLoginController extends Controller
// Check our email matches // Check our email matches
if (Arr::get($aoo->oauth_data,'email','invalid') !== $request->post('email')) if (Arr::get($aoo->oauth_data,'email','invalid') !== $request->post('email'))
return redirect('/login') return redirect('login')
->with('error','Account details didnt match to make link.'); ->with('error','Account details didnt match to make link.');
// Check our token matches // Check our token matches
if ($aoo->link_token !== $request->post('token')) if ($aoo->link_token !== $request->post('token'))
return redirect('/login') return redirect('login')
->with('error','Token details didnt match to make link.'); ->with('error','Token details didnt match to make link.');
// Load our email. // Load our email.

View File

@ -34,8 +34,7 @@ class CheckoutController extends Controller
} }
return $o->wasRecentlyCreated return $o->wasRecentlyCreated
? redirect() ? redirect('a/checkout/'.$o->id)
->to('a/checkout/'.$o->id)
->with('success','Checkout added') ->with('success','Checkout added')
: redirect() : redirect()
->back() ->back()

View File

@ -50,7 +50,7 @@ class HomeController extends Controller
} catch (ExpiredInviteCode $e) { } catch (ExpiredInviteCode $e) {
Log::alert(sprintf('User is using an expired token for invoice [%s] using [%s]',$o->id,$code)); Log::alert(sprintf('User is using an expired token for invoice [%s] using [%s]',$o->id,$code));
return redirect()->to('/login'); return redirect('login');
} catch (DoormanException $e) { } catch (DoormanException $e) {
Log::alert(sprintf('An attempt to read invoice id [%s] using [%s]',$o->id,$code)); Log::alert(sprintf('An attempt to read invoice id [%s] using [%s]',$o->id,$code));
@ -72,6 +72,6 @@ class HomeController extends Controller
*/ */
public function service_progress(Service $o,string $status) public function service_progress(Service $o,string $status)
{ {
return redirect()->to($o->action($status) ?: url('u/service',$o->id)); return redirect($o->action($status) ?: url('u/service',$o->id));
} }
} }

View File

@ -63,8 +63,7 @@ class PaymentController extends Controller
} }
return $o->wasRecentlyCreated return $o->wasRecentlyCreated
? redirect() ? redirect('r/payment/'.$o->id)
->to('r/payment/'.$o->id)
->with('success','Payment added') ->with('success','Payment added')
: redirect() : redirect()
->back() ->back()

View File

@ -34,8 +34,7 @@ class PaypalController extends Controller
public function cancel() public function cancel()
{ {
return redirect() return redirect(self::cart_url);
->to(self::cart_url);
} }
/** /**
@ -52,8 +51,7 @@ class PaypalController extends Controller
$cart = request()->session()->get('invoice.cart'); $cart = request()->session()->get('invoice.cart');
if (! $cart) if (! $cart)
return redirect() return redirect('home');
->to('u/home');
$invoices = Invoice::find($cart); $invoices = Invoice::find($cart);
@ -113,15 +111,13 @@ class PaypalController extends Controller
} catch (HttpException $e) { } catch (HttpException $e) {
Log::error('Paypal Exception',['request'=>$paypal,'response'=>$e->getMessage()]); Log::error('Paypal Exception',['request'=>$paypal,'response'=>$e->getMessage()]);
return redirect() return redirect(self::cart_url)
->to(self::cart_url)
->withErrors('Paypal Exception: '.$e->getCode()); ->withErrors('Paypal Exception: '.$e->getCode());
} catch (\HttpException $e) { } catch (\HttpException $e) {
Log::error('HTTP Exception',['request'=>$this->client,'response'=>$e->getMessage()]); Log::error('HTTP Exception',['request'=>$this->client,'response'=>$e->getMessage()]);
return redirect() return redirect(self::cart_url)
->to(self::cart_url)
->withErrors('HTTP Exception: '.$e->getCode()); ->withErrors('HTTP Exception: '.$e->getCode());
} }
@ -138,8 +134,7 @@ class PaypalController extends Controller
return redirect() return redirect()
->away($redirect_url); ->away($redirect_url);
return redirect() return redirect(self::cart_url)
->to(self::cart_url)
->withErrors('An error occurred with Paypal?'); ->withErrors('An error occurred with Paypal?');
} }
@ -192,23 +187,20 @@ class PaypalController extends Controller
->away($redirect_url); ->away($redirect_url);
} }
return redirect() return redirect(self::cart_url)
->to(self::cart_url)
->withErrors('An error occurred with Paypal?'); ->withErrors('An error occurred with Paypal?');
} catch (\HttpException $e) { } catch (\HttpException $e) {
Log::error('HTTP Exception',['request'=>$paypal,'response'=>$e->getMessage()]); Log::error('HTTP Exception',['request'=>$paypal,'response'=>$e->getMessage()]);
return redirect() return redirect(self::cart_url)
->to(self::cart_url)
->withErrors('HTTP Exception: '.$e->getCode()); ->withErrors('HTTP Exception: '.$e->getCode());
} }
if ((! $response) || (! $response->result->purchase_units)) { if ((! $response) || (! $response->result->purchase_units)) {
Log::error('Paypal Capture: No Purchase Units?'); Log::error('Paypal Capture: No Purchase Units?');
return redirect() return redirect(self::cart_url)
->to(self::cart_url)
->withErrors('Paypal Exception: NPU'); ->withErrors('Paypal Exception: NPU');
} }
@ -267,8 +259,7 @@ class PaypalController extends Controller
Log::info('Paypal Payment Recorded',['po'=>$po->id]); Log::info('Paypal Payment Recorded',['po'=>$po->id]);
return redirect() return redirect('home')
->to('u/home')
->with('success','Payment recorded thank you.'); ->with('success','Payment recorded thank you.');
} }
} }

View File

@ -113,7 +113,7 @@ class ServiceController extends Controller
$np->pivot->effective_at = Carbon::now(); $np->pivot->effective_at = Carbon::now();
$np->pivot->save(); $np->pivot->save();
return redirect()->to(url('u/service',[$o->id])); return redirect(url('u/service',[$o->id]));
} }
return view('theme.backend.adminlte.service.change_pending') return view('theme.backend.adminlte.service.change_pending')
@ -191,7 +191,7 @@ class ServiceController extends Controller
// An Error Condition // An Error Condition
if (is_null($result)) if (is_null($result))
return redirect()->to('u/service/'.$o->id); return redirect('u/service/'.$o->id);
elseif ($result instanceof RedirectResponse) elseif ($result instanceof RedirectResponse)
return $result; return $result;
@ -213,7 +213,7 @@ class ServiceController extends Controller
$stage = ''; // @todo this is temporary, we havent written the code to automatically jump to the next stage if wecan $stage = ''; // @todo this is temporary, we havent written the code to automatically jump to the next stage if wecan
} }
return redirect()->to('u/service/'.$o->id); return redirect('u/service/'.$o->id);
} }
/** /**
@ -412,7 +412,10 @@ class ServiceController extends Controller
'suspend_billing' => 'nullable|in:on', 'suspend_billing' => 'nullable|in:on',
'recur_schedule' => ['required',Rule::in(collect(Invoice::billing_periods)->keys())], 'recur_schedule' => ['required',Rule::in(collect(Invoice::billing_periods)->keys())],
'invoice_next_at' => 'nullable|date', 'invoice_next_at' => 'nullable|date',
'price' => 'nullable|numeric', 'product_id'=>'required|exists:products,id',
'price' => 'nullable|numeric|min:0', // Price we charge the client, if we dont charge supplied/price
'cost' => 'nullable|numeric|min:0', // Price we are charged by supplier, if we arent charged supplier/price
'supplierid' => 'nullable|string|min:1', // As used on invoices
$o->product->category => 'array|min:1', $o->product->category => 'array|min:1',
] ]
) )
@ -456,6 +459,9 @@ class ServiceController extends Controller
$o->suspend_billing = ($validated->get('suspend_billing') == 'on'); $o->suspend_billing = ($validated->get('suspend_billing') == 'on');
$o->external_billing = ($validated->get('external_billing') == 'on'); $o->external_billing = ($validated->get('external_billing') == 'on');
$o->price = $validated->get('price'); $o->price = $validated->get('price');
$o->cost = $validated->get('cost');
$o->supplierid = $validated->get('supplierid');
$o->product_id = $validated->get('product_id');
// Also update our service start_at date. // Also update our service start_at date.
// @todo We may want to make start_at/stop_at dynamic values calculated by the type records // @todo We may want to make start_at/stop_at dynamic values calculated by the type records

View File

@ -30,7 +30,9 @@ class SupplierController extends Controller
$o->save(); $o->save();
} catch (\Exception $e) { } catch (\Exception $e) {
return redirect()->back()->withErrors($e->getMessage())->withInput(); return redirect()
->back()
->withErrors($e->getMessage())->withInput();
} }
$o->load(['detail']); $o->load(['detail']);

View File

@ -45,6 +45,7 @@ use App\Traits\{ScopeAccountUserAuthorised,ScopeServiceActive,SiteID};
* *
* Methods: * Methods:
* + isChargeOverridden : Has the price been overridden? * + isChargeOverridden : Has the price been overridden?
* + isCostOverridden : Has the cost been overridden?
* + isPending : Is this a pending active service * + isPending : Is this a pending active service
* *
* @package App\Models * @package App\Models
@ -535,6 +536,11 @@ class Service extends Model implements IDs
return $this->account->taxed($this->billing_charge()); return $this->account->taxed($this->billing_charge());
} }
public function getBillingCostAttribute(): float
{
return $this->account->taxed($this->billing_cost());
}
/** /**
* Determine a monthly price for a service, even if it is billed at a different frequency * Determine a monthly price for a service, even if it is billed at a different frequency
* *
@ -543,7 +549,16 @@ class Service extends Model implements IDs
*/ */
public function getBillingChargeNormalisedAttribute(): float public function getBillingChargeNormalisedAttribute(): float
{ {
return number_format($this->getBillingChargeAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),2); return number_format(
$this->getBillingChargeAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),
2);
}
public function getBillingCostNormalisedAttribute(): float
{
return number_format(
$this->getBillingCostAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),
2);
} }
/** /**
@ -567,7 +582,7 @@ class Service extends Model implements IDs
} }
/** /**
* Return the earliest date that the service can be cancelled * Return the earliest date that the service can be canceled as per contract/billing intervals
* *
* @return Carbon * @return Carbon
*/ */
@ -916,17 +931,21 @@ class Service extends Model implements IDs
/** /**
* Provide billing charge to a future date * Provide billing charge to a future date
* *
* If $date is earlier than our contract end date, then our charge is to the contract end date.
* If $date is after our contract end date:
* + and we are still in contract, then our charge is to contract end date plus additional time, else
* + then our charge is the months to the $date
*
* @param Carbon $date * @param Carbon $date
* @return float * @return float
* @throws Exception * @throws Exception
*/ */
public function billing_charge_to(Carbon $date): float public function billing_charge_to(Carbon $date): float
{ {
// if the date is less than the paid to, but less than the cancel date to, return cancel-paid to charge $max = max($date,$this->getCancelDateAttribute())->clone();
// If the date is greater than the paid to, and less than the cancel date to, return cancel-paid to charge
if ($this->getPaidToAttribute()->lessThan($this->getCancelDateAttribute())) { if ($this->getPaidToAttribute()->lessThan($max)) {
$max = max($date,$this->getPaidToAttribute())->clone(); $d = $this->getPaidToAttribute()->diffInDays($max);
$d = $max->diffInDays($this->getCancelDateAttribute());
return $this->account->taxed($d/30*$this->getBillingChargeNormalisedAttribute()); return $this->account->taxed($d/30*$this->getBillingChargeNormalisedAttribute());
} }
@ -934,6 +953,43 @@ class Service extends Model implements IDs
return 0; return 0;
} }
/**
* The amount we are charged for the client to have this service
*
* @return float
*/
public function billing_cost(): float
{
return is_null($this->cost)
? $this->product->getBaseCostAttribute()
: $this->cost*Invoice::billing_change($this->product->type->billing_interval,$this->product->billing_interval);
}
/**
* Calculate our costs to keep this service to a future date
*
* If $date is earlier than the contract end date, then our cost is to the contract end date.
* If $date is after the contract end date:
* + and we are still in contract, then our cost is to contract end date plus additional time, else
* + then our cost is the months to the $date
*
* @param Carbon $date
* @return float
* @throws Exception
*/
public function billing_cost_to(Carbon $date): float
{
$max = max($date,$this->getCancelDateAttribute())->clone();
if ($this->getInvoicedToAttribute()->lessThan($max)) {
$d = $this->getInvoicedToAttribute()->diffInDays($max);
return $this->account->taxed($d/30*$this->getBillingCostNormalisedAttribute());
}
return 0;
}
/** /**
* Get the stage parameters * Get the stage parameters
* *
@ -1065,9 +1121,14 @@ class Service extends Model implements IDs
return ! is_null($this->price); return ! is_null($this->price);
} }
public function isCostOverridden(): bool
{
return ! is_null($this->cost);
}
public function isContract(): bool public function isContract(): bool
{ {
return $this->getContractEndAttribute()->greaterThan(Carbon::now()); return $this->getContractEndAttribute() && $this->getContractEndAttribute()->greaterThan(Carbon::now());
} }
/** /**

View File

@ -57,16 +57,6 @@ class Broadband extends Type implements ServiceUsage
/* ATTRIBUTES */ /* ATTRIBUTES */
/**
* @deprecated use $o->service_name;
* @return mixed|string
*/
public function getNameAttribute()
{
abort(500,'deprecated - use $o->service_name');
return $this->service_number ?: $this->service_address;
}
/** /**
* The type of technology used to provide this Internet Service * The type of technology used to provide this Internet Service
* *
@ -104,15 +94,14 @@ class Broadband extends Type implements ServiceUsage
/* METHODS */ /* METHODS */
/** /**
* Return the suppliers offering that this service is providing * Return the supplier's offering that this service is providing
* *
* @return SupplierType * @return SupplierType
* @todo This column provided_adsl_plan_id should either be deprecated or renamed.
*/ */
public function supplied(): SupplierType public function supplied(): SupplierType
{ {
return $this->provided_adsl_plan_id return $this->provided_supplier_broadband_id
? SupplierBroadband::findOrFail($this->provided_adsl_plan_id) ? SupplierBroadband::findOrFail($this->provided_supplier_broadband_id)
: $this->service->offering->supplied; : $this->service->offering->supplied;
} }

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('services', function (Blueprint $table) {
$table->float('cost')->nullable();
$table->string('supplierid')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('service_broadband', function (Blueprint $table) {
$table->dropColumn('cost');
$table->dropColumn('supplierid');
});
}
};

View File

@ -19,7 +19,7 @@
$ao->load(['services:id,active,account_id']); $ao->load(['services:id,active,account_id']);
@endphp @endphp
<tr> <tr>
<td><a href="{{ url('r/switch/start',$ao->user_id) }}"><i class="fas fa-external-link-alt"></i></a></td> <td><a href="{{ url('switch/start',$ao->user_id) }}"><i class="fas fa-external-link-alt"></i></a></td>
<td><a href="{{ url('u/home',$ao->user_id) }}">{{ $ao->name }}</a></td> <td><a href="{{ url('u/home',$ao->user_id) }}">{{ $ao->name }}</a></td>
<td class="text-right">{{ $ao->services->where('active',TRUE)->count() }} <small>/{{ $ao->services->count() }}</small></td> <td class="text-right">{{ $ao->services->where('active',TRUE)->count() }} <small>/{{ $ao->services->count() }}</small></td>
</tr> </tr>

View File

@ -1,4 +1,5 @@
<!-- $co=Checkout::class --> <!-- $co=Checkout::class -->
@extends('adminlte::layouts.app') @extends('adminlte::layouts.app')
@section('htmlheader_title') @section('htmlheader_title')

View File

@ -1,4 +1,5 @@
<!-- $o=Invoice::class --> <!-- $o=Invoice::class -->
@use(App\Models\Checkout) @use(App\Models\Checkout)
@use(App\Models\Service) @use(App\Models\Service)

View File

@ -1,4 +1,5 @@
<!-- po=Payment::class --> <!-- $po=Payment::class -->
@use(Carbon\Carbon) @use(Carbon\Carbon)
@use(App\Models\Account) @use(App\Models\Account)
@use(App\Models\Checkout) @use(App\Models\Checkout)

View File

@ -1,3 +1,5 @@
<!-- $o=Service::class,$np=Product::class -->
@use(App\Models\Product) @use(App\Models\Product)
@extends('adminlte::layouts.app') @extends('adminlte::layouts.app')
@ -16,7 +18,6 @@
{{ $o->sid }} {{ $o->sid }}
@endsection @endsection
<!-- $o=Service::class, $np=Product::class -->
@section('main-content') @section('main-content')
<div class="row"> <div class="row">
<div class="col-12 col-lg-4"> <div class="col-12 col-lg-4">

View File

@ -25,6 +25,7 @@
<th>ID</th> <th>ID</th>
<th>Service</th> <th>Service</th>
<th>Product</th> <th>Product</th>
<th>Supplier ID</th>
<th class="text-right">Monthly</th> <th class="text-right">Monthly</th>
<th class="text-right">Cost</th> <th class="text-right">Cost</th>
<th class="text-right">Usage</th> <th class="text-right">Usage</th>
@ -38,13 +39,26 @@
<td><a href="{{ url('u/service',[$o->id]) }}">{{ $o->id }}</a></td> <td><a href="{{ url('u/service',[$o->id]) }}">{{ $o->id }}</a></td>
<td>{{ $o->name }}</td> <td>{{ $o->name }}</td>
<td>{{ $o->product->name }}</td> <td>{{ $o->product->name }}</td>
<td>{{ $o->supplierid }}</td>
<td class="text-right">{{ number_format($o->billing_charge_normalised,2) }}</td> <td class="text-right">{{ number_format($o->billing_charge_normalised,2) }}</td>
<td class="text-right">{{ number_format($o->product->cost_normalized(),2) }}</td> <td class="text-right">{{ number_format($o->billing_cost_normalised,2) }}</td>
<td class="text-right">{{ $o->product->hasUsage() ? number_format($o->type->usage_summary(0)->sum()/1000,1) : '-' }}</td> <td class="text-right">{{ $o->product->hasUsage() ? number_format($o->type->usage_summary(0)->sum()/1000,1) : '-' }}</td>
<td>{{ $o->product->supplier->name }}</td> <td>{{ $o->product->supplier->name }}</td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>
<tfoot>
<tr>
<td></td>
<th>TOTAL:</th>
<td></td>
<td class="text-right"></td>
<td class="text-right"></td>
<td></td>
<td></td>
<td></td>
</tr>
</tfoot>
</table> </table>
</div> </div>
</div> </div>
@ -77,6 +91,41 @@
], ],
rowGroup: { rowGroup: {
dataSrc: [2], dataSrc: [2],
endRender: function (rows, group) {
// Remove the formatting to get integer data for summation
let intVal = function (i) {
return typeof i === 'string'
? i.replace(/[\$,]/g, '') * 1
: typeof i === 'number'
? i
: 0;
};
// Total over all pages
let month = rows
.data()
.pluck(4)
.reduce((a, b) => intVal(a) + intVal(b), 0);
let cost = rows
.data()
.pluck(5)
.reduce((a, b) => intVal(a) + intVal(b), 0);
let usage = rows
.data()
.pluck(6)
.reduce((a, b) => intVal(a) + intVal(b), 0);
return $('<tr/>')
.append('<td/>')
.append('<td><strong>SUB-TOTAL:</strong></td>')
.append('<td/>')
.append('<td class="text-right"><strong>'+month.toFixed(2)+'</strong></td>')
.append('<td class="text-right"><strong>'+cost.toFixed(2)+'</strong></td>')
.append('<td class="text-right"><strong>'+usage.toFixed(2)+'</strong></td>')
.append('<td/>');
}
}, },
columnDefs: [ columnDefs: [
{ {
@ -84,7 +133,7 @@
visible: false, visible: false,
}, },
{ {
targets: [0,1,3,4,5], targets: [0,1,4,5,6],
searchPanes: { searchPanes: {
show: false, show: false,
} }
@ -104,6 +153,46 @@
controls: false, controls: false,
}, },
dom: '<"dtsp-verticalContainer"<"dtsp-verticalPanes"P><"dtsp-dataTable"Bfrtip>>', dom: '<"dtsp-verticalContainer"<"dtsp-verticalPanes"P><"dtsp-dataTable"Bfrtip>>',
footerCallback: function (row, data, start, end, display) {
let api = this.api();
// Remove the formatting to get integer data for summation
let intVal = function (i) {
return typeof i === 'string'
? i.replace(/[\$,]/g, '') * 1
: typeof i === 'number'
? i
: 0;
};
// Total over all pages
month = api
.column(4,{ search: 'applied' })
.data()
.reduce((a, b) => intVal(a) + intVal(b), 0);
// Total over this page
monthTotal = api
.column(4, { page: 'current' })
.data()
.reduce((a, b) => intVal(a) + intVal(b), 0);
// Total over all pages
cost = api
.column(5,{ search: 'applied' })
.data()
.reduce((a, b) => intVal(a) + intVal(b), 0);
// Total over this page
costTotal = api
.column(5, { page: 'current' })
.data()
.reduce((a, b) => intVal(a) + intVal(b), 0);
// Update footer
api.column(4).footer().innerHTML = monthTotal.toFixed(2)+'<br><small>('+month.toFixed(2)+')</small>';
api.column(5).footer().innerHTML = costTotal.toFixed(2)+'<br><small>('+cost.toFixed(2)+')</small>';
}
}); });
}); });
</script> </script>

View File

@ -1,4 +1,5 @@
<!-- $o = App\Models\Service\Broadband::class --> <!-- $o=Service\Broadband::class -->
<div class="card"> <div class="card">
@if($o->service->isPending()) @if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg"> <div class="ribbon-wrapper ribbon-lg">

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col"> <div class="col-12">
Connection Type Connection Type
<x-leenooks::form.toggle id="pppoe" name="broadband[pppoe]" label="PPPoE" old="broadband.pppoe" :value="$o->pppoe"/> <x-leenooks::form.toggle id="pppoe" name="broadband[pppoe]" label="PPPoE" old="broadband.pppoe" :value="$o->pppoe"/>
</div> </div>

View File

@ -1,4 +1,5 @@
<!-- $o = App\Models\Service\Domain::class --> <!-- $o=Service\Domain::class -->
<div class="card"> <div class="card">
@if($o->service->isPending()) @if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg"> <div class="ribbon-wrapper ribbon-lg">

View File

@ -1,4 +1,5 @@
<!-- $o = App\Models\Service\Email::class --> <!-- $o=Service\Email::class -->
<div class="card"> <div class="card">
@if($o->service->isPending()) @if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg"> <div class="ribbon-wrapper ribbon-lg">

View File

@ -1,4 +1,5 @@
<!-- $o = App\Models\Service\Host::class --> <!-- $o=Service\Host::class -->
<div class="card"> <div class="card">
@if($o->service->isPending()) @if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg"> <div class="ribbon-wrapper ribbon-lg">

View File

@ -1,7 +1,10 @@
<!-- $o=Service::class,$p=Product::class -->
@use(Carbon\CarbonInterface)
@use(App\Models\Invoice) @use(App\Models\Invoice)
@php($c=$o->product) @php($c=$o->product)
<!-- $o=Service::class, $p=Product::class -->
<table class="table table-sm"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
@ -29,7 +32,8 @@
<tbody> <tbody>
<tr> <tr>
<th>Product</th> <th>Product</th>
<td class="text-center" colspan="2"><a href="{{ url('a/product/details',$c->id) }}">#{{ $c->supplied->id }}: {{ $c->supplied->name_long }}</a></td> <td><a href="{{ url('a/product/details',$o->product_id) }}">#{{ $o->product_id }}: {{ $o->product->name }}</a></td>
<td><a href="{{ url('a/product/details',$c->id) }}">#{{ $c->supplied->id }}: {{ $c->supplied->name_long }}</a></td>
@if($p->exists) @if($p->exists)
<th>&nbsp;</th> <th>&nbsp;</th>
<td class="text-center" colspan="2">#{{ $p->supplied->id }}: {{ $p->supplied->name_long }}</td> <td class="text-center" colspan="2">#{{ $p->supplied->id }}: {{ $p->supplied->name_long }}</td>
@ -64,7 +68,7 @@
<tr> <tr>
<th>Billing Price</th> <th>Billing Price</th>
<td @if($o->isChargeOverridden())class="text-danger"@endif>${{ number_format($b=$o->billing_charge,2) }}</td> <td @if($o->isChargeOverridden())class="text-danger"@endif>${{ number_format($b=$o->billing_charge,2) }}</td>
<td>${{ number_format($a=$o->account->taxed($c->base_cost),2) }}</td> <td @if($o->isCostOverridden())class="text-danger"@endif>${{ number_format($a=$o->billing_cost,2) }}</td>
<td>{!! markup($a,$b) !!}</td> <td>{!! markup($a,$b) !!}</td>
@if($p->exists) @if($p->exists)
<td @if($o->isChargeOverridden())class="text-danger"@endif>${{ number_format($b=$o->account->taxed($p->base_charge),2) }}</td> <td @if($o->isChargeOverridden())class="text-danger"@endif>${{ number_format($b=$o->account->taxed($p->base_charge),2) }}</td>
@ -79,10 +83,16 @@
@if($x) @if($x)
<abbr title="${{ number_format($b=$o->account->taxed($c->base_charge)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}">${{ number_format($b=$o->billing_charge_normalised,2) }} <abbr title="${{ number_format($b=$o->account->taxed($c->base_charge)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}">${{ number_format($b=$o->billing_charge_normalised,2) }}
@else @else
{{ number_format($b=$o->billing_charge_normalised,2) }} ${{ number_format($b=$o->billing_charge_normalised,2) }}
@endif
</td>
<td @if($x=$o->isCostOverridden()) class="text-danger" @endif>
@if($x)
<abbr title="${{ number_format($a=$o->account->taxed($c->base_cost)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}">${{ number_format($b=$o->billing_cost_normalised,2) }}
@else
${{ number_format($a=$o->billing_cost_normalised,2) }}
@endif @endif
</td> </td>
<td>${{ number_format($a=$o->account->taxed($c->base_cost)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}</td>
<td>{!! markup($a,$b) !!}</td> <td>{!! markup($a,$b) !!}</td>
@if($p->exists) @if($p->exists)
<td @if($x=$o->isChargeOverridden()) class="text-danger" @endif>${{ number_format($b=$o->account->taxed($p->base_charge)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}</td> <td @if($x=$o->isChargeOverridden()) class="text-danger" @endif>${{ number_format($b=$o->account->taxed($p->base_charge)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}</td>
@ -109,11 +119,25 @@
<td>${{ number_format($b=$o->account->taxed($o->product->min_charge),2) }}</td> <td>${{ number_format($b=$o->account->taxed($o->product->min_charge),2) }}</td>
<td>${{ number_format($a=$o->account->taxed($c->supplied->min_cost),2) }}</td> <td>${{ number_format($a=$o->account->taxed($c->supplied->min_cost),2) }}</td>
<td>{!! markup($a,$b) !!}</td> <td>{!! markup($a,$b) !!}</td>
@if($p->exists) @if($p->exists)
<td>${{ number_format($a=$o->account->taxed($p->min_charge),2) }}</td> <td>${{ number_format($a=$o->account->taxed($p->min_charge),2) }}</td>
<td>${{ number_format($a=$o->account->taxed($p->supplied->min_cost ?? 0),2) }}</td> <td>${{ number_format($a=$o->account->taxed($p->supplied->min_cost ?? 0),2) }}</td>
<td>{!! markup($a,$b) !!}</td> <td>{!! markup($a,$b) !!}</td>
@endif @endif
</tr> </tr>
<tr>
<th>Contract Left</th>
@if($o->isContract())
<td>${{ number_format($o->billing_charge_to($o->contract_end),2) }} (<small>{{ $o->paid_to->format('Y-m-d') }}</small>)</td>
<td>${{ number_format($o->billing_cost_to($o->contract_end),2) }} (<small>{{ $o->invoiced_to->format('Y-m-d') }}</small>)</td>
<td>{{ $o->contract_end->format('Y-m-d') }}<br><small>({{ $o->contract_end->diffForHumans(now(),CarbonInterface::DIFF_RELATIVE_TO_OTHER,FALSE,2) }} today)</small></td>
@else
<td colspan="2" class="text-center">Not on contract</td>
<td>&nbsp;</td>
@endif
</tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,4 +1,5 @@
<!-- $o = App\Models\Service\Phone::class --> <!-- $o=Service\Phone::class -->
<div class="card"> <div class="card">
@if($o->service->isPending()) @if($o->service->isPending())
<div class="ribbon-wrapper ribbon-lg"> <div class="ribbon-wrapper ribbon-lg">

View File

@ -1,9 +1,10 @@
<!-- $o=Service::class --> <!-- $o=Service::class -->
@use(App\Models\Invoice) @use(App\Models\Invoice)
@use(App\Models\Product)
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h4>Update Service details <x-leenooks::button.success class="float-right"/></h4> <h4>Update Service Details <x-leenooks::button.success class="float-right"/></h4>
<hr> <hr>
<form method="POST" action="{{ url('a/service/update',[$o->id]) }}"> <form method="POST" action="{{ url('a/service/update',[$o->id]) }}">
@ -29,13 +30,36 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-3"></div> <div class="col-12 col-sm-3">
<x-leenooks::form.text name="supplierid" icon="fa-hashtag" label="Supplier ID" :value="$o->supplierid"/>
</div>
<div class="col-12 col-sm-9 col-md-6 col-xl-5"> <div class="col-12 col-sm-9 col-md-6 col-xl-5">
<x-leenooks::form.select id="recur_schedule" name="recur_schedule" icon="fa-redo" label="Renew Term" :value="$o->recur_schedule" :options="collect(Invoice::billing_periods)->map(fn($item,$key)=>['id'=>$key,'value'=>$item['name']])"/> <x-leenooks::form.select id="recur_schedule" name="recur_schedule" icon="fa-redo" label="Renew Term" :value="$o->recur_schedule" :options="collect(Invoice::billing_periods)->map(fn($item,$key)=>['id'=>$key,'value'=>$item['name']])"/>
</div> </div>
<!-- Cost -->
<div class="col-12 col-sm-4 col-xl-3">
<x-leenooks::form.text name="cost" icon="fa-dollar-sign" label="Cost (Monthly)" :value="$o->cost"/>
</div>
</div> </div>
<div class="row">
<div class="col-12">
<!-- PRODUCT -->
<x-leenooks::form.select name="product_id" icon="fa-list" label="Product" :helper="$o->product->category_name"
:value="$o->product_id"
:options="Product::get()
->filter(fn($item)=>$item->active && ($item->category === $o->product->category))
->sortBy('name')
->map(fn($item)=>[
'id'=>$item->id,
'value'=>sprintf('%s (%3.2f/%3.2f)',
$item->name,
$o->account->taxed($item->base_charge)*Invoice::billing_change($item->billing_interval,Invoice::BILL_MONTHLY),
$o->account->taxed($item->base_cost)*Invoice::billing_change($item->billing_interval,Invoice::BILL_MONTHLY))])" :required="true"/>
</div>
</div>
<hr> <hr>
@includeIf('theme.backend.adminlte.service.widget.'.$o->product->category.'.update',['o'=>$o->type]) @includeIf('theme.backend.adminlte.service.widget.'.$o->product->category.'.update',['o'=>$o->type])

View File

@ -1,6 +1,7 @@
<!-- $spo=Supplier::class -->
@use(Carbon\Carbon) @use(Carbon\Carbon)
<!-- $spo=Supplier::class -->
@extends('adminlte::layouts.app') @extends('adminlte::layouts.app')
@section('htmlheader_title') @section('htmlheader_title')

View File

@ -1,6 +1,8 @@
<!-- $spo=Supplier::class --> <!-- $spo=Supplier::class -->
<!-- $oo=Supplier/{type}::class --> <!-- $oo=Supplier/{type}::class -->
@use(App\Models\Supplier) @use(App\Models\Supplier)
@php @php
if(isset($spo)) { if(isset($spo)) {
$oo = $spo->detail?->find(request()->route()->parameter('type'),request()->route()->parameter('id')); $oo = $spo->detail?->find(request()->route()->parameter('type'),request()->route()->parameter('id'));

View File

@ -1,4 +1,5 @@
<!-- $o = Supplier{type}::class --> <!-- $o=Supplier/Broadband::class -->
<div class="row"> <div class="row">
<!-- Supplier Name --> <!-- Supplier Name -->
<div class="col-4"> <div class="col-4">

View File

@ -1,4 +1,5 @@
<!-- ($o??$user)=User::class --> <!-- ($o??$user)=User::class -->
@use(App\Models\Country) @use(App\Models\Country)
@extends('adminlte::layouts.app') @extends('adminlte::layouts.app')

View File

@ -46,7 +46,7 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
@if($user->switched) @if($user->switched)
<a href="{{ url('/admin/switch/stop') }}" class="dropdown-item"> <a href="{{ url('switch/stop') }}" class="dropdown-item">
<i class="fas fa-sign-out-alt mr-2"></i> {{ trans('adminlte_lang::message.switchoff') }} <i class="fas fa-sign-out-alt mr-2"></i> {{ trans('adminlte_lang::message.switchoff') }}
</a> </a>
@else @else

View File

@ -18,8 +18,7 @@ use App\Http\Controllers\{AdminController,
SearchController, SearchController,
ServiceController, ServiceController,
SupplierController, SupplierController,
UserController, UserController};
Wholesale\ReportController};
use App\Models\Supplier; use App\Models\Supplier;
/* /*
@ -42,8 +41,8 @@ Auth::routes([
'confirm' => false, // for additional password confirmations 'confirm' => false, // for additional password confirmations
'verify' => false, // for email verification 'verify' => false, // for email verification
]); ]);
Route::get('logout',[LoginController::class,'logout']) // Auth::routes doesnt provide a get /logout path, so we'll specify it here
->name('logout-get'); Route::get('logout',[LoginController::class,'logout']);
// Frontend Routes (Non-Authed Users) // Frontend Routes (Non-Authed Users)
Route::view('/','theme.frontend.metronic.welcome.home'); Route::view('/','theme.frontend.metronic.welcome.home');
@ -54,21 +53,35 @@ Route::redirect('passkey/loggedin','/u/home');
Route::get('search',[SearchController::class,'search']); Route::get('search',[SearchController::class,'search']);
Route::get('pay/paypal/authorise',[PaypalController::class,'authorise']); // Paypal paths
Route::get('pay/paypal/cancel',[PaypalController::class,'cancel']); Route::controller(PaypalController::class)
Route::get('pay/paypal/capture',[PaypalController::class,'capture']); ->prefix('pay/paypal')
->group(function() {
Route::get('authorise','authorise');
Route::get('cancel','cancel');
Route::get('capture','capture');
});
// Account linking to OPENID host // Account linking to OPENID host
Route::get('auth/{socialProvider}',[SocialLoginController::class,'redirectToProvider']); Route::controller(PaypalController::class)
Route::get('auth/{socialProvider}/callback',[SocialLoginController::class,'handleProviderCallback']); ->prefix('auth')
Route::get('auth/{socialProvider}/token',[SocialLoginController::class,'handleBearerTokenCallback']); ->group(function() {
Route::get('auth/{socialProvider}/link',[SocialLoginController::class,'link']); Route::get('{socialProvider}','redirectToProvider');
Route::post('auth/{socialProvider}/linkcomplete',[SocialLoginController::class,'linkcomplete']); Route::get('{socialProvider}/callback','handleProviderCallback');
Route::get('{socialProvider}/token','handleBearerTokenCallback');
Route::get('{socialProvider}/link','link');
Route::post('{socialProvider}/linkcomplete','linkcomplete');
});
// Return from user switch // User Switch
Route::get('admin/switch/stop',[SwitchUserController::class,'switch_stop']) Route::controller(SwitchUserController::class)
->prefix('switch')
->middleware('auth') ->middleware('auth')
->name('switch.stop'); ->group(function() {
Route::get('stop','switch_stop');
Route::get('start/{user}','switch_start')
->middleware(['role:reseller','can:assume,user']);
});
// Our Admin Routes - for wholesalers // Our Admin Routes - for wholesalers
Route::group(['middleware'=>['auth','role:wholesaler'],'prefix'=>'a'],function() { Route::group(['middleware'=>['auth','role:wholesaler'],'prefix'=>'a'],function() {
@ -137,11 +150,6 @@ Route::group(['middleware'=>['auth','role:wholesaler'],'prefix'=>'a'],function()
// Our Reseller Routes // Our Reseller Routes
Route::group(['middleware'=>['auth','role:reseller'],'prefix'=>'r'],function() { Route::group(['middleware'=>['auth','role:reseller'],'prefix'=>'r'],function() {
// Enable user switch
Route::get('switch/start/{user}',[SwitchUserController::class,'switch_start'])
->middleware('can:assume,user')
->name('switch.start');
// Reseller Reports // Reseller Reports
Route::group(['prefix'=>'report'],function() { Route::group(['prefix'=>'report'],function() {
Route::view('charge/pending','theme.backend.adminlte.charge.pending'); Route::view('charge/pending','theme.backend.adminlte.charge.pending');