Service cancellation and ordering

This commit is contained in:
Deon George
2021-09-29 14:57:25 +10:00
parent b2e45fcaee
commit f7439172b6
11 changed files with 637 additions and 299 deletions

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use Exception;
use Illuminate\Database\Eloquent\Casts\AsCollection;
use Illuminate\Database\Eloquent\Collection as DatabaseCollection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@@ -62,7 +63,7 @@ class Service extends Model implements IDs
];
protected $casts = [
'order_info'=>'collection',
'order_info'=>AsCollection::class,
];
public $dateFormat = 'U';
@@ -106,16 +107,30 @@ class Service extends Model implements IDs
/**
* Valid status shows the applicable next status for an action on a service
* Each status can be
* + Approved, to proceed to the next valid status'
* + Held, to a holding pattern status
* + Rejected, reverted to an different status
* + Cancel, to progress down a decomission route
* + Updated, stay on the current status with new information
*
* The structure of each item is:
* <ORDER_STATUS> => [
* 'next' == next possible levels, initiated by who (array).
* who = 'role','system'
* 'enter_method' == the method run when we enter this stage - could be used to send email for example, if this method doesnt complete successfully, we dont enter this stage
* + If it returns NULL, An error condition
* + If it returns FALSE, the service cannot enter this stage
* + If it returns TRUE, the service has successfully changed to this stage
* + If it returns a VIEW/REDIRECT, that resulting page will handle the status change
* 'exit_method' == the method that determines that the order can leave this status (true = can, false = cant)
* + If it returns NULL, An error condition
* + If it returns FALSE, the service CANNOT leave this stage
* + If it returns TRUE, the service CAN leave this stage
* it will receive the next method as an argument
* 'title' == title shown on the menu, for the user to choose
* ]
* So when an order goes to the next state, the exit method will be tested immediately (if there is only 1 exit method for the user)
* to see if it can proceed further if not, it'll wait here for user/admin intervention
*
* @var array
*/
public static $action_progress = [
// Order Submitted
private const ACTION_PROGRESS = [
// Order Submitted @todo redo
'ORDER-SUBMIT' => [
'fail'=>FALSE,
// Progress to next stages by who
@@ -128,7 +143,7 @@ class Service extends Model implements IDs
'method'=>'action_order_submit',
'title'=>'Order Submit',
],
// Client accepts order, if performed by RW
// Client accepts order, if performed by RW @todo redo
'ORDER-ACCEPT' => [
'fail'=>FALSE,
'next'=>[
@@ -138,7 +153,7 @@ class Service extends Model implements IDs
'method'=>'action_order_accept',
'title'=>'Client Accept Order',
],
// If the product has a setup, collect payment information
// If the product has a setup, collect payment information @todo redo
'SETUP-PAYMENT-WAIT' => [
'fail'=>FALSE,
'next'=>[
@@ -148,6 +163,7 @@ class Service extends Model implements IDs
'method'=>'action_setup_payment_wait',
'title'=>'Setup Payment',
],
// @todo redo
'PAYMENT-WAIT' => [
'fail'=>FALSE,
'next'=>[
@@ -157,6 +173,7 @@ class Service extends Model implements IDs
'method'=>'action_payment_wait',
'title'=>'Service Payment',
],
// @todo redo
'PAYMENT-CHECK' => [
'fail'=>'ORDER-HOLD',
'next'=>[
@@ -166,13 +183,13 @@ class Service extends Model implements IDs
'method'=>'action_payment_check',
'title'=>'Validate Payment Method',
],
// Order On Hold (Reason)
// Order On Hold (Reason) @todo redo
'ORDER-HOLD' => ['release'=>'ORDER-SUBMIT','update_reference'=>'ORDER-SENT'],
// Order Rejected (Reason)
// Order Rejected (Reason) @todo redo
'ORDER-REJECTED' => [],
// Order Cancelled
// Order Cancelled @todo redo
'ORDER-CANCELLED' => [],
// Order Sent to Supplier
// Order Sent to Supplier @todo redo
'ORDER-SENT' => [
'fail'=>'ORDER-HOLD',
'next'=>[
@@ -182,7 +199,7 @@ class Service extends Model implements IDs
'method'=>'action_order_sent',
'title'=>'Send Order',
],
// Order Confirmed by Supplier
// Order Confirmed by Supplier @todo redo
'ORDERED' => [
'fail'=>false,
'next'=>[
@@ -194,7 +211,7 @@ class Service extends Model implements IDs
'method'=>'action_ordered',
'title'=>'Service Ordered',
],
// Service confirmed by supplier, optional connection date
// Service confirmed by supplier, optional connection date @todo redo
'PROVISION-PLANNED' => [
'fail'=>false,
'next'=>[
@@ -204,7 +221,7 @@ class Service extends Model implements IDs
'method'=>'action_provision_planned',
'title'=>'Provision Planned',
],
// Service has been provisioned by supplier
// Service has been provisioned by supplier @todo redo
'PROVISIONED' => [
'fail'=>false,
'next'=>[
@@ -216,34 +233,56 @@ class Service extends Model implements IDs
],
// Service is Active
'ACTIVE' => [
'fail'=>FALSE,
'next'=>[
'UPGRADE-REQUEST'=>['customer'],
'CANCEL-REQUEST'=>['customer'],
'CHANGE-REQUEST'=>['customer'],
],
'system'=>FALSE,
'method'=>'action_active',
'exit'=>'action_active_exit',
'title'=>'Service Active',
],
// Service to be Upgraded
'UPGRADE-REQUEST' => [
'fail'=>FALSE,
'CANCEL-CANCEL' => [
'next'=>[
'UPGRADE-PENDING'=>[],
'ACTIVE'=>['wholesaler'],
],
'system'=>FALSE,
'method'=>FALSE,
'title'=>'Upgrade Service',
'enter_method'=>'action_cancel_cancel',
'title'=>'Cancel Cancellation Request',
],
// Service to be Cancelled
'CANCEL-REQUEST' => [
'fail'=>FALSE,
'next'=>[
'CANCEL-PENDING'=>[],
'CANCEL-CANCEL'=>['wholesaler'],
'CANCEL-PENDING'=>['wholesaler'],
],
'system'=>FALSE,
'method'=>'action_cancel_request',
'title'=>'Cancel Service',
'enter_method'=>'action_request_enter_redirect',
'exit_method'=>'action_cancel_request_exit',
'title'=>'Cancel Request',
],
// Service Cancellation being processed
'CANCEL-PENDING' => [
'next'=>[
'CANCELLED'=>['wholesaler'],
],
'enter_method'=>'action_cancel_pending_enter',
'exit_method'=>'action_cancel_pending_exit',
'title'=>'Cancel Pending',
],
// Service to be Upgraded
'CHANGE-CANCEL' => [
'next'=>[
'ACTIVE'=>['wholesaler'],
],
'enter_method'=>'action_change_cancel',
'title'=>'Cancel Change Request',
],
// Service to be Upgraded
'CHANGE-REQUEST' => [
'next'=>[
'CHANGE-PENDING'=>['wholesaler'],
'CHANGE-CANCEL'=>['wholesaler'],
],
'enter_method'=>'action_request_enter_redirect',
'title'=>'Change Service',
],
];
@@ -906,36 +945,13 @@ class Service extends Model implements IDs
$this->attributes['date_last'] = $value->timestamp;
}
/* GENERAL METHODS */
// The action methods will return: NULL for no progress|FALSE for a failed status|next stage name.
/**
* Action required before order can leave the ACTIVE status.
*
* @return bool
*/
private function action_active(): ?bool
{
// N/A
return TRUE;
}
/**
* Request cancellation for an order when status ACTIVE stage.
* This method should have the client confirm/accept the cancellation, if it was placed by a reseller/wholesaler.
*
* @return bool
*/
private function action_cancel_request(): ?bool
{
throw new HttpException(301,url('u/service/cancel',$this->id));
}
/* METHODS */
/**
* Processing when service has been ordered.
*
* @return bool|null
* @todo Check
*/
private function action_ordered(): ?bool
{
@@ -948,6 +964,7 @@ class Service extends Model implements IDs
* This method should have the client confirm/accept the order, if it was placed by a reseller/wholesaler.
*
* @return bool
* @todo Check
*/
private function action_order_accept(): ?bool
{
@@ -958,6 +975,8 @@ class Service extends Model implements IDs
/**
* Action method when status ORDER_SENT
* This method redirects to a form, where updating the form will progress to the next stage.
*
* @todo Check
*/
private function action_order_sent(string $next)
{
@@ -972,6 +991,7 @@ class Service extends Model implements IDs
* Action method when status ORDER_SUBMIT
*
* @return bool
* @todo Check
*/
private function action_order_submit(): ?bool
{
@@ -984,6 +1004,7 @@ class Service extends Model implements IDs
*
* @param string $next
* @return bool
* @todo Check
*/
private function action_provision_planned(string $next)
{
@@ -995,6 +1016,7 @@ class Service extends Model implements IDs
* This method should collect any setup fees payment.
*
* @return bool
* @todo Check
*/
private function action_setup_payment_wait(): ?bool
{
@@ -1007,6 +1029,7 @@ class Service extends Model implements IDs
* This method should validate any payment details.
*
* @return bool
* @todo Check
*/
private function action_payment_check(): ?bool
{
@@ -1019,6 +1042,7 @@ class Service extends Model implements IDs
* This method should collect any service payment details.
*
* @return bool
* @todo Check
*/
private function action_payment_wait(): ?bool
{
@@ -1026,145 +1050,78 @@ class Service extends Model implements IDs
return TRUE;
}
/**
* Work out the next applicable actions for this service status, taking into account the user's role
*
* @notes
* + Clients can only progress 1 step, if they are in the next step.
* + Resellers/Wholesales can progress to the next Reseller/Wholesaler and any steps in between.
* @return Collection
*/
public function actions(): Collection
{
$next = $this->getStageParameters($this->order_status)->get('next');
return $next
? $next->map(function($item,$key) {
$authorized = FALSE;
if ($x=Arr::get(self::ACTION_PROGRESS,$key))
foreach ($item as $role) {
if ($this->isAuthorised($role)) {
$authorized = TRUE;
break;
}
}
return $authorized ? $x['title'] : NULL;
})->filter()->sort()
: collect();
}
private function getOrderInfoValue(string $key): ?string
{
return $this->order_info ? $this->order_info->get($key) : NULL;
}
/**
* Get the current stage parameters
* Get the stage parameters
*
* @param string $stage
* @return array
* @return Collection
*/
private function getStageParameters(string $stage): Collection
public function getStageParameters(string $stage): Collection
{
$result = collect(Arr::get(self::$action_progress,$stage));
$result = Arr::get(self::ACTION_PROGRESS,$stage);
$myrole = array_search(Auth::user()->role(),User::$role_order);
// If we have no valid next stage, return an empty collection.
if (! $result->count() OR $myrole===FALSE)
return $result;
if (($myrole === FALSE) || (! $result))
return collect();
// Filter the result based on who we are
$next = collect();
foreach ($result->get('next') as $action=>$roles) {
if (array_key_exists('next',$result) && count($result['next'])) {
foreach ($result['next'] as $action => $roles) {
// Can the current user do this role?
$cando = FALSE;
// Can the current user do this role?
$cando = FALSE;
foreach ($roles as $role) {
if ($myrole <= array_search($role,User::$role_order)) {
$cando = TRUE;
break;
}
}
foreach ($roles as $role) {
if ($myrole <= array_search($role,User::$role_order)) {
$cando = TRUE;
if ($cando OR $result->get('system')) {
$next->put($action,$roles);
}
}
$result->put('next',$next);
return $result;
}
/**
* @notes
* + When progressing stages, we know who the user is that initiated the stage,
* + If no user, then we perform stages SYSTEM=TRUE
* + We need to validate that the current stage is complete, before progressing to the next stage
* + The current stage may require input from a user, or automation process to progress
* + Before leaving this method, we update the service with the stage that it is currently on.
*
* @param string $stage
* @return bool|int|string|null
*/
public function action(string $stage)
{
// 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 = $this->getStageParameters($stage);
// If valid, call the method to confirm that the current stage is complete
if (method_exists($this,$current['method'])) {
try {
$result = $this->{$current['method']}($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());
}
// @todo Implement a status message
if (is_null($result)) {
$stage = NULL;
abort(500,'Current Method Cannot Proceed: '.$result);
// @todo Implement a status message
} elseif (! $result) {
$stage = NULL;
abort(500,'Current Method FAILED: '.$result);
} else {
$this->order_status = $stage;
$this->save();
// If we have more than 1 next step for the next stage, we'll have to end.
if ($this->actions()->count() > 1) {
$stage = NULL;
} else {
$stage = $this->actions()->keys()->first();
break;
}
}
// @todo Implement a status message
} else {
// Cant do anything, dont have a method to check if we can leave
$stage = NULL;
abort(500,'NO Method Cannot Proceed to leave this stage: '.$current['method']);
if ($cando)
$next->put($action,$roles);
}
// If valid, call the method to start the next stage
}
}
/**
* Work out the next applicable actions for this service status
*
* @notes
* + Clients can only progress 1 step, if they are in the next step.
* + Resellers/Wholesales can progress to the next Reseller/Wholesaler and any steps in between.
*
* @param bool $next Only show next actions
* @return Collection
*/
public function actions(): Collection
{
$result = collect();
$action = $this->getStageParameters($this->order_status);
if (! $action->count())
return $result;
// Next Action
foreach ($this->getStageParameters($this->order_status)->get('next') as $k=>$v) {
$result->put($k,Arr::get(self::$action_progress,$k.'.title'));
$result['next'] = $next;
}
// No next actions, that will mean the current action hasnt completed.
if (! $result->count())
$result->put($this->order_status,Arr::get($action,'title'));
return $result;
return collect($result);
}
/**
@@ -1209,6 +1166,54 @@ class Service extends Model implements IDs
return $this->active OR ($this->order_status AND ! in_array($this->order_status,$this->inactive_status));
}
/**
* Determine if the current user has the role for this service
*
* @param string $role
* @return bool
*/
public function isAuthorised(string $role): bool
{
switch(Auth::user()->role()) {
// Wholesalers are site admins, they can see everything
case 'wholesaler':
return TRUE;
case 'reseller':
switch ($role) {
case 'wholesaler':
return FALSE;
// Check service is in the resellers/customers list
case 'reseller':
case 'customer':
dd(['m'=>__METHOD__,'not written']);
default:
abort(500,'Unknown role for reseller: '.$role);
}
case 'customer':
switch ($role) {
case 'reseller':
case 'wholesaler':
return FALSE;
// Check service is in the customers list
case 'customer':
dd(['m'=>__METHOD__,'not written']);
default:
abort(500,'Unknown role for customer: '.$role);
}
default:
abort(500,'Unknown user role: ',Auth::user()->role());
}
return FALSE;
}
/**
* Do we bill for this service
*