lamoda/payture-inpay-bundle
Symfony bundle for integrating the Payture InPay payment API, with bundle configuration and service wiring that make Payture payment operations easier to use in Symfony-based PHP applications.
This package is a thin Laravel wrapper around payture-inpay-php-client, exposing the Payture InPay merchant API (session initialisation, 3-D Secure completion, state queries, refunds, and card registration/charging) through config-driven bindings and, usually, a facade.
Minimal path to first payment:
composer require <vendor>/laravel-payture-inpay (v1.2 now pulls payture-inpay-php-client v1.2).php artisan vendor:publish --tag=payture-config..env — merchant KEY, PASSWORD, and the API host. Keep sandbox and production hosts in separate env blocks; never share keys across environments.GetSessionState and persist the result.Where to look first:
config/payture.php for host/terminal/key wiring and any timeout settings.vendor/payture/... for the v1.2 client itself — the release notes only say the client was bumped to v1.2, so diff the vendored client against what you previously ran to spot renamed response fields, new methods, or changed signatures before writing code against it.The first real use case is almost always a one-off redirect payment for an order. Get that working end-to-end in sandbox with the provider's test cards before touching refunds or recurring charges — those require the password credential and are the parts most likely to differ in client v1.2.
Persist before you redirect. Create your own payments row (order id, session id, amount, status, raw response) before sending the user to Payture. The session id is your only handle for everything that follows; losing it means an orphaned payment you can only resolve by reconciliation.
Order ids. Generate a unique, URL-safe, alphanumeric order id per attempt and store it. Do not reuse an order id across retries — the gateway will reject or conflate attempts. Keep a separate human-facing order number if you need one.
Never trust the return URL. The browser lands back with query parameters that are trivially forgeable. Treat the return purely as a trigger to call GetSessionState(sessionId) (or the client v1.2 equivalent) server-side, then transition your payment record based on that response. This is also where you fulfil the order — inside a DB transaction with lockForUpdate() on the payment row so a duplicate browser refresh or a concurrent poll cannot double-fulfil.
Wrap the client, don't scatter it. Bind the client in a small PaytureGateway service that maps the client's response objects into your own value objects (PaymentResult, RefundResult). When the underlying client changes shape — which is precisely what a v1.2 bump risks — you fix one class instead of every controller.
Refunds and partial refunds. These need the merchant password in addition to the key. Amounts are in minor units, so refunding 100.00 RUB means 10000. Queue refunds rather than doing them inline in a request: they are slow, they can fail transiently, and you want a retry budget plus a refunds table recording request/response per attempt. Refund against the original order id, and cap cumulative refunds against the captured amount in your own code — don't rely on the gateway to reject over-refunds gracefully.
3-D Secure. Handle the two-phase flow explicitly: initial call returns a redirect to the ACS, then you submit the MD/PaRes pair back through the client's pay/3DS method. Model this as a distinct state (requires_3ds) rather than a special case inside the success branch. Keep the ACS return URL deterministic per order so a lost session doesn't strand the user.
Recurring / card-on-file. Use the register-then-charge pair; store only the returned card token, never the PAN. Treat the token as sensitive: encrypt at rest and keep it out of logs and serialised API responses.
Reconciliation job. Schedule a command that re-queries GetSessionState for payments stuck in pending/New older than N minutes. Webhooks and browser returns both get lost; polling is the backstop that keeps your ledger honest. Make the job idempotent — it will race with the return-URL handler.
Testing. Point the config at the sandbox host, use the provider's published test cards, and for unit tests bind a fake gateway (or Http::fake() if the client is Guzzle-based) so CI never hits the network. Assert on your own value objects, not on the client's DTOs.
199.00 where 19900 is expected, or the reverse on refunds. Enforce integers in a money value object and format only at the presentation layer. Never put a float near this API.Success=false on refund while payments work fine is almost always a missing password, not a bad key.200 OK with Success=false and an ErrCode. If the client v1.2 doesn't throw, your try/catch is decorative. Check the success flag explicitly in the gateway and map ErrCode values to your own exception types so callers can distinguish "declined" from "malformed request" from "gateway down".composer.lock, run the full suite, and exercise one sandbox payment and one sandbox refund manually. A transitive client bump is exactly the class of change that passes CI and fails in production.Init is not idempotent; a retried timeout can create two sessions for one order and confuse reconciliation. Retry only state queries.sessionId on every branch, and always reproduce with the sandbox host first. Most "gateway is broken" tickets resolve to a wrong host, a trailing slash in the host URL, or an amount-unit mistake.How can I help you explore Laravel packages today?