phase2(notifications-stage3): NotificationsTab schema-aligned + prefs API
Закрывает архитектурное расхождение v1.28 — Tab сохранял prefs только локально без API. Backend events не совпадали с handoff'ом. Backend: - PATCH /api/auth/me/notification-preferences под auth:sanctum. - Replace-семантика: незадекларированные events/channels отбрасываются. - userResource расширен: notification_preferences + sound_enabled. - UserFactory с schema-default JSON (Eloquent не перечитывает после INSERT, DB-DEFAULT JSONB виден как null без явного override). - Pest +10: 401 / replace / неизвестные events/channels отбрасываются / 422 без prefs / sound_enabled опционален / bool-cast 1/'1' / replace- семантика (отсутствующие events исчезают). Frontend: - api/auth.ts: типы NotificationChannel/EventKey/Preferences + updateNotificationPreferences helper. AuthUser получил optional поля. - NotificationsTab.vue переписан под schema: 8 событий (new_lead/reminder/low_balance/zero_balance/topup_success/ invoice_paid/new_device_login/marketing) × 3 канала (inapp/push/email, НЕ sms). Sync-init prefs (без onMounted — иначе v-if блокирует рендер и тесты mount-then-find падают). dirty через computed-сравнение с originalPrefs snapshot. save async + success/error alerts. - SettingsView.spec.ts: legacy event-имена → schema-aligned. - Vitest +10: 8 schema events / 3 channels (НЕ sms) / legacy отсутствуют / читает prefs из user / save calls API + alerts / Отменить возвращает. cspell-words: +prefs. PHPStan baseline регенерирован. Pest 315/315 (+10) за 36.73 сек, 1130 assertions. Vitest 349/349 (+10) за 20.42 сек. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ use App\Mail\SuspiciousLoginNotification;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRecoveryCode;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -537,6 +538,58 @@ class AuthController extends Controller
|
||||
'tenant_id' => $user->tenant_id,
|
||||
'totp_enabled' => $user->totp_enabled,
|
||||
'last_login_at' => $user->last_login_at,
|
||||
'notification_preferences' => $user->notification_preferences,
|
||||
'sound_enabled' => $user->sound_enabled,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/auth/me/notification-preferences — сохранить матрицу
|
||||
* 8 событий × 3 каналов (inapp/push/email) + sound_enabled.
|
||||
*
|
||||
* Источник: schema.sql:699 users.notification_preferences JSONB DEFAULT.
|
||||
* Валидация: события ∈ NotificationService::ALL_EVENTS, каналы ∈
|
||||
* {inapp, push, email}. Незадекларированные ключи отбрасываются.
|
||||
*/
|
||||
public function updateNotificationPreferences(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'prefs' => 'required|array',
|
||||
'prefs.*' => 'array',
|
||||
'sound_enabled' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$allEvents = NotificationService::ALL_EVENTS;
|
||||
$allChannels = [
|
||||
NotificationService::CHANNEL_INAPP,
|
||||
NotificationService::CHANNEL_PUSH,
|
||||
NotificationService::CHANNEL_EMAIL,
|
||||
];
|
||||
|
||||
// Очищенная матрица (только known events × known channels).
|
||||
$sanitized = [];
|
||||
foreach ($validated['prefs'] as $event => $channelPrefs) {
|
||||
if (! in_array($event, $allEvents, true)) {
|
||||
continue;
|
||||
}
|
||||
$sanitized[$event] = [];
|
||||
foreach ($allChannels as $channel) {
|
||||
if (isset($channelPrefs[$channel])) {
|
||||
$sanitized[$event][$channel] = (bool) $channelPrefs[$channel];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
$update = ['notification_preferences' => $sanitized];
|
||||
if (array_key_exists('sound_enabled', $validated)) {
|
||||
$update['sound_enabled'] = (bool) $validated['sound_enabled'];
|
||||
}
|
||||
$user->update($update);
|
||||
|
||||
return response()->json([
|
||||
'user' => $this->userResource($user->fresh()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user