x-laravel / payline-hoppa-gateway
Hoppa payment gateway for x-laravel/payline
Package info
github.com/x-laravel/payline-hoppa-gateway
pkg:composer/x-laravel/payline-hoppa-gateway
Requires
- php: ^8.3
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- x-laravel/payline: dev-master
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-28 22:49:51 UTC
README
Hoppa payment gateway for x-laravel/payline.
Requirements
- PHP ^8.3
- Laravel ^12.0 | ^13.0
- x-laravel/payline ^2.0
Installation
composer require x-laravel/payline-hoppa-gateway
Configuration
Add the hoppa block to config/payline.php under gateways:
'gateways' => [ 'hoppa' => [ 'merchant_id' => env('HOPPA_MERCHANT_ID'), 'merchant_key' => env('HOPPA_MERCHANT_KEY'), 'three_ds_session_minutes' => env('HOPPA_3DS_SESSION_MINUTES', 30), 'blocking_days' => env('HOPPA_BLOCKING_DAYS'), ], ],
Set the corresponding environment variables in .env:
PAYLINE_GATEWAY=hoppa PAYLINE_TEST_MODE=true HOPPA_MERCHANT_ID=your-merchant-id HOPPA_MERCHANT_KEY=your-merchant-key
Test and Live
The gateway ships both Hoppa addresses and picks between them with payline.test_mode:
test_mode |
Host |
|---|---|
true |
https://posservicetest.esnekpos.com |
false |
https://posservice.esnekpos.com |
PAYLINE_TEST_MODE defaults to false, so an installation that never sets it talks to
the live one. The test environment issues its own merchant identifier and key, so
switching the flag also means switching those values.
A base_url in the hoppa block wins over both. It carries a scheme and a host only;
the gateway appends its own paths.
Usage
Charging a payment
use XLaravel\Payline\DTOs\Card; use XLaravel\Payline\DTOs\PaymentRequest; $data = PaymentRequest::fromPayable( payable: $order, card: new Card( holderName: 'John Doe', number: '4111111111111111', expiryMonth: '12', expiryYear: '2030', cvv: '123', ), installments: 1, customerIp: $request->ip(), ); $response = $order->pay('hoppa')->charge($data);
Hoppa uses a 3DS redirect flow. On success, pay() returns a PaymentResponse with status = Pending and a redirectUrl pointing to Hoppa's 3DS page:
if ($response->requiresRedirect()) { return redirect($response->redirectUrl); }
Handling the callback
Payline handles the callback automatically via its built-in route (/payline/callback/hoppa). After 3DS completes, Hoppa POSTs to this URL and the customer is sent on according to payline.routes.callback_response.
The gateway does not take the outcome from that POST. Hoppa's callback carries a HASH, but the algorithm behind it is not published — the integration document tells merchants to ask support for it. The gateway therefore reads the order reference from the callback and asks /api/services/ProcessQuery what actually happened, so a forged POST cannot settle a payment. This costs one extra request per callback.
The order reference is generated per attempt rather than taken from the merchant reference, because Hoppa caps ORDER_REF_NUMBER at 24 characters and rejects a reference it has already seen. It is recorded as gateway_transaction_id, and refunds and reconciliation are keyed on it.
You can listen to the dispatched events for any post-payment logic:
use XLaravel\Payline\Events\PaymentSucceeded; use XLaravel\Payline\Events\PaymentFailed; class HandlePaymentSucceeded { public function handle(PaymentSucceeded $event): void { $event->payment; // Payment model $event->transaction; // Transaction model $event->response; // PaymentResponse DTO } }
Refund
use XLaravel\Payline\Facades\Payline; Payline::payment($payment)->refund(amount: 5000);
Payline finds the sale itself. The amount is in minor units, and an idempotencyKey makes
a retry safe.
Cancelling
Payline::payment($payment)->void();
Hoppa cancels through the same /api/services/OrderReturn endpoint it refunds through. There
is no field that names the operation: a reversal that returns the whole order amount is
recorded as a cancellation, a smaller one as a refund. Partial refunds accumulate, so the
reversal that brings the returned total up to the order amount is the one that cancels it.
void() therefore sends the full amount of the sale and reports voided, while refund()
sends the amount it was given and reports successful. Both reach the same endpoint.
Hoppa records the outcome as its own entry under TRANSACTIONS, named İptal - Başarılı for
a cancellation and İade - Başarılı for a refund, each carrying its amount. The gateway totals
both into PaymentResponse::$refundedAmount, because a cancellation returns money just as a
refund does.
Reconciliation
Payline::payment($payment)->reconcile();
php artisan payline:reconcile --gateway=hoppa
queryPayment() reads /api/services/ProcessQuery and derives the status from the STATUS_NAME of each entry in TRANSACTIONS:
STATUS_NAME |
Result |
|---|---|
İptal - Başarılı |
voided, and its amount counts as returned |
Ödeme - Başarılı |
successful |
Ödeme - Başarısız |
failed |
Ödeme - Bekliyor |
pending |
| anything else | unknown |
The order status PAYMENT_WAITING means the same as the last row and is read as well. An
order whose customer has not come back from the 3D Secure page answers with both:
{ "STATUS": "PAYMENT_WAITING", "RETURN_CODE": "106", "RETURN_MESSAGE": "Ödeme - Bekliyor",
"TRANSACTIONS": [{ "STATUS_NAME": "Ödeme - Bekliyor", "AMOUNT": "-1,00" }] }
Hoppa sends that same answer while the customer is still on the page, so the gateway reports
it as pending and Payline settles it as expired once the transaction passes the deadline
set from three_ds_session_minutes. An order Hoppa cannot find answers RETURN_CODE 400
with a null TRANSACTIONS, which stays unknown.
İade - Başarılı does not change the status of the sale, since a refund is its own
transaction in Payline. It surfaces as refund_state on the metadata, either refunded or
none, and its AMOUNT values are totalled into PaymentResponse::$refundedAmount so
Payline can settle an open refund. Amounts arrive in Turkish notation and carry a sign that
depends on the direction, so they are parsed as -1.250,00 and taken as absolute values;
when one of them cannot be read the gateway reports no total rather than a wrong one.
Currencies
Hoppa takes PRICES_CURRENCY on the payment request and accepts TRY, USD, EUR and
GBP, but it never reports a currency back: neither the EYV3DPay answer, nor the callback
POSTed to BACK_URL, nor the ProcessQuery response carries one. The gateway therefore
leaves PaymentResponse::$currency null after a callback rather than claiming TRY, and
uses PaymentQuery::$currency when reconciliation names the currency it is asking about.
BIN Lookup
Hoppa's BIN service resolves a card family and card type from the first eight digits of a card number, which is what Payline's commission routing needs. It lives in its own package, because a merchant charging through another gateway can use it just as well:
composer require x-laravel/payline-hoppa-bin-lookup
Commission Rates
php artisan payline:sync-rates --gateway=hoppa --dry-run php artisan payline:sync-rates --gateway=hoppa
commissionRates() reads /api/services/GetInstallments, which returns a rate per card
family and installment count. The provider reports the rate as a fraction, 0.0275 for a
single installment, and Payline stores a percentage, so the gateway multiplies by a hundred.
A family reported as * becomes a wildcard row.
The listing carries no settlement delay, so blocking_days comes from the gateway entry
and is written onto every rate the sync stores. It is the number of days Hoppa holds a
payment before the money reaches you, and it belongs to your agreement rather than to
this package, so nothing is assumed when the key is absent: the rates are stored without
it and ranking prices them on the commission alone.
'blocking_days' => 14,
Payline turns that into a cost through routing.cost_of_capital. A gateway that holds
the money nine days longer than another is not free, and a lower commission can lose to
a shorter wait once the difference is priced.
Supported Operations
| Operation | Supported | Notes |
|---|---|---|
| Pay (3DS) | ✓ | Redirects to Hoppa's 3DS page |
| Refund | ✓ | Partial or full, through /api/services/OrderReturn |
| Reconcile | ✓ | /api/services/ProcessQuery, keyed on the order reference |
| Authorize | ✗ | Hoppa takes no authorizations |
| Capture | ✗ | Follows from the above |
| Void/Cancel | ✓ | Full amount through /api/services/OrderReturn; Hoppa records it as a cancellation |
| Webhooks | ✗ | Hoppa uses a callback-only flow |
Amounts are sent as lira with two decimal places: Payline's 10050 in the minor unit leaves as 100.50. The basket, when the request carries one, is sent as the Product group.
The gateway declares the table above through ProvidesGatewayCapabilities, so commission
routing skips it for a request it cannot take. Credit and debit cards are both accepted,
and the currencies are the four PRICES_CURRENCY values the provider documents.
Testing
# Build first (once per PHP version) DOCKER_BUILDKIT=0 docker compose --profile php83 build # Run tests docker compose --profile php83 up docker compose --profile php84 up docker compose --profile php85 up
Or directly:
composer test
License
This package is open-sourced software licensed under the MIT license.