# Payment reconciliation

This document defines how an existing unallocated payment may be assigned to a
group-class attendance entry or a pass.

## Settlement contract

The application treats a non-empty `id_payment` on a payable record as settled.
Balance calculations and pass availability therefore do not repeat the payment
status check. The operation which creates the relationship is responsible for
ensuring that only a confirmed payment can be assigned.

An unallocated payment is available only when all of the following are true:

- it belongs to the same participant as the target record;
- `payment.is_paid = 1` and `payment.is_active = 1`;
- it is not marked as a whole-balance payment;
- none of the target identifiers stored on `payment` is populated;
- no payable table already references it through `id_payment`;
- it is within the existing 731-day reconciliation window.

The reverse-reference check includes group attendance, passes, pass bundles,
individual lessons, course enrollments, section-period payments, and rental
orders. This is required because a confirmed whole-balance payment can be
referenced by several balance items while its own single-target identifiers
remain empty.

The amount is not part of availability. This preserves the established manual
reconciliation behavior; introducing allocation or exact-amount rules requires
a separate business decision.

## Assignment workflow

The staff action supports only group attendance (`z`) and passes (`k`). It is a
`POST` operation protected by a CSRF token scoped to the target type, target ID,
and payment ID. The controller repeats object authorization for group
attendance and delegates the business operation to `Payment`.

`Payment::assignAvailableLoosePaymentToTarget()` locks the target and payment
rows in one transaction, repeats the complete availability check, verifies the
owner and unpaid target state, and writes both sides of the relationship. Each
conditional update must affect exactly one row. Consequently, concurrent
requests cannot use the same payment twice or replace an existing target
payment.

Pass activation remains the post-assignment behavior after the transaction has
successfully linked the payment and pass.

Every successful assignment is recorded in `user_action_log` with the payment
ID and the prefixed target identifier.

## Data and deployment impact

The change does not modify the database schema, configuration, or existing
payment relationships. It does not perform an automated historical-data audit
or correction.

No user-facing help update is required because staff still use the same
"Wykorzystaj niepowiązaną wpłatę" operation and confirmation dialog.

## Verification

1. Start and abandon a whole-balance online payment. Confirm that it is not
   offered as an unallocated payment.
2. Confirm a whole-balance payment and verify that it cannot be reused even
   though its balance items reference it from their `id_payment` fields.
3. Create a paid, active and otherwise unallocated payment. Confirm that it is
   offered only for an unpaid attendance entry or pass owned by the same user.
4. Assign the payment and verify both relationship directions, pass activation
   where applicable, and the `user_action_log` entry.
5. Repeat the request, change a target/payment identifier, send a GET request,
   and submit an invalid CSRF token. Confirm that each attempt is rejected
   without changing payment relationships.
6. Submit concurrent assignments for the same payment or target and confirm
   that exactly one succeeds.
7. Run `tests/loose_payment_security_test.php` and PHP syntax checks for every
   changed PHP file.
