Skip to main content

Announcement System

HAWKI's announcement system lets administrators publish notices to users — policy updates, maintenance windows, or welcome messages — with flexible targeting, scheduling, and display control.

Known technical debt in AnnouncementService

App\Services\Announcements\AnnouncementService currently uses Auth::user() (facade call), Session::put() (session access from a service), and now() (direct time construction) — all of which violate HAWKI's coding standards for services. This is a confirmed deviation tracked in the Technical Debt Register. Do not copy these patterns; follow the standard (constructor-injected CarbonClockInterface, no facades in services, no session access) in any new code you write in this area.


Creating an Announcement

Call AnnouncementService::createAnnouncement() with these parameters:

ParameterTypeDefaultDescription
titlestringrequiredDisplay title of the announcement
viewstringrequiredPath to the Markdown content folder under resources/announcements/ (e.g. terms_update)
typestring'info'Display type; typically 'info' or 'policy'
isForcedboolfalseIf true, inject into session and display immediately regardless of UI state
isGlobalbooltrueIf true, show to all users; if false, restrict to targetUsers
targetUsersarray|nullnullArray of user IDs to target when isGlobal is false
anchorstring|nullnullUI element selector — attach the announcement to a specific element instead of displaying as a modal
startsAtstring|nullnullISO 8601 datetime string; null = active immediately
expiresAtstring|nullnullISO 8601 datetime string; null = never expires

Example:

$service->createAnnouncement(
title: 'Privacy Policy Update',
view: 'privacy_update',
type: 'policy',
isForced: true,
isGlobal: true,
startsAt: '2025-09-01T00:00:00Z',
expiresAt: '2025-09-30T23:59:59Z'
);

The Announcement model is stored in the announcements table. target_users is cast to an array; starts_at and expires_at are cast to datetime.


Markdown Content Files

Announcement content is written as Markdown files. Place one file per supported locale:

resources/announcements/
└── privacy_update/ ← matches the `view` parameter
├── de.md
└── en.md

AnnouncementService::renderAnnouncement($announcement) resolves the correct language file from the current locale and returns the Markdown string for the frontend to render.


Global vs Targeted Announcements

  • Global (is_global = true): displayed to every user.
  • Targeted (is_global = false): the target_users column holds a JSON array of user IDs. AnnouncementService::validateUserAccess($user, $announcement) checks whether the user's ID appears in this list.

The announcement_user pivot table tracks each user's interaction with each announcement.


Forced Display

When is_forced is true and anchor is null, the announcement is injected into the session under the force_announcements key by AnnouncementService::getUserAnnouncements(). The frontend reads this key from the session and displays the announcements immediately, regardless of what the user is currently doing in the UI.

Anchored announcements (anchor is set) are attached to a specific UI element rather than displayed as a modal. Forced anchored announcements are not injected into the session.


Scheduling

starts_at and expires_at define the display window. getActiveAnnouncements() filters to announcements where:

  • starts_at is null or starts_at <= now()
  • expires_at is null or expires_at >= now()

Expired announcements remain in the database for audit purposes. The fetchLatestPolicy() convenience method returns the most recently active announcement of type policy.


Per-User Tracking

The announcement_user pivot table (managed by AnnouncementUser) records:

ColumnTypeMeaning
user_idintThe user
announcement_idintThe announcement
seen_atdatetimeWhen the user first saw the announcement
accepted_atdatetimeWhen the user explicitly accepted (for policy announcements)

These columns are exposed via the users() BelongsToMany relationship on Announcement (with ->withPivot(['seen_at', 'accepted_at'])).

Convenience methods on the User model:

  • User::markAnnouncementAsSeen(int $id) — records seen_at
  • User::markAnnouncementAsAccepted(int $id) — records accepted_at
  • User::unreadAnnouncements() — returns announcements that are active and not yet seen

AnnouncementService wraps these calls with access validation before updating.


Artisan Commands

CommandPurpose
announcement:make {title}Scaffold per-locale Markdown files under resources/announcements/
announcement:publishInteractively persist an announcement to the database

See Artisan Commands for full command reference.


JSON:API Resource

Announcements are exposed via the announcements JSON:API resource at /api/hawki/v1/announcements. The frontend fetches this resource to display announcement banners and policy acceptance dialogs.