# Group management

## Date range validation

Creating and editing a recurring group uses the same server-side validation in
`Event::validate()`. Both dates must be valid calendar dates in `Y-m-d` format,
and the end date must be equal to or later than the begin date. A rejected range
does not save the group and does not create, deactivate, or refresh any
occurrences.

The management form mirrors this rule in the browser. It updates the minimum end
date whenever the begin date changes and shows a Polish validation message before
submission. Browser validation is only an early usability check; the model rule
remains authoritative for every caller.

For a new group, event and occurrence creation remains transactional. The group
is committed only when at least one occurrence has been generated and saved. An
existing historical group with invalid dates remains accessible to an
administrator through group management so its configuration can be corrected.

## Recurring enrollment availability

The public group page uses
`Event::getRecurringEnrollmentEligibilityForOccurence()` separately for each
participant, including each child, for both recurring enrollment controls:
"Zapisz do grupy na stałe" and "zapisz także na kolejne". Group membership capacity
alone is not enough: every occurrence targeted by recurring enrollment must
have room for that participant. The model uses `event.users_maximum`, active
`user_event` memberships and active `user_event_occurence` bookings. Target
occurrences are active and start after `NOW() - INTERVAL 65 MINUTE`, matching the
existing enrollment window.

If the nearest or any later target occurrence is full, recurring enrollment is
disabled and the page explains the lack of a place for that participant. An
existing active booking for the participant satisfies capacity on that
occurrence, even when it is full. Switching from a one-time booking therefore
remains possible when group capacity and all other target occurrences allow it.
Existing time-conflict and absence-limit restrictions still apply. One-time
signup availability remains independent of capacity on later occurrences.

Recurring enrollment starts with the nearest occurrence; it does not skip full
dates. The UI check is read-only. `Event::EnrollUser()` rechecks capacity under
transactional row locks and saves all recurring bookings or none, so a place
taken after the page loads can still prevent enrollment. The button correction
does not alter existing bookings or historical data and requires no migration
or configuration change.

### Availability verification

1. With a free group membership place and a full nearest occurrence, verify that
   a new participant's recurring button is disabled.
2. With a free nearest occurrence and a full later occurrence, verify that the
   recurring button is disabled while one-time signup remains available.
3. Give a participant an active one-time booking on the full occurrence and
   verify that recurring signup is available when every other capacity check
   passes. Another child without that booking must remain blocked.
4. Verify that free capacity on every occurrence enables recurring signup,
   while a full group, a time conflict or the absence limit still blocks it.

## Recurring enrollment timestamp

`Event::EnrollUser()` sets `user_event.signup_datetime` to the current application
time when creating a membership or reactivating a cancelled membership. Repeating
enrollment for an already active membership preserves its existing timestamp.
Group reporting uses this timestamp when classifying occurrence bookings as
permanent enrollments, excluding occurrences before the current enrollment.

Existing missing timestamps remain unchanged unless the participant is enrolled
again after cancellation. No historical backfill, migration, or configuration
change is required.

Verification: a new membership receives the current timestamp; reactivating a
cancelled membership replaces its old timestamp; repeating an active enrollment
does not change its timestamp, including a historical `NULL` value.

## Quarterly reported-absence limit

`MAX_NO_ABSENCES` defines the reported-absence allowance for one participant in
one recurring group per quarter. The regular recurring-enrollment UI
excludes sports-section absences from this allowance. Public help and account
screens describe the allowance as applying within a quarter, without specifying
reset dates. After using the allowance and withdrawing from a group, the
absence-limit restriction on re-enrollment ends with renewal; available places
and other enrollment checks still apply. Displayed numeric limits come from the
application setting.

The existing implementation stores `user_event.no_reported_absences`. An
administrator renews the allowance through **Reset limitu nieobecności**, which
calls `UserEvent::resetReportedAbsencesForAll()` and zeros that counter for all
memberships, including inactive ones. No automatic quarterly scheduler is
implemented in this repository. The copy must not promise an automatic job.

The Classic pass rule is separate: one reported absence per covered group can
extend a particular pass. Renewing the group's quarterly allowance does not
renew that pass-specific allowance. The purchase pages explain both limits.

Copy verification covers the Classic purchase variants, the upcoming-class
legend, group withdrawal confirmations, recurring-enrollment rejection messages,
the public and parent FAQ, application settings and the administration reset
screen. Updating these descriptions requires no data reset or migration.

### Terms document

The existing uploaded terms PDF is intentionally retained for this website copy
update. No PDF replacement or terms-version change is part of this work.

## Data and deployment impact

This rule does not modify existing group records and requires no database
migration or environment configuration change. Existing invalid ranges must be
corrected manually when an administrator next edits the affected group.

## Season 16 ZWM weekday correction

Migration `20260903141329_move_season_16_zwm_groups_to_tuesday.sql` moves the
four season 16 groups at location 4 (ZWM 10) from Wednesday to Tuesday:

| Event | Level | Instructor | Start time |
| --- | --- | --- | --- |
| 2287 | Children - basic | 5 | 15:00 |
| 2288 | Children - intermediate | 9 | 15:00 |
| 2289 | Children - advanced | 5 | 16:00 |
| 2290 | Children - basic | 9 | 16:00 |

The recurring group records change from weekday 3 to weekday 2. All 158 saved
occurrences move one calendar day earlier, so the first date changes from
2026-09-16 to 2026-09-15 and the last date changes from 2027-06-09 to
2027-06-08. The season date range, times, instructors, locations, active state,
cancellation state and existing website messages remain unchanged. In
particular, weeks that were already excluded from the schedule stay excluded;
the migration does not add extra classes or restore cancelled occurrences.

The migration updates the existing rows instead of generating replacements.
The following relationships therefore keep the same identifiers:

- recurring enrolments and event-specific passes continue to point to the same
  `event` rows;
- participant occurrence rows and attendance continue to point to the same
  `event_occurence` rows;
- one-off payments, pass consumption and rental orders continue to point to the
  same participant occurrence rows.

Consequently, account calendars and instructor attendance lists read the new
Tuesday dates without moving payment ownership or consuming another pass entry.
At the source-data snapshot there were 18 active recurring enrolments, 576
active occurrence bookings, one already assigned one-off payment, two active
event-specific passes and no active rentals in this scope.

The migration is guarded against a different set of groups, a partial weekday
change, a different occurrence count, an already checked or settled occurrence,
or an unrelated active instructor conflict. It is also safe to execute against
the fully migrated state. No file or environment configuration change is
required. The migration does not queue a new notification; customer
communication about the schedule change remains an operational responsibility.

### Verification after migration

1. Confirm events 2287-2290 have `day_of_week = 2` and retain their original
   category, instructor, start time, location and season.
2. Confirm all 158 related occurrences fall on Tuesdays, the active/cancelled
   split remains 128/30 and all 576 participant occurrence rows retain their
   original occurrence IDs.
3. Confirm the one-off payment and both event-specific pass links retain their
   original IDs and owners.
4. Open the public location page and participant accounts to confirm Tuesday
   dates beginning on 2026-09-15.
5. Open the instructors' attendance lists for all four groups and confirm the
   same participants appear under the moved Tuesday occurrence IDs.

## Verification

1. Try to create a group with an end date earlier than its begin date. Confirm
   that the browser blocks submission and shows the date-range message.
2. Submit the same invalid range without browser validation. Confirm that the
   server rejects it, preserves the entered form values, and creates neither the
   group nor occurrences.
3. Try the same range while editing an existing group. Confirm that no group or
   occurrence data changes.
4. Confirm that equal begin and end dates are accepted when the selected weekday
   produces a valid occurrence.
5. Correct an existing invalid group and confirm that its occurrences are
   generated or refreshed according to the corrected range.
