Skip to content

Commit c111356

Browse files
[ADVAPP-2541] : Improve campaigns by introducing the ability to archive campaigns (#2445)
* feature: Implement campaign archiving feature with migration, model updates, and UI actions * delete: Remove ListCampaignsTest.php as part of campaign feature refactoring * Update code formatting and copyright headers * refactor: Update EditCampaignTest to remove redundant test and adjust redirect action * chore: Update phpstan configuration to include common extension Update ListCampaigns query modification for clarity and maintainability * feature: update common package to version 2.12.1 and add new PHPStan rule for anonymous function return type * Update code formatting and copyright headers * chore: change trademarks to newly created files * feat: enhance archive action with transaction handling and notifications * Update code formatting and copyright headers * fix: remove exception rethrow in archive action error handling --------- Co-authored-by: monali-canyon <263637457+monali-canyon@users.noreply.github.com>
1 parent 9888d5a commit c111356

9 files changed

Lines changed: 321 additions & 1 deletion

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
<COPYRIGHT>
5+
6+
Copyright © 2016-2026, Canyon GBS Inc. All rights reserved.
7+
8+
Advising App® is licensed under the Elastic License 2.0. For more details,
9+
see https://github.com/canyongbs/advisingapp/blob/main/LICENSE.
10+
11+
Notice:
12+
13+
- You may not provide the software to third parties as a hosted or managed
14+
service, where the service provides users with access to any substantial set of
15+
the features or functionality of the software.
16+
- You may not move, change, disable, or circumvent the license key functionality
17+
in the software, and you may not remove or obscure any functionality in the
18+
software that is protected by the license key.
19+
- You may not alter, remove, or obscure any licensing, copyright, or other notices
20+
of the licensor in the software. Any use of the licensor’s trademarks is subject
21+
to applicable law.
22+
- Canyon GBS Inc. respects the intellectual property rights of others and expects the
23+
same in return. Canyon GBS® and Advising App® are registered trademarks of
24+
Canyon GBS Inc., and we are committed to enforcing and protecting our trademarks
25+
vigorously.
26+
- The software solution, including services, infrastructure, and code, is offered as a
27+
Software as a Service (SaaS) by Canyon GBS Inc.
28+
- Use of this software implies agreement to the license terms and conditions as stated
29+
in the Elastic License 2.0.
30+
31+
For more information or inquiries please visit our website at
32+
https://www.canyongbs.com or contact us via email at legal@canyongbs.com.
33+
34+
</COPYRIGHT>
35+
*/
36+
37+
use App\Features\CampaignArchivingFeature;
38+
use Illuminate\Database\Migrations\Migration;
39+
use Illuminate\Support\Facades\DB;
40+
use Tpetry\PostgresqlEnhanced\Schema\Blueprint;
41+
use Tpetry\PostgresqlEnhanced\Support\Facades\Schema;
42+
43+
return new class () extends Migration {
44+
public function up(): void
45+
{
46+
DB::transaction(function () {
47+
Schema::table('campaigns', function (Blueprint $table) {
48+
$table->timestamp('archived_at')->nullable();
49+
});
50+
51+
CampaignArchivingFeature::activate();
52+
});
53+
}
54+
55+
public function down(): void
56+
{
57+
DB::transaction(function () {
58+
CampaignArchivingFeature::deactivate();
59+
60+
Schema::table('campaigns', function (Blueprint $table) {
61+
$table->dropColumn('archived_at');
62+
});
63+
});
64+
}
65+
};

app-modules/campaign/src/Filament/Resources/Campaigns/Pages/EditCampaign.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,20 @@
3737
namespace AdvisingApp\Campaign\Filament\Resources\Campaigns\Pages;
3838

3939
use AdvisingApp\Campaign\Filament\Resources\Campaigns\CampaignResource;
40+
use AdvisingApp\Campaign\Models\Campaign;
4041
use AdvisingApp\Group\Models\Group;
42+
use App\Features\CampaignArchivingFeature;
4143
use App\Filament\Resources\Pages\EditRecord\Concerns\EditPageRedirection;
44+
use CanyonGBS\Common\Filament\Actions\ArchiveAction;
4245
use Filament\Actions\DeleteAction;
4346
use Filament\Forms\Components\Select;
4447
use Filament\Forms\Components\TextInput;
4548
use Filament\Forms\Components\Toggle;
49+
use Filament\Notifications\Notification;
4650
use Filament\Resources\Pages\EditRecord;
4751
use Filament\Schemas\Schema;
52+
use Illuminate\Support\Facades\DB;
53+
use Throwable;
4854

4955
class EditCampaign extends EditRecord
5056
{
@@ -76,6 +82,34 @@ public function form(Schema $schema): Schema
7682
protected function getHeaderActions(): array
7783
{
7884
return [
85+
ArchiveAction::make()
86+
->label(fn (Campaign $record): string => $record->enabled ? 'Disable and Archive' : 'Archive')
87+
->modalHeading(fn (Campaign $record): string => $record->enabled ? 'Disable and Archive Campaign' : 'Archive Campaign')
88+
->modalSubmitActionLabel(fn (Campaign $record): string => $record->enabled ? 'Disable and Archive' : 'Archive')
89+
->hidden(fn (): bool => ! CampaignArchivingFeature::active())
90+
->action(function (Campaign $record): void {
91+
try {
92+
DB::transaction(function () use ($record) {
93+
if ($record->enabled) {
94+
$record->update(['enabled' => false]);
95+
}
96+
$record->archive();
97+
});
98+
99+
Notification::make()
100+
->success()
101+
->title('Campaign archived successfully')
102+
->send();
103+
} catch (Throwable $exception) {
104+
report($exception);
105+
106+
Notification::make()
107+
->danger()
108+
->title('Failed to archive campaign')
109+
->body($exception->getMessage())
110+
->send();
111+
}
112+
}),
79113
DeleteAction::make(),
80114
];
81115
}

app-modules/campaign/src/Filament/Resources/Campaigns/Pages/ListCampaigns.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
use AdvisingApp\Campaign\Filament\Resources\Campaigns\CampaignResource;
4040
use AdvisingApp\Campaign\Models\Campaign;
41+
use App\Features\CampaignArchivingFeature;
4142
use App\Filament\Tables\Columns\IdColumn;
4243
use App\Models\User;
4344
use Filament\Actions\CreateAction;
@@ -83,6 +84,15 @@ public function table(Table $table): Table
8384
->searchable()
8485
->sortable(),
8586
])
87+
// @phpstan-ignore argument.templateType
88+
->modifyQueryUsing(function (Builder $query) {
89+
/** @var Builder<Campaign> $query */
90+
if (CampaignArchivingFeature::active()) {
91+
$query->withoutArchived();
92+
}
93+
94+
return $query;
95+
})
8696
->recordActions([
8797
ViewAction::make(),
8898
EditAction::make()

app-modules/campaign/src/Models/Campaign.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use AdvisingApp\Campaign\Observers\CampaignObserver;
4141
use AdvisingApp\Group\Models\Group;
4242
use App\Models\BaseModel;
43+
use CanyonGBS\Common\Models\Concerns\CanBeArchived;
4344
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
4445
use Illuminate\Database\Eloquent\Builder;
4546
use Illuminate\Database\Eloquent\Model;
@@ -57,6 +58,7 @@ class Campaign extends BaseModel implements Auditable
5758
{
5859
use AuditableTrait;
5960
use SoftDeletes;
61+
use CanBeArchived;
6062

6163
protected $fillable = [
6264
'name',

app-modules/campaign/src/Policies/CampaignPolicy.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ public function delete(Authenticatable $authenticatable, Campaign $campaign): Re
117117
);
118118
}
119119

120+
public function archive(Authenticatable $authenticatable, Campaign $campaign): Response
121+
{
122+
if ($authenticatable->cannot('view', $campaign->group)) {
123+
return Response::deny('You do not have permission to archive this campaign.');
124+
}
125+
126+
return $authenticatable->canOrElse(
127+
abilities: ["campaign.{$campaign->getKey()}.delete"],
128+
denyResponse: 'You do not have permission to archive this campaign.'
129+
);
130+
}
131+
120132
public function restore(Authenticatable $authenticatable, Campaign $campaign): Response
121133
{
122134
if ($authenticatable->cannot('view', $campaign->group)) {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
/*
4+
<COPYRIGHT>
5+
6+
Copyright © 2016-2026, Canyon GBS Inc. All rights reserved.
7+
8+
Advising App® is licensed under the Elastic License 2.0. For more details,
9+
see https://github.com/canyongbs/advisingapp/blob/main/LICENSE.
10+
11+
Notice:
12+
13+
- You may not provide the software to third parties as a hosted or managed
14+
service, where the service provides users with access to any substantial set of
15+
the features or functionality of the software.
16+
- You may not move, change, disable, or circumvent the license key functionality
17+
in the software, and you may not remove or obscure any functionality in the
18+
software that is protected by the license key.
19+
- You may not alter, remove, or obscure any licensing, copyright, or other notices
20+
of the licensor in the software. Any use of the licensor’s trademarks is subject
21+
to applicable law.
22+
- Canyon GBS Inc. respects the intellectual property rights of others and expects the
23+
same in return. Canyon GBS® and Advising App® are registered trademarks of
24+
Canyon GBS Inc., and we are committed to enforcing and protecting our trademarks
25+
vigorously.
26+
- The software solution, including services, infrastructure, and code, is offered as a
27+
Software as a Service (SaaS) by Canyon GBS Inc.
28+
- Use of this software implies agreement to the license terms and conditions as stated
29+
in the Elastic License 2.0.
30+
31+
For more information or inquiries please visit our website at
32+
https://www.canyongbs.com or contact us via email at legal@canyongbs.com.
33+
34+
</COPYRIGHT>
35+
*/
36+
37+
namespace AdvisingApp\Campaign\Tests\Tenant\Filament\Resources\Campaigns\Pages;
38+
39+
use AdvisingApp\Authorization\Enums\LicenseType;
40+
use AdvisingApp\Campaign\Filament\Resources\Campaigns\Pages\EditCampaign;
41+
use AdvisingApp\Campaign\Filament\Resources\Campaigns\Pages\ListCampaigns;
42+
use AdvisingApp\Campaign\Models\Campaign;
43+
use App\Models\User;
44+
45+
use function Pest\Laravel\actingAs;
46+
use function Pest\Livewire\livewire;
47+
use function Tests\asSuperAdmin;
48+
49+
test('archive action is visible on edit page', function () {
50+
$user = User::factory()->licensed(LicenseType::cases())->create();
51+
$campaign = Campaign::factory()->enabled()->create();
52+
53+
// Minimum permissions to access the edit page
54+
$user->givePermissionTo('campaign.view-any');
55+
$user->givePermissionTo('campaign.*.view');
56+
$user->givePermissionTo('campaign.*.update');
57+
$user->givePermissionTo('group.*.view');
58+
59+
// Without delete permission, archive action should be hidden
60+
actingAs($user);
61+
62+
livewire(EditCampaign::class, ['record' => $campaign->getRouteKey()])
63+
->assertActionHidden('archive');
64+
65+
// Add campaign delete permission — now archive should be visible
66+
$user->givePermissionTo('campaign.*.delete');
67+
68+
livewire(EditCampaign::class, ['record' => $campaign->getRouteKey()])
69+
->assertActionVisible('archive');
70+
});
71+
72+
test('archive action shows disable and archive label for enabled campaigns', function () {
73+
asSuperAdmin();
74+
75+
$campaign = Campaign::factory()->enabled()->create();
76+
77+
livewire(EditCampaign::class, ['record' => $campaign->getRouteKey()])
78+
->assertActionHasLabel('archive', 'Disable and Archive');
79+
});
80+
81+
test('archive action shows archive label for disabled campaigns', function () {
82+
asSuperAdmin();
83+
84+
$campaign = Campaign::factory()->disabled()->create();
85+
86+
livewire(EditCampaign::class, ['record' => $campaign->getRouteKey()])
87+
->assertActionHasLabel('archive', 'Archive');
88+
});
89+
90+
test('archive action disables and archives enabled campaigns', function () {
91+
asSuperAdmin();
92+
93+
$campaign = Campaign::factory()->enabled()->create();
94+
95+
expect($campaign->enabled)->toBeTrue()
96+
->and($campaign->isArchived())->toBeFalse();
97+
98+
livewire(EditCampaign::class, ['record' => $campaign->getRouteKey()])
99+
->callAction('archive');
100+
101+
$campaign = $campaign->fresh();
102+
103+
expect($campaign->enabled)->toBeFalse()
104+
->and($campaign->isArchived())->toBeTrue();
105+
});
106+
107+
test('archive action archives disabled campaigns', function () {
108+
asSuperAdmin();
109+
110+
$campaign = Campaign::factory()->disabled()->create();
111+
112+
expect($campaign->enabled)->toBeFalse()
113+
->and($campaign->isArchived())->toBeFalse();
114+
115+
livewire(EditCampaign::class, ['record' => $campaign->getRouteKey()])
116+
->callAction('archive');
117+
118+
$campaign = $campaign->fresh();
119+
120+
expect($campaign->enabled)->toBeFalse()
121+
->and($campaign->isArchived())->toBeTrue();
122+
});
123+
124+
test('archive action redirects to index after archiving', function () {
125+
asSuperAdmin();
126+
127+
$campaign = Campaign::factory()->enabled()->create();
128+
129+
livewire(EditCampaign::class, ['record' => $campaign->getRouteKey()])
130+
->callAction('archive')
131+
->assertRedirect(ListCampaigns::getUrl());
132+
});

app-modules/campaign/tests/Tenant/Actions/ListCampaignsTest.php renamed to app-modules/campaign/tests/Tenant/Filament/Resources/Campaigns/Pages/ListCampaignsTest.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
</COPYRIGHT>
3535
*/
3636

37-
namespace AdvisingApp\Campaign\Tests\Actions;
37+
namespace AdvisingApp\Campaign\Tests\Tenant\Filament\Resources\Campaigns\Pages;
3838

3939
use AdvisingApp\Authorization\Enums\LicenseType;
4040
use AdvisingApp\Campaign\Filament\Resources\Campaigns\Pages\ListCampaigns;
@@ -180,3 +180,17 @@
180180
$completeCampaign,
181181
]);
182182
});
183+
184+
it('excludes archived campaigns from the list', function () {
185+
asSuperAdmin();
186+
187+
$activeCampaign = Campaign::factory()->create();
188+
$archivedCampaign = Campaign::factory()->create();
189+
190+
$archivedCampaign->archive();
191+
192+
livewire(ListCampaigns::class)
193+
->assertCanSeeTableRecords([$activeCampaign])
194+
->assertCanNotSeeTableRecords([$archivedCampaign])
195+
->assertCountTableRecords(1);
196+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
<COPYRIGHT>
5+
6+
Copyright © 2016-2026, Canyon GBS Inc. All rights reserved.
7+
8+
Advising App® is licensed under the Elastic License 2.0. For more details,
9+
see https://github.com/canyongbs/advisingapp/blob/main/LICENSE.
10+
11+
Notice:
12+
13+
- You may not provide the software to third parties as a hosted or managed
14+
service, where the service provides users with access to any substantial set of
15+
the features or functionality of the software.
16+
- You may not move, change, disable, or circumvent the license key functionality
17+
in the software, and you may not remove or obscure any functionality in the
18+
software that is protected by the license key.
19+
- You may not alter, remove, or obscure any licensing, copyright, or other notices
20+
of the licensor in the software. Any use of the licensor’s trademarks is subject
21+
to applicable law.
22+
- Canyon GBS Inc. respects the intellectual property rights of others and expects the
23+
same in return. Canyon GBS® and Advising App® are registered trademarks of
24+
Canyon GBS Inc., and we are committed to enforcing and protecting our trademarks
25+
vigorously.
26+
- The software solution, including services, infrastructure, and code, is offered as a
27+
Software as a Service (SaaS) by Canyon GBS Inc.
28+
- Use of this software implies agreement to the license terms and conditions as stated
29+
in the Elastic License 2.0.
30+
31+
For more information or inquiries please visit our website at
32+
https://www.canyongbs.com or contact us via email at legal@canyongbs.com.
33+
34+
</COPYRIGHT>
35+
*/
36+
37+
namespace App\Features;
38+
39+
use App\Support\AbstractFeatureFlag;
40+
41+
class CampaignArchivingFeature extends AbstractFeatureFlag
42+
{
43+
public function resolve(mixed $scope): mixed
44+
{
45+
return false;
46+
}
47+
}

0 commit comments

Comments
 (0)