Skip to main content

Certifications

Track operational certifications (Hygieniapassi, First Aid, ride operator training, etc.) per employee, with full historical fidelity. The capability covers digitizing the records, surfacing them in the UI, ad-hoc reporting, and automated renewal reminders. Roster-time gating and PDF uploads remain out of scope and will land in later phases.

Roles

RoleWhat they can do
Employee(Phase 1) cannot view their own certifications.
SupervisorRead certifications for employees in their assigned venues.
Manager / AdminRead tenant-wide; create/edit types and records; run reports.

Read access for Supervisors is location-scoped through the same UserScopeService used elsewhere — they can see records only for employees they share at least one venue assignment with.

Certification types

Set up certification types in Settings → Certifications (Manager+). A type has:

  • Name (e.g., "Hygieniapassi", "First Aid")
  • Category (safety / food / first_aid / other)
  • Requires renewal — when checked, you can also set:
  • Default validity (months) — pre-fills the expires-at field when adding a new record. Optional even when renewals are required (some types have a custom cadence).

Categories drive UI grouping and are displayed as badges. They have no business logic attached.

Adding a certification to an employee

  1. Open the employee detail page (Employees → <employee>)
  2. In the Certifications card, click Add certification
  3. Pick the type, set issued at and (optionally) expires at, fill in issued by and notes if relevant
  4. Save

If the type carries a default validity, the expires at field is pre-filled when you set the issue date.

Renewals

The system never modifies an existing record's issued_at or expires_at. A renewal is a new row. The historical record stays in place forever so that audit questions like "was X certified on date Y?" remain answerable.

You can correct the issued by and notes on an existing row via the inline edit affordance. Anything else requires a new record.

Currently-certified status

The system computes status at query time, not as a stored flag:

active → today between issued_at and expires_at (inclusive),
or expires_at is null
expiring_30 → expires_at is within the next 30 calendar days
expired → expires_at is in the past

All date comparisons evaluate against today's calendar date in Europe/Helsinki.

Reports

Manager+ can run an ad-hoc report at Reports → Certifications. Filters:

  • Type — single certification type
  • Status — active / expiring within 30 days / expired
  • Location / Department — narrows by employee assignments

Each (employee × type) pair collapses to its latest record. Older renewal rows do not appear as separate report rows — they're available on the employee detail page if needed.

The Export CSV button downloads the current report. The CSV includes a UTF-8 BOM so Excel reads Finnish characters (ä / ö / å) correctly.

Dashboard widget

Manager+ sees an "Expiring in 30 days" widget on the dashboard that links into the full report. The heading and "View full report" link both deep-link to the report with status=expiring_30 pre-selected.

Renewal reminders

For renewal-required certification types, the system automatically dispatches notifications on two days:

  • 30 days before expiryCertificationExpiringSoon notification fires.
  • The day of expiry itselfCertificationExpired follow-up notification fires.

Recipients are the affected employee plus every Manager and CompanyAdmin in the tenant. Supervisors are not included by default — Managers are typically the ones who book renewal training, so they get the action handle. The reminders are dispatched via push and email per the standard notification channels; clicking through opens the certifications report.

A few details worth knowing:

  • Idempotent. Each (employee × certification × reminder type) pair fires exactly once. Re-running the cron, restarting the worker, or running concurrent workers does not double-send. A small ledger (certification_reminders_sent) backs this guarantee at the database level.
  • Open-ended certs are skipped. A certification with expires_at left blank (the "valid indefinitely" case) never triggers reminders.
  • One-shot types are skipped. A certification type with requires_renewal = false (e.g., onboarding training) never triggers reminders.
  • Renewals reset the loop. When you record a renewal as a new certification row, that new row gets its own ledger entries — no manual cancellation of the old reminders is needed.
  • Cron schedule. The sweep runs daily at 09:00 Europe/Helsinki. If a worker is down on the qualifying day, that one reminder is missed (the dashboard widget and report still surface the cert in time).

Audit log

Every write — certification_type.created, certification_type.updated, employee_certification.created, employee_certification.updated — is recorded via the existing AuditService.

Tenant isolation

Both certification_types and employee_certifications enforce tenant isolation via PostgreSQL FORCE ROW LEVEL SECURITY. Queries from one tenant never return another tenant's rows, even if the application code forgets to filter.