env('ERROR_NOTIFICATION_THROTTLE_MINUTES', 15) returns the literal string
"15" from production's .env (env() doesn't cast numeric strings), which
Carbon 3.11's addMinutes() rejects with a TypeError. That crash happened
before the throttle-dedup cache write, so it silently masked the *original*
exception on every notify-worthy error, indefinitely (discovered while
investigating a missing workshop-registration email — the real SMTP error
was hidden behind this crash). Cast to (int).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4.6 KiB
id, title, status, date, parent
| id | title | status | date | parent |
|---|---|---|---|---|
| 23 | 23 Fix AdminNotificationService Carbon TypeError Masking Exceptions | Done | 2026-09-28 | null |
Description
Investigating a client report of a missing workshop-registration email led to discovering a real
bug: App\Services\AdminNotificationService::notifyError() throws its own TypeError whenever
it runs, which masks the original exception it was trying to report and prevents the admin
notification from ever being sent. Root cause: config('libra.notifications.throttle_minutes', 15)
resolves to env('ERROR_NOTIFICATION_THROTTLE_MINUTES', 15) (see config/libra.php line 14).
Production's .env has ERROR_NOTIFICATION_THROTTLE_MINUTES=15 set — Laravel's env() helper
does not cast a numeric-looking string to an int, so this returns the string "15", not the
int 15. That string is then passed to now()->addMinutes($throttleMinutes) in
AdminNotificationService::notifyError() (line 32), and Carbon 3.11 (this project's installed
version, per composer.lock) requires addMinutes()'s argument to be int|float — passing a
string throws TypeError: Carbon\Carbon::rawAddUnit(): Argument #3 ($value) must be of type int|float, string given.
This is a live production bug, confirmed via storage/logs/laravel.log on h1
(/home/libra/public_html/storage/logs/laravel.log, entries timestamped 2026-09-27 21:35:26
and 21:40:31) — every exception that reaches Handler->report() and is judged
notify-worthy by AdminNotificationService::shouldNotifyAdmin() (e.g. any
Symfony\Component\Mailer\Exception\TransportExceptionInterface, or a critical
QueryException) currently crashes this method instead of sending the notification, and the
crash happens before Cache::put() on line 32 ever executes, so the throttle-dedup cache key
is never written — meaning this crash also can't self-heal or throttle, it fires on every single
qualifying exception, indefinitely, until fixed.
File to modify: app/Services/AdminNotificationService.php
Current (lines 25-32):
$errorKey = $this->getErrorKey($exception);
$throttleMinutes = config('libra.notifications.throttle_minutes', 15);
if (Cache::has($errorKey)) {
return;
}
Cache::put($errorKey, true, now()->addMinutes($throttleMinutes));
Change to cast the config value to an int before use:
$errorKey = $this->getErrorKey($exception);
$throttleMinutes = (int) config('libra.notifications.throttle_minutes', 15);
if (Cache::has($errorKey)) {
return;
}
Cache::put($errorKey, true, now()->addMinutes($throttleMinutes));
This is the only line that changes. No other method, no other file.
Acceptance Criteria
app/Services/AdminNotificationService.php's$throttleMinutesassignment casts the config value with(int)- A test constructs
AdminNotificationServiceand callsnotifyError()with a throwable that makesshouldNotifyAdmin()return true (e.g. a mock/stub implementingSymfony\Component\Mailer\Exception\TransportExceptionInterface, or simply an instance of a class implementingApp\Contracts\ShouldNotifyAdmin), withconfig(['libra.notifications.throttle_minutes' => '15'])explicitly set as a string in the test (reproducing production's actual condition —env()never returns a string this way in tests, so it must be set explicitly to catch this class of bug) — the call completes without throwing, and a mail notification is sent (assert viaNotification::fake()/Mail::fake()as appropriate for howeverSystemErrorNotificationdelivers) - The same test, run against the old code (temporarily, to confirm the test actually
catches the bug) throws a
TypeError— state in the completion report that this was confirmed, per this task's RED step
QA Verification (2026-09-28)
All 3 acceptance criteria verified directly:
- Confirmed
(int)cast present atapp/Services/AdminNotificationService.php:26. - Ran
tests/Unit/Services/AdminNotificationServiceTest.php's new test (does not throw and still sends notification when throttle_minutes config is a string) — passed. - Temporarily removed the
(int)cast, re-ran the same test — reproduced the exact TypeError (Carbon\Carbon::rawAddUnit(): Argument #3 ($value) must be of type int|float, string given), confirming RED. Restored the(int)cast. Full suite re-run: 17/17 passed.git diffconfirms only the single intended line differs from the pre-fix version.
Sub-tasks
None.