Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADVAPP-185] & [ADVAPP-186]: Enforce license relationship for Retention CRM with respect to students / prospects #415

Merged
merged 21 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions _ide_helper_models.php
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,8 @@ class IdeHelperSystemUser {}
* @method static \Illuminate\Database\Eloquent\Builder|User admins()
* @method static \Illuminate\Database\Eloquent\Builder|User advancedFilter($data)
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|User hasAnyLicense(\AdvisingApp\Authorization\Enums\LicenseType|array|string|null $type)
* @method static \Illuminate\Database\Eloquent\Builder|User hasLicense(\AdvisingApp\Authorization\Enums\LicenseType|array|string|null $type)
* @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User onlyTrashed()
Expand Down Expand Up @@ -434,6 +436,7 @@ class IdeHelperUser {}
* @property-read \Illuminate\Database\Eloquent\Collection<int, \AdvisingApp\Audit\Models\Audit> $audits
* @property-read int|null $audits_count
* @method static \AdvisingApp\Alert\Database\Factories\AlertFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|Alert licensedToEducatable(string $relationship)
* @method static \Illuminate\Database\Eloquent\Builder|Alert newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Alert newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Alert onlyTrashed()
Expand Down Expand Up @@ -613,6 +616,7 @@ class IdeHelperApplicationStep {}
* @property-read \AdvisingApp\Application\Models\ApplicationSubmissionState $state
* @property-read \AdvisingApp\Application\Models\Application $submissible
* @method static \AdvisingApp\Application\Database\Factories\ApplicationSubmissionFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|Submission licensedToEducatable(string $relationship)
* @method static \Illuminate\Database\Eloquent\Builder|ApplicationSubmission newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ApplicationSubmission newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ApplicationSubmission query()
Expand Down Expand Up @@ -1347,6 +1351,7 @@ class IdeHelperEmailTemplate {}
* @method static \Illuminate\Database\Eloquent\Builder|Engagement isAwaitingDelivery()
* @method static \Illuminate\Database\Eloquent\Builder|Engagement isNotPartOfABatch()
* @method static \Illuminate\Database\Eloquent\Builder|Engagement isScheduled()
* @method static \Illuminate\Database\Eloquent\Builder|Engagement licensedToEducatable(string $relationship)
* @method static \Illuminate\Database\Eloquent\Builder|Engagement newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Engagement newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Engagement query()
Expand Down Expand Up @@ -1721,6 +1726,7 @@ class IdeHelperFormStep {}
* @property-read \AdvisingApp\Form\Models\Form $submissible
* @method static \Illuminate\Database\Eloquent\Builder|FormSubmission canceled()
* @method static \AdvisingApp\Form\Database\Factories\FormSubmissionFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|Submission licensedToEducatable(string $relationship)
* @method static \Illuminate\Database\Eloquent\Builder|FormSubmission newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FormSubmission newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FormSubmission notCanceled()
Expand Down Expand Up @@ -1804,6 +1810,7 @@ class IdeHelperTwilioConversation {}
* @property-read \AdvisingApp\Interaction\Models\InteractionType|null $type
* @property-read \App\Models\User|null $user
* @method static \AdvisingApp\Interaction\Database\Factories\InteractionFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|Interaction licensedToEducatable(string $relationship)
* @method static \Illuminate\Database\Eloquent\Builder|Interaction newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Interaction newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Interaction query()
Expand Down Expand Up @@ -2619,6 +2626,7 @@ class IdeHelperOutboundDeliverable {}
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Model|\Eloquent $subscribable
* @property-read \App\Models\User $user
* @method static \Illuminate\Database\Eloquent\Builder|Subscription licensedToEducatable(string $relationship)
* @method static \Illuminate\Database\Eloquent\Builder|Subscription newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Subscription newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Subscription query()
Expand Down Expand Up @@ -2837,6 +2845,7 @@ class IdeHelperProspectStatus {}
* @property-read \AdvisingApp\ServiceManagement\Models\ServiceRequestStatus|null $status
* @property-read \AdvisingApp\ServiceManagement\Models\ServiceRequestType|null $type
* @method static \AdvisingApp\ServiceManagement\Database\Factories\ServiceRequestFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|ServiceRequest licensedToEducatable(string $relationship)
* @method static \Illuminate\Database\Eloquent\Builder|ServiceRequest newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ServiceRequest newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ServiceRequest onlyTrashed()
Expand Down Expand Up @@ -3322,6 +3331,7 @@ class IdeHelperSurveyStep {}
* @property-read \AdvisingApp\Survey\Models\Survey $submissible
* @method static \Illuminate\Database\Eloquent\Builder|SurveySubmission canceled()
* @method static \AdvisingApp\Survey\Database\Factories\SurveySubmissionFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|Submission licensedToEducatable(string $relationship)
* @method static \Illuminate\Database\Eloquent\Builder|SurveySubmission newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|SurveySubmission newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|SurveySubmission notCanceled()
Expand Down Expand Up @@ -3370,6 +3380,7 @@ class IdeHelperSurveySubmission {}
* @property-read \App\Models\User|null $createdBy
* @method static \Illuminate\Database\Eloquent\Builder|Task byNextDue()
* @method static \AdvisingApp\Task\Database\Factories\TaskFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder|Task licensedToEducatable(string $relationship)
* @method static \Illuminate\Database\Eloquent\Builder|Task newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Task newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Task onlyTrashed()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,15 @@
use Illuminate\Database\Eloquent\Model;
use AdvisingApp\Alert\Enums\AlertStatus;
use AdvisingApp\Prospect\Models\Prospect;
use App\Filament\Fields\EducatableSelect;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Filters\SelectFilter;
use Illuminate\Database\Eloquent\Builder;
use AdvisingApp\Alert\Enums\AlertSeverity;
use Filament\Forms\Components\MorphToSelect;
use Filament\Infolists\Components\TextEntry;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use AdvisingApp\StudentDataModel\Models\Student;
use Filament\Forms\Components\MorphToSelect\Type;
use AdvisingApp\CaseloadManagement\Models\Caseload;
use AdvisingApp\Alert\Filament\Resources\AlertResource;
use AdvisingApp\StudentDataModel\Models\Scopes\EducatableSearch;
Expand Down Expand Up @@ -174,15 +173,8 @@ protected function getHeaderActions(): array
return [
CreateAction::make()
->form([
MorphToSelect::make('concern')
EducatableSelect::make('concern')
->label('Related To')
->types([
Type::make(Student::class)
->titleAttribute(Student::displayNameKey()),
Type::make(Prospect::class)
->titleAttribute(Prospect::displayNameKey()),
])
->searchable()
->required(),
Group::make()
->schema([
Expand Down
10 changes: 10 additions & 0 deletions app-modules/alert/src/Models/Alert.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
use AdvisingApp\Notification\Models\Contracts\Subscribable;
use AdvisingApp\StudentDataModel\Models\Contracts\Educatable;
use AdvisingApp\Audit\Models\Concerns\Auditable as AuditableTrait;
use AdvisingApp\StudentDataModel\Models\Scopes\LicensedToEducatable;
use AdvisingApp\StudentDataModel\Models\Concerns\BelongsToEducatable;
use AdvisingApp\Campaign\Models\Contracts\ExecutableFromACampaignAction;
use AdvisingApp\Notification\Models\Contracts\CanTriggerAutoSubscription;

Expand All @@ -62,6 +64,7 @@ class Alert extends BaseModel implements Auditable, CanTriggerAutoSubscription,
{
use SoftDeletes;
use AuditableTrait;
use BelongsToEducatable;

protected $fillable = [
'concern_id',
Expand Down Expand Up @@ -113,4 +116,11 @@ public static function executeFromCampaignAction(CampaignAction $action): bool|s

// Do we need to be able to relate campaigns/actions to the RESULT of their actions?
}

protected static function booted(): void
{
static::addGlobalScope('licensed', function (Builder $builder) {
$builder->tap(new LicensedToEducatable('concern'));
});
}
}
31 changes: 31 additions & 0 deletions app-modules/alert/src/Policies/AlertPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,20 @@
use App\Models\Authenticatable;
use AdvisingApp\Alert\Models\Alert;
use Illuminate\Auth\Access\Response;
use AdvisingApp\Prospect\Models\Prospect;
use AdvisingApp\StudentDataModel\Models\Student;

class AlertPolicy
{
public function before(Authenticatable $authenticatable): ?Response
{
if (! $authenticatable->hasAnyLicense([Student::getLicenseType(), Prospect::getLicenseType()])) {
return Response::deny('You are not licensed for the Retention or Recruitment CRM.');
}

return null;
}

public function viewAny(Authenticatable $authenticatable): Response
{
return $authenticatable->canOrElse(
Expand All @@ -52,6 +63,10 @@ public function viewAny(Authenticatable $authenticatable): Response

public function view(Authenticatable $authenticatable, Alert $alert): Response
{
if (! $authenticatable->hasLicense($alert->concern?->getLicenseType())) {
return Response::deny('You do not have permission to view this alert.');
}

return $authenticatable->canOrElse(
abilities: ['alert.*.view', "alert.{$alert->id}.view"],
denyResponse: 'You do not have permission to view this alert.'
Expand All @@ -68,6 +83,10 @@ public function create(Authenticatable $authenticatable): Response

public function update(Authenticatable $authenticatable, Alert $alert): Response
{
if (! $authenticatable->hasLicense($alert->concern?->getLicenseType())) {
return Response::deny('You do not have permission to update this alert.');
}

return $authenticatable->canOrElse(
abilities: ['alert.*.update', "alert.{$alert->id}.update"],
denyResponse: 'You do not have permission to update this alert.'
Expand All @@ -76,6 +95,10 @@ public function update(Authenticatable $authenticatable, Alert $alert): Response

public function delete(Authenticatable $authenticatable, Alert $alert): Response
{
if (! $authenticatable->hasLicense($alert->concern?->getLicenseType())) {
return Response::deny('You do not have permission to delete this alert.');
}

return $authenticatable->canOrElse(
abilities: ['alert.*.delete', "alert.{$alert->id}.delete"],
denyResponse: 'You do not have permission to delete this alert.'
Expand All @@ -84,6 +107,10 @@ public function delete(Authenticatable $authenticatable, Alert $alert): Response

public function restore(Authenticatable $authenticatable, Alert $alert): Response
{
if (! $authenticatable->hasLicense($alert->concern?->getLicenseType())) {
return Response::deny('You do not have permission to restore this alert.');
}

return $authenticatable->canOrElse(
abilities: ['alert.*.restore', "alert.{$alert->id}.restore"],
denyResponse: 'You do not have permission to restore this alert.'
Expand All @@ -92,6 +119,10 @@ public function restore(Authenticatable $authenticatable, Alert $alert): Respons

public function forceDelete(Authenticatable $authenticatable, Alert $alert): Response
{
if (! $authenticatable->hasLicense($alert->concern?->getLicenseType())) {
return Response::deny('You do not have permission to permanently delete this alert.');
}

return $authenticatable->canOrElse(
abilities: ['alert.*.force-delete', "alert.{$alert->id}.force-delete"],
denyResponse: 'You do not have permission to permanently delete this alert.'
Expand Down
5 changes: 3 additions & 2 deletions app-modules/alert/tests/AlertCreateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@
use function Pest\Laravel\actingAs;

use Illuminate\Support\Facades\Notification;
use AdvisingApp\Authorization\Enums\LicenseType;
use AdvisingApp\StudentDataModel\Models\Student;
use AdvisingApp\Alert\Notifications\AlertCreatedNotification;

it('creates a subscription for the user that created the Alert', function () {
$user = User::factory()->create();
$user = User::factory()->licensed(LicenseType::cases())->create();

actingAs($user);

Expand All @@ -60,7 +61,7 @@
it('dispatches the proper notifications to subscribers on created', function () {
Notification::fake();

$users = User::factory()->count(5)->create();
$users = User::factory()->licensed(LicenseType::cases())->count(5)->create();

/** @var Student $student */
$student = Student::factory()->create();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
use Filament\Forms\Components\Grid;
use AdvisingApp\Form\Enums\Rounding;
use AdvisingApp\Form\Rules\IsDomain;
use App\Forms\Components\ColorSelect;
use App\Filament\Fields\ColorSelect;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\Section;
Expand Down
14 changes: 11 additions & 3 deletions app-modules/authorization/src/Enums/LicenseType.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,22 @@ public function hasAvailableLicenses(): bool
{
$totalLicensesInUse = License::query()->where('type', $this)->count();

return $totalLicensesInUse < $this->getSeats();
}

public function isLicenseable(): bool
{
return $this->getSeats() > 0;
}

public function getSeats(): int
{
$licenseSettings = app(LicenseSettings::class);

$licenseLimit = match ($this) {
return match ($this) {
LicenseType::ConversationalAi => $licenseSettings->data->limits->conversationalAiSeats,
LicenseType::RetentionCrm => $licenseSettings->data->limits->retentionCrmSeats,
LicenseType::RecruitmentCrm => $licenseSettings->data->limits->recruitmentCrmSeats,
};

return $totalLicensesInUse < $licenseLimit;
}
}
11 changes: 11 additions & 0 deletions app-modules/campaign/src/Models/Campaign.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,15 @@ public function hasBeenExecuted(): bool
{
return $this->actions->contains(fn (CampaignAction $action) => $action->hasBeenExecuted());
}

protected static function booted(): void
{
static::addGlobalScope('licensed', function (Builder $builder) {
if (! auth()->check()) {
return;
}

$builder->whereHas('caseload');
});
}
}
20 changes: 20 additions & 0 deletions app-modules/campaign/src/Policies/CampaignPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public function viewAny(Authenticatable $authenticatable): Response

public function view(Authenticatable $authenticatable, Campaign $campaign): Response
{
if ($authenticatable->cannot('view', $campaign->caseload)) {
return Response::deny('You do not have permission to view this campaign.');
}

return $authenticatable->canOrElse(
abilities: ['campaign.*.view', "campaign.{$campaign->id}.view"],
denyResponse: 'You do not have permission to view this campaign.'
Expand All @@ -68,6 +72,10 @@ public function create(Authenticatable $authenticatable): Response

public function update(Authenticatable $authenticatable, Campaign $campaign): Response
{
if ($authenticatable->cannot('view', $campaign->caseload)) {
return Response::deny('You do not have permission to update this campaign.');
}

return $authenticatable->canOrElse(
abilities: ['campaign.*.update', "campaign.{$campaign->id}.update"],
denyResponse: 'You do not have permission to update this campaign.'
Expand All @@ -76,6 +84,10 @@ public function update(Authenticatable $authenticatable, Campaign $campaign): Re

public function delete(Authenticatable $authenticatable, Campaign $campaign): Response
{
if ($authenticatable->cannot('view', $campaign->caseload)) {
return Response::deny('You do not have permission to delete this campaign.');
}

return $authenticatable->canOrElse(
abilities: ['campaign.*.delete', "campaign.{$campaign->id}.delete"],
denyResponse: 'You do not have permission to delete this campaign.'
Expand All @@ -84,6 +96,10 @@ public function delete(Authenticatable $authenticatable, Campaign $campaign): Re

public function restore(Authenticatable $authenticatable, Campaign $campaign): Response
{
if ($authenticatable->cannot('view', $campaign->caseload)) {
return Response::deny('You do not have permission to restore this campaign.');
}

return $authenticatable->canOrElse(
abilities: ['campaign.*.restore', "campaign.{$campaign->id}.restore"],
denyResponse: 'You do not have permission to restore this campaign.'
Expand All @@ -92,6 +108,10 @@ public function restore(Authenticatable $authenticatable, Campaign $campaign): R

public function forceDelete(Authenticatable $authenticatable, Campaign $campaign): Response
{
if ($authenticatable->cannot('view', $campaign->caseload)) {
return Response::deny('You do not have permission to permanently delete this campaign.');
}

return $authenticatable->canOrElse(
abilities: ['campaign.*.force-delete', "campaign.{$campaign->id}.force-delete"],
denyResponse: 'You do not have permission to permanently delete this campaign.'
Expand Down
Loading