# Payment and pass administration

This document defines the administrative integrity rules for editing passes and
payments and for recalling a payment.

## Administrative edits

The payment and pass edit forms are `POST` operations with CSRF tokens scoped
to the edited record. The models expose dedicated update methods and copy only
fields presented by the form:

- a pass edit may change remaining entries, validity dates, comment, and active
  state;
- a payment edit may change payment type, amount, comment, and active state;
- an unlinked payment's `receipt_item_key` and configured `receipt_name` are
  captured at initial entry and cannot change after the payment is first saved
  as paid;
- locked historical payment types cannot be replaced, and an online payment's
  confirmed amount remains read-only;
- identifiers such as `id_user`, `id_payment`, and target relationship IDs are
  never mass-assigned from request data.

Pass ownership changes remain a separate operation. They use their own CSRF
scope and `Pass::transferToAccountUser()`, which accepts only another user from
the same account.

The payment edit page shows the read-only `Wpłatę przyjął` information below the
payment date. It resolves the current first and last name from the user referenced
by `payment.id_instructor`, as the accepted-payments report does. Missing users,
missing references or empty names display `Brak danych`. This also applies to
historical payments; no names are inferred or backfilled. The display requires no
migration or configuration changes. Verify a payment with a recorded recipient
and one without recipient data; the recipient must not become an editable field.

For a classified **Inny zakup**, the same page also shows the immutable category
label (**Getry**, **Odzież** or **Inna opłata**) and the `payment.receipt_name`
snapshot used for its receipt. These values are read-only and continue to describe
the original purchase after later dictionary changes.

## Actual manual receipt date and time

When the Fakturownia integration is enabled, newly paid cash and traditional bank
payments use the same central `Payment::save()` receipt event as Przelewy24.
For linked payments, receipt creation follows successful purchase assignment;
explicitly classified unlinked sales can create their receipt without assignment.
Fakturownia controls automatic fiscalization and email delivery through its own
settings. Rollschool creates the document and does not send separate fiscalization
or delivery requests. See [Fakturownia receipts](fakturownia-receipts.md) for the
dictionary, exclusions, recipient rules and retry process.
P24 confirmation displays an e-receipt notice whenever any source is not
explicitly excluded in the receipt dictionary. Confirmation covers that purchase;
there is no separate consent field or stored consent record.

On DEV, P24 and Fakturownia use `MAIL_TO`, the same configured recipient as
application email. This address is required in the environment's ignored
`application/config/config.php` and is never read from database settings or
replaced by a customer's or administrator's address. See
[environment-specific mail and SMS configuration](test-email-routing.md) for
configuration templates, deployment order, and database-copy safeguards.
P24 registration validates
it before sending the API request; missing configuration produces a specific
message naming the setting. Production uses the paying account or voucher buyer.

The shared linked and unlinked manual-payment forms show
`Data i godzina otrzymania wpłaty`, defaulting to the current date and time.
Cash and traditional bank payments record the actual nonfuture timestamp in
`payment.received_datetime` independently of Fakturownia. It also supplies the
manual payment time used by reports. Existing callers using the former cash
date input remain supported. Invalid or future dates return a validation alert
without creating a payment. The payer is captured as `payment.id_user_payer`;
later assignment to a beneficiary does not move points to another account.

RollPoints does not change the existing payment-edit form. Editing amount,
payment type, comment or visibility has no effect on points. The original receipt
timestamp and purchase snapshot remain frozen; the form has no RollPoints fields
or additional reason requirement. Partial refunds are not registered.
Only explicit payment recall removes the related RollPoints and referral bonus,
with reason and actor recorded atomically in the loyalty audit. Restoring
visibility does not restore revoked points, even before deferred reversal
processing. Any justified point correction is a separate audited operation in
participant administration. See [RollPoints](rollpoints.md) for migrations,
request-time settlement, historical exclusions and concurrency guarantees.

## Class order payments: rentals and skate covers

`Payment.id_skate_cover_order` links checkout type `g` to a purchased pair or pairs
of textile skate covers. Rentals retain `id_rental_order` and type `r`. Both use
the shared `ClassOrder` payment lifecycle, while each order and payment remain
independent on the same class registration. New skate-cover payments use the
order's stored `quantity × unit_price`; neither the manual entry form nor later
payment administration can change that amount. Price settings affect new orders.

Online checkout locks the order before creating its payment and freezes the
receipt source, including the pair count. Manual acceptance and verified P24
callbacks take the same order-first lock. A manual winner cannot be overwritten
by a delayed online callback. Money actually received after cancellation, an
order change, or another payment is retained and marks the order
`settlement_required`; any additional payment ID is recorded in its settlement
note for office handling. A repeated callback does not create another payment
or reactivate a recalled payment. Unpaid attempts are deactivated after payment.
When multiple active paid rows exist, the order cannot be closed through its
single-payment refund action. Refund and explicitly recall the extra payment
first. If the original payment remains complete and the order has not been
cancelled, recall restores its approved state. Otherwise every remaining
received payment stays visible as requiring settlement, including when the
original payment was recalled before the extra one.

Getry receipts use the canonical `skate_covers` dictionary entry shared with
unlinked Getry payments, with the actual pair count for an order. The migration
converts historical dictionary, payment and receipt-source keys to the canonical
name. There is no runtime alias; prepared and issued invoice payloads stay unchanged. Purchased covers earn RollPoints under type `g`, using
the same real-money rules as existing other purchases, without class prepayment
requirements or referral rewards. Deferred processing requires the order's
payment assignment; duplicate money does not create a second points award.
Instructor reports show a separate skate-cover count and amount. Payment recall
clears the linked cover order and invokes the existing explicit loyalty reversal.

See [class orders](class-orders.md) for the migration and full order lifecycle.

## Recording an unlinked payment

The existing unlinked-payment entry is available to active instructors and
administrators. `Payment::recordUnlinkedPayment()` checks the cashier's current
permissions, active participant, payment type, amount, actual received date/time
and comment before saving. Instructors retain cash access; administrators retain
the broader allowed manual payment types. Request fields cannot grant additional
permissions or select a different cashier.

For a positive cash or traditional bank-transfer payment, the cashier must select
**Getry**, **Odzież** or **Inna opłata**, identified by the canonical dictionary keys
`skate_covers`, `unlinked_clothing` and `unlinked_other`. Administrators edit
each category's receipt description and quantity unit in the existing
**Konfiguracja paragonów** dictionary. The initial descriptions are **Getry**,
**Odzież** and **Opłata Rollschool**; they are migration defaults, not hardcoded
fallbacks. The three categories always have **Wystawiaj paragon**, and dictionary
validation rejects other policies for them. VAT remains ZW and quantity is one.

The entry form submits `receipt_item_key` and displays its configured receipt
description. The model rejects missing/arbitrary/non-unlinked keys and missing,
inactive or invalid dictionary entries before recording the payment, also when
integration is disabled. It stores the key in `payment.receipt_item_key` and the
configured description in `payment.receipt_name`. The internal comment never
supplies a receipt description. The form shows the saved account email, or the
parent's account email for a child. There is no separate recipient input in this
entry flow. With the receipt integration enabled, the recipient and optional NIP
are validated before the payment is saved. DEV still uses configured `MAIL_TO`
for delivery. Validation errors preserve the amount, type, position, date/time,
comment and NIP for correction.

Before submission, the shared confirmation modal **Sprawdź dane wpłaty** warns
that the system will immediately issue and send the fiscal receipt and asks the
cashier to verify amount, position, email and any NIP. Its confirmation is
**Potwierdź i zaksięguj wpłatę**, with **Wróć i popraw dane** for correction.
The warning applies to positive cash/bank sales with integration enabled. When
integration is disabled, it explicitly says the payment will be saved without
automatic receipt creation or delivery. Donation, technical and zero-value
entries hide and disable the position/email section and use ordinary payment
confirmation; they do not create a receipt.

After the paid payment is saved, the central receipt event freezes the selected
description and unit, full gross amount, recipient, optional NIP and cashier in a ready
receipt snapshot. It queues processing for the end of the same PHP request,
without a second administrator approval. If `Payment::save()` owns its
transaction, the payment commits before the receipt event; a capture/provider
failure does not undo the payment. When the caller owns a transaction, local
payment and receipt writes remain within it, and rollback or an open transaction
prevents provider calls. See [receipt event boundaries](fakturownia-receipts.md#one-payment-event).

The fully automatic production flow requires Fakturownia's automatic API receipt
fiscalization and automatic e-receipt email delivery to be enabled and verified
with the provider's printer setup. Successful document creation alone does not
confirm either step. This change does not alter provider settings or local
integration configuration.

Integration-off entries still require and store a sale position, but do not
capture or automatically issue a receipt. Enabling integration later or editing
an already-paid payment does not backfill it. Historical payments with a NULL
key retain the administrator preparation/recovery route; no key is inferred from
an old description. The recovery form uses the same category keys and current
configured descriptions and units. Prepared/issued `invoice_json` snapshots
retain their original descriptions and units despite later dictionary changes.
The receipt-list alert continues to identify older unfinished payments and failed
attempts.

## Payment recall

Recalling a payment clears every supported target relationship and deactivates
the payment in one database transaction. Every statement is checked. If any
update or the final payment save fails, the transaction is rolled back and the
payment remains in its previous state. Existing course-attendance locks are
checked before the transaction begins.

## Data and deployment impact

`20260915120000_add_unlinked_payment_receipt_name.sql` adds nullable
`payment.receipt_name VARCHAR(100)` without backfilling existing rows. It has
already run locally and remains unchanged.
`20260915130000_configure_unlinked_receipt_items.sql` adds nullable
`payment.receipt_item_key` and seeds the three dictionary rows with their initial
descriptions, unit `szt.` and policy `include`. Replay does not overwrite
administrator edits or infer keys for historical payments. Apply both migrations
in order through the standard backed-up runner before serving the new code,
first on test-next and then on production. Verify all three descriptions and units
are editable in the dictionary and displayed by the payment and recovery forms.
`20260915230000_add_skate_covers_receipt_key.sql` renames the existing Getry
category and migrates `payment.receipt_item_key` and receipt-source `item_key`
values to `skate_covers`. It preserves the dictionary ID and configuration,
payment amounts and audit fields, source quantities and prices, and all prepared
or issued `invoice_json` payloads. There is no old-key row or runtime alias.
Verify both Getry entry paths and migrated historical sources before promotion.
The migration is repeatable and rejects conflicting dictionary rows or invalid
legacy sources before updating data.

No external file operations or configuration edits are required. Apply the data
migration before serving the new code, with old-key writers suspended. A rollback
to code expecting the old category requires a separately reviewed inverse data
conversion or a compatible runtime. Code without cover-order support also cannot
process those orders and callbacks. Prepared/issued invoice payloads must remain
unchanged. See [class orders](class-orders.md#migration-and-deployment).

The individual-lesson expected amount snapshot requires
`20260912120000_add_solo_expected_amount_at_payment.sql`; see
[individual lessons](individual-lessons.md#historical-amount-at-payment).
Apply it with the standard backup and migration runner before using the new
code, first on test-next and then on production. No configuration or file moves
are required. Rollback may restore the previous code while retaining the nullable
column; do not remove it or reinterpret the existing paid-amount column.
Verify a manual 150 PLN payment for a lesson due at 170 PLN, an exact payment,
a voucher settlement and a historical payment with a NULL expected amount.
Mail and SMS configuration requirements are documented in
[environment-specific mail and SMS configuration](test-email-routing.md).
RollPoints additionally requires its additive migrations described in
[RollPoints deployment](rollpoints.md#deployment). It has no scheduled point
processing or expiry task. Historical
paid records are not backfilled with invented receipt dates or point grants.
Future individual-lesson settlements may use `payment.payment_type = 7` for a
historical paper voucher without codes. The value uses the existing integer
column and therefore needs no migration. New gift-voucher codes redeemed by an
instructor continue to use the existing technical payment type together with
`payment.promo_type` and `payment.promo_code`.

The accepted-payments report resolves a related gift voucher from either the
voucher purchase relation (`payment.id_gift_voucher`) or the redeemed discount
code (`promo_code.id_gift_voucher`). Its HTML row links to the voucher details,
and its XLSX export contains the public voucher number and URL. Payments settled with a
gift-voucher code use one model-defined comment format; for older payments with
an empty or unrelated comment, the report derives and appends that reference
without modifying historical data.

The HTML accepted-payments report exposes the complete set of active, paid
payments through SQL pagination, 50 records per page. It no longer truncates
the display to the latest 500 entries or loads the entire report for one page.
Ordering is payment time descending, then payment ID descending to make equal
timestamps stable. Invalid page values fall back to page 1; out-of-range values
are clamped to the last page. The page shows the total count and the shared
first/previous/next/last pagination controls. The XLSX export continues to use
the full report regardless of the selected HTML page. No migration is required
for pagination. Verify multiple pages, equal timestamps, an empty report,
excluded inactive/unpaid records, and an export with more than 500 payments.

In the HTML report, the individual-lesson icon opens the existing lesson form
for the exact `payment.id_solo_event`. The participant route parameter comes
from `solo_event.id_user`, including `0` for a guest, rather than the paying
account. Administrators can open lessons owned by any instructor through the
existing authorization rule. Each of the two bundle icons opens its respective
pass form, resolved from `pass_bundle.id_pass1` and `id_pass2`. These relations
are loaded with the report query, without per-row database reads. Missing lesson
records or missing bundle pass IDs retain a labelled icon without a link.
The XLSX export and all stored payment relationships remain unchanged.

The HTML report includes a single `Legenda ikon` covering receipt statuses,
lesson, course, pass, bundle, balance, section, rental and gift-voucher
references and unlinked payments. Editing and export actions are not legend entries. The
bundle entry explains that each ticket icon opens a separate pass.
The balance-settlement icon opens the same payment edit page as the pencil
icon. The pencil has an accessible label without a hover tooltip.

The changes constrain future administrative requests and make failed payment
assignment, recall and exceptional code redemption atomic. No public FAQ update
is required because these operations and their guidance are staff-only.

## Verification

1. Submit both edit forms with valid data and confirm only their documented
   editable fields change.
2. Add `id_user`, `id_payment`, or a target ID to a request and confirm the
   relationship and owner remain unchanged.
3. Submit a missing, invalid, or replayed edit token and confirm no data changes.
4. Transfer a pass to a child or parent on the same account, then attempt a user
   from another account and confirm only the first request succeeds.
5. Force any related-table update during recall to fail and confirm every
   relationship and `payment.is_active` retain their previous values.
6. Confirm that a payment linked to recorded course attendance still cannot be
   recalled.
7. For linked and unlinked manual cash/bank payments, enter a historical receipt
   date/time and verify the payment retains it after purchase assignment and
   appears in the correct reporting period.
8. Verify invalid/future receipt times are rejected, a new form defaults to now,
   and repeated online callbacks preserve the first successful confirmation.
9. For an individual lesson, select
   `Voucher papierowy starszego typu bez indywidualnych kodów` and verify that
   both the payment and historical lesson amount are `0`, even if the submitted
   amount is changed manually. Confirm the type is rejected for another target.
10. Redeem a valid new individual gift-voucher code through the exceptional
    instructor action, including for a participant outside the system. Confirm
    the payment, lesson and `last_used` change together and a second use fails.
11. In the accepted-payments report, open an individual lesson belonging to
    another instructor, including a child or guest participant, and verify the
    exact lesson and participant. Open each bundle icon and verify that the two
    links lead to the corresponding, distinct pass records.
12. Record each of the three unlinked sale positions as an authorized cashier.
    Confirm the pre-save modal, stored key/name, account/parent recipient, optional
    NIP and one ready receipt processed in the same request without admin
    preparation. Rename each description and change its unit in the dictionary;
    new entries and legacy preparation must use the configured values. The
    defaults include **Opłata Rollschool** for **Inna opłata**. The comment must
    remain internal.
13. Reject missing/arbitrary/non-unlinked keys, missing/inactive/invalid dictionary
    entries, invalid amounts/dates/recipients/NIP,
    inactive users/cashiers and unauthorized payment types before creating a
    payment. Confirm correction retains safe form values.
14. Switch to donation, technical or zero-value entries and confirm receipt
    controls are disabled, the modal does not promise a receipt and no provider
    call occurs. With integration disabled, confirm a sale still stores its
    position and uses the integration-off warning, without capture or backfill
    after an ordinary edit.
15. Verify that editing a paid payment cannot replace its key/name or frozen
    receipt payload. Dictionary edits must not rewrite prepared names/units or
    retry payloads, and migration replay must retain edited dictionary values.
    Confirm unlinked categories reject `exclude`/`pending` while linked categories
    keep their policy choices. A legacy NULL-key payment must still be recoverable
    through administrator preparation. Test a caller-owned rollback and provider
    failure without duplicate documents or reversal of a committed payment.
16. Verify new linked and unlinked Getry payments and new receipt sources use
    `skate_covers`, and the runtime rejects the old key. Run the identifier
    migration with a customized old dictionary row, an existing canonical row,
    an empty dictionary and repeated execution. Preserve row IDs, configuration,
    amounts and invoice payloads; change only the dictionary, payment and source
    item keys. Conflicting dictionary rows or malformed legacy source snapshots
    must stop the migration before updates and retain the data for review.

## Receipt register and buyer details

Receipt configuration and the full receipt register are separate admin pages.
The register uses 50 records per page and offers status/date/email/NIP/payment-ID
filters, including uncaptured paid payments. Newly entered unlinked sales use the
cashier's mandatory position choice and automatic receipt flow described above.
The register retains administrator-only preparation for historical or uncaptured
unlinked payments and guarded recovery/retry for failed attempts; prepared or
issued documents cannot be replaced through the position form.
Instructors and administrators may enter an optional NIP while recording manual
payments; the payment and receipt retain its normalized value. Individual guest
email is collected at lesson scheduling. The payment report shows a receipt status
icon linking to the register; Excel adds an issued flag and document URL. These
indicate document creation, not completed fiscalization or email delivery.
See [receipt rules, migration and verification](fakturownia-receipts.md).

Manual payment submissions use POST and scoped CSRF tokens, including the shared
non-rental form and unlinked payment entry. Validation keeps submitted amount,
type, receipt position, received date/time, comment and NIP for correction.
Existing per-lesson/course/rental/voucher scopes
remain in use.

Database-backed payment regression tests require an explicit isolated
`RP_TEST_DATABASE=codex_rollpoints_<name>` prepared with
`scripts/prepare-rollpoints-tests.php`. Legacy temporary fixtures also isolate
the full RollPoints ledger; verify this with `tests/payment_test_isolation_test.php`.
