Skip to content

Commit ad69447

Browse files
authored
Merge pull request #17858 from grokability/ignore-expiring-licenses-with-past-termination-date
Ignore expiring licenses with past termination date
2 parents b2406b6 + b4614df commit ad69447

File tree

6 files changed

+151
-52
lines changed

6 files changed

+151
-52
lines changed

app/Console/Commands/SendExpirationAlerts.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Console\Commands;
44

5+
use App\Helpers\Helper;
56
use App\Mail\ExpiringAssetsMail;
67
use App\Mail\ExpiringLicenseMail;
78
use App\Models\Asset;
@@ -57,14 +58,27 @@ public function handle()
5758
if ($assets->count() > 0) {
5859
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
5960
Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval));
61+
62+
$this->table(
63+
['ID', 'Tag', 'Model', 'Model Number', 'EOL', 'EOL Months', 'Warranty Expires', 'Warranty Months'],
64+
$assets->map(fn($item) => ['ID' => $item->id, 'Tag' => $item->asset_tag, 'Model' => $item->model->name, 'Model Number' => $item->model->model_number, 'EOL' => $item->asset_eol_date, 'EOL Months' => $item->model->eol, 'Warranty Expires' => $item->warranty_expires, 'Warranty Months' => $item->warranty_months])
65+
);
6066
}
6167

6268
// Expiring licenses
6369
$licenses = License::getExpiringLicenses($alert_interval);
6470
if ($licenses->count() > 0) {
6571
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
6672
Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $alert_interval));
73+
74+
$this->table(
75+
['ID', 'Name', 'Expires', 'Termination Date'],
76+
$licenses->map(fn($item) => ['ID' => $item->name, 'Name' => $item->name, 'Expires' => $item->expiration_date, 'Termination Date' => $item->termination_date])
77+
);
6778
}
79+
80+
81+
6882
} else {
6983
if ($settings->alert_email == '') {
7084
$this->error('Could not send email. No alert email configured in settings');

app/Models/Asset.php

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ public function declinedCheckout(User $declinedBy, $signature)
161161
'eol_explicit',
162162
'last_audit_date',
163163
'next_audit_date',
164-
'asset_eol_date',
165164
'last_checkin',
166165
'last_checkout',
167166
];
@@ -826,19 +825,24 @@ public function model()
826825
*/
827826
public static function getExpiringWarrantee($days = 30)
828827
{
829-
$days = (is_null($days)) ? 30 : $days;
830828

831-
return self::where('archived', '=', '0') // this can stay for right now, as `archived` defaults to 0 at the db level, but should probably be replaced with assetstatus->archived?
832-
->whereNotNull('warranty_months')
833-
->whereNotNull('purchase_date')
834-
->whereNull('deleted_at')
829+
return self::where('archived', '=', '0')
835830
->NotArchived()
836-
->whereRaw(
837-
'DATE_ADD(`purchase_date`, INTERVAL `warranty_months` MONTH) <= DATE_ADD(NOW(), INTERVAL '
838-
. $days
839-
. ' DAY) AND DATE_ADD(`purchase_date`, INTERVAL `warranty_months` MONTH) > NOW()'
840-
)
841-
->orderByRaw('DATE_ADD(`purchase_date`,INTERVAL `warranty_months` MONTH)')
831+
->whereNull('deleted_at')
832+
833+
// Check for manual asset EOL first
834+
->where(function ($query) use ($days) {
835+
$query->whereNotNull('asset_eol_date')
836+
->whereBetween('asset_eol_date', [Carbon::now(), Carbon::now()->addDays($days)]);
837+
})
838+
// Otherwise use the warranty months + purchase date + threshold
839+
->orWhere(function ($query) use ($days) {
840+
$query->whereNotNull('purchase_date')
841+
->whereNotNull('warranty_months')
842+
->whereDate('purchase_date', '<=', Carbon::now()->addMonths('assets.warranty_months')->addDays($days));
843+
})
844+
->orderBy('asset_eol_date', 'ASC')
845+
->orderBy('purchase_date', 'ASC')
842846
->get();
843847
}
844848

app/Models/License.php

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -708,24 +708,45 @@ public function freeSeats()
708708
}
709709

710710
/**
711-
* Returns expiring licenses
711+
* Returns expiring licenses.
712712
*
713-
* @todo should refactor. I don't like get() in model methods
713+
* This checks if:
714+
*
715+
* 1) The license has not been deleted
716+
* 2) The expiration date is between now and the number of days specified
717+
* 3) There is an expiration date set and the termination date has not passed
718+
* 4) The license termination date is null or has not passed
714719
*
715720
* @author A. Gianotto <[email protected]>
716721
* @since [v1.0]
717722
* @return \Illuminate\Database\Eloquent\Relations\Relation
723+
* @see \App\Console\Commands\SendExpiringLicenseNotifications
718724
*/
719725
public static function getExpiringLicenses($days = 60)
720726
{
721-
$days = (is_null($days)) ? 60 : $days;
722727

723-
return self::whereNotNull('expiration_date')
724-
->whereNull('deleted_at')
725-
->whereRaw('DATE_SUB(`expiration_date`,INTERVAL '.$days.' DAY) <= DATE(NOW()) ')
726-
->where('expiration_date', '>', date('Y-m-d'))
727-
->where('termination_date', '>', date('Y-m-d'))
728+
return self::whereNull('deleted_at')
729+
730+
// The termination date is null or within range
731+
->where(function ($query) use ($days) {
732+
$query->whereNull('termination_date')
733+
->orWhereBetween('termination_date', [Carbon::now(), Carbon::now()->addDays($days)]);
734+
})
735+
->where(function ($query) use ($days) {
736+
$query->whereNotNull('expiration_date')
737+
// Handle expired licenses without termination dates
738+
->where(function ($query) use ($days) {
739+
$query->whereNull('termination_date')
740+
->whereBetween('expiration_date', [Carbon::now(), Carbon::now()->addDays($days)]);
741+
})
742+
743+
// Handle expired licenses with termination dates in the future
744+
->orWhere(function ($query) use ($days) {
745+
$query->whereBetween('termination_date', [Carbon::now(), Carbon::now()->addDays($days)]);
746+
});
747+
})
728748
->orderBy('expiration_date', 'ASC')
749+
->orderBy('termination_date', 'ASC')
729750
->get();
730751
}
731752

resources/lang/en-US/mail.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
'Expected_Checkin_Date' => 'An asset checked out to you is due to be checked back in on :date',
2222
'Expected_Checkin_Notification' => 'Reminder: :name checkin deadline approaching',
2323
'Expected_Checkin_Report' => 'Expected asset checkin report',
24-
'Expiring_Assets_Report' => 'Expiring Assets Report.',
25-
'Expiring_Licenses_Report' => 'Expiring Licenses Report.',
24+
'Expiring_Assets_Report' => 'Expiring Assets Report',
25+
'Expiring_Licenses_Report' => 'Expiring Licenses Report',
2626
'Item_Request_Canceled' => 'Item Request Canceled',
2727
'Item_Requested' => 'Item Requested',
2828
'License_Checkin_Notification' => 'License checked in',
@@ -42,8 +42,9 @@
4242
'asset_name' => 'Asset Name',
4343
'asset_requested' => 'Asset requested',
4444
'asset_tag' => 'Asset Tag',
45-
'assets_warrantee_alert' => 'There is :count asset with a warranty expiring in the next :threshold days.|There are :count assets with warranties expiring in the next :threshold days.',
45+
'assets_warrantee_alert' => 'There is :count asset with an expiring warranty or that are reaching their end of life in the next :threshold days.|There are :count assets with expiring warranties or that are reaching their end of life in the next :threshold days.',
4646
'assigned_to' => 'Assigned To',
47+
'eol' => 'EOL',
4748
'best_regards' => 'Best regards,',
4849
'canceled' => 'Canceled',
4950
'checkin_date' => 'Checkin Date',
@@ -68,7 +69,7 @@
6869
'inventory_report' => 'Inventory Report',
6970
'item' => 'Item',
7071
'item_checked_reminder' => 'This is a reminder that you currently have :count items checked out to you that you have not accepted or declined. Please click the link below to confirm your decision.',
71-
'license_expiring_alert' => 'There is :count license expiring in the next :threshold days.|There are :count licenses expiring in the next :threshold days.',
72+
'license_expiring_alert' => 'There is :count license expiring or terminating in the next :threshold days.|There are :count licenses expiring or terminating in the next :threshold days.',
7273
'link_to_update_password' => 'Please click on the following link to update your :web password:',
7374
'login' => 'Login',
7475
'login_first_admin' => 'Login to your new Snipe-IT installation using the credentials below:',
Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,48 @@
11
@component('mail::message')
22
{{ trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count'=>$assets->count(), 'threshold' => $threshold]) }}
3-
@component('mail::table')
43

4+
<style>
5+
6+
th, td {
7+
vertical-align: top;
8+
}
9+
hr {
10+
display: block;
11+
height: 1px;
12+
border: 0;
13+
border-top: 1px solid #ccc;
14+
margin: 1em 0;
15+
padding: 0;
16+
}
17+
</style>
18+
<x-mail::table>
19+
20+
| | | |
21+
| ------------- | ------------- | ------------- |
522
@foreach ($assets as $asset)
623
@php
7-
$expires = Helper::getFormattedDateObject($asset->present()->warranty_expires, 'date');
8-
$diff = round(abs(strtotime($asset->present()->warranty_expires) - strtotime(date('Y-m-d')))/86400);
9-
$icon = ($diff <= ($threshold / 2)) ? '🚨' : (($diff <= $threshold) ? '⚠️' : ' ');
24+
$warranty_expires = \App\Helpers\Helper::getFormattedDateObject($asset->present()->warranty_expires, 'date');
25+
$eol_date = \App\Helpers\Helper::getFormattedDateObject($asset->asset_eol_date, 'date');
26+
$warranty_diff = ($asset->present()->warranty_expires) ? round(\Carbon\Carbon::now()->diffInDays(\Carbon\Carbon::parse($warranty_expires['date']), false), 1) : '';
27+
$eol_diff = round(\Carbon\Carbon::now()->diffInDays(\Carbon\Carbon::parse($asset->asset_eol_date), false), 1);
28+
$icon = ($warranty_diff <= $threshold && $warranty_diff >= 0) ? '⚠️' : (($eol_diff <= $threshold && $eol_diff >= 0) ? '🚨' : 'ℹ️');
1029
@endphp
11-
@component('mail::table')
12-
| | | |
13-
| ------------- | ------------- | ------------- |
1430
| {{ $icon }} **{{ trans('mail.name') }}** | <a href="{{ route('hardware.show', $asset->id) }}">{{ $asset->display_name }}</a> <br><small>{{trans('mail.serial').': '.$asset->serial}}</small> |
15-
| **{{ trans('mail.expires') }}** | {{ !is_null($expires) ? $expires['formatted'] : '' }} (<strong>{{ $diff }} {{ trans('mail.Days') }}</strong>) |
31+
@if ($warranty_expires)
32+
| **{{ trans('mail.expires') }}** | {{ !is_null($warranty_expires) ? $warranty_expires['formatted'] : '' }} (<strong>{{ $warranty_diff }} {{ trans('mail.Days') }}</strong>) |
33+
@endif
34+
@if ($eol_date)
35+
| **{{ trans('mail.eol') }}** | {{ !is_null($eol_date) ? $eol_date['formatted'] : '' }} (<strong>{{ $eol_diff }} {{ trans('mail.Days') }}</strong>) |
36+
@endif
1637
@if ($asset->supplier)
1738
| **{{ trans('mail.supplier') }}** | {{ ($asset->supplier ? e($asset->supplier->name) : '') }} |
1839
@endif
1940
@if ($asset->assignedTo)
2041
| **{{ trans('mail.assigned_to') }}** | {{ e($asset->assignedTo->present()->display_name) }} |
2142
@endif
22-
@endcomponent
43+
| <hr> | <hr> |
2344
@endforeach
45+
</x-mail::table>
46+
2447
@endcomponent
25-
@endcomponent
48+

tests/Feature/Notifications/Email/ExpiringAlertsNotificationTest.php

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use App\Models\Asset;
99
use App\Models\License;
1010
use App\Models\Setting;
11-
use App\Models\User;
1211
use Illuminate\Support\Facades\Mail;
1312
use Tests\TestCase;
1413

@@ -17,49 +16,62 @@ class ExpiringAlertsNotificationTest extends TestCase
1716
{
1817
public function testExpiringAssetsEmailNotification()
1918
{
20-
$this->markIncompleteIfSqlite();
2119
Mail::fake();
2220

2321
$this->settings->enableAlertEmail('[email protected]');
2422
$this->settings->setAlertInterval(30);
2523

2624
$alert_email = Setting::first()->alert_email;
2725

28-
$expiringAsset = Asset::factory()->create([
29-
'purchase_date' => now()->subDays(350)->format('Y-m-d'),
26+
$expiringWarrantyAsset = Asset::factory()->create([
27+
'purchase_date' => now()->subDays(356)->format('Y-m-d'),
3028
'warranty_months' => 12,
3129
'archived' => 0,
32-
'deleted_at' => null,
3330
]);
3431

35-
$expiredAsset = Asset::factory()->create([
36-
'purchase_date' => now()->subDays(370)->format('Y-m-d'),
32+
33+
$alreadyExpiredAsset = Asset::factory()->create([
34+
'purchase_date' => now()->subDays(396)->format('Y-m-d'),
3735
'warranty_months' => 12,
3836
'archived' => 0,
39-
'deleted_at' => null,
4037
]);
4138

39+
// Asset has a manually entered EOL date that's coming up
40+
$expiringEOLAsset = Asset::factory()->create([
41+
'archived' => 0,
42+
]);
43+
44+
// We have to set this here because of the configure() method in the Asset factory :(
45+
$expiringEOLAsset->asset_eol_date = now()->addDays(5)->format('Y-m-d');
46+
$expiringEOLAsset->save();
47+
4248
$notExpiringAsset = Asset::factory()->create([
43-
'purchase_date' => now()->subDays(330)->format('Y-m-d'),
49+
'purchase_date' => now()->addDays(330)->format('Y-m-d'),
4450
'warranty_months' => 12,
4551
'archived' => 0,
46-
'deleted_at' => null,
4752
]);
53+
// We have to set this here because of the configure() method in the Asset factory :(
54+
$notExpiringAsset->asset_eol_date = null;
55+
$expiringEOLAsset->save();
56+
4857

4958
$this->artisan('snipeit:expiring-alerts')->assertExitCode(0);
5059

51-
Mail::assertSent(ExpiringAssetsMail::class, function($mail) use ($alert_email, $expiringAsset) {
52-
return $mail->hasTo($alert_email) && $mail->assets->contains($expiringAsset);
60+
Mail::assertSent(ExpiringAssetsMail::class, function($mail) use ($alert_email, $expiringWarrantyAsset) {
61+
return $mail->hasTo($alert_email) && $mail->assets->contains($expiringWarrantyAsset);
62+
});
63+
64+
Mail::assertSent(ExpiringAssetsMail::class, function($mail) use ($alert_email, $expiringEOLAsset) {
65+
return $mail->hasTo($alert_email) && $mail->assets->contains($expiringEOLAsset);
5366
});
5467

55-
Mail::assertNotSent(ExpiringAssetsMail::class, function($mail) use ($expiredAsset, $notExpiringAsset) {
56-
return $mail->assets->contains($expiredAsset) || $mail->assets->contains($notExpiringAsset);
68+
Mail::assertNotSent(ExpiringAssetsMail::class, function($mail) use ($alert_email, $notExpiringAsset, $alreadyExpiredAsset) {
69+
return $mail->assets->contains($alert_email) || ($mail->assets->contains($alreadyExpiredAsset) && ($mail->assets->contains($notExpiringAsset)));
5770
});
5871
}
5972

6073
public function testExpiringLicensesEmailNotification()
6174
{
62-
$this->markIncompleteIfSqlite();
6375
Mail::fake();
6476
$this->settings->enableAlertEmail('[email protected]');
6577
$this->settings->setAlertInterval(60);
@@ -69,6 +81,7 @@ public function testExpiringLicensesEmailNotification()
6981
$expiringLicense = License::factory()->create([
7082
'expiration_date' => now()->addDays(30)->format('Y-m-d'),
7183
'deleted_at' => null,
84+
'termination_date' => null,
7285
]);
7386

7487
$expiredLicense = License::factory()->create([
@@ -80,20 +93,43 @@ public function testExpiringLicensesEmailNotification()
8093
'deleted_at' => null,
8194
]);
8295

96+
$expiringButTerminatedLicense = License::factory()->create([
97+
'termination_date' => now()->subDays(10)->format('Y-m-d'),
98+
'expiration_date' => now()->subDays(10)->format('Y-m-d'),
99+
'deleted_at' => null,
100+
]);
101+
102+
$deletedExpiringLicense = License::factory()->create([
103+
'expiration_date' => now()->addDays(30)->format('Y-m-d'),
104+
'deleted_at' => now()->subDays(10)->format('Y-m-d'),
105+
]);
106+
83107
$this->artisan('snipeit:expiring-alerts')->assertExitCode(0);
84108

85109
Mail::assertSent(ExpiringLicenseMail::class, function($mail) use ($alert_email, $expiringLicense) {
86110
return $mail->hasTo($alert_email) && $mail->licenses->contains($expiringLicense);
87111
});
88112

89-
Mail::assertNotSent(ExpiringLicenseMail::class, function($mail) use ($expiredLicense, $notExpiringLicense) {
90-
return $mail->licenses->contains($expiredLicense) || $mail->licenses->contains($notExpiringLicense);
113+
Mail::assertNotSent(ExpiringLicenseMail::class, function($mail) use ($alert_email, $expiredLicense) {
114+
return $mail->hasTo($alert_email) && $mail->licenses->contains($expiredLicense);
115+
});
116+
117+
Mail::assertNotSent(ExpiringLicenseMail::class, function($mail) use ($alert_email, $notExpiringLicense) {
118+
return $mail->licenses->contains($alert_email) || $mail->licenses->contains($notExpiringLicense);
119+
});
120+
121+
Mail::assertNotSent(ExpiringLicenseMail::class, function($mail) use ($alert_email, $expiringButTerminatedLicense) {
122+
return $mail->licenses->contains($alert_email) || $mail->licenses->contains($expiringButTerminatedLicense);
91123
});
124+
125+
Mail::assertNotSent(ExpiringLicenseMail::class, function($mail) use ($alert_email, $deletedExpiringLicense) {
126+
return $mail->licenses->contains($alert_email) || $mail->licenses->contains($deletedExpiringLicense);
127+
});
128+
92129
}
93130

94131
public function testAuditWarningThresholdEmailNotification()
95132
{
96-
$this->markIncompleteIfSqlite();
97133
Mail::fake();
98134
$this->settings->enableAlertEmail('[email protected]');
99135
$this->settings->setAuditWarningDays(15);

0 commit comments

Comments
 (0)