PaymentPresenter
extends BasePresenter
in package
uses
CrudTrait, PaymentCreateTrait, PaymentExecuteTrait, PaymentDeliveryTrait, PaymentExpressExecuteTrait, PaymentExpressOffersTrait, PaymentPayPageTrait, PaymentProcessorTrait, PaymentLoggingTrait, NotificationProducerTrait
Table of Contents
Constants
- PAY_PAGE_FIELDS : array<string|int, mixed> = ['id', 'status', 'userId', 'paymentGateId', 'pa...
- IMPORTANT: allowlist of payment page fields. Exactly what `PaymentShowTrait` (Front) and the `paymentMobilniplatby` component need. A key added here is opened to an anonymous visitor.
- SESSION_SUBSECTION : mixed = 'userPaymnet'
- CALLER_TIER_PRIVILEGED : string = 'privileged'
- Caller tier: `privileged` means the grant for this key came from a role with `privileged = 1`; `user` means anything else (`guest`, `registrated`, `owner`) and is also the fail-closed default of all error branches.
- CALLER_TIER_USER : string = 'user'
- SESSION_NAMESPACE : mixed = 'api'
- AMOUNT_TOLERANCE : mixed = 0.01
- Rounding tolerance of the amount comparison (an overpayment or underpayment below the limit passes).
- CALLER_BACKTRACE_LIMIT : int = 14
- Depth of `debug_backtrace()` when looking for `caller=` (a limit, not the whole stack).
- CALLER_SKIPPED_PATHS : array<string|int, mixed> = [ // The Api layer itself. 'app/Core/Traits/Api...
- Path prefixes (relative to the project root) skipped when looking for `caller=`.
- EXPRESS_DEFAULT_GATE : string = 'mobilniplatby'
- EXPRESS_FAILURE_TEXTS : array<string|int, mixed> = [\App\Core\Payments\Express\ExpressSmsParser::I...
- EXPRESS_MAX_ATTEMPTS : int = 12
- The aggregator repeats a delivery up to 12 times (`att`).
- EXPRESS_REPLAY_WAIT_ATTEMPTS : int = 10
- Race on the same SMS: how many times and how many ms the losing attempt waits for the winner reply (about 3 s of the 20 s limit).
- EXPRESS_REPLAY_WAIT_MS : int = 300
- EXPRESS_REPLY_ACCEPTED : string = 'Platba prijata, kod %d. Dekujeme.'
- Reply for a paid payment whose service text can no longer be built (the object disappeared meanwhile).
- EXPRESS_REPLY_DONE : string = '%s pro %s aktivovano. Dekujeme.'
- EXPRESS_REPLY_FAILED : string = '%s Sluzba nebyla aktivovana, reklamace s kodem...
- EXPRESS_REPLY_LOG : string = 'expressReply'
- EXPRESS_REPLY_PENDING : string = '%s pro %s bude aktivovano po potvrzeni platby....
- EXPRESS_REPLY_REJECTED : string = 'Pozadavek nebyl prijat.'
- EXPRESS_REPLY_RETRY : string = 'Docasna chyba, opakujte pozadavek.'
- Temporary failure (HTTP 503): the SMS was neither processed nor rejected, so the aggregator must deliver it again (`att` + 1, up to 12 times). Never use the permanent rejection text instead.
- PAY_PAGE_CLOSED_STATUSES : array<string|int, mixed> = [\App\UI\Api\System\Enums\PaymentStatus::COMPLE...
- States in which the payment page must not be opened (parity with `actionExecute()` and the front end `isPaymentAccessible()`): reopening the URL would overwrite `gatewayPaymentId` of a finished payment.
Properties
- $accessTokenValidator : AccessTokenValidator
- The rule "signature + expiry + `jti` deny-list" has a single implementation ({@see AccessTokenValidator}), shared by this guard and the admin/front `checkAuthentication()`.
- $allowedMethods : array<string|int, mixed>
- $apiClientContext : ApiClientContext
- IMPORTANT: access only through {@see self::apiClientContext()}. A presenter built outside DI (unit tests) lacks it, and reading an uninitialized typed property throws `Error`.
- $apiClientKeyConfig : ApiClientKeyConfig
- $apiClientManager : ApiClientManager
- $apiClientRateLimiter : ApiClientRateLimiter
- $apiRbacConfig : ApiRbacConfig
- $authLogManager : AuthLogManager
- $corsConfig : CorsConfig
- $currencyContext : CurrencyContext
- $currencyManager : CurrencyManager
- $fieldAudienceContext : FieldAudienceContext
- Audience of the caller for the emit filter ({@see \App\Core\Attributes\Hydration\FieldAccess}).
- $jtiDenyList : JtiDenyList
- $jwtGuardConfig : JwtGuardConfig
- $jwtManager : JwtManager
- $lang : int|string|null
- $languageContext : LanguageContext
- $languageManager : LanguageManager
- $localizationContext : LocalizationContext
- $localizationManager : LocalizationManager
- $localizationPaymentRegionManager : PaymentRegionManager
- NOTE: deliberately named differently; presenters often have `$paymentRegionManager` in the constructor (property collision).
- $notificationManager : NotificationManager
- $notificationTypeManager : NotificationTypeManager
- $operationScope : OperationScope
- Boundary of one Api operation (see {@see self::operation()}).
- $pageManager : PageManager
- $permissionResolver : PermissionResolver
- $personalDataPolicy : PersonalDataPolicy
- Personal data policy applied to the output (see {@see self::sendJson()}).
- $presenterContext : PresenterContext
- $queryCache : QueryCache
- $queryPathPolicy : QueryPathPolicy
- Policy for field paths in filters and ordering (see {@see self::rejectForbiddenQueryPaths()}).
- $securityConfig : SecurityConfig
- $settingContext : SettingContext
- $settingManager : SettingManager
- $debugMode : bool
- $manager : Manager|null
- $productionMode : bool
- $anonymousDenyReported : array<string, true>
- $callerTierReported : array<string, true>
- Dedup of `API_COLTIER` lines within one request. The key is `kind|rbac key|tier|userId|field|caller`, so only a literally identical line is dropped; a different caller, tier or field set is logged separately.
- $container : Container
- $expressServiceRegistry : ExpressServiceRegistry
- $hydrator : OrmEntityHydrator
- $jwtPayload : JwtPayload|null
- $paymentCallbackManager : PaymentCallbackManager
- $paymentErrorLogManager : PaymentErrorLogManager
- $paymentGateManager : PaymentGateManager
- $paymentGatewayRegistry : PaymentGatewayRegistry
- $paymentManager : PaymentManager
- $paymentTransactionManager : PaymentTransactionManager
- $rbacDecision : PermissionDecision|null
- Decision after the ownership check; the one that allows or denies.
- $rbacEnforced : bool
- True only when the gate actually rejected the request (`enforce` + deny).
- $rbacKey : string|null
- Key of the last evaluation, e.g. `:Api:Eshop:Order:save`.
- $rbacRawDecision : PermissionDecision|null
- Resolver decision before the ownership check (input of the field tier).
Methods
- __construct() : mixed
- actionCreate() : void
- actionDelivery() : void
- actionExecute() : void
- actionExpressExecute() : void
- assertPaymentPayable() : array<string, mixed>|null
- Single definition of "may this payment be opened / started at the gateway right now".
- getApiClient() : ApiClientIdentity|null
- Returns the client whose key came with this request; `null` means no key (web or admin through the bridge, a `#[NoClientKey]` exception, or `required: false` without a key).
- getCurrencyContext() : CurrencyContext
- getJwtPayload() : JwtPayload|null
- getLanguageContext() : LanguageContext
- getPersonalDataCapabilityRows() : array<string, array<int, int>>
- Returns the records whose personal data the caller receives in full thanks to a capability (advert password).
- internalActionCreate() : array<string, mixed>
- internalActionDelivery() : array<string, mixed>
- internalActionExecute() : array<string, mixed>
- internalActionExpressExecute() : array<string, mixed>
- internalActionExpressOffers() : array<string, mixed>
- internalActionPayPage() : array<string, mixed>
- internalActionSave() : array<string, mixed>
- Raises the `PAYMENT_NEW` bell notification: only for a payment created from the front end (payment form); admin does not notify. A payment from the eshop checkout is reported by `Api\Eshop\OrderPresenter::internalActionCheckout()`.
- isPersonalDataPolicyExempt() : bool
- Returns whether the response of the action skips the personal data policy; the default is no action.
- sendJson() : void
- Sends the JSON response after personal data of other users passed through {@see PersonalDataPolicy}.
- setApiCallSource() : void
- Sets the call source; only {@see \App\Core\Bridge\ApiBridge::internalCall()} calls it (same pattern as `setQueryCacheEnabled()`).
- setAuthenticatedPayload() : void
- Pre-populate the authenticated payload from a trusted caller (ApiBridge internal mode). Internal calls skip startup() and therefore the Bearer middleware — this setter is how ApiBridge propagates the calling Front user's identity into the Api presenter so internalAction* methods can see it via getJwtPayload().
- setConfigParameters() : void
- setQueryCacheEnabled() : void
- Sets the query cache flag; only {@see \App\Core\Bridge\ApiBridge::internalCall()} calls it, before `internalAction*()` (same pattern as `setAuthenticatedPayload()`).
- startup() : void
- applyCors() : void
- Applies the CORS headers and short-circuits a preflight (OPTIONS) with a 204 response.
- checkApiPermission() : void
- Gate entry point; call in `startup()` right after `checkJwtAuth()`.
- checkClientCallerLimit() : void
- Step 4 of an external request: limit of one caller under the key. A logged-in caller is counted by `userId` from the JWT (never from the request), an anonymous one by address.
- checkClientKey() : void
- Step 2 of an external request, see the trait docblock. A rejection ends in `sendJson()` (`AbortException`); otherwise the client is set in the request context.
- checkJwtAuth() : void
- createLog() : bool
- exemptPersonalDataForCapabilityHolder() : void
- IMPORTANT: exempts the capability holder's own record from personal data masking.
- filterReadPayload() : array<string, mixed>
- Read field tier: the last payload filter before the response is sent to the caller.
- getRbacDecision() : PermissionDecision|null
- Decision after the ownership check (null when the gate did not run, i.e. mode `off`).
- getRbacKey() : string|null
- getRbacRawDecision() : PermissionDecision|null
- Resolver decision before the ownership check; input of the field tier.
- getRequestParameters() : array<string, mixed>
- Returns the parameters of the current request.
- getSessionKey() : string
- Returns a session key string, e.g. 'front.auth.step1'.
- getSessionSection() : SessionSection
- Returns a Nette\SessionSection instance using a dynamically built session key.
- initializePresenterContext() : void
- isCallerTierProbeActive() : bool
- Returns whether the `API_COLTIER` measuring probe is active. All three conditions are needed:
- isClientKeyExempt() : bool
- Returns whether the current action is exempt from the key (`#[NoClientKey]` on its `action*()` method). An unknown action is not exempt (fail-closed, like {@see JwtAuthTrait::isPublicAction()}).
- isFurtherPaidTransaction() : bool
- Is a notification about a completed payment a further paid transaction worth processing?
- isOwner() : bool
- Default ownership check, fail-closed: without {@see OwnerAwareInterface} on the entity it is always `false`. Presenters with their own semantics (`Bazaar/Comcat Advert` have `getInsertUserId()`; `Base\Rating` has the owner of the rated object, not the author) override it. The generic `getUserId()` fallback with the `> 0` edge lives in `OwnershipCheckTrait`.
- isPublicAction() : bool
- Tells whether the current action carries `#[PublicAction]`.
- isQueryCacheEnabled() : bool
- isReceiver() : bool
- Default check of the `receiver` role: "is the caller the recipient of this record?" (whom it was sent to, not who created it). Fail-closed: without {@see ReceiverAwareInterface} on the entity it is always `false`. {@see self::requireApiPermission()} calls it only when the resolver returned a conditional `receiver` grant (without an unconditional one). Each domain defines its recipient itself (Response: privileged role, advert owner, password capability or a valid `read_key`).
- isSameSiteRequest() : bool
- Tells whether the request comes from the own site (Nette SameSite cookie); protects GET actions with a side effect.
- operation() : T
- Runs the body of one `internalAction*()` method as one operation (one Api call = one action).
- ownsByCapability() : bool
- Returns whether the caller is the owner of the record by a capability in the JWT (not by identity).
- processPayment() : int|null
- Processes one gateway notification and writes it to the notification history (one row per call, with the outcome collected along the way). An exception is recorded as an error and rethrown.
- readBodyParameter() : string|null
- Returns a value from the request body: `application/x-www-form-urlencoded` or `multipart` (POST fields) and `application/json`.
- rejectForbiddenQueryPaths() : array<string|int, mixed>|null
- IMPORTANT: rejects filters, ordering and `columns` that reach fields the caller would not get in the output.
- reportCallerTierGap() : void
- Writes one machine-parsable `API_COLTIER` line to the `auth` channel. The format follows `API_RBAC ...` from {@see logApiPermissionDecision()}, so both parse with one report:
- requireApiPermission() : ApiPermissionDeniedShape|null
- Guards one Api action. Returns `null` to continue, or an array to return immediately.
- requireManager() : Manager
- Returns the manager for the shared CRUD/tree actions.
- resolveApiPresenterName() : string
- Maps `App\UI\Api\Eshop\Presenters\OrderPresenter` to `Api:Eshop:Order`.
- resolveCallerFrame() : string
- Returns e.g. `app/UI/Front/Eshop/Presenters/ProductPresenter.php:88`: the first stack frame outside {@see self::CALLER_SKIPPED_PATHS}, i.e. the first place that really asked for the data.
- resolveCallerTier() : string
- Returns the caller tier for this presenter and action: `privileged` or `user`.
- resolveExternalFieldTier() : string
- Resolves the field tier of the caller (`#[ExternalAdminOnly]`).
- sendInvalidRequestParameter() : void
- shouldProduceNotifications() : bool
- Returns whether notifications in the bell should be created for this operation.
- verifyOwnership() : bool
- Default `owner` check: is the caller the owner of this record? Fail-closed.
- verifyReceiver() : bool
- Default `receiver` check: is the caller the receiver of this record (who it was sent to, not who created it)? Fail-closed.
- apiClientContext() : ApiClientContext|null
- apiPermissionTokenHint() : string
- Returns the first 12 characters of the Bearer token, in the same shape as `API_RBAC ... token=` from the external gate. On the internal path the `Authorization` header is usually empty, giving `-`.
- applyGatewayCheck() : int|null
- applyResolvedStatus() : bool
- The payment status is derived from the transactions and freezes after `COMPLETED`.
- awaitExpressReply() : string|null
- Stored reply of an express payment; when missing, waits briefly (race: the winner is just activating).
- buildApiPermissionKey() : string
- Builds the key such as `:Api:Eshop:Order:updateColumns` from `getName()` + `getAction()`, never from the URL: Nette `Route::$styles['action']` applies `path2action`, so the URL `update-columns` is `updateColumns` inside the presenter, and the optional `<loc>` route segment is not part of the key.
- buildExpressFailureReply() : string
- buildExpressFreeSuffix() : string
- MT number without charging gets `;FREE<level>`; for an MO number (level 0) the operator charged the money when the SMS was sent and the reply carries no level.
- buildRedirectUrl() : string
- closeInterruptedExpressSms() : array<string, mixed>
- Final end of an interrupted payment without activation: optionally marks it failed, leaves a trace in the payment log and in the `payment` channel, and stores the reply with the complaint code (MT: `;FREE<level>` = do not charge) for later replay. HTTP 200, so the aggregator stops repeating.
- continueExpressSms() : array<string, mixed>
- Finishes an interrupted express payment from the first missing step (under the claim):
- createdIdForNotification() : int|null
- Id of a newly created record from the `internalActionSave()` response, or `null` when nothing should be notified: update (`$data` had the id), failure, or a call from the admin.
- currencyMismatchReason() : string|null
- The currency is verified only where the gateway reports it and where the ISO code of the payment can be found.
- doCallback() : void
- The callback also receives the id of the transaction it runs for.
- ensurePaymentId() : string
- expressParam() : string
- expressRetryResult() : array<string, mixed>
- extractBearerToken() : string|null
- failExpressSms() : array<string, mixed>
- Error branch: a failed payment with `gateway_payment_id` (traceability + idempotence), without a callback.
- fieldAudience() : FieldAudienceContext|null
- Returns the audience context, or `null` when the presenter did not get it through DI.
- findExpressCurrencyCode() : string|null
- findExpressPayment() : Payment|null
- findExpressPhoneForTarget() : string|null
- Number the customer should have sent the SMS to for the service price (hint in the reply).
- findExpressReply() : string
- Stored reply of the payment (`PaymentErrorLog`, `statusText = expressReply`); empty string = not stored yet.
- findShortcodePhones() : array<int, PaymentGatePhone>
- finishExpressSms() : array<string, mixed>
- Step after the saved payment (+ snapshot): module items (only when `$createItems`), then `processPayment()` (activation via callback), then the stored reply. Called by the first processing and by resume, always under the claim {@see \App\UI\Api\System\Models\Managers\PaymentManager::claimExpressSms()}. Repeating `processPayment()` is safe: activation is guarded by the atomic claim on the transaction ({@see \App\UI\Api\System\Presenters\Traits\Helpers\PaymentProcessorTrait::runCallbackForTransaction()}).
- formatExpressPrice() : string
- getOwnershipRecordId() : int|null
- Returns the id of the decided record, taken only from the request identifiers: the `id` parameter (get/delete) or `data.id` (save UPDATE). Returns `null` when both are given and differ (ambiguous), when `id` is an array (bulk delete) and for an INSERT.
- getPaymentGateSetting() : string
- initializeCurrency() : void
- initializeLanguage() : void
- interruptedExpressResult() : array<string, mixed>
- Processing of the stored payment died with an exception: 503 (the aggregator delivers again and resume continues) plus a trace in the `payment` channel. After the last aggregator attempt (`att` >= {@see EXPRESS_MAX_ATTEMPTS}) no further delivery comes, so the message says so explicitly and the payment stays for manual handling.
- isAllowedReturnUrl() : bool
- Checks a return URL against {@see ReturnUrlPolicy}; also shared by `PaymentExecuteTrait::buildRedirectUrl()`.
- isAlternateTransaction() : bool
- Does the gateway report a different transaction than the one stored on the payment?
- isExpressPayment() : bool
- An express payment is created from an SMS without a form code; a form payment has the `code` from `createPayment()`.
- isInternalTransport() : bool
- Returns true when `Presenter::run()` did not run, i.e. the call is on the internal (bridge) path.
- isPaymentTargetPayable() : bool
- Module-level payability ({@see PaymentCallbackRegistry::PAYABILITY_CHECKS}) by the payment `callback_key`.
- isRepeatableCallback() : bool
- May the callback of this payment run again for a further paid transaction ({@see PaymentCallbackRegistry::REPEATABLE})?
- keepSameHostUrls() : array<string, mixed>
- Keeps only the return URLs that point to this website. Foreign and nonsensical values are dropped and `resolvePaymentUrls()` fills in the server default instead.
- loadLocalization() : Localization|null
- localizationCurrencyKey() : string|null
- Returns the currency key of the localization's payment region.
- logApiPermissionDecision() : void
- Writes the `API_RBAC` line to the `auth` channel. The format is identical to {@see RbacCheckTrait::logRbacDecision()} (so both parse with one report), extended with: - `path=internal|external`: tells the two modes apart in one log; - `caller=File.php:123`: makes the report a list of pages instead of a list of keys; - `key_exists=yes|no`: see below.
- logRbacDecision() : void
- Writes one machine-parsable line to the `auth` channel, in a format that follows `JWT_GUARD ...` from {@see JwtAuthTrait::reportJwtAuthFailure()} so that a report can be built from the log.
- logRejectedCallback() : void
- matchExpressPrice() : PaymentGatePhone}
- First (target, number) pair with the same currency and price. There are several targets only when the service is offered by several payment regions, so the number price and currency decide the region, not the SMS text.
- pauseExpressReplay() : void
- payPageFailure() : array<string, mixed>
- Failure in the same shape the guard returns (`Result::failure(...)->toArray()`), so the fail-soft boundary recognizes it by one predicate {@see Result::isFailureEnvelope()} and the caller gets "no data" instead of an entity with meaningless fields.
- permissionDeniedPayload() : ApiPermissionDeniedShape
- Returns the denial shape, identical to what the external gate sends ({@see RbacCheckTrait::checkApiPermission()}), but returned instead of `sendJson()` because the internal path has nowhere to send an HTTP status.
- persistGatewayPaymentId() : void
- Writes `gatewayPaymentId` from a verified path.
- processExpressSms() : array<string, mixed>
- Express payment: creates the payment, the snapshot item and the module items, then runs the standard processing.
- processFormSms() : array<string, mixed>
- Form SMS (`KEYWORD <code>`): `loadPayment()`, `checkPayment()`, `processPayment()`.
- processStatus() : bool
- produceNotification() : void
- rbacTokenHint() : string
- Returns the first 12 characters of the Bearer token, in the same shape as `JWT_GUARD ... token=`.
- refuseClientKey() : void
- Sends one response for a missing, unknown, disabled, revoked and expired key, so the caller cannot tell which key exists. The reason, client id and key id (`cms_system_api_client_keys`, not the key itself) go only to the log; the key or any part of it never does.
- refuseClientLimit() : void
- Sends 429 with `Retry-After`. Only the first refusal in the window goes to the auth log, so a flood does not flood the log too.
- rememberExpressReply() : bool
- Stores the reply for replay on a repeated delivery. A failure is only logged and this attempt still answers itself; without a stored reply a later delivery of the same SMS would end in a temporary failure (503).
- replayExpressReply() : array<string, mixed>
- Repeated SMS (same `id`): returns the original reply (the same level for MT) and processes nothing.
- reportJwtAuthFailure() : void
- resolveCallbackTarget() : array{0: string, 1: string}|null
- IMPORTANT: the callback target is determined only by {@see PaymentCallbackRegistry} by `callback_key`. The DB triple namespace/class/function and a direct `Payment.callback` string are no longer an authority; a direct string passes only when it points to an approved handler. A rejection is logged like an unreachable callback (still fail-open towards the gateway).
- resolveExpressTargetForResume() : ExpressTarget, 1: string, 2: int}
- Target of the resumed payment, taken from the text of the repeated delivery (the aggregator sends the same parameters). The number is the one whose price and currency equal the payment price (for MT this also gives the level).
- resolveGuardMode() : string
- Returns the mode for the transport. Never throws: a missing configuration service means the guard is inert (the pre-guard behavior), not deny.
- resolvePaymentUrls() : array{notifyUrl: string, successUrl: string, cancelUrl: string}
- resolveUnsavedExpressSms() : array<string, mixed>
- The payment of this SMS was not saved: it either lost the race against another delivery (unique index gateway + gateway_payment_id) or the write failed. Reloads the winner and replays its reply; when the payment does not exist (write failed, winner rolled back) returns a temporary failure, never a permanent rejection: with an MO number the SMS is already paid and the aggregator delivers it again after the 503.
- resumeExpressSms() : array<string, mixed>|null
- Resumes an express SMS whose first processing died (or failed) between saving the payment and saving the reply.
- runCallbackForTransaction() : void
- The module callback may run once per transaction.
- saveExpressPayment() : int|null
- Saves the (failed) payment; when two attempts of the same SMS race (unique index gateway + gateway_payment_id) returns null and the caller replays the winner reply ({@see resolveUnsavedExpressSms()}).
- saveExpressPaymentWithSnapshot() : int|null
- Saves the payment and the accounting snapshot (`cms_system_payment_items`) in one DB transaction.
- toSmsText() : string
- verifyAmount() : GatewayCheckResult
- Verifies the amount and the currency against `payment.total_price`.
Constants
PAY_PAGE_FIELDS
IMPORTANT: allowlist of payment page fields. Exactly what `PaymentShowTrait` (Front) and the `paymentMobilniplatby` component need. A key added here is opened to an anonymous visitor.
public
array<string|int, mixed>
PAY_PAGE_FIELDS
= ['id', 'status', 'userId', 'paymentGateId', 'paymentRegionId', 'currencyId', 'totalPrice', 'variableSymbol', 'code', 'name', 'description', 'successUrl', 'cancelUrl', 'insertDatetime', 'paymentDatetime', 'requirePaymentEet', 'paymentGateFeePrice', 'paymentGateFeePercents', 'paymentGateFeeTotal', 'paymentGateFeePercentsPrice']
Deliberately missing: email, ip, encryptedSignature, gatewayPaymentId, callback, notifyUrl,
paymentCallbackId, paymentEetId - the page does not need them and they are either personal data or gateway secrets.
SESSION_SUBSECTION
public
mixed
SESSION_SUBSECTION
= 'userPaymnet'
CALLER_TIER_PRIVILEGED
Caller tier: `privileged` means the grant for this key came from a role with `privileged = 1`; `user` means anything else (`guest`, `registrated`, `owner`) and is also the fail-closed default of all error branches.
protected
string
CALLER_TIER_PRIVILEGED
= 'privileged'
The values are the same strings as $meta['__externalTier'] on the external path
(RbacCheckTrait::resolveExternalFieldTier()); it is the same concept from the same
core (PermissionResolver::isGrantedByPrivilegedRole()),
detected on the other transport. No second decision path exists.
The constants are protected, not private: SaveTrait is flattened into concrete
presenters (descendants of Api\BasePresenter), while this trait sits on
Api\BasePresenter, so a private constant of the ancestor would be unreachable.
CALLER_TIER_USER
protected
string
CALLER_TIER_USER
= 'user'
SESSION_NAMESPACE
protected
mixed
SESSION_NAMESPACE
= 'api'
AMOUNT_TOLERANCE
Rounding tolerance of the amount comparison (an overpayment or underpayment below the limit passes).
private
mixed
AMOUNT_TOLERANCE
= 0.01
CALLER_BACKTRACE_LIMIT
Depth of `debug_backtrace()` when looking for `caller=` (a limit, not the whole stack).
private
int
CALLER_BACKTRACE_LIMIT
= 14
12 plus the two frames operation() adds (the call and its body closure).
CALLER_SKIPPED_PATHS
Path prefixes (relative to the project root) skipped when looking for `caller=`.
private
array<string|int, mixed>
CALLER_SKIPPED_PATHS
= [
// The Api layer itself.
'app/Core/Traits/Api/',
'app/UI/Api/',
// `operation()` calls the body closure of every `internalAction*()`; without this entry
// every `caller=` would be that one line (degenerate by construction, not by measurement).
'app/Core/Base/Api/BasePresenter.php',
// Forwarding plumbing, see the docblock above.
'app/Core/Bridge/',
'app/Core/Base/Front/Models/',
'app/Core/Base/Admin/Models/',
]
The Api layer itself is skipped because it is "who called the guard", not "who wanted the
data". The bridge and generic front/admin model classes are skipped too: with only the Api
prefixes caller= was app/Core/Bridge/ApiBridge.php for nearly every line, because
internalCall() calls internalAction* by reflection. A report with a single file is useless.
The extra prefixes are pure forwarding plumbing, identical for every internal call:
app/Core/Bridge/: the transport itself;app/Core/Base/{Front,Admin}/Models/: genericApiRepository,ApiMapper,ApiServiceandSingleQueryshared by most front and admin repositories.
The first non-skipped frame is then the first module file (concrete repository, init trait, presenter, component), the one that really asked for the data.
NOTE: before adding another prefix, measure the caller= breakdown in log mode and show
that the field is degenerate without it.
EXPRESS_DEFAULT_GATE
private
string
EXPRESS_DEFAULT_GATE
= 'mobilniplatby'
EXPRESS_FAILURE_TEXTS
private
array<string|int, mixed>
EXPRESS_FAILURE_TEXTS
= [\App\Core\Payments\Express\ExpressSmsParser::INVALID_KEYWORD_MISMATCH => 'Neplatny tvar SMS.', \App\Core\Payments\Express\ExpressSmsParser::INVALID_KEYWORD_NOT_CONFIGURED => 'Platba SMS neni nastavena.', \App\Core\Payments\Express\ExpressSmsParser::INVALID_MALFORMED => 'Neplatny tvar SMS.', 'unknownModule' => 'Neznama sluzba.', \App\Core\Payments\Express\ExpressResolution::ERROR_UNKNOWN_SERVICE => 'Neznama sluzba.', \App\Core\Payments\Express\ExpressResolution::ERROR_OBJECT_NOT_FOUND => 'Polozka %s neexistuje.', \App\Core\Payments\Express\ExpressResolution::ERROR_OBJECT_INACTIVE => 'Polozka %s neni aktivni.', 'unknownShortcode' => 'Nezname cislo.', 'priceMismatch' => 'Cena neodpovida sluzbe.', 'priceMismatchHint' => 'Cena neodpovida sluzbe, spravne cislo je %s.', 'unknownPaymentCode' => 'Platba s timto kodem neexistuje.', 'itemsFailed' => 'Chyba pri zakladani sluzby.', 'callbackMissing' => 'Chyba pri zakladani sluzby.', 'interrupted' => 'Pozadavek se nepodarilo dokoncit.']
failure reason to the customer sentence (SMS is ASCII, keep it short)
EXPRESS_MAX_ATTEMPTS
The aggregator repeats a delivery up to 12 times (`att`).
private
int
EXPRESS_MAX_ATTEMPTS
= 12
EXPRESS_REPLAY_WAIT_ATTEMPTS
Race on the same SMS: how many times and how many ms the losing attempt waits for the winner reply (about 3 s of the 20 s limit).
private
int
EXPRESS_REPLAY_WAIT_ATTEMPTS
= 10
EXPRESS_REPLAY_WAIT_MS
private
int
EXPRESS_REPLAY_WAIT_MS
= 300
EXPRESS_REPLY_ACCEPTED
Reply for a paid payment whose service text can no longer be built (the object disappeared meanwhile).
private
string
EXPRESS_REPLY_ACCEPTED
= 'Platba prijata, kod %d. Dekujeme.'
EXPRESS_REPLY_DONE
private
string
EXPRESS_REPLY_DONE
= '%s pro %s aktivovano. Dekujeme.'
EXPRESS_REPLY_FAILED
private
string
EXPRESS_REPLY_FAILED
= '%s Sluzba nebyla aktivovana, reklamace s kodem %d.'
EXPRESS_REPLY_LOG
private
string
EXPRESS_REPLY_LOG
= 'expressReply'
EXPRESS_REPLY_PENDING
private
string
EXPRESS_REPLY_PENDING
= '%s pro %s bude aktivovano po potvrzeni platby. Dekujeme.'
EXPRESS_REPLY_REJECTED
private
string
EXPRESS_REPLY_REJECTED
= 'Pozadavek nebyl prijat.'
EXPRESS_REPLY_RETRY
Temporary failure (HTTP 503): the SMS was neither processed nor rejected, so the aggregator must deliver it again (`att` + 1, up to 12 times). Never use the permanent rejection text instead.
private
string
EXPRESS_REPLY_RETRY
= 'Docasna chyba, opakujte pozadavek.'
PAY_PAGE_CLOSED_STATUSES
States in which the payment page must not be opened (parity with `actionExecute()` and the front end `isPaymentAccessible()`): reopening the URL would overwrite `gatewayPaymentId` of a finished payment.
private
array<string|int, mixed>
PAY_PAGE_CLOSED_STATUSES
= [\App\UI\Api\System\Enums\PaymentStatus::COMPLETED, \App\UI\Api\System\Enums\PaymentStatus::REFUNDED]
Properties
$accessTokenValidator
The rule "signature + expiry + `jti` deny-list" has a single implementation ({@see AccessTokenValidator}), shared by this guard and the admin/front `checkAuthentication()`.
public
AccessTokenValidator
$accessTokenValidator
Attributes
- #[Inject]
$allowedMethods
public
array<string|int, mixed>
$allowedMethods
= ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']
$apiClientContext
IMPORTANT: access only through {@see self::apiClientContext()}. A presenter built outside DI (unit tests) lacks it, and reading an uninitialized typed property throws `Error`.
public
ApiClientContext
$apiClientContext
Attributes
- #[Inject]
$apiClientKeyConfig
public
ApiClientKeyConfig
$apiClientKeyConfig
Attributes
- #[Inject]
$apiClientManager
public
ApiClientManager
$apiClientManager
Attributes
- #[Inject]
$apiClientRateLimiter
public
ApiClientRateLimiter
$apiClientRateLimiter
Attributes
- #[Inject]
$apiRbacConfig
public
ApiRbacConfig
$apiRbacConfig
Attributes
- #[Inject]
$authLogManager
public
AuthLogManager
$authLogManager
Attributes
- #[Inject]
$corsConfig
public
CorsConfig
$corsConfig
Attributes
- #[Inject]
$currencyContext
public
CurrencyContext
$currencyContext
Attributes
- #[Inject]
$currencyManager
public
CurrencyManager
$currencyManager
Attributes
- #[Inject]
$fieldAudienceContext
Audience of the caller for the emit filter ({@see \App\Core\Attributes\Hydration\FieldAccess}).
public
FieldAudienceContext
$fieldAudienceContext
Filled by the guard from the decision it has just made, never from the request.
IMPORTANT: access it only through self::fieldAudience(). An uninitialized typed
property throws Error on first access, which lands in the fail-closed catch-all of
ApiPermissionGuardTrait::requireApiPermission(); a presenter built outside DI
would then be denied everything because of a field filter unrelated to authorization.
NOTE: the type cannot be nullable, InjectExtension rejects it; hence the non-nullable
property with an isset() getter.
Attributes
- #[Inject]
$jtiDenyList
public
JtiDenyList
$jtiDenyList
Attributes
- #[Inject]
$jwtGuardConfig
public
JwtGuardConfig
$jwtGuardConfig
Attributes
- #[Inject]
$jwtManager
public
JwtManager
$jwtManager
Attributes
- #[Inject]
$lang
public
int|string|null
$lang
= NULL
$languageContext
public
LanguageContext
$languageContext
Attributes
- #[Inject]
$languageManager
public
LanguageManager
$languageManager
Attributes
- #[Inject]
$localizationContext
public
LocalizationContext
$localizationContext
Attributes
- #[Inject]
$localizationManager
public
LocalizationManager
$localizationManager
Attributes
- #[Inject]
$localizationPaymentRegionManager
NOTE: deliberately named differently; presenters often have `$paymentRegionManager` in the constructor (property collision).
public
PaymentRegionManager
$localizationPaymentRegionManager
Attributes
- #[Inject]
$notificationManager
public
NotificationManager
$notificationManager
Attributes
- #[Inject]
$notificationTypeManager
public
NotificationTypeManager
$notificationTypeManager
Attributes
- #[Inject]
$operationScope
Boundary of one Api operation (see {@see self::operation()}).
public
OperationScope
$operationScope
Attributes
- #[Inject]
$pageManager
public
PageManager
$pageManager
Attributes
- #[Inject]
$permissionResolver
public
PermissionResolver
$permissionResolver
Attributes
- #[Inject]
$personalDataPolicy
Personal data policy applied to the output (see {@see self::sendJson()}).
public
PersonalDataPolicy
$personalDataPolicy
Attributes
- #[Inject]
$presenterContext
public
PresenterContext
$presenterContext
Attributes
- #[Inject]
$queryCache
public
QueryCache
$queryCache
Attributes
- #[Inject]
$queryPathPolicy
Policy for field paths in filters and ordering (see {@see self::rejectForbiddenQueryPaths()}).
public
QueryPathPolicy
$queryPathPolicy
Attributes
- #[Inject]
$securityConfig
public
SecurityConfig
$securityConfig
Attributes
- #[Inject]
$settingContext
public
SettingContext
$settingContext
Attributes
- #[Inject]
$settingManager
public
SettingManager
$settingManager
Attributes
- #[Inject]
$debugMode
protected
bool
$debugMode
= false
$manager read-only
protected
Manager|null
$manager
= null
$productionMode
protected
bool
$productionMode
= false
$anonymousDenyReported
private
static array<string, true>
$anonymousDenyReported
= []
keys of denied guest reads already logged in this request
$callerTierReported
Dedup of `API_COLTIER` lines within one request. The key is `kind|rbac key|tier|userId|field|caller`, so only a literally identical line is dropped; a different caller, tier or field set is logged separately.
private
static array<string, true>
$callerTierReported
= []
IMPORTANT: it is static, not an instance property: ApiBridge::internalCall() creates a
new presenter instance for every call, so an instance property would deduplicate nothing.
Only one class uses this trait (BasePresenter), so the storage is
one per request. With internalMode: off nothing is ever written to it.
$container read-only
private
Container
$container
$expressServiceRegistry read-only
private
ExpressServiceRegistry
$expressServiceRegistry
$hydrator read-only
private
OrmEntityHydrator
$hydrator
$jwtPayload
private
JwtPayload|null
$jwtPayload
= null
$paymentCallbackManager read-only
private
PaymentCallbackManager
$paymentCallbackManager
$paymentErrorLogManager read-only
private
PaymentErrorLogManager
$paymentErrorLogManager
$paymentGateManager read-only
private
PaymentGateManager
$paymentGateManager
$paymentGatewayRegistry read-only
private
PaymentGatewayRegistry
$paymentGatewayRegistry
$paymentManager read-only
private
PaymentManager
$paymentManager
$paymentTransactionManager read-only
private
PaymentTransactionManager
$paymentTransactionManager
$rbacDecision
Decision after the ownership check; the one that allows or denies.
private
PermissionDecision|null
$rbacDecision
= null
$rbacEnforced
True only when the gate actually rejected the request (`enforce` + deny).
private
bool
$rbacEnforced
= false
$rbacKey
Key of the last evaluation, e.g. `:Api:Eshop:Order:save`.
private
string|null
$rbacKey
= null
$rbacRawDecision
Resolver decision before the ownership check (input of the field tier).
private
PermissionDecision|null
$rbacRawDecision
= null
Methods
__construct()
public
__construct(OrmEntityHydrator $hydrator, PaymentManager $paymentManager, PaymentGateManager $paymentGateManager, PaymentCallbackManager $paymentCallbackManager, PaymentErrorLogManager $paymentErrorLogManager, PaymentTransactionManager $paymentTransactionManager, PaymentGatewayRegistry $paymentGatewayRegistry, Container $container, ExpressServiceRegistry $expressServiceRegistry) : mixed
Parameters
- $hydrator : OrmEntityHydrator
- $paymentManager : PaymentManager
- $paymentGateManager : PaymentGateManager
- $paymentCallbackManager : PaymentCallbackManager
- $paymentErrorLogManager : PaymentErrorLogManager
- $paymentTransactionManager : PaymentTransactionManager
- $paymentGatewayRegistry : PaymentGatewayRegistry
- $container : Container
- $expressServiceRegistry : ExpressServiceRegistry
actionCreate()
public
actionCreate() : void
actionDelivery()
public
actionDelivery() : void
Attributes
- #[NoClientKey]
- #[PublicAction]
actionExecute()
public
actionExecute() : void
Attributes
- #[NoClientKey]
- #[PublicAction]
actionExpressExecute()
public
actionExpressExecute() : void
Attributes
- #[NoClientKey]
- #[PublicAction]
assertPaymentPayable()
Single definition of "may this payment be opened / started at the gateway right now".
public
assertPaymentPayable(Payment $payment) : array<string, mixed>|null
Used by self::internalActionPayPage() and PaymentCreateTrait::internalActionCreate(); two copies of the conditions are exactly how authorization drifts apart.
The rule equals the front end check
(App\UI\Front\System\Presenters\Traits\Details\PaymentShowTrait::isPaymentAccessible()), evaluated
server-side, so it also holds for a caller that bypassed the front end:
- A completed/refunded payment is not reopened (repeated GET would overwrite
gatewayPaymentIdof a finished payment; parity withactionExecute()). - A payment bound to an account belongs only to its owner (identity only from the JWT, never from a parameter).
- A payment without an account (
user_id IS NULL) stays open on purpose: it does not matter who pays, whoever has the link.
A privileged role (currently admin) bypasses the rule, because the admin must be able to open a foreign
payment. PermissionResolver::isGrantedByPrivilegedRole() decides it, not a
hardcoded role name.
Parameters
- $payment : Payment
Return values
array<string, mixed>|null —null = may continue; array = return it to the caller
getApiClient()
Returns the client whose key came with this request; `null` means no key (web or admin through the bridge, a `#[NoClientKey]` exception, or `required: false` without a key).
public
getApiClient() : ApiClientIdentity|null
Return values
ApiClientIdentity|nullgetCurrencyContext()
public
getCurrencyContext() : CurrencyContext
Return values
CurrencyContextgetJwtPayload()
public
getJwtPayload() : JwtPayload|null
Return values
JwtPayload|nullgetLanguageContext()
public
getLanguageContext() : LanguageContext
Return values
LanguageContextgetPersonalDataCapabilityRows()
Returns the records whose personal data the caller receives in full thanks to a capability (advert password).
public
getPersonalDataCapabilityRows(string $action) : array<string, array<int, int>>
The shape is [entity class => [id]] for PersonalDataPolicy::apply(). Only get
returns rows; other actions (lists, getBy) return none. Filled by
self::exemptPersonalDataForCapabilityHolder().
Parameters
- $action : string
Return values
array<string, array<int, int>>internalActionCreate()
public
internalActionCreate(int $paymentId[, array<string|int, mixed> $urls = [] ]) : array<string, mixed>
Parameters
- $paymentId : int
- $urls : array<string|int, mixed> = []
Return values
array<string, mixed>internalActionDelivery()
public
internalActionDelivery(string $pg[, array<string, mixed> $params = [] ]) : array<string, mixed>
Parameters
- $pg : string
- $params : array<string, mixed> = []
Return values
array<string, mixed>internalActionExecute()
public
internalActionExecute(int $paymentId[, array<string, mixed> $params = [] ]) : array<string, mixed>
Parameters
- $paymentId : int
- $params : array<string, mixed> = []
Return values
array<string, mixed>internalActionExpressExecute()
public
internalActionExpressExecute(string $pg[, array<string, mixed> $params = [] ]) : array<string, mixed>
Parameters
- $pg : string
- $params : array<string, mixed> = []
Return values
array<string, mixed> —output = reply for the aggregator; status/paymentId/reason = diagnostics (harness)
internalActionExpressOffers()
public
internalActionExpressOffers([string $moduleCode = '' ][, int $objectId = 0 ]) : array<string, mixed>
Parameters
- $moduleCode : string = ''
- $objectId : int = 0
Return values
array<string, mixed> —offers: list of {tariffId, paymentRegionId, serviceCode, smsText, phone, price, currencyId, level}
internalActionPayPage()
public
internalActionPayPage([int|null $id = null ]) : array<string, mixed>
Parameters
- $id : int|null = null
Return values
array<string, mixed> —allowlist from self::PAY_PAGE_FIELDS, or a failed Result envelope (which the
fail-soft boundary turns into "no data": EntityHydrator::rejectFailureEnvelope() / FailSoftReadTrait)
internalActionSave()
Raises the `PAYMENT_NEW` bell notification: only for a payment created from the front end (payment form); admin does not notify. A payment from the eshop checkout is reported by `Api\Eshop\OrderPresenter::internalActionCheckout()`.
public
internalActionSave(array<string|int, mixed> $data[, array<string, mixed> $meta = [] ][, array<int, string> $skipFields = [] ]) : array<string, mixed>
The permission guard is the first statement, in the same shape as
SaveTrait::internalActionSave() (checked by
ApiPermissionCoverageTest); the save itself is the trait's saveEntityData().
Parameters
- $data : array<string|int, mixed>
- $meta : array<string, mixed> = []
- $skipFields : array<int, string> = []
Return values
array<string, mixed>isPersonalDataPolicyExempt()
Returns whether the response of the action skips the personal data policy; the default is no action.
public
isPersonalDataPolicyExempt(string $action) : bool
Only a presenter that returns exclusively the caller's own data and has no identity in the audience context at response time may override it. Typically login: it returns the account just verified, but the JWT is only being created, so without the exemption users would get their own e-mail masked.
Parameters
- $action : string
Return values
boolsendJson()
Sends the JSON response after personal data of other users passed through {@see PersonalDataPolicy}.
public
sendJson(mixed $data) : void
The policy runs right before sending, after the query cache, where the caller is known. The internal path is handled the same way in ApiBridge.
Parameters
- $data : mixed
setApiCallSource()
Sets the call source; only {@see \App\Core\Bridge\ApiBridge::internalCall()} calls it (same pattern as `setQueryCacheEnabled()`).
public
setApiCallSource(string|null $source) : void
Parameters
- $source : string|null
setAuthenticatedPayload()
Pre-populate the authenticated payload from a trusted caller (ApiBridge internal mode). Internal calls skip startup() and therefore the Bearer middleware — this setter is how ApiBridge propagates the calling Front user's identity into the Api presenter so internalAction* methods can see it via getJwtPayload().
public
setAuthenticatedPayload(JwtPayload|null $payload) : void
Parameters
- $payload : JwtPayload|null
setConfigParameters()
public
setConfigParameters([bool $debugMode = false ][, bool $productionMode = false ]) : void
Parameters
- $debugMode : bool = false
- $productionMode : bool = false
setQueryCacheEnabled()
Sets the query cache flag; only {@see \App\Core\Bridge\ApiBridge::internalCall()} calls it, before `internalAction*()` (same pattern as `setAuthenticatedPayload()`).
public
setQueryCacheEnabled(bool $enabled) : void
A presenter is created anew for every internal call
(PresenterFactoryCallback calls createService(),
not getService()), so the value has nowhere to carry over; the bridge still sets it on
every call and resets it to the default in finally.
Parameters
- $enabled : bool
startup()
public
startup() : void
applyCors()
Applies the CORS headers and short-circuits a preflight (OPTIONS) with a 204 response.
protected
applyCors() : void
IMPORTANT: it must be called in startup() before checkJwtAuth(); a preflight sends no
Authorization, so it would otherwise end with 401.
checkApiPermission()
Gate entry point; call in `startup()` right after `checkJwtAuth()`.
protected
checkApiPermission() : void
Tags
checkClientCallerLimit()
Step 4 of an external request: limit of one caller under the key. A logged-in caller is counted by `userId` from the JWT (never from the request), an anonymous one by address.
protected
checkClientCallerLimit() : void
checkClientKey()
Step 2 of an external request, see the trait docblock. A rejection ends in `sendJson()` (`AbortException`); otherwise the client is set in the request context.
protected
checkClientKey() : void
checkJwtAuth()
protected
checkJwtAuth() : void
createLog()
protected
createLog(int|null $paymentId, int|null $paymentGateId[, string|null $gatewayPaymentId = null ][, string|null $shortMessage = null ][, string|null $longMessage = null ][, mixed $statusCode = null ][, string|null $statusText = null ]) : bool
Parameters
- $paymentId : int|null
- $paymentGateId : int|null
- $gatewayPaymentId : string|null = null
- $shortMessage : string|null = null
- $longMessage : string|null = null
- $statusCode : mixed = null
- $statusText : string|null = null
Return values
boolexemptPersonalDataForCapabilityHolder()
IMPORTANT: exempts the capability holder's own record from personal data masking.
protected
exemptPersonalDataForCapabilityHolder(int|null $id) : void
An anonymous editor holding the advert password (capability) is an owner for isOwner(),
but PersonalDataPolicy recognizes an owner only by identity (userId). Without this
exemption the anonymous editor would get a masked e-mail (and masked e-mails in parameter
texts) in the edit form, and saving would fail validation or write the mask back.
Called from internalActionGet() after the guard. Without a capability in the token it costs
no queries; with one it costs a single find(). The caller is an owner only for the
response of this get, only when the capability matches this record (scope, id, expiry,
password fingerprint from the DB) and only for the rows of that record (advert and its
parameters). The user from includes, reactions or another advert in the same response
stay masked.
The query cache is unaffected: it stores the hydrator output before the policy, and both the mask and the exemption apply only to the outgoing copy (see PersonalDataPolicy).
Parameters
- $id : int|null
filterReadPayload()
Read field tier: the last payload filter before the response is sent to the caller.
protected
filterReadPayload(array<string, mixed> $payload, string $action) : array<string, mixed>
The default is identity. There is no blanket read tier (#[ExternalReadonly] and
#[ExternalAdminOnly] strip only writes); this hook gives a presenter a place to
implement one when its data requires it. Overridden by
UserPresenter (the owner grant on
:Api:System:User:get would otherwise let an owner read their own password, salt and
activationKey over external HTTP) and by CustomAssetPresenter and PageSectionPresenter
(an unprivileged caller gets active records only, see InactiveRecordsReadFilterTrait).
IMPORTANT: it runs after the cache (QueryCache), because the result depends on the caller,
not on the query. Filtering inside the cache would freeze the payload shape of the first
caller for everyone else.
It is called from get (GetTrait), getBy
(GetByTrait), getAll
(GetAllTrait) and search
(SearchTrait), each with its action name.
Because the default is identity, only presenters that override the hook are affected.
Parameters
- $payload : array<string, mixed>
- $action : string
Return values
array<string, mixed>getRbacDecision()
Decision after the ownership check (null when the gate did not run, i.e. mode `off`).
protected
getRbacDecision() : PermissionDecision|null
Return values
PermissionDecision|nullgetRbacKey()
protected
getRbacKey() : string|null
Return values
string|nullgetRbacRawDecision()
Resolver decision before the ownership check; input of the field tier.
protected
getRbacRawDecision() : PermissionDecision|null
Return values
PermissionDecision|nullgetRequestParameters()
Returns the parameters of the current request.
protected
getRequestParameters() : array<string, mixed>
The request is always set during action*(); the helper only turns the nullable
getRequest() into an always-array.
Return values
array<string, mixed>getSessionKey()
Returns a session key string, e.g. 'front.auth.step1'.
protected
getSessionKey([string|null $namespace = null ][, string|null $subsection = null ], string ...$extra) : string
Parameters
- $namespace : string|null = null
-
Base namespace (module prefix). Uses SESSION_NAMESPACE if null.
- $subsection : string|null = null
-
Subsection under the namespace. Uses SESSION_SUBSECTION constant if null.
- $extra : string
-
Additional parts to append (e.g. 'step1', 'wizard', etc.).
Return values
stringgetSessionSection()
Returns a Nette\SessionSection instance using a dynamically built session key.
protected
getSessionSection([string|null $namespace = null ][, string|null $subsection = null ], string ...$extra) : SessionSection
Parameters
- $namespace : string|null = null
- $subsection : string|null = null
- $extra : string
Tags
Return values
SessionSectioninitializePresenterContext()
protected
initializePresenterContext() : void
isCallerTierProbeActive()
Returns whether the `API_COLTIER` measuring probe is active. All three conditions are needed:
protected
isCallerTierProbeActive() : bool
apiRbacConfigexists (a presenter outside DI leaves the probe inert, like the guard);internalModeis notoff: then it isfalseafter two comparisons, so no DB queries, no log lines and nodebug_backtrace(), exactly like the guard (requireApiPermission());- the call is on the internal (bridge) path; the external path has its own tier mechanism
(
$meta['__externalTier']).
The order is deliberate: cheapest first, isInternalTransport() is not called in off.
Return values
boolisClientKeyExempt()
Returns whether the current action is exempt from the key (`#[NoClientKey]` on its `action*()` method). An unknown action is not exempt (fail-closed, like {@see JwtAuthTrait::isPublicAction()}).
protected
isClientKeyExempt() : bool
Return values
boolisFurtherPaidTransaction()
Is a notification about a completed payment a further paid transaction worth processing?
protected
isFurtherPaidTransaction(PaymentGate $paymentGate, GatewayCheckResult $check) : bool
Only a transaction the gateway confirmed as paid, with its own id that has not activated yet. Anything else (a repeat of the transaction that already activated, a late failure, a rejected or unidentified notification) is only recorded in the notification history; the payment and its transactions stay untouched. The decision is a cheap pre-filter: idempotence itself is the atomic claim in runCallbackForTransaction(), and the reference and amount checks of processPayment() still apply to what passes.
A forged notification with an invented transaction id passes only if the gateway confirms that id as paid: card
gateways ask their server (checkPayment()/checkDelivery()), and a paid transaction of another payment fails
on its reference.
Parameters
- $paymentGate : PaymentGate
- $check : GatewayCheckResult
Return values
boolisOwner()
Default ownership check, fail-closed: without {@see OwnerAwareInterface} on the entity it is always `false`. Presenters with their own semantics (`Bazaar/Comcat Advert` have `getInsertUserId()`; `Base\Rating` has the owner of the rated object, not the author) override it. The generic `getUserId()` fallback with the `> 0` edge lives in `OwnershipCheckTrait`.
protected
isOwner(object $entity, int $userId[, array<string|int, mixed> $data = [] ]) : bool
IMPORTANT: never change this to return true; it would open every endpoint with one line.
Parameters
- $entity : object
-
entity loaded by the
$ownedclosure from the DB by the id from the method parameter, never built from request data - $userId : int
-
ID from the JWT payload, never from a
internalAction*parameter - $data : array<string|int, mixed> = []
-
write payload; on INSERT the only source of identifiers (parent); ownership is never read from it
Return values
boolisPublicAction()
Tells whether the current action carries `#[PublicAction]`.
protected
isPublicAction() : bool
IMPORTANT: the visibility is protected (not private) because RbacCheckTrait
uses it too: the RBAC gate must evaluate #[PublicAction] exactly like the JWT guard,
otherwise the two layers diverge. Single source of truth.
An empty action returns false (fail-closed): "action not recognized" must not mean
"public", otherwise a presenter without an action* method would pass without
authentication and authorization. On the external HTTP path the action is never empty
because ApiRouteFactory defaults the route to 'action' => 'show'; the empty case
occurs only in-process or if someone removes that default, and then it ends in
401 (guard) / 403 (gate) instead of a bypass.
Return values
boolisQueryCacheEnabled()
protected
isQueryCacheEnabled() : bool
Tags
Return values
boolisReceiver()
Default check of the `receiver` role: "is the caller the recipient of this record?" (whom it was sent to, not who created it). Fail-closed: without {@see ReceiverAwareInterface} on the entity it is always `false`. {@see self::requireApiPermission()} calls it only when the resolver returned a conditional `receiver` grant (without an unconditional one). Each domain defines its recipient itself (Response: privileged role, advert owner, password capability or a valid `read_key`).
protected
isReceiver(object|null $entity, int|null $userId[, array<string|int, mixed> $data = [] ]) : bool
Unlike self::isOwner(), $userId may be null: the recipient can also be an
anonymous editor with a capability or the holder of a key from an e-mail link.
IMPORTANT: never change this to return true; it would let anyone read records addressed
to others.
Parameters
- $entity : object|null
-
entity loaded by the
$ownedclosure from the DB by the id from the method parameter, never from request data;nullmeans nothing to judge - $userId : int|null
-
ID from the JWT payload,
nullmeans no identity - $data : array<string|int, mixed> = []
-
explicit parameters of the internal call supplied by the calling action (e.g.
['readKey' => ...]formarkReadByKey); never present on the external path
Return values
boolisSameSiteRequest()
Tells whether the request comes from the own site (Nette SameSite cookie); protects GET actions with a side effect.
protected
isSameSiteRequest() : bool
getHttpRequest() returns the IRequest interface, while isSameSite() exists only on
Nette\Http\Request.
Return values
booloperation()
Runs the body of one `internalAction*()` method as one operation (one Api call = one action).
protected
operation(string $method, callable(): T $body) : T
Every internalAction*() consists of return $this->operation(__FUNCTION__, function () { ... });;
both transports call it (HTTP from action*(), ApiBridge by reflection), so this is the single
place where an operation begins and ends. The presenter only reports the transport and the
outcome; what a store does about it is up to the participants of OperationScope.
Observation off (the default): only the body runs. On: an operation started inside another one
only joins it. The outcome comes from the returned envelope: success=true succeeded,
success=false rejected (failed for a technical ResultCode), anything else unknown.
An exception is recorded as failed and rethrown unchanged; the result is never changed. The only
exception of its own is OperationContractViolationException after a
body that did not throw (scoped mode, outside production by default): the result is then dropped.
Parameters
- $method : string
- $body : callable(): T
Tags
Return values
TownsByCapability()
Returns whether the caller is the owner of the record by a capability in the JWT (not by identity).
protected
ownsByCapability(object $entity) : bool
Default false; overridden by a presenter whose records can be edited with a password
(Bazaar/Comcat advert). The decision is made on the entity loaded from the DB, never on
request data.
Parameters
- $entity : object
Return values
boolprocessPayment()
Processes one gateway notification and writes it to the notification history (one row per call, with the outcome collected along the way). An exception is recorded as an error and rethrown.
protected
processPayment(Payment|null $payment, PaymentGate $paymentGate, GatewayCheckResult $check[, GatewayNotificationTransport $transport = GatewayNotificationTransport::Execute ]) : int|null
Parameters
- $payment : Payment|null
- $paymentGate : PaymentGate
- $check : GatewayCheckResult
- $transport : GatewayNotificationTransport = GatewayNotificationTransport::Execute
Return values
int|nullreadBodyParameter()
Returns a value from the request body: `application/x-www-form-urlencoded` or `multipart` (POST fields) and `application/json`.
protected
readBodyParameter(string $name) : string|null
Use it for secret inputs (passwords, reset and activation keys, tokens) that must not end
up in the URL: the query string is written to web server and proxy access logs and to the
client history. Nette takes action*() parameters only from the URL, so these actions
have no parameters in the signature and read them here.
Returns null when the value is missing from the body or is not a scalar.
Parameters
- $name : string
Return values
string|nullrejectForbiddenQueryPaths()
IMPORTANT: rejects filters, ordering and `columns` that reach fields the caller would not get in the output.
protected
rejectForbiddenQueryPaths(array<string|int, mixed> $filters[, array<string|int, mixed>|null $orderBys = null ][, array<string|int, mixed>|null $columns = null ]) : array<string|int, mixed>|null
Fields hidden by FieldAccess read or PersonalData must not be usable in a query,
otherwise the presence of a record in the response is an oracle (blind extraction of a
password hash through filters[...][field]=user.password with like). The rule is held
by QueryPathPolicy.
Call it in internalAction*() right after the guard (so the audience is set) and before the
first query; it is the single place for getAll, getBy, search, deleteAll,
myList, myOne and the advert lists. Same contract as the guard: null means continue,
an array is the response to return. On the external path it sets HTTP 400 (see
RequestShapeGuardTrait); nothing is silently ignored.
Parameters
- $filters : array<string|int, mixed>
- $orderBys : array<string|int, mixed>|null = null
- $columns : array<string|int, mixed>|null = null
Return values
array<string|int, mixed>|nullreportCallerTierGap()
Writes one machine-parsable `API_COLTIER` line to the `auth` channel. The format follows `API_RBAC ...` from {@see logApiPermissionDecision()}, so both parse with one report:
protected
reportCallerTierGap(string $kind, string $action, string $tier, string $label, array<int, string> $fields) : void
API_COLTIER path=internal kind=save key=:Api:Bazaar:Advert:save tier=user \
userId=11 would_strip=insertUserId,user caller=app/UI/Front/.../EditTrait.php:88
NOTE: the line filters and changes nothing; it only says what a future enforcement would
take. An empty set is logged too (would_*=-) as proof that the probe ran there and
nothing would break.
Parameters
- $kind : string
-
saveorupdateColumns - $action : string
-
camelCase literal of the action (to build the RBAC key)
- $tier : string
- $label : string
-
field name in the log:
would_striporwould_drop - $fields : array<int, string>
requireApiPermission()
Guards one Api action. Returns `null` to continue, or an array to return immediately.
protected
requireApiPermission(string $action[, Closure|null $owned = null ][, array<string|int, mixed> $ownedData = [] ]) : ApiPermissionDeniedShape|null
Parameters
- $action : string
-
camelCase literal of the action (
getAll,updateColumns,moveSubtreeTo); nevergetAction()and never kebab-case from the URL - $owned : Closure|null = null
-
lazy entity loader for
ALLOW_IF_OWNER; called only when the resolver returns a conditional grant, so an admindeletedoes not pay a DB query for a check that never runs;nullmeans fail-closed DENY - $ownedData : array<string|int, mixed> = []
-
write payload passed to self::isOwner() for the insert branch (
$entity === null), where it is the only source of the parent identifier; empty for all other actions
Tags
Return values
ApiPermissionDeniedShape|null —exact shape of permissionDeniedPayload() instead of a
generic array<string, mixed>; guarded methods with their own precise @return array{...} union this type into theirs
requireManager()
Returns the manager for the shared CRUD/tree actions.
protected
requireManager() : Manager
Presenters without a manager (login, payment callbacks) do not have these actions. If one
is reached anyway, it throws LogicException naming the presenter instead of an
unhelpful "call on null".
Return values
ManagerresolveApiPresenterName()
Maps `App\UI\Api\Eshop\Presenters\OrderPresenter` to `Api:Eshop:Order`.
protected
static resolveApiPresenterName(class-string $class) : string
Inverse of the mapping in config/common.neon (Api: [App\UI, Api\*, Presenters\*Presenter]);
ApiPermissionCoverageTest verifies it against the live Nette\Application\PresenterFactory.
IMPORTANT: the module is deliberately only one segment. The Nette mapping Api\*\ behaves
oddly for multi-level names (Api:A:B:C maps to App\UI\Api\A\Api\B\Presenters\CPresenter),
so "smart" extra segments would silently build a key that does not exist in the DB.
Fail-closed: an unknown class shape throws and ends as DENY, no guessing.
Parameters
- $class : class-string
Tags
Return values
stringresolveCallerFrame()
Returns e.g. `app/UI/Front/Eshop/Presenters/ProductPresenter.php:88`: the first stack frame outside {@see self::CALLER_SKIPPED_PATHS}, i.e. the first place that really asked for the data.
protected
static resolveCallerFrame() : string
The path is relative to the project root, not a basename: two ProductPresenter.php in two
modules would otherwise merge into one report line.
Return values
stringresolveCallerTier()
Returns the caller tier for this presenter and action: `privileged` or `user`.
protected
resolveCallerTier(string $action) : string
Decided solely by PermissionResolver::isGrantedByPrivilegedRole(),
the same method the external path uses (RbacCheckTrait::resolveExternalFieldTier()),
so there is one decision core. Only the key source differs: the external path uses
$this->rbacKey; the internal path uses static::class plus the literal $action,
because on the bridge getName() is null and getAction() is '', which would build
the key '::'.
Cost: isGrantedByPrivilegedRole() reuses grantingRoleCodes(), which runs from the same
per-request branchCache as decide() in the guard, so it is an isset(). The only extra
query per request is privilegedRoleCodes(), also request-cached. No own cache is added on
purpose: a second cache is a second place to forget invalidation.
Fail-closed to user (the stricter tier): a missing resolver (presenter outside DI) and
any exception.
Parameters
- $action : string
-
camelCase literal of the action, the same one requireApiPermission() gets
Return values
stringresolveExternalFieldTier()
Resolves the field tier of the caller (`#[ExternalAdminOnly]`).
protected
resolveExternalFieldTier() : string
Returns 'privileged' only when the grant for this request ($this->rbacKey, computed
only in checkApiPermission(), never again from the request) comes from a role with
privileged = 1 (the flag decides, never the role name). Called by
SaveTrait::actionSave(), the same place that sets
$meta['__external'].
Fail-closed to 'user':
$rbacKey === null: the gate isoffor the key could not be built; without a decision nobody can be declared privileged.- A caller without a token (guest, even with an application key) cannot hold a privileged role.
- A client whose application key does not allow a privileged role: the resolver drops it
from the caller roles (role ceiling), so the tier falls to
'user'.
In log mode the tier works the same as in enforce: it is an independent second layer of
field protection, not part of the URL/action gate (same as #[ExternalReadonly]).
Return values
stringsendInvalidRequestParameter()
protected
sendInvalidRequestParameter(string $name) : void
Parameters
- $name : string
shouldProduceNotifications()
Returns whether notifications in the bell should be created for this operation.
protected
shouldProduceNotifications() : bool
Admin does not create them; an unknown source does, as a safety net.
Return values
boolverifyOwnership()
Default `owner` check: is the caller the owner of this record? Fail-closed.
protected
verifyOwnership(int $userId) : bool
Same base as self::verifyReceiver(): takes the record id from the request (id for
get/delete, data.id for save), loads the record from the database via
$this->manager->find() and passes only an entity implementing
OwnerAwareInterface whose
getOwnerUserId() is positive and equals $userId. Returns false without an id (an INSERT,
i.e. save without data.id, has no owner; presenters handle it in their own
canUserWrite), without a manager, without a record, without the interface, or with an
owner of null/0 (anonymous record).
Concrete Api presenters override it and delegate to their Api manager (OwnershipCheckTrait); this base applies only to a presenter that does not.
IMPORTANT: never change this to return true, it would open every CRUD presenter at once.
Parameters
- $userId : int
-
ID from the JWT payload, never from the request
Return values
boolverifyReceiver()
Default `receiver` check: is the caller the receiver of this record (who it was sent to, not who created it)? Fail-closed.
protected
verifyReceiver(int|null $userId) : bool
External path: reads only the request identifiers (id) and decides against the database.
The identity comes from the JWT; $userId = null is legitimate (anonymous editor with a
capability). read_key is never taken from outside; that branch exists only on the
internal path (ApiPermissionGuardTrait::isReceiver()).
A concrete presenter may extend it (each domain defines its own receiver).
IMPORTANT: never change this to true.
Parameters
- $userId : int|null
-
ID from the JWT payload,
nullmeans no identity
Return values
boolapiClientContext()
private
apiClientContext() : ApiClientContext|null
Return values
ApiClientContext|nullapiPermissionTokenHint()
Returns the first 12 characters of the Bearer token, in the same shape as `API_RBAC ... token=` from the external gate. On the internal path the `Authorization` header is usually empty, giving `-`.
private
apiPermissionTokenHint() : string
Return values
stringapplyGatewayCheck()
private
applyGatewayCheck(Payment|null $payment, PaymentGate $paymentGate, GatewayCheckResult $check) : int|null
Parameters
- $payment : Payment|null
- $paymentGate : PaymentGate
- $check : GatewayCheckResult
Return values
int|nullapplyResolvedStatus()
The payment status is derived from the transactions and freezes after `COMPLETED`.
private
applyResolvedStatus(Payment $payment, int|null $paymentGateId, GatewayCheckResult $check) : bool
Freezing is what prevents repeated activation: a completed payment no longer changes its status, further events
are written only to the transactions. The one exception one might want, REFUNDED, is deliberately not made: a
refund is handled manually in the gateway portal (mobilniplatby has no refund API at all, for cards it usually
goes through the bank), so it is recorded as a transaction and is traceable, but does not overwrite the status.
Parameters
- $payment : Payment
- $paymentGateId : int|null
- $check : GatewayCheckResult
Return values
bool —false = COMPLETED could not be written to the DB, so the caller must not run the callback
awaitExpressReply()
Stored reply of an express payment; when missing, waits briefly (race: the winner is just activating).
private
awaitExpressReply(int $paymentId) : string|null
null = nothing even after waiting, the caller returns a temporary failure.
Parameters
- $paymentId : int
Return values
string|nullbuildApiPermissionKey()
Builds the key such as `:Api:Eshop:Order:updateColumns` from `getName()` + `getAction()`, never from the URL: Nette `Route::$styles['action']` applies `path2action`, so the URL `update-columns` is `updateColumns` inside the presenter, and the optional `<loc>` route segment is not part of the key.
private
buildApiPermissionKey() : string
Return values
stringbuildExpressFailureReply()
private
buildExpressFailureReply(string $reason, int $paymentId, array<int, PaymentGatePhone> $phones[, string $argument = '' ]) : string
Parameters
- $reason : string
- $paymentId : int
- $phones : array<int, PaymentGatePhone>
- $argument : string = ''
Return values
stringbuildExpressFreeSuffix()
MT number without charging gets `;FREE<level>`; for an MO number (level 0) the operator charged the money when the SMS was sent and the reply carries no level.
private
buildExpressFreeSuffix(array<int, PaymentGatePhone> $phones) : string
Parameters
- $phones : array<int, PaymentGatePhone>
Return values
stringbuildRedirectUrl()
private
buildRedirectUrl(Payment|null $payment, int $status) : string
Parameters
- $payment : Payment|null
- $status : int
Return values
stringcloseInterruptedExpressSms()
Final end of an interrupted payment without activation: optionally marks it failed, leaves a trace in the payment log and in the `payment` channel, and stores the reply with the complaint code (MT: `;FREE<level>` = do not charge) for later replay. HTTP 200, so the aggregator stops repeating.
private
closeInterruptedExpressSms(Payment $payment, int $paymentGateId, string $smsId, string $reason, array<int, PaymentGatePhone> $phones, string $argument, bool $markFailed) : array<string, mixed>
Parameters
- $payment : Payment
- $paymentGateId : int
- $smsId : string
- $reason : string
- $phones : array<int, PaymentGatePhone>
- $argument : string
- $markFailed : bool
Return values
array<string, mixed>continueExpressSms()
Finishes an interrupted express payment from the first missing step (under the claim):
private
continueExpressSms(Payment $payment, PaymentGate $paymentGate, ExpressSmsGatewayInterface $gateway, array<string, mixed> $params, string $smsId) : array<string, mixed>
NEW:processPayment()has not changed the status yet, so nothing was activated. The service is verified again from the SMS text (same rules as the first time: the object exists and is active, number price = service price = payment price); module items only when missing; thenprocessPayment(). A permanent error (object deleted, service canceled, price changed) ends in failed plus a reply with the complaint code (200, so the aggregator stops repeating), never an endless 503.PENDING/COMPLETED/BAD_PRICE:processPayment()already ran; repeating it is safe (the status freezes after completed, the callback is guarded by the claim on the transaction) and catches up a crash between writing the status and the callback. Only the reply is missing.- any other status (failed, canceled...): activate nothing, only the final reply with the complaint code.
Parameters
- $payment : Payment
- $paymentGate : PaymentGate
- $gateway : ExpressSmsGatewayInterface
- $params : array<string, mixed>
- $smsId : string
Return values
array<string, mixed>createdIdForNotification()
Id of a newly created record from the `internalActionSave()` response, or `null` when nothing should be notified: update (`$data` had the id), failure, or a call from the admin.
private
createdIdForNotification(array<string, mixed> $data, array<string, mixed> $response) : int|null
Parameters
- $data : array<string, mixed>
-
payload before saving
- $response : array<string, mixed>
-
response of
internalActionSave()
Return values
int|nullcurrencyMismatchReason()
The currency is verified only where the gateway reports it and where the ISO code of the payment can be found.
private
currencyMismatchReason(Payment $payment, GatewayCheckResult $check) : string|null
$this->currencyManager is already injected in the Api base presenter (CurrencyInitTrait). An unknown payment
currency (missing currency_id) is no reason to fail the payment: the amount already matches and the eshop
additionally guards the currency in EshopPaymentHook::coversOrderTotal().
Parameters
- $payment : Payment
- $check : GatewayCheckResult
Return values
string|nulldoCallback()
The callback also receives the id of the transaction it runs for.
private
doCallback(Payment $payment[, int $transactionId = 0 ][, bool $isDuplicateActivation = false ]) : void
$isDuplicateActivation is passed too: true means that one transaction already activated this payment and this
is a further, different paid transaction (the customer paid twice). The module activates the same way; items that
are already active stack on their own validity, so the period is added again.
Old callbacks need not change their signature: PHP silently drops a surplus positional argument of a userland
function, so handlePayment(Payment $p) keeps working.
Parameters
- $payment : Payment
- $transactionId : int = 0
- $isDuplicateActivation : bool = false
ensurePaymentId()
private
ensurePaymentId(string $url, string $paymentId) : string
Parameters
- $url : string
- $paymentId : string
Return values
stringexpressParam()
private
expressParam(array<string, mixed> $params, string $key) : string
Parameters
- $params : array<string, mixed>
- $key : string
Return values
stringexpressRetryResult()
private
expressRetryResult(string $reason[, int|null $paymentId = null ]) : array<string, mixed>
Parameters
- $reason : string
- $paymentId : int|null = null
Return values
array<string, mixed>extractBearerToken()
private
extractBearerToken() : string|null
Return values
string|nullfailExpressSms()
Error branch: a failed payment with `gateway_payment_id` (traceability + idempotence), without a callback.
private
failExpressSms(PaymentGate $paymentGate, string $smsId, string $reason, array<int, PaymentGatePhone> $phones[, ExpressTarget|null $target = null ][, int|null $objectId = null ]) : array<string, mixed>
Parameters
- $paymentGate : PaymentGate
- $smsId : string
- $reason : string
- $phones : array<int, PaymentGatePhone>
-
numbers matching
shortcode - $target : ExpressTarget|null = null
- $objectId : int|null = null
Return values
array<string, mixed>fieldAudience()
Returns the audience context, or `null` when the presenter did not get it through DI.
private
fieldAudience() : FieldAudienceContext|null
The audience is only a supplement and must never be able to decide access.
Return values
FieldAudienceContext|nullfindExpressCurrencyCode()
private
findExpressCurrencyCode(int|null $currencyId) : string|null
Parameters
- $currencyId : int|null
Return values
string|nullfindExpressPayment()
private
findExpressPayment(int $paymentGateId, string $smsId) : Payment|null
Parameters
- $paymentGateId : int
- $smsId : string
Return values
Payment|nullfindExpressPhoneForTarget()
Number the customer should have sent the SMS to for the service price (hint in the reply).
private
findExpressPhoneForTarget(int $paymentGateId, ExpressTarget $target) : string|null
Parameters
- $paymentGateId : int
- $target : ExpressTarget
Return values
string|nullfindExpressReply()
Stored reply of the payment (`PaymentErrorLog`, `statusText = expressReply`); empty string = not stored yet.
private
findExpressReply(int $paymentId) : string
Parameters
- $paymentId : int
Return values
stringfindShortcodePhones()
private
findShortcodePhones(int $paymentGateId, string $shortcode) : array<int, PaymentGatePhone>
Parameters
- $paymentGateId : int
- $shortcode : string
Return values
array<int, PaymentGatePhone> —gateway numbers with this shortcode (MT has several, one per level)
finishExpressSms()
Step after the saved payment (+ snapshot): module items (only when `$createItems`), then `processPayment()` (activation via callback), then the stored reply. Called by the first processing and by resume, always under the claim {@see \App\UI\Api\System\Models\Managers\PaymentManager::claimExpressSms()}. Repeating `processPayment()` is safe: activation is guarded by the atomic claim on the transaction ({@see \App\UI\Api\System\Presenters\Traits\Helpers\PaymentProcessorTrait::runCallbackForTransaction()}).
private
finishExpressSms(Payment $payment, PaymentGate $paymentGate, string $smsId, ExpressTarget $target, PaymentGatePhone $phone, bool $createItems) : array<string, mixed>
Parameters
- $payment : Payment
- $paymentGate : PaymentGate
- $smsId : string
- $target : ExpressTarget
- $phone : PaymentGatePhone
- $createItems : bool
Return values
array<string, mixed>formatExpressPrice()
private
formatExpressPrice(float $price) : string
Parameters
- $price : float
Return values
stringgetOwnershipRecordId()
Returns the id of the decided record, taken only from the request identifiers: the `id` parameter (get/delete) or `data.id` (save UPDATE). Returns `null` when both are given and differ (ambiguous), when `id` is an array (bulk delete) and for an INSERT.
private
getOwnershipRecordId() : int|null
Return values
int|nullgetPaymentGateSetting()
private
getPaymentGateSetting(PaymentGate $paymentGate, string $key, string $default) : string
Parameters
- $paymentGate : PaymentGate
- $key : string
- $default : string
Return values
stringinitializeCurrency()
private
initializeCurrency(int|string|null $defaultCurr) : void
Parameters
- $defaultCurr : int|string|null
initializeLanguage()
private
initializeLanguage(int|string|null $defaultLang) : void
Parameters
- $defaultLang : int|string|null
interruptedExpressResult()
Processing of the stored payment died with an exception: 503 (the aggregator delivers again and resume continues) plus a trace in the `payment` channel. After the last aggregator attempt (`att` >= {@see EXPRESS_MAX_ATTEMPTS}) no further delivery comes, so the message says so explicitly and the payment stays for manual handling.
private
interruptedExpressResult(Throwable $e, int $paymentGateId, string $smsId, int $paymentId, string $attempt) : array<string, mixed>
Parameters
- $e : Throwable
- $paymentGateId : int
- $smsId : string
- $paymentId : int
- $attempt : string
Return values
array<string, mixed>isAllowedReturnUrl()
Checks a return URL against {@see ReturnUrlPolicy}; also shared by `PaymentExecuteTrait::buildRedirectUrl()`.
private
isAllowedReturnUrl(mixed $url) : bool
Parameters
- $url : mixed
Return values
boolisAlternateTransaction()
Does the gateway report a different transaction than the one stored on the payment?
private
isAlternateTransaction(Payment $payment, GatewayCheckResult $check) : bool
The stored id was created when the transaction was started, i.e. on our initiated path. When the verified one
differs, it is either a second transaction of the same payment (repeated attempt) or a foreign transaction in our
callback; only referenceId decides.
Parameters
- $payment : Payment
- $check : GatewayCheckResult
Return values
boolisExpressPayment()
An express payment is created from an SMS without a form code; a form payment has the `code` from `createPayment()`.
private
isExpressPayment(Payment $payment) : bool
Parameters
- $payment : Payment
Return values
boolisInternalTransport()
Returns true when `Presenter::run()` did not run, i.e. the call is on the internal (bridge) path.
private
isInternalTransport() : bool
IMPORTANT: do not write it as $this->getRequest() === null. In this nette/application
version Presenter::$request is private ?Request $request = null and the final public function getRequest(): Application\Request throws
InvalidStateException('Request is not set, presenter is not running.') when no
request is set, because its return type is not nullable. A naive null comparison would
throw on the internal path.
The catch is narrow (only InvalidStateException) and stands outside the catch-all of the
guard; otherwise "no request" would mean DENY instead of "I am internal". The transport is
decided by framework state, not by a value from the wire.
Return values
boolisPaymentTargetPayable()
Module-level payability ({@see PaymentCallbackRegistry::PAYABILITY_CHECKS}) by the payment `callback_key`.
private
isPaymentTargetPayable(Payment $payment) : bool
Fail-open: no callback, no registered check or an unresolvable service means "nothing to object", the usual guards above/below still apply.
Parameters
- $payment : Payment
Return values
boolisRepeatableCallback()
May the callback of this payment run again for a further paid transaction ({@see PaymentCallbackRegistry::REPEATABLE})?
private
isRepeatableCallback(Payment $payment) : bool
Parameters
- $payment : Payment
Return values
boolkeepSameHostUrls()
Keeps only the return URLs that point to this website. Foreign and nonsensical values are dropped and `resolvePaymentUrls()` fills in the server default instead.
private
keepSameHostUrls(array<string, mixed> $urls) : array<string, mixed>
IMPORTANT: the decision is a positive allowlist (ReturnUrlPolicy), not a host
comparison: parse_url() yields a null host for javascript:, data:, mailto: and
https:evil.example, which would pass as relative.
For the current caller (the front PaymentShowTrait) it is a no-op: it sends absolute links
built by $this->link('//...') on the same host.
Parameters
- $urls : array<string, mixed>
Return values
array<string, mixed>loadLocalization()
private
loadLocalization() : Localization|null
Return values
Localization|nulllocalizationCurrencyKey()
Returns the currency key of the localization's payment region.
private
localizationCurrencyKey(object $localization) : string|null
A localization has no currency of its own. Null means a localization without a region or
a region without a currency; callers fall back to eur.
Parameters
- $localization : object
Return values
string|nulllogApiPermissionDecision()
Writes the `API_RBAC` line to the `auth` channel. The format is identical to {@see RbacCheckTrait::logRbacDecision()} (so both parse with one report), extended with: - `path=internal|external`: tells the two modes apart in one log; - `caller=File.php:123`: makes the report a list of pages instead of a list of keys; - `key_exists=yes|no`: see below.
private
logApiPermissionDecision(string $mode, bool $internal, string $key, string $presenter, string $action, int|null $userId, PermissionDecision $raw, PermissionDecision $effective, bool $denied) : void
mode= is always the mode of this path (internalMode internally, mode externally),
otherwise the report would claim something other than what was just evaluated.
IMPORTANT: key_exists=no separates two cases that decision=DENY merges: "the key exists
in the policy and the caller has no right to it" and "the key is not in
cms_system_user_role_privileges at all" (a typo in the literal, a forgotten seed). A
method that reuses a foreign literal gets ALLOW from decide(), but not for its own
policy. It is computed by PermissionResolver::hasKey(), the
same probe SaveTrait::refineSaveAction() uses. Radar for this class of mistake:
grep 'API_RBAC.*key_exists=no' auth.log.
NOTE: it is not free as in SaveTrait. hasKey() reads the anonymous branch of
branchCache (rowsForKey(null, ...)), while decide() reads the caller with their
$userId; only anonymous callers share the cache. For a logged-in caller it is one small
indexed query, paid only when the line is actually written (log always, enforce only
on DENY), the same cost as caller=.
caller= (i.e. debug_backtrace()) is computed only when the line is written, otherwise
it would be a needless cost on every request. DEBUG_BACKTRACE_IGNORE_ARGS is required:
a backtrace with arguments would put huge objects into the log.
Parameters
- $mode : string
- $internal : bool
- $key : string
- $presenter : string
- $action : string
- $userId : int|null
- $raw : PermissionDecision
- $effective : PermissionDecision
- $denied : bool
logRbacDecision()
Writes one machine-parsable line to the `auth` channel, in a format that follows `JWT_GUARD ...` from {@see JwtAuthTrait::reportJwtAuthFailure()} so that a report can be built from the log.
private
logRbacDecision(string $key, int|null $userId, PermissionDecision $raw, PermissionDecision $effective) : void
key_exists= has the same field and meaning as
ApiPermissionGuardTrait::logApiPermissionDecision(): it distinguishes "key exists,
role denies" from "key is missing in cms_system_user_role_privileges". It is computed via
PermissionResolver::hasKey() only when a line is actually written.
Parameters
- $key : string
- $userId : int|null
- $raw : PermissionDecision
- $effective : PermissionDecision
logRejectedCallback()
private
logRejectedCallback(Payment $payment, string $target) : void
Parameters
- $payment : Payment
- $target : string
matchExpressPrice()
First (target, number) pair with the same currency and price. There are several targets only when the service is offered by several payment regions, so the number price and currency decide the region, not the SMS text.
private
matchExpressPrice(array<int, ExpressTarget> $targets, array<int, PaymentGatePhone> $phones) : PaymentGatePhone}
Parameters
- $targets : array<int, ExpressTarget>
- $phones : array<int, PaymentGatePhone>
Return values
PaymentGatePhone}pauseExpressReplay()
private
pauseExpressReplay(int $milliseconds) : void
Parameters
- $milliseconds : int
payPageFailure()
Failure in the same shape the guard returns (`Result::failure(...)->toArray()`), so the fail-soft boundary recognizes it by one predicate {@see Result::isFailureEnvelope()} and the caller gets "no data" instead of an entity with meaningless fields.
private
static payPageFailure(ResultCode $code, string $message) : array<string, mixed>
Parameters
- $code : ResultCode
- $message : string
Return values
array<string, mixed>permissionDeniedPayload()
Returns the denial shape, identical to what the external gate sends ({@see RbacCheckTrait::checkApiPermission()}), but returned instead of `sendJson()` because the internal path has nowhere to send an HTTP status.
private
static permissionDeniedPayload() : ApiPermissionDeniedShape
Return values
ApiPermissionDeniedShapepersistGatewayPaymentId()
Writes `gatewayPaymentId` from a verified path.
private
persistGatewayPaymentId(Payment $payment, int|null $paymentGateId, GatewayCheckResult $check) : void
Written only for states that mean "the gateway knows the transaction and this is its state". Failed / invalid / unknown also come from a query error or a nonexistent id, so the id could be slipped in through them. A filled different value is never overwritten: it is no longer our transaction and belongs in the log, not in the DB.
Parameters
- $payment : Payment
- $paymentGateId : int|null
- $check : GatewayCheckResult
processExpressSms()
Express payment: creates the payment, the snapshot item and the module items, then runs the standard processing.
private
processExpressSms(PaymentGate $paymentGate, string $smsId, ExpressTarget $target, PaymentGatePhone $phone) : array<string, mixed>
Parameters
- $paymentGate : PaymentGate
- $smsId : string
- $target : ExpressTarget
- $phone : PaymentGatePhone
Return values
array<string, mixed>processFormSms()
Form SMS (`KEYWORD <code>`): `loadPayment()`, `checkPayment()`, `processPayment()`.
private
processFormSms(ExpressSmsCommand $command, string $keyword, PaymentGate $paymentGate, ExpressSmsGatewayInterface $gateway, array<string, mixed> $params, string $smsId, array<int, PaymentGatePhone> $phones) : array<string, mixed>
Parameters
- $command : ExpressSmsCommand
- $keyword : string
- $paymentGate : PaymentGate
- $gateway : ExpressSmsGatewayInterface
- $params : array<string, mixed>
- $smsId : string
- $phones : array<int, PaymentGatePhone>
Return values
array<string, mixed>processStatus()
private
processStatus(Payment $payment, int|null $paymentGateId, GatewayCheckResult $check) : bool
Parameters
- $payment : Payment
- $paymentGateId : int|null
- $check : GatewayCheckResult
Return values
boolproduceNotification()
private
produceNotification(string $typeKey, int $itemId, string $text) : void
Parameters
- $typeKey : string
- $itemId : int
- $text : string
rbacTokenHint()
Returns the first 12 characters of the Bearer token, in the same shape as `JWT_GUARD ... token=`.
private
rbacTokenHint() : string
Read from the header directly so the trait does not depend on private methods of JwtAuthTrait.
Return values
stringrefuseClientKey()
Sends one response for a missing, unknown, disabled, revoked and expired key, so the caller cannot tell which key exists. The reason, client id and key id (`cms_system_api_client_keys`, not the key itself) go only to the log; the key or any part of it never does.
private
refuseClientKey(ApiClientKeyStatus $status, int|null $clientId[, int|null $keyId = null ]) : void
Parameters
- $status : ApiClientKeyStatus
- $clientId : int|null
- $keyId : int|null = null
refuseClientLimit()
Sends 429 with `Retry-After`. Only the first refusal in the window goes to the auth log, so a flood does not flood the log too.
private
refuseClientLimit(ApiClientIdentity $client, ApiClientLimitResult $limit) : void
Parameters
- $client : ApiClientIdentity
- $limit : ApiClientLimitResult
rememberExpressReply()
Stores the reply for replay on a repeated delivery. A failure is only logged and this attempt still answers itself; without a stored reply a later delivery of the same SMS would end in a temporary failure (503).
private
rememberExpressReply(int $paymentId, int $paymentGateId, string $smsId, string $output) : bool
Parameters
- $paymentId : int
- $paymentGateId : int
- $smsId : string
- $output : string
Return values
boolreplayExpressReply()
Repeated SMS (same `id`): returns the original reply (the same level for MT) and processes nothing.
private
replayExpressReply(Payment $payment, PaymentGate $paymentGate, ExpressSmsGatewayInterface $gateway, array<string, mixed> $params) : array<string, mixed>
Parameters
- $payment : Payment
- $paymentGate : PaymentGate
- $gateway : ExpressSmsGatewayInterface
- $params : array<string, mixed>
Return values
array<string, mixed>reportJwtAuthFailure()
private
reportJwtAuthFailure(string $reason, string|null $token) : void
Parameters
- $reason : string
- $token : string|null
resolveCallbackTarget()
IMPORTANT: the callback target is determined only by {@see PaymentCallbackRegistry} by `callback_key`. The DB triple namespace/class/function and a direct `Payment.callback` string are no longer an authority; a direct string passes only when it points to an approved handler. A rejection is logged like an unreachable callback (still fail-open towards the gateway).
private
resolveCallbackTarget(Payment $payment) : array{0: string, 1: string}|null
Parameters
- $payment : Payment
Return values
array{0: string, 1: string}|nullresolveExpressTargetForResume()
Target of the resumed payment, taken from the text of the repeated delivery (the aggregator sends the same parameters). The number is the one whose price and currency equal the payment price (for MT this also gives the level).
private
resolveExpressTargetForResume(ExpressSmsGatewayInterface $gateway, PaymentGate $paymentGate, array<string, mixed> $params, PaymentGatePhone|null $phone) : ExpressTarget, 1: string, 2: int}
Parameters
- $gateway : ExpressSmsGatewayInterface
- $paymentGate : PaymentGate
- $params : array<string, mixed>
- $phone : PaymentGatePhone|null
Return values
ExpressTarget, 1: string, 2: int} —target | failure reason | object id from the text
resolveGuardMode()
Returns the mode for the transport. Never throws: a missing configuration service means the guard is inert (the pre-guard behavior), not deny.
private
resolveGuardMode(bool $internal) : string
$apiRbacConfig can be missing only in a presenter built outside DI (new XPresenter() in
a harness), where no RBAC is evaluated anyway. isset() on an uninitialized typed property
returns false without throwing.
Parameters
- $internal : bool
Return values
stringresolvePaymentUrls()
private
resolvePaymentUrls(Payment $payment, PaymentGate $paymentGate, array<string|int, mixed> $urls) : array{notifyUrl: string, successUrl: string, cancelUrl: string}
Parameters
- $payment : Payment
- $paymentGate : PaymentGate
- $urls : array<string|int, mixed>
Return values
array{notifyUrl: string, successUrl: string, cancelUrl: string}resolveUnsavedExpressSms()
The payment of this SMS was not saved: it either lost the race against another delivery (unique index gateway + gateway_payment_id) or the write failed. Reloads the winner and replays its reply; when the payment does not exist (write failed, winner rolled back) returns a temporary failure, never a permanent rejection: with an MO number the SMS is already paid and the aggregator delivers it again after the 503.
private
resolveUnsavedExpressSms(PaymentGate $paymentGate, string $smsId, string $reason) : array<string, mixed>
Parameters
- $paymentGate : PaymentGate
- $smsId : string
- $reason : string
Return values
array<string, mixed>resumeExpressSms()
Resumes an express SMS whose first processing died (or failed) between saving the payment and saving the reply.
private
resumeExpressSms(Payment $payment, PaymentGate $paymentGate, ExpressSmsGatewayInterface $gateway, array<string, mixed> $params) : array<string, mixed>|null
When the reply is not 200 the aggregator repeats the delivery with a growing interval, so each further delivery of the same SMS finishes the processing from the first missing step:
- Atomic claim (gateway + SMS id), also held by the first processing, so resume never runs concurrently with it
or with another resume;
null= another run holds it (the caller waits or answers 503). - Under the claim the reply is read again: the previous run may have finished meanwhile, then replay it verbatim.
- continueExpressSms(): missing module items,
processPayment(), reply.
An exception means 503 plus a trace in the payment log channel (SMS id only as a digest); the aggregator retries.
Parameters
- $payment : Payment
- $paymentGate : PaymentGate
- $gateway : ExpressSmsGatewayInterface
- $params : array<string, mixed>
-
parameters of the repeated delivery (same text and
shortcode)
Return values
array<string, mixed>|nullrunCallbackForTransaction()
The module callback may run once per transaction.
private
runCallbackForTransaction(Payment $payment, int|null $paymentGateId, GatewayCheckResult $check, int $transactionId) : void
The claim is made atomically (PaymentTransactionManager::claimExecution()), not as read-then-write: two notifications about the same transaction arrive even concurrently (a gateway retry overlaps with the customer return) and would otherwise both start the activation. A repeated notification of the same transaction therefore never activates anything again.
A further, different paid transaction of the same payment (another id at the gateway) means the money came again: a tariff callback (PaymentCallbackRegistry::REPEATABLE) activates the payment again and the period is added (credits too); other callbacks (an e-shop order) run once per payment. Such a transaction must carry its gateway id; without it it cannot be told from a replay of the one that already activated, so it is only logged. A paid payment the administrator closed afterwards (canceled/voided, frozen for the gateway) activates nothing more.
Parameters
- $payment : Payment
- $paymentGateId : int|null
- $check : GatewayCheckResult
- $transactionId : int
saveExpressPayment()
Saves the (failed) payment; when two attempts of the same SMS race (unique index gateway + gateway_payment_id) returns null and the caller replays the winner reply ({@see resolveUnsavedExpressSms()}).
private
saveExpressPayment(Payment $payment) : int|null
Parameters
- $payment : Payment
Return values
int|nullsaveExpressPaymentWithSnapshot()
Saves the payment and the accounting snapshot (`cms_system_payment_items`) in one DB transaction.
private
saveExpressPaymentWithSnapshot(Payment $payment, ExpressTarget $target, PaymentGatePhone $phone) : int|null
The invoice prefers the snapshot over the live price list; the price is the number price (final, the gateway has
final_prices), the rate is computed by the invoice (tax_treatment null, the engine resolves it for the date).
Unlike a form payment it is not fail-open: a snapshot failure rolls the payment back too (null) and nothing is activated.
Parameters
- $payment : Payment
- $target : ExpressTarget
- $phone : PaymentGatePhone
Return values
int|nulltoSmsText()
private
toSmsText(string $text) : string
Parameters
- $text : string
Return values
stringverifyAmount()
Verifies the amount and the currency against `payment.total_price`.
private
verifyAmount(Payment $payment, PaymentGate $paymentGate, GatewayCheckResult $check) : GatewayCheckResult
Only the completed state is checked (the only one meaning "paid"); other states claim nothing about money. A mismatch gives PaymentStatus::BAD_PRICE: the payment is saved and the callback runs, but as unpaid, so an eshop order does not jump to paid.
A gateway that reports no amount at all also gives bad price (fail-closed). Escape hatch without a code change:
require_amount[0] in the gateway settings; then an unreported amount is only logged and the payment passes. A
mismatch of a reported amount is always bad price, even with require_amount off.
Parameters
- $payment : Payment
- $paymentGate : PaymentGate
- $check : GatewayCheckResult