CoolCMS API

PaymentExpressExecuteTrait

Receives SMS from the mobilniplatby.cz aggregator on one fixed address.

api/system/payment/express-execute?pg=mobilniplatby&secret=<secret> plus the aggregator parameters (timestamp, phone, sms, shortcode, country, operator, att, id). Two SMS shapes:

  • express, KEYWORD MODULE SERVICE ID: the payment does not exist yet. The module (provider from ExpressServiceRegistry) verifies the service and the object, the payment is created here and goes through the same processing as a form payment (PaymentProcessorTrait::processPayment() with a synthetic GatewayCheckResult): callback, activation, notification, invoice by cron.
  • form, KEYWORD <code>: the payment was created first by the payment form: loadPayment() then checkPayment().

Rules:

  • The origin is proven by the shared secret (fail-closed); a rejection creates and changes nothing.
  • The price comes only from PaymentGatePhone by shortcode and must equal the tariff (quick price) price.
  • Idempotent by the aggregator id (it repeats up to 12 times with a growing att): payments.gateway_payment_id plus a unique index (gateway, gateway_payment_id). A repeat returns the original reply and processes nothing.
  • The reply is always text/plain, HTTP 200, non-empty; an MT number appends ;<level>, or ;FREE<level> when not charged. The only exception is a temporary failure (payment and snapshot not saved, or the reply of the race winner not stored yet): HTTP 503, so that the aggregator delivers the SMS again.
  • The payment is created atomically with the accounting snapshot (PaymentManager::saveWithSnapshotItems()).
  • Processing is resumable: if it dies between saving the payment and the reply, a repeated delivery finishes it from the first missing step under an atomic claim (gateway + SMS id), see resumeExpressSms(). Module items are created only when missing, activation runs at most once.
  • Every error branch creates a failed payment with gateway_payment_id: the operator has already charged the money for an MO number, so a complaint must be traceable.

MT numbers (level > 0): the payment stays pending and the delivery receipt completes it (delivery, pairing request = gateway_payment_id), same as for a form SMS.

Table of Contents

Constants

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.

Methods

actionExpressExecute()  : void
internalActionExpressExecute()  : array<string, mixed>
awaitExpressReply()  : string|null
Stored reply of an express payment; when missing, waits briefly (race: the winner is just activating).
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.
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):
expressParam()  : string
expressRetryResult()  : array<string, mixed>
failExpressSms()  : array<string, mixed>
Error branch: a failed payment with `gateway_payment_id` (traceability + idempotence), without a callback.
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
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.
isExpressPayment()  : bool
An express payment is created from an SMS without a form code; a form payment has the `code` from `createPayment()`.
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
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()`.
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.
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).
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.
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

Constants

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_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_PENDING

private string EXPRESS_REPLY_PENDING = '%s pro %s bude aktivovano po potvrzeni platby. Dekujeme.'

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.'

Methods

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)

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|null

buildExpressFailureReply()

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
string

buildExpressFreeSuffix()

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
string

closeInterruptedExpressSms()

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; then processPayment(). 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>

expressParam()

private expressParam(array<string, mixed> $params, string $key) : string
Parameters
$params : array<string, mixed>
$key : string
Return values
string

expressRetryResult()

private expressRetryResult(string $reason[, int|null $paymentId = null ]) : array<string, mixed>
Parameters
$reason : string
$paymentId : int|null = null
Return values
array<string, mixed>

failExpressSms()

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>

findExpressCurrencyCode()

private findExpressCurrencyCode(int|null $currencyId) : string|null
Parameters
$currencyId : int|null
Return values
string|null

findExpressPhoneForTarget()

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|null

findExpressReply()

Stored reply of the payment (`PaymentErrorLog`, `statusText = expressReply`); empty string = not stored yet.

private findExpressReply(int $paymentId) : string
Parameters
$paymentId : int
Return values
string

findShortcodePhones()

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
string

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>

isExpressPayment()

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
bool

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}

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>

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
bool

resolveExpressTargetForResume()

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

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:

  1. 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).
  2. Under the claim the reply is read again: the previous run may have finished meanwhile, then replay it verbatim.
  3. 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>|null

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|null

saveExpressPaymentWithSnapshot()

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|null
On this page

Search results