CoolCMS API

PriceSnapshotTrait uses trait:short, trait:short

Price list tab of the set edit component (edit only); saves itself through the Api bridge (`savePriceSnapshot`/`deletePriceSnapshot`), independently of saving the set.

A history snapshot can be edited or deleted only when dated today or scheduled in the future (isSnapshotEditable()). The matrix tier x group x currency is edited in a modal (createComponentPriceSnapshotForm()).

The modal content is a renderToString() partial put into the shared presenter ModalWindow (ModalAwareTrait), not a live sub-component. IMPORTANT: adding or removing a tier (multiplier tiers) therefore regenerates the whole modal content (showPriceSnapshotModal()) instead of calling redrawControl() on the form; the ModalWindow and the form are children of the same presenter, so their snippets arrive in one AJAX response.

Table of Contents

Properties

$editingPriceSnapshotTime  : string|null
Snapshot being edited in the modal (`publishedTime`); null when adding.
$snapshotSet  : ProductSet|null
Set with loaded price history, loaded separately from `$editedSet` because the main set form does not load `prices`/`vats` at all (see `EDIT_INCLUDES`). Lazy.

Methods

handleAddPriceSnapshot()  : void
handleDeletePriceSnapshot()  : void
The link is not AJAX (styled `data-grid-confirm`), so after deleting a redirect (PRG) returns to the tab.
handleEditPriceSnapshot()  : void
onPriceSnapshotFormSuccess()  : void
createComponentPriceSnapshotForm()  : Form
Builds the modal form: matrix of tier (`fromAmount`) x user group x currency to price, original price and points.
isTargetAllowed()  : bool
Returns whether the logged-in user may open the given admin target. For visibility (tiles, autocomplete, buttons); it does not throw.
requirePrivilege()  : void
Throws 403 when the logged-in user lacks the given admin privilege.
resolvePageTarget()  : string|null
Returns the RBAC key of the target page: `<presenter>:<action>`, e.g. `:Admin:System:User:show`.
addPriceGroupCurrencyFields()  : void
Adds the per-currency cells (price, original price, points) for one group; shared so that `group{id}` and the base `groupAll` have identical fields.
buildPriceSnapshotDefaults()  : array{sameForAll: bool, tiers: list>}
Builds the matrix prefill for the edited snapshot: rows of the given `publishedTime` grouped by tier (`fromAmount`), inside by group x currency (matches the multiplier containers).
buildPriceSnapshotModalContent()  : string
Renders the modal content (partial) to a string for `ModalWindow::setContent()`.
buildPriceTierMatrix()  : void
Adds the tier x group x currency matrix to the form: the `$form['tiers']` multiplier with create and remove buttons.
buildProductSetPriceHistory()  : array<int, array{publishedTime: string, displayTime: string, future: bool, editable: bool, rowCount: int, groups: list>}>}>
Returns the read-only price list history for the tab: snapshots grouped by `publishedTime` (newest first) with an `editable` flag.
collectPriceSnapshotRows()  : array<int, array{fromAmount: int, userGroupId: int, currencyId: int, price: string|float, originalPrice: string|float|null, points: int}>
Collects the matrix rows to send to the bridge; an empty price means the combination is not stored. The Api manager decides the rest (collisions, guard, denormalized recalculation).
currentProductSetPriceFor()  : ProductSetPrice|null
Returns the currently valid price for the key (`publishedTime <= $now`, latest) from the existing rows.
decimal()  : string
Formats a money value as a `decimal(15,4)` string (columns `price`/`original_price`).
extractPriceCell()  : array<string, mixed>|null
Extracts one cell (price, original price, points) from the raw tier array.
extractPriceGroupBase()  : array<string, mixed>
Returns the cells of one representative group of a tier as the base for `groupAll`; empty when no group exists (empty tier).
groupPriceRowsByUserGroup()  : array<int, array{userGroup: string, rows: list>}>
Turns the rows of one snapshot into one table per user group (order of `$this->userGroupOptions`, unknown groups last), sorted by currency x tier inside. The group is the table heading, so rows do not carry it.
isPriceTiersSameForAllGroups()  : bool
Returns true when every tier x currency has identical values across all user groups (a missing row is null and must match too).
isSnapshotEditable()  : bool
Tells whether a snapshot may be edited or deleted: only one dated today or scheduled in the future. `publishedTime` is a lexicographically comparable string.
loadSnapshotSet()  : ProductSet|null
Returns the set with loaded price history (`includes: ['prices']`). Lazy.
resolvePriceVatPercents()  : string|null
Returns the VAT rate for the client-side "price with VAT" computing when the form has no VAT select (the "Price list" edit modal); null when the rate is unknown (no seller or supply kind), then no preview is shown.
resolveSubmittedSnapshotTime()  : string|null
Returns the validity time from the form for the Api (`resolvePublishedTime()` takes the string as Europe/Prague).
setPriceSnapshotFormDefaults()  : void
Prefills the form when editing; a new snapshot (`editingPriceSnapshotTime === null`) stays empty.
showPriceSnapshotModal()  : void
Fills the shared admin `ModalWindow` with the current form state and opens it.
snapshotErrorKey()  : string
Maps a bridge response code (`OK`, `IMMUTABLE`, `TIME_COLLISION`, `NOT_FOUND`, `DENORM`, `VALIDATION`) to a message key.
toDateTimeLocalValue()  : string
Converts `Y-m-d H:i:s` (ORM string, UTC) to a `datetime-local` value in Prague time.

Properties

$editingPriceSnapshotTime

Snapshot being edited in the modal (`publishedTime`); null when adding.

private string|null $editingPriceSnapshotTime = null

$snapshotSet

Set with loaded price history, loaded separately from `$editedSet` because the main set form does not load `prices`/`vats` at all (see `EDIT_INCLUDES`). Lazy.

private ProductSet|null $snapshotSet = null

Methods

handleDeletePriceSnapshot()

The link is not AJAX (styled `data-grid-confirm`), so after deleting a redirect (PRG) returns to the tab.

public handleDeletePriceSnapshot(string $time) : void
Parameters
$time : string
Attributes
#[Requires]
$methods: 'POST'

handleEditPriceSnapshot()

public handleEditPriceSnapshot(string $time) : void
Parameters
$time : string

onPriceSnapshotFormSuccess()

public onPriceSnapshotFormSuccess(Form $form, ArrayHash<string|int, mixed> $values) : void
Parameters
$form : Form
$values : ArrayHash<string|int, mixed>

createComponentPriceSnapshotForm()

Builds the modal form: matrix of tier (`fromAmount`) x user group x currency to price, original price and points.

protected createComponentPriceSnapshotForm() : Form

Switching tiers and groups is client-side only (JS hides and shows), so all fields stay in the DOM; the server always receives the full matrix and filters out empty cells itself (collectPriceSnapshotRows()).

Return values
Form

isTargetAllowed()

Returns whether the logged-in user may open the given admin target. For visibility (tiles, autocomplete, buttons); it does not throw.

protected isTargetAllowed(string|null $key) : bool

IMPORTANT: fail-closed like requirePrivilege(): an empty key and a presenter without isAllowed() both mean no. A component that skips this question lists the target to everyone.

Parameters
$key : string|null
Return values
bool

requirePrivilege()

Throws 403 when the logged-in user lacks the given admin privilege.

protected requirePrivilege(string $key) : void

Call it as the first statement of a mutating signal, before loading data, so the check cannot be bypassed by something being written first.

Parameters
$key : string

resolvePageTarget()

Returns the RBAC key of the target page: `<presenter>:<action>`, e.g. `:Admin:System:User:show`.

protected static resolvePageTarget(object|null $page) : string|null

Same interpretation as Menu::filterByPrivileges(), BasePresenter::authorizeListing() and PrivilegeCheckTrait::checkCurrentPrivilege(). The key is derived from the page target only, never from a file or class name.

Returns null when the page is missing or has no presenter, so there is nothing to allow.

Parameters
$page : object|null

AdminPage entity (hydrated, hence just object)

Return values
string|null

addPriceGroupCurrencyFields()

Adds the per-currency cells (price, original price, points) for one group; shared so that `group{id}` and the base `groupAll` have identical fields.

private addPriceGroupCurrencyFields(Container $groupContainer) : void
Parameters
$groupContainer : Container

buildPriceSnapshotDefaults()

Builds the matrix prefill for the edited snapshot: rows of the given `publishedTime` grouped by tier (`fromAmount`), inside by group x currency (matches the multiplier containers).

private buildPriceSnapshotDefaults(ProductSet $set, string $publishedTime) : array{sameForAll: bool, tiers: list>}

The "same price for all groups" checkbox is detected in reverse: when all groups have identical price, original price and points for every tier x currency (isPriceTiersSameForAllGroups()), it is prefilled checked and groupAll takes the values of any group; otherwise it stays unchecked and only the per-group matrix is filled. So the checkbox matches the data after reopening.

Parameters
$set : ProductSet
$publishedTime : string
Return values
array{sameForAll: bool, tiers: list>}

buildPriceSnapshotModalContent()

Renders the modal content (partial) to a string for `ModalWindow::setContent()`.

private buildPriceSnapshotModalContent() : string
Return values
string

buildPriceTierMatrix()

Adds the tier x group x currency matrix to the form: the `$form['tiers']` multiplier with create and remove buttons.

private buildPriceTierMatrix(Form $form, callable(): void $onChange) : void

Shared by the edit modal (createComponentPriceSnapshotForm(), $onChange regenerates the whole modal) and by wizard step 2 on add (ProductSetEdit::Traits\FormTrait::createComponentEditForm(), $onChange redraws the wizardPriceStep snippet).

Parameters
$form : Form
$onChange : callable(): void

called after a tier is added or removed (create/remove button and multiplier onCreate/onRemove); the caller decides what to redraw

buildProductSetPriceHistory()

Returns the read-only price list history for the tab: snapshots grouped by `publishedTime` (newest first) with an `editable` flag.

private buildProductSetPriceHistory(ProductSet $set) : array<int, array{publishedTime: string, displayTime: string, future: bool, editable: bool, rowCount: int, groups: list>}>}>

The flag is only a UI hint for icons and badges; the server enforces immutability even for a forged signal.

Snapshot rows are further grouped into groups by user group (one table per group, in the order of $this->userGroupOptions, inside by currency x tier). rowCount is the total number of rows across groups (for the summary counter).

Parameters
$set : ProductSet
Return values
array<int, array{publishedTime: string, displayTime: string, future: bool, editable: bool, rowCount: int, groups: list>}>}>

collectPriceSnapshotRows()

Collects the matrix rows to send to the bridge; an empty price means the combination is not stored. The Api manager decides the rest (collisions, guard, denormalized recalculation).

private collectPriceSnapshotRows(ArrayHash<string|int, mixed> $values) : array<int, array{fromAmount: int, userGroupId: int, currencyId: int, price: string|float, originalPrice: string|float|null, points: int}>
Parameters
$values : ArrayHash<string|int, mixed>
Return values
array<int, array{fromAmount: int, userGroupId: int, currencyId: int, price: string|float, originalPrice: string|float|null, points: int}>

currentProductSetPriceFor()

Returns the currently valid price for the key (`publishedTime <= $now`, latest) from the existing rows.

private currentProductSetPriceFor(ProductSet $set, int $fromAmount, int $userGroupId, int $currencyId, string $now) : ProductSetPrice|null
Parameters
$set : ProductSet
$fromAmount : int
$userGroupId : int
$currencyId : int
$now : string
Return values
ProductSetPrice|null

decimal()

Formats a money value as a `decimal(15,4)` string (columns `price`/`original_price`).

private decimal(mixed $value) : string
Parameters
$value : mixed
Return values
string

extractPriceCell()

Extracts one cell (price, original price, points) from the raw tier array.

private extractPriceCell(array<string, mixed> $tier, int|string $groupId, string $currencyKey) : array<string, mixed>|null

The is_array() guards confirm to PHPStan that the nested group and currency values are arrays. Returns a newly built array with literal keys (not $group[$currencyKey] directly), which gives the exact shape instead of a generic array<mixed, mixed> that would fail the declared return type.

Parameters
$tier : array<string, mixed>
$groupId : int|string
$currencyKey : string
Return values
array<string, mixed>|null

extractPriceGroupBase()

Returns the cells of one representative group of a tier as the base for `groupAll`; empty when no group exists (empty tier).

private extractPriceGroupBase(array<string, mixed> $tier) : array<string, mixed>

Called only after isPriceTiersSameForAllGroups() confirmed identical groups, so the chosen group does not matter. Cells are rebuilt via extractPriceCell(), which returns literal keys, so PHPStan can verify the array<string, mixed> shape.

Parameters
$tier : array<string, mixed>
Return values
array<string, mixed>

groupPriceRowsByUserGroup()

Turns the rows of one snapshot into one table per user group (order of `$this->userGroupOptions`, unknown groups last), sorted by currency x tier inside. The group is the table heading, so rows do not carry it.

private groupPriceRowsByUserGroup(array<int, array<string, mixed>> $rows) : array<int, array{userGroup: string, rows: list>}>
Parameters
$rows : array<int, array<string, mixed>>
Return values
array<int, array{userGroup: string, rows: list>}>

isPriceTiersSameForAllGroups()

Returns true when every tier x currency has identical values across all user groups (a missing row is null and must match too).

private isPriceTiersSameForAllGroups(array<int|string, array<string, mixed>> $tiers) : bool

An empty matrix or no groups is trivially true, so a new snapshot keeps the default "For all".

$tiers is keyed by getFromAmount() (?int, so PHPStan also sees the implicit string conversion of a null key); values are the raw arrays of buildPriceSnapshotDefaults().

Parameters
$tiers : array<int|string, array<string, mixed>>
Return values
bool

isSnapshotEditable()

Tells whether a snapshot may be edited or deleted: only one dated today or scheduled in the future. `publishedTime` is a lexicographically comparable string.

private isSnapshotEditable(string $publishedTime) : bool

IMPORTANT: publishedTime is UTC, so the "today" boundary is Prague calendar midnight converted to UTC, not raw date('Y-m-d'); otherwise the admin would refuse to edit even today's snapshot shortly after midnight (same approach as ProductSetManager::todayMidnight()).

Parameters
$publishedTime : string
Return values
bool

resolvePriceVatPercents()

Returns the VAT rate for the client-side "price with VAT" computing when the form has no VAT select (the "Price list" edit modal); null when the rate is unknown (no seller or supply kind), then no preview is shown.

private resolvePriceVatPercents() : string|null

The rate comes from the unified engine (TaxManager by the supply kind of the set). The admin must not call the engine directly, so it uses the same bridge as the "current price" panel (ProductSetPresenter::internalActionCurrentPrices(), keys vat/withVat).

This is only an input aid: nothing with VAT is stored, so a regional difference does not affect saved data.

Return values
string|null

resolveSubmittedSnapshotTime()

Returns the validity time from the form for the Api (`resolvePublishedTime()` takes the string as Europe/Prague).

private resolveSubmittedSnapshotTime(mixed $submitted, string|null $replaceTime) : string|null

When the edit did not change the time, the original UTC identity is sent with an explicit offset; otherwise in the ambiguous hour at the end of daylight saving time (02:00-02:59 occurs twice) the snapshot would shift by an hour and collide with its neighbor.

Parameters
$submitted : mixed
$replaceTime : string|null
Return values
string|null

setPriceSnapshotFormDefaults()

Prefills the form when editing; a new snapshot (`editingPriceSnapshotTime === null`) stays empty.

private setPriceSnapshotFormDefaults(Form $form) : void
Parameters
$form : Form

showPriceSnapshotModal()

Fills the shared admin `ModalWindow` with the current form state and opens it.

private showPriceSnapshotModal() : void

snapshotErrorKey()

Maps a bridge response code (`OK`, `IMMUTABLE`, `TIME_COLLISION`, `NOT_FOUND`, `DENORM`, `VALIDATION`) to a message key.

private snapshotErrorKey(string $domain, string $code) : string
Parameters
$domain : string
$code : string
Return values
string

toDateTimeLocalValue()

Converts `Y-m-d H:i:s` (ORM string, UTC) to a `datetime-local` value in Prague time.

private toDateTimeLocalValue(string $publishedTime) : string

Saving (Api ProductSetManager::resolvePublishedTime()) takes the input as Europe/Prague and converts to UTC; a prefill without the reverse conversion would shift the snapshot back by 1-2 h on every save.

Parameters
$publishedTime : string
Return values
string
On this page

Search results