cpr / laravel-cecabank
Cecabank TPV integration for Laravel — models, signature engine, polymorphic payment transactions, events and webhook routes. Frontend-agnostic: ship your own Blade/Vue/React UI.
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
README
Cecabank TPV (Virtual POS) integration for Laravel — frontend-agnostic.
The package ships the data model, signature engine, polymorphic transaction log, lifecycle events and public callback routes. Everything user-facing (admin CRUD, redirect page, sandbox UI) is the host's job: render it with Blade, Inertia + Vue, Livewire, React… the package only hands you DTOs.
Security at a glance
- Server-to-server callback authenticated by Cecabank's SHA-256 signature, verified with
hash_equals. - Browser return URLs authenticated by a TTL'd HMAC token bound to the operation number (
config('cecabank.return_token.ttl'), default 30 min). - All state transitions run inside
DB::transaction { lockForUpdate; … }— concurrent callbacks cannot double-fulfil. payable_type/payable_idare intentionally NOT mass-assignable; usePaymentTransaction::attachPayable($payable).- The provider refuses to boot if
cecabank.urls.{test,production}aren'thttps://to a.ceca.eshost. - The callback route lives OUTSIDE the
webmiddleware group so CSRF can never reject a legitimate Cecabank confirmation.
See SECURITY-AUDIT.md for the full third-party review and the patches that addressed it.
Install
composer require cpr/laravel-cecabank php artisan vendor:publish --tag=cecabank-config php artisan migrate
Optionally:
php artisan vendor:publish --tag=cecabank-views # Blade auto-submit fallback php artisan vendor:publish --tag=cecabank-lang # Flash-message translations
Make your host model payable
use Cpr\Cecabank\Contracts\Payable; use Cpr\Cecabank\Models\PaymentTransaction; class Order extends Model implements Payable { public function paymentAmount(): float { return (float) $this->total; } public function paymentReference(): string { return $this->order_number; } public function paymentDescription(): ?string { return "Order {$this->order_number}"; } public function isPayable(): bool { return $this->status === 'pending_payment'; } public function paymentSuccessRoute(): string { return 'orders.index'; } public function paymentFailureRoute(): string { return 'orders.index'; } public function paymentTransactions() { return $this->morphMany(PaymentTransaction::class, 'payable'); } }
Start a checkout
Cecabank::checkout() persists a pending PaymentTransaction and returns a
CheckoutPayload DTO with the fields and gateway URL. You decide how to
render the redirect.
With Inertia + Vue
use Cpr\Cecabank\Facades\Cecabank; public function pay(Order $order) { abort_unless($order->isPayable() && $this->ownsOrder($order), 403); $payload = Cecabank::checkout($order); return Inertia::render('Payment/Redirect', [ 'fields' => $payload->fields, 'gatewayUrl' => $payload->gatewayUrl, 'reference' => $order->paymentReference(), ]); }
With Blade (using the bundled view)
return view('cecabank::redirect', [ 'fields' => $payload->fields, 'action' => $payload->gatewayUrl, 'title' => 'Redirigiendo a la pasarela…', ]);
As JSON (SPA / API)
return response()->json($payload->toArray());
React to payment lifecycle events
use Cpr\Cecabank\Events\{PaymentCompleted, PaymentFailed, PaymentCanceled}; Event::listen(PaymentCompleted::class, function ($e) { $e->payable?->update(['status' => 'confirmed']); });
Routes the package owns
| Name | Verb | URI | Purpose |
|---|---|---|---|
cecabank.success |
GET/POST | /payment/success |
URL_OK browser return — redirects to Payable::paymentSuccessRoute() |
cecabank.failure |
GET/POST | /payment/failure |
URL_NOK browser return — redirects to Payable::paymentFailureRoute() |
cecabank.callback |
POST | /payment/callback |
Server-to-server confirmation — returns $*$OKY$*$ / $*$NOK$*$ |
URI prefix and middleware are configurable in config/cecabank.php.
Admin CRUD & sandbox
Out of scope for this package. You own the gateway CRUD UI; use
Cpr\Cecabank\Models\PaymentGateway directly. For sandbox flows the package
exposes:
$payload = Cecabank::sandboxCheckout($gateway, 1.00, 'demo', 'test'); $preview = Cecabank::previewSandboxSignature($gateway, 1.00, 'test'); $tx = Cecabank::reconcileSandboxReturn($operationNumber, $request->all(), success: true);
Wire your own admin sandbox return routes and point the package at them:
// config/cecabank.php 'sandbox_return_routes' => [ 'ok' => 'admin.cecabank.sandbox.return-ok', 'nok' => 'admin.cecabank.sandbox.return-nok', ],
Service API quick reference
All methods are exposed through the Cecabank facade
(Cpr\Cecabank\CecabankService):
Cecabank::checkout($payable, ?$gateway = null): CheckoutPayload Cecabank::sandboxCheckout($gateway, $amount, ?$description = null, ?$environment = null): SandboxPayload Cecabank::previewSandboxSignature($gateway, $amount, ?$environment = null): array Cecabank::reconcileSandboxReturn($operationNumber, $params, $success): ?PaymentTransaction Cecabank::verifyCallbackSignature($params, $gateway, ?$environment = null): bool Cecabank::sanitizeResponse($params): array Cecabank::calculateSignature($data): string Cecabank::returnToken($operationNumber): string Cecabank::verifyReturnToken($operationNumber, $token): bool Cecabank::amountToCents($amount): string
Testing
composer install vendor/bin/phpunit