# Attendance list access

## Purpose

Group attendance lists use separate permissions for viewing and management. This
allows an instructor to inspect the full participant list before deciding whether
to accept an advertised substitution without allowing that instructor to change
the attendance record or related workflows.

## Permission rules

`EventOccurence::canBeViewedBy()` allows access when at least one of these rules
is satisfied:

- the current user is an administrator;
- the occurrence is active, in the future, and advertised as needing a
  substitute, and the current account has instructor access;
- the current instructor is the occurrence instructor, the current instructor
  of the event, or the accepted substitute.

Previewing an advertised substitution is based on the account's instructor
access level. It does not require the account username to resolve to an active
row in `instructor`. That mapping is still required for assignment and
management rules that need a concrete instructor identifier.

`EventOccurence::canBeManagedBy()` is stricter. It allows management only for an
administrator, the occurrence instructor, the current instructor of the event,
or the accepted substitute. The `needs_substitute` flag alone never grants
management permission.

The event-level instructor check preserves access after an event instructor is
changed, while the occurrence-level instructor check preserves the instructor
snapshot assigned to the individual occurrence.

## Read-only substitute preview

An instructor who can view an occurrence only because `needs_substitute` is set
receives the normal attendance-list data, including participant and contact
details, but the page is rendered in read-only mode. In this mode:

- attendance switches and the instructor comment field are disabled;
- group, substitution, participant, payment, rental-payment, and attendance
  confirmation actions are not rendered;
- participant details, attendance state, contact links, rental information, and
  collapsible list sections remain visible.

After the instructor accepts the substitution, the occurrence records that
instructor as `id_instructor_substitute` and the standard management controls
become available. The acceptance endpoint verifies that the occurrence still
needs a substitute, so an instructor cannot assign themselves to an arbitrary
occurrence by changing the URL. Acceptance is a conditional database update, so
only the first instructor can claim a substitution when multiple requests arrive
at the same time. Advertising, withdrawing, and accepting a substitution are
`POST` operations protected by CSRF tokens scoped to the occurrence and action.
Only an active, future occurrence without an assigned substitute can be
advertised or accepted. Both `NULL` and `0` mean no substitute is assigned.
The list of available substitutions uses the same database eligibility condition
as advertising and acceptance. The conditional updates check the current stored
assignment, so an old form cannot reopen or overwrite an accepted substitution.
An existing request can still be withdrawn after the occurrence becomes
unavailable. Assigning a substitute manually also closes the advertisement.
The substitute selector excludes the original instructor recorded on the
occurrence (`event_occurence.id_instructor`), even if the group's current instructor
has since changed. The model also rejects manual assignment and acceptance by that
original instructor. Manual assignment accepts only active existing instructors,
updates only the assignment and advertisement fields, and permits saving an
unchanged valid assignment. Selecting `Brak zastępstwa` clears the assignment to
`NULL` and closes any advertisement.

The occurrence instructor edit form also offers `Wycofaj zastępstwo` when an
assignment exists. The shared confirmation modal explains that the original
instructor resumes teaching and no new request is advertised. The button uses the
existing occurrence-scoped POST/CSRF and management checks, then removes only the
expected assignment and closes the advertisement. A stale form cannot remove a
different substitute assigned in the meantime. This action returns to current
classes, because a withdrawn substitute may no longer access the occurrence.
It sends no assignment notification. Existing records that name the original
instructor as their own substitute can also be cleared with this button; existing
assignments are not rewritten in bulk. To find another substitute after withdrawal,
an authorized manager must explicitly advertise the occurrence again.

## Server-side enforcement

The UI state is not the security boundary. `ZarzadzanieController` uses
`canViewEventOccurence()` only when opening the list and continues to use
`canManageEventOccurence()` for list mutations such as attendance changes,
comments, confirmation, participant transfers, removals, and additions. Changing
the substitution-request state and recording attendance-linked payments also
require management permission.

Editing an occurrence, including assigning a substitute, changing its time or
location, sending participant notifications, cancelling it, or restoring it,
requires the same management permission. The edit form uses a CSRF token scoped
to the occurrence, and the occurrence identifier from the route must match the
submitted identifier. A manually selected substitute must reference an active
instructor.

Opening group management without selecting an occurrence prefers the nearest
active occurrence. If the group has no current or future occurrence, as with a
group from a completed season, the screen selects its latest active or cancelled
occurrence instead. The selected historical occurrence remains subject to the
same management permission, so this fallback restores archived-group access for
administrators and assigned instructors without exposing unrelated groups.

Group-level management permission is independent of occurrence availability.
An administrator or the instructor currently assigned to the group can open and
inspect a group that has no occurrences. Administrators can additionally reach
group editing so that invalid dates or other configuration can be corrected;
the existing administrator-level restriction on that form remains unchanged.
An unrelated instructor remains blocked. When an occurrence identifier is
explicitly present in the URL, it must belong to the requested group before
occurrence-level access can be used.

## Manual pass settlement

The group setting `event.is_pass_friendly` gates settlement with every pass type:
FLEX (including OPEN), CLASSIC, sports-section (`sek`) and technical (`tec`) passes.
`UserEventOccurence::chargeUser()` checks it before selecting a sports-section
pass. The group editor label explicitly lists these types and states that
disabling the option blocks all pass settlement. Existing payment links remain
unchanged; free classes still receive their technical zero payment before this
check. This wording clarification does not change settlement rules, group
settings or historical data and requires no migration.

Manual settlement is exposed only as a `POST` action protected by a CSRF token
scoped to the occurrence and selected settlement mode. It is incremental:
participants who already have an attendance payment remain unchanged, while the
system retries only active attendance rows that are still unpaid. It does not
undo an existing payment, return a previously consumed pass entry, or replace an
already linked pass.

Administrators may retry settlement in two modes: with payment reminders or
silently. Reminders are sent only for a future occurrence and only to
participants who remain unpaid. Instructors do not receive either manual
settlement action.

Inactive and cancelled occurrences cannot be settled manually in either mode.

Opening a list through substitution preview never grants manual settlement. The
controller repeats the occurrence management, role, confirmed-list, request
method, and CSRF checks instead of relying on whether the forms were rendered.
Every successful manual retry is recorded in `user_action_log` with the
occurrence identifier and settlement mode.

When an active participant booking is added or reactivated after the occurrence
has already completed its first settlement (`is_charge_users_performed`) or its
attendance list has been confirmed (`presence_check`), the enrollment flow
immediately calls `UserEventOccurence::chargeUser()` for that booking only. This
uses an eligible pass when one exists but does not run occurrence-wide unpaid
payment reminders. Manual silent settlement remains available as a fallback for
older unpaid rows and later data corrections.

## Data and deployment impact

Migration `20260909120000_close_assigned_substitution_requests.sql` clears
`needs_substitute` on existing occurrences that already have a nonzero substitute
identifier, including historical occurrences. It preserves instructor assignments,
attendance, payments, and all other occurrence data. The correction is idempotent
and compatible with the previous code; no schema, environment configuration, or
server-file changes are required. The normal deployment runner backs up the
database before applying it, first on test-next and then on production.
Rolling back the code does not reopen the corrected advertisements.

After deployment, this query must return zero:

```sql
SELECT COUNT(*) FROM event_occurence
WHERE needs_substitute = 1
  AND id_instructor_substitute IS NOT NULL
  AND id_instructor_substitute <> 0;
```

## Verification

Verify these scenarios on the test environment:

1. An unrelated instructor can open a future active occurrence with
   `needs_substitute = 1`, see the complete list, expand participant details,
   and use contact links.
2. Repeat the preview with an instructor-level account whose username does not
   resolve to an active `instructor` row; the advertised list must still open in
   read-only mode.
3. The same instructor sees the read-only notice, disabled attendance switches
   and comment field, and no management or payment actions.
4. Direct requests to attendance/comment/confirmation/participant mutation
   endpoints are rejected for that instructor.
5. After accepting the substitution, the instructor can manage the list.
6. The original occurrence instructor and administrators retain full management
   access.
7. An unrelated instructor cannot open an occurrence that is not advertised as
   needing a substitute.
8. A `GET` request and a `POST` request without the correct occurrence-and-mode
   CSRF token cannot trigger manual pass settlement.
9. An instructor cannot run either manual settlement mode, including after list
   confirmation.
10. An administrator can run both modes. A retry leaves existing attendance
   payments unchanged and processes only attendance rows that remain unpaid.
11. Add a participant after the first settlement or after list confirmation and
    confirm that only the new booking is reconciled. When no eligible pass
    exists, the booking remains unpaid and no unpaid-payment reminder is sent.
12. Confirm that `GET` requests and `POST` requests without the action-specific
    CSRF token cannot advertise, withdraw, or accept a substitution.
13. Confirm that an unrelated instructor cannot open or submit an occurrence edit
    form, even after replacing the occurrence identifier in the URL or form body.
14. Confirm that an occurrence edit rejects a mismatched route/body identifier
    and a substitute identifier that does not reference an active instructor.
15. Confirm that a past or inactive occurrence cannot be newly advertised as
    needing a substitute, while an existing stale request can be withdrawn.
16. Accept an advertised occurrence and confirm that it disappears from the
    available list and no longer offers the advertising button. An old advertising
    form must not reopen it, and another acceptance must not change its substitute.
17. On isolated test data, combine an assigned substitute with `needs_substitute = 1`.
    The occurrence must be absent from the available list, unavailable for unrelated
    instructor preview, and impossible to accept. Withdrawal must remain possible.
18. Run the cleanup migration twice on isolated fixtures. Only advertisements with
    assigned substitutes must close; unassigned requests and assignments stay intact.
19. In the instructor edit form, confirm that the original occurrence instructor is
    absent from the selector. A modified form and acceptance request naming that
    instructor must fail. Inactive and missing instructors must also fail manual assignment.
20. Use `Wycofaj zastępstwo`, cancel the confirmation, and verify no change. Confirm
    it and verify that the substitute is removed, the advertisement is closed, and
    the original instructor still manages the occurrence. Repeat for a legacy self-assignment.
21. Open the edit form, change the substitute in another session, and submit the
    old withdrawal. It must report a conflict and preserve the newer assignment.
22. Select `Brak zastępstwa` and save. It must continue to clear the assignment and
    close the request without creating a new advertisement.
23. Open a group from a completed season without adding an occurrence identifier
    to the URL. Confirm that its latest active or cancelled occurrence opens for
    an administrator and its assigned instructor, while an unrelated instructor
    is still rejected.
24. Open a group that has no occurrences. Confirm that an administrator and the
    group's assigned instructor can inspect it, that only the administrator can
    reach group editing, and that an unrelated instructor cannot open it. Also
    confirm that an occurrence identifier from a different group is rejected.
