osb/app/Http/Controllers/ServiceController.php
Deon George d6a2c70146
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
Update service update to use components, enhanced form handling and submission. Added pppoe to broadband and changed validation to allow for longer service number.
2024-07-24 14:33:14 +10:00

502 lines
14 KiB
PHP

<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use Symfony\Component\HttpKernel\Exception\HttpException;
use App\Http\Requests\ServiceChangeRequest;
use App\Mail\{CancelRequest,ChangeRequest};
use App\Models\{Charge,Invoice,Product,Service};
class ServiceController extends Controller
{
/* SERVICE WORKFLOW METHODS */
/**
* Cancel a request to cancel a service
*
* @param Service $o
* @return bool
*/
private function action_cancel_cancel(Service $o): bool
{
if (! $o->order_info)
$o->order_info = collect();
$o->order_info->put('cancel_cancel',Carbon::now()->format('Y-m-d H:i:s'));
$o->order_status = 'ACTIVE';
return $o->save();
}
private function action_cancel_pending_enter(Service $o): bool
{
$o->order_status = 'CANCEL-PENDING';
return $o->save();
}
private function action_cancelled(Service $o): bool
{
$o->order_status = 'CANCELLED';
$o->active = FALSE;
return $o->save();
}
/**
* Cancel a request to change a service
*
* @param Service $o
* @return bool
*/
private function action_change_cancel(Service $o): bool
{
if (! $o->order_info)
$o->order_info = collect();
// @todo add some validation if this doesnt return a result
$np = $o->changes()->where('service__change.active',TRUE)->where('complete',FALSE)->get()->pop();
$np->pivot->active = FALSE;
$np->pivot->save();
$o->order_status = 'ACTIVE';
return $o->save();
}
/**
* Action to change a service order_status to another stage
* This is a generic function that can redirect the user to a page that is required to completed to enter
* the new stage
*
* @param Service $o
* @param string $stage
* @return \Illuminate\Contracts\Foundation\Application|RedirectResponse|\Illuminate\Routing\Redirector
*/
private function action_request_enter_redirect(Service $o,string $stage)
{
return redirect(sprintf('u/service/%d/%s',$o->id,strtolower($stage)));
}
/* OTHER METHODS */
public function change_pending(ServiceChangeRequest $request,Service $o)
{
// @todo add some validation if this doesnt return a result
$np = $o->changes()->where('service__change.active',TRUE)->where('complete',FALSE)->get()->pop();
if ($request->post()) {
foreach ($this->service_change_charges($request,$o) as $co)
$co->save();
$o->product_id = Arr::get($request->broadband,'product_id');
$o->price = Arr::get($request->broadband,'price');
$o->order_status = 'ACTIVE';
$o->save();
$np->pivot->complete = TRUE;
$np->pivot->effective_at = Carbon::now();
$np->pivot->save();
return redirect()->to(url('u/service',[$o->id]));
}
return view('theme.backend.adminlte.service.change_pending')
->with('breadcrumb',collect()->merge($o->account->breadcrumb))
->with('o',$o)
->with('np',$np);
}
/**
* Process a request to cancel a service
*
* @param Request $request
* @param Service $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|RedirectResponse|\Illuminate\Routing\Redirector
*/
public function cancel_request(Request $request,Service $o)
{
if ($request->post()) {
$request->validate([
'stop_at'=>'required|date',
]);
if (! $o->order_info)
$o->order_info = collect();
$o->stop_at = $request->stop_at;
$o->order_info->put('cancel_note',$request->notes);
$o->order_status = 'CANCEL-REQUEST';
$o->save();
//@todo Get email from DB.
Mail::to('help@graytech.net.au')
->queue((new CancelRequest($o,$request->notes))->onQueue('email'));
return redirect('u/service/'.$o->id)->with('success','Cancellation lodged');
}
return view('theme.backend.adminlte.service.cancel_request')
->with('o',$o);
}
/**
* Change the status of a service
* @todo This needs to be optimized
*
* @note This route is protected by middleware @see ServicePolicy::progress()
* It is assumed that the next stage is valid for the services current stage - validated in ServicePolicy::progress()
* @param Service $o
* @param string $stage
* @return RedirectResponse
*/
public function change(Service $o,string $stage): RedirectResponse
{
// While stage has a string value, that indicates the next stage we want to go to
// If stage is NULL, the current stage hasnt been completed
// If stage is FALSE, then the current stage failed, and may optionally be directed to another stage.
while ($stage) {
// Check that stage is a valid next action for the user currently performing it
//$current = $this->getStageParameters($this->order_status);
$next = $o->getStageParameters($stage);
// If valid, call the method to confirm that the current stage is complete
if ($x=$next->get('enter_method')) {
if (! method_exists($this,$x))
abort(500,sprintf('ENTER_METHOD [%s]defined doesnt exist',$x));
Log::debug(sprintf('Running ENTER_METHOD [%s] on Service [%d] to go to stage [%s]',$x,$o->id,$stage));
// @todo Should call exit_method of the current stage first, to be sure we can change
try {
$result = $this->{$x}($o,$stage);
// If we have a form to complete, we need to return with a URL, so we can catch that with an Exception
} catch (HttpException $e) {
if ($e->getStatusCode() == 301)
return ($e->getMessage());
}
// An Error Condition
if (is_null($result))
return redirect()->to('u/service/'.$o->id);
elseif ($result instanceof RedirectResponse)
return $result;
// The service cannot enter the next stage
elseif (! $result)
abort(500,'Current Method FAILED: '.$result);
} else {
$o->order_status = $stage;
if ($stage == 'ACTIVE')
$o->active = TRUE;
$o->save();
}
// If valid, call the method to start the next stage
$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);
}
/**
* Process a request to cancel a service
*
* @param Request $request
* @param Service $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|RedirectResponse|\Illuminate\Routing\Redirector
*/
public function change_request(Request $request,Service $o)
{
if ($request->post()) {
$request->validate([
'product_id'=>'required|exists:products,id',
'change_date'=>'required|date',
'notes'=>'nullable|min:10',
]);
$o->changes()->attach([$o->id=>[
'site_id'=> $o->site_id,
'ordered_by' => Auth::id(),
'ordered_at' => Carbon::now(),
'effective_at' => $request->change_date,
'product_id' => $request->product_id,
'notes' => $request->notes,
'active' => TRUE,
'complete' => FALSE,
]]);
$o->order_status = 'CHANGE-REQUEST';
$o->save();
//@todo Get email from DB.
Mail::to('help@graytech.net.au')
->queue((new ChangeRequest($o,$request->notes))->onQueue('email'));
return redirect('u/service/'.$o->id)->with('success','Upgrade requested');
}
switch (get_class($o->type)) {
default:
return view('theme.backend.adminlte.service.change_request')
->with('breadcrumb',collect()->merge($o->account->breadcrumb))
->with('o',$o);
}
}
/**
* List all the domains managed by the user
*
* @return View
* @todo revalidate
*/
public function domain_list(): View
{
$o = Service\Domain::serviceActive()
->serviceUserAuthorised(Auth::user())
->select('service_domain.*')
->join('services',['services.id'=>'service_domain.service_id'])
->with(['service.account','registrar'])
->get();
return view('theme.backend.adminlte.service.domain.list')
->with('o',$o);
}
public function email_list(): View
{
// @todo Need to add the with path when calculating next_billed and price
$o = Service\Email::serviceActive()
->serviceUserAuthorised(Auth::user())
->select('service_email.*')
->join('services',['services.id'=>'service_email.service_id'])
->with(['service.account','service.product.type.supplied.supplier_detail.supplier','tld'])
->get();
return view('theme.backend.adminlte.service.email.list')
->with('o',$o);
}
/**
* Return details on the users service
*
* @param Service $o
* @return View
*/
public function home(Service $o): View
{
return view('theme.backend.adminlte.service.home')
->with('breadcrumb',collect()->merge($o->account->breadcrumb))
->with('o',$o);
}
public function hosting_list(): View
{
// @todo Need to add the with path when calculating next_billed and price
$o = Service\Host::serviceActive()
->serviceUserAuthorised(Auth::user())
->select('service_host.*')
->join('services',['services.id'=>'service_host.service_id'])
->with(['service.account','service.product.type.supplied.supplier_detail.supplier','tld'])
->get();
return view('theme.backend.adminlte.service.host.list')
->with('o',$o);
}
private function service_change_charges(Request $request,Service $o): Collection
{
$charges = collect();
$po = Product::findOrFail(Arr::get($request->broadband,'product_id'));
$start_at = Carbon::create(Arr::get($request->broadband,'start_at'));
// Get the invoiced items covering the start_at date
foreach ($o->invoice_items->filter(function($item) use ($start_at) {
return ($item->start_at < $start_at) && ($item->stop_at > $start_at) && ($item->item_type === 0);
}) as $iio)
{
// Reverse the original charge
$co = new Charge;
$co->active = TRUE;
$co->service_id = $o->id;
$co->account_id = $o->account_id;
$co->sweep_type = 6;
$co->product_id = $iio->product_id;
$co->description = 'Plan Upgrade Adjustment';
$co->user_id = Auth::id();
$co->type = $iio->item_type;
$co->start_at = $start_at;
$co->stop_at = $iio->stop_at;
$co->amount = $iio->price_base;
$co->taxable = TRUE; // @todo this should be determined
$co->quantity = -1*$start_at->diff($iio->stop_at)->days/$iio->start_at->diff($iio->stop_at)->days;
$charges->push($co);
// Add the new charge
$co = new Charge;
$co->active = TRUE;
$co->service_id = $o->id;
$co->account_id = $o->account_id;
$co->sweep_type = 6;
$co->product_id = $po->id;
$co->description = 'Plan Upgrade Adjustment';
$co->user_id = Auth::id();
$co->type = $iio->item_type;
$co->start_at = $start_at;
$co->stop_at = $iio->stop_at;
$co->amount = Arr::get($request->broadband,'price') ?: $po->base_charge;
$co->taxable = TRUE; // @todo this should be determined
$co->quantity = $start_at->diff($iio->stop_at)->days/$iio->start_at->diff($iio->stop_at)->days;
$charges->push($co);
}
// Add any fee
if (Arr::get($request->broadband,'change_fee')) {
$co = new Charge;
$co->active = TRUE;
$co->service_id = $o->id;
$co->account_id = $o->account_id;
$co->sweep_type = 6;
$co->product_id = $po->id;
$co->description = 'Plan Upgrade Fee';
$co->user_id = Auth::id();
$co->type = 3;
$co->start_at = $start_at;
$co->stop_at = $start_at;
$co->amount = Arr::get($request->broadband,'change_fee');
$co->taxable = TRUE; // @todo this should be determined
$co->quantity = 1;
$charges->push($co);
}
return $charges;
}
/**
* This is an API method, that works with service change - to return the new charges as a result of changing a service
*
* @param Request $request
* @param Service $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function service_change_charges_display(Request $request,Service $o)
{
return view('theme.backend.adminlte.a.charge.service_change')
->with('charges',$this->service_change_charges($request,$o));
}
/**
* Update details about a service
*
* @param Request $request
* @param Service $o
* @return RedirectResponse
* @throws ValidationException
*/
public function update(Request $request,Service $o)
{
Session::put('service_update',true);
// We dynamically create our validation
$validator = Validator::make(
$request->post(),
$x=collect($o->type->validation())
->keys()
->transform(fn($item)=>sprintf('%s.%s',$o->category,$item))
->combine(array_values($o->type->validation()))
->transform(fn($item)=>is_string($item)
? preg_replace('/^exclude_without:/',sprintf('exclude_without:%s.',$o->category),$item)
: $item)
->merge(
[
'external_billing' => 'nullable|in:on',
'suspend_billing' => 'nullable|in:on',
'recur_schedule' => ['required',Rule::in(collect(Invoice::billing_periods)->keys())],
'invoice_next_at' => 'nullable|date',
'price' => 'nullable|numeric',
$o->category => 'array|min:1',
]
)
->toArray()
);
if ($validator->fails()) {
return redirect()
->back()
->withErrors($validator)
->withInput();
}
$validated = collect($validator->validated());
// Store our service type values
$o->type->forceFill($validated->get($o->category));
// Some special handling
switch ($o->category) {
case 'broadband':
// If pppoe is not set, then we dont need username/password
$o->type->pppoe = ($x=data_get($validated,$o->category.'.pppoe',FALSE));
if (! $x) {
$o->type->service_username = NULL;
$o->type->service_password = NULL;
}
break;
}
$o->type->save();
if ($validated->has('invoice_next_at'))
$o->invoice_next_at = $validated->get('invoice_next_at');
if ($validated->has('recur_schedule'))
$o->recur_schedule = $validated->get('recur_schedule');
$o->suspend_billing = ($validated->get('suspend_billing') == 'on');
$o->external_billing = ($validated->get('external_billing') == 'on');
$o->price = $validated->get('price');
// Also update our service start_at date.
// @todo We may want to make start_at/stop_at dynamic values calculated by the type records
if ($validated->has('start_at'))
$o->start_at = $validated->get('start_at');
else {
// For broadband, start_at is connect_at in the type record
switch ($o->category) {
case 'broadband':
$o->start_at = $o->type->connect_at;
break;
}
}
$o->save();
return redirect()
->back()
->with('success','Record Updated');
}
}