# Gift vouchers

## Offer and purchase

Gift vouchers are separate from discount codes. The public offer at
`/home/vouchery/` contains group and individual variants for one to five hours.
The unit price is read from the existing `PRICE_1` setting for group lessons and
`PRICE_SOLO_1` for individual lessons. The order stores both the unit price and
the calculated total, so a later price-list change does not alter an existing
order.

The purchase form at `/konto/kup_voucher/` is public and does not require an account.
It collects the lesson type, number of hours, recipient name and the buyer's
email address entered twice. For a signed-in user, the current account email is
used and cannot be edited. A random public access token is returned only in the
order URL; the database stores its SHA-256 hash.

The order can be paid online through Przelewy24 or by a traditional bank
transfer. Traditional-transfer instructions are opened with a POST request and
a scoped CSRF token. Their transfer title uses the `V<id>` order identifier.
Opening the browser return URL does not mark the order as paid.

Voucher purchase and checkout are handled by `KontoController`, consistently
with pass purchases and the existing account payment flow. Guest access is
limited to a specific gift-voucher order by its public access token; all other
account payment types retain their normal signed-in authorization. The checkout
reuses the standard payment-start, transfer-instruction and payment-result views.

## Payment and issuance lifecycle

`gift_voucher.fulfillment_status` has four states:

- `pending`: the order exists and normally waits for payment;
- `processing`: payment is confirmed and delivery artifacts are being prepared;
- `ready`: the PDF exists and its email has been added to the communication queue;
- `failed`: the paid order needs an administrator to retry delivery.

Management views present these states as `oczekuje`, `generowanie`, `gotowy`
and `błąd`. Both the list and detail screen use the same model dictionary.

An online order is issued only after a successful signed Przelewy24 status
notification and provider verification. An administrator can alternatively
open the existing manual-payment flow for a bank transfer or cash payment from
the voucher details screen. The
payment is linked by `payment.id_gift_voucher`; gift-voucher payments are
excluded from the pool of unassigned payments.
Gift-voucher payments are administrative sales and are excluded from instructor
cash-payment reports, including their rows, counts and totals.

Issuance is idempotent. It locks the voucher, verifies a paid and active linked
payment, assigns the issue date and creates exactly one code per purchased hour
only when no linked codes exist. Repeated provider notifications or an
administrator retry do not create duplicate codes or queue another email once
`email_queue_id` has been stored.

Manual and online confirmation serialize on the voucher row. Recording a manual
payment deactivates an unfinished online attempt. If the provider confirmation
and administrator action arrive concurrently, the first confirmed payment wins;
the other path does not create a second paid payment.

The public voucher number has the form `year/month/id`, for example `2026/7/3`.
Its issue date is the payment date and its validity ends exactly one calendar
year later.

## Discount codes

Every generated code:

- stores its source in `promo_code.id_gift_voucher`;
- gives a 100% discount;
- is single-use;
- is valid for the same date range as the voucher;
- targets a single group lesson (`z`) or an individual lesson for exactly one
  participant (`i`) according to the purchased variant;
- includes the voucher number and its position in the internal comment.

Both voucher variants are sold for one participant. A group code settles one
participant's attendance at a group lesson, while an individual code can be
used only for an individual lesson whose `solo_event.participant_count` is `1`.

Codes continue to be redeemed by the normal discount-code field on a lesson
payment. A gift voucher itself is therefore an issued document and order, while
its codes use the existing `PromoCode` settlement mechanism.

Every future payment settled with a code generated for a gift voucher stores a
consistent comment containing the public voucher number and code. The accepted
payments report also derives this description for older payments whose stored
comment predates that rule. Both a voucher purchase payment and a lesson
payment settled with its code link from the report to the related gift-voucher
details; the XLSX export includes the same public number and URL. The technical
`V<id>` order identifier is used only in bank-transfer and Przelewy24 references.
The
`payment.id_gift_voucher` relation remains reserved for the voucher purchase,
while a redeemed code is traced through `promo_code.id_gift_voucher` so that the
payment remains correctly linked to the lesson it settles.

Participant self-service remains the preferred redemption path: the participant
uses one code from their account and a person without an account should register
first. For an individual lesson, the instructor has a separate exceptional
action on the manual-payment screen. It may redeem a linked, active, valid,
unused 100% one-participant individual gift-voucher code even when the lesson
participant is outside the system. The selected lesson must itself have exactly
one participant. The code, zero-value technical payment and lesson assignment
are written atomically; the action is not presented as an ordinary instructor
payment method.

## PDF and email delivery

The tracked background template is
`application/Resources/pdf/gift_voucher_template.pdf`. FPDI/TCPDF imports this
page and overlays flattened, non-editable data using the tracked Ubuntu font
files from `application/Resources/fonts/`. The generated output is stored in
`Files/gift_vouchers/<id>.pdf`; the directory is created automatically and must
remain writable by PHP. `Files/` remains server data and is not tracked by Git.

The PDF contains the lesson type, number of hours, recipient, voucher number,
issue and expiry dates, variant-specific information and one to five readable
codes arranged in up to three columns.

After successful PDF generation, an email is queued through `CommQueue` with
the PDF attached. Its editable body is the active `StaticText` record with code
`mail_gift_voucher` (display name `Email z voucherem`). The email-template editor
shows its sending description separately from the `tags` instructions. It
supports these placeholders:

- `{voucher_number}`;
- `{recipient_name}`;
- `{lesson_type}`;
- `{hours}`;
- `{valid_to}`;
- `{website}`.

The first deployed migration inserts a proposed Polish message. Administrators
can later change its body in the email-template section of the editable-text
panel without a code release. See [Editable texts](editable-texts.md) for the
named-lookup migration, supported-tag contract and usage inventory.

## Administration

Administrators use `/zarzadzanie/vouchery_prezentowe/` to list orders, create an
order, inspect payment and fulfillment state, record a manual payment, open the
generated PDF and verify the linked codes. A failed paid fulfillment can be
retried from the detail screen. State-changing actions use POST and scoped CSRF
tokens.

The management reports page links to the same voucher list and to
`/zarzadzanie/vouchery_prezentowe/xlsx`. The XLSX export contains one row per
active voucher with order, recipient, price, payment, fulfillment, code-usage,
validity, email-queue and last-error fields. Its payment and fulfillment labels
come from the same model dictionaries as the HTML views.

## Schema and deployment

Migration `20260813160000_add_gift_vouchers.sql` creates `gift_voucher`, adds
the gift-voucher relation to `payment` and `promo_code`, and inserts the editable
email template. It is additive and safe to rerun. Composer installs
`setasign/fpdi` and `tecnickcom/tcpdf` from the committed lock file.

No manual template copy is required because the source PDF and fonts are
tracked. The normal writable `Files/` permission is the only file-system
requirement. Test-next must be verified before promoting the same application
SHA to production.

## Verification

1. Verify the public price table for both lesson types and all one-to-five-hour
   variants.
2. Create guest and signed-in orders and verify email confirmation and locking.
3. Confirm that direct GET or invalid-CSRF transfer and payment requests are
   rejected.
4. Complete a Przelewy24 test payment and separately record a manual payment.
5. Verify the voucher number, one-year validity and exact number and type of
   100% codes.
6. Repeat the payment notification or fulfillment retry and verify that codes
   and queued email are not duplicated.
7. Render the generated PDF and inspect every dynamic field, the five-code
   layout, fonts and clipping.
8. Process the queue and verify that the editable message and PDF attachment
   reach the buyer's address.
