Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Sisp Laravel Package

akira/laravel-sisp

A Laravel package for integrating SISP into your applications. It provides configuration, service bindings, and convenient helpers for common SISP workflows, helping you connect and interact with SISP services with minimal boilerplate and clean, maintainable code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, publish, migrate

composer require akira/laravel-sisp:^3.0

php artisan vendor:publish --provider="Akira\Sisp\SispServiceProvider"
php artisan migrate

v3 ships more than config: migrations, routes, a callback controller, console commands, models (transactions, invoices, refunds) and events. Publish and run migrations before anything else — the callback writes transaction rows, and the refund ledger lives in the database. Skipping migrate gets you a callback that 500s on the first real payment.

Start with four things, in this order:

  1. config/sisp.php — POS ID, POS auth code, merchant ID, endpoint URLs, currency, callback/return routes, rate-limit settings, encryption settings.
  2. src/SispServiceProvider.php — what is auto-registered: facade alias, callback routes, rate limiters, migrations, commands, event listeners.
  3. php artisan route:list --path=sisp — confirms the callback URL the package actually exposes. Do not hand-roll your own callback controller until you've seen this.
  4. The package's own models and events (src/Models, src/Events) — in v3 the package owns payment persistence, so your integration point is usually an event listener, not a controller.

Minimum .env for a first run:

SISP_POS_ID=...
SISP_POS_AUTH_CODE=...
SISP_MERCHANT_ID=...
SISP_URL=https://sandbox.sisp.cv/...
SISP_CALLBACK_URL=https://your-app.test/sisp/callback

Then clear the config cache — this trips up almost everyone on a fresh install:

php artisan config:clear

First use case: pay for an order.

Generate your own merchant reference and store it on your order before redirecting. This is now mandatory in practice: v3 changed the default reference to be unpredictable, so if you rely on the package-generated default you have nothing to reconcile against.

public function checkout(Order $order)
{
    $order->update([
        'sisp_reference' => $reference = "order-{$order->id}-" . Str::ulid(),
    ]);

    $payment = Sisp::payment()
        ->amount($order->totalInMinorUnits())   // see Gotchas — thousandths
        ->reference($reference)
        ->create();

    return redirect($payment->url());
}

Get that single round-trip working in the sandbox before anything else: redirect out, pay, land back on the package's signed result page, and see a transaction row appear in the database with a status derived from SISP's message_type.

Implementation Patterns

Let the package persist; use the reference as the join key. v3's callback validates the fingerprint, writes the transaction, maps status from SISP's documented message-type table, and cancels the transaction on the user-cancelled branch. Don't duplicate that. What you own is the link between your order and their transaction row:

// order.sisp_reference is written at checkout, so the lookup is deterministic
$transaction = $order->sispTransaction; // or: Transaction::where('merchant_ref', $order->sisp_reference)->first()

If you already maintain your own transactions table from a v2 integration, decide whether it stays as a projection or goes away. Two writes from two sources of truth is how you get mismatched statuses at 2am.

Hook side effects through events/jobs, not by editing the callback. The callback is user-facing and rate limited; anything beyond the state update (email receipt, invoice PDF, ERP sync, stock decrement) belongs in a queued listener:

Event::listen(TransactionPaid::class, fn ($e) => SendReceipt::dispatch($e->transaction));

Make every listener idempotent and replay-safe — callbacks retry.

Serve the return URL through the package's signed link. v3 signs the payment result page link (path-signed, with unsignable amounts refused). Do not construct result URLs yourself with route(); anything unsigned is rejected. If you need your own post-payment page, redirect to it from the signed page or from an event, and sign your own routes with URL::temporarySignedRoute().

Cancellations are a real branch now, not an edge case. v3 records the transaction on customer cancel, ignores cancellations whose reference isn't a string, rejects empty merchant ref/session before cancelling, and throttles the cancellation branch per IP under both spellings of the route. Test the "user clicks cancel/back" path explicitly — it used to be the path everyone forgot.

Wrap state transitions and expect refusals. Cancelling now refuses failed and refunded transactions and takes a row lock; refunding moves the invoice to refunded and cancelling moves the invoice to cancelled. So:

DB::transaction(function () use ($transaction) {
    $transaction->cancel();   // throws on failed/refunded, or on a concurrent transition
});

Catch the package's domain exceptions and translate them to a 409/422. Don't try/catch around the redirect path — the lock only helps if the exception surfaces before you show "cancelled" to a user.

Refunds need an idempotency key, and the payload is validated. v3 rejects blank or oversized keys, refuses amounts it can't express in thousandths, refunds a retried request with the same key exactly once, and keeps the ledger atomic and encrypted. Generate the key from your own refund record's UUID — never from now() or a random value per attempt, or retries will double-refund.

Sisp::refund($transaction, amount: $cents, idempotencyKey: $refund->uuid);

Don't double-handle expiry. sisp:expire-pending asks SISP before expiring and keeps a transaction pending when SISP can't answer; --older-than must be a whole number of days and not below 1. Schedule it and stop writing your own "pending > 30 min = failed" reaper — that rule now contradicts the package's behaviour and can cancel orders SISP considers live.

Prune under the package's rules. prune-request-payloads uses a durable pruned_at column and a cursor, takes a row lock, marks undecryptable payloads so it can't stall, and skips unreadable rows. Let it own request-payload retention rather than adding your own DELETE against the table.

Wrap the facade behind your own contract. Still a small package. Define PaymentGateway with createPayment(), refund(), cancel() and bind the package implementation in a service provider, but keep the package's domain exceptions in the signatures so callers can react to refusals.

Multi-POS / multi-tenant: resolve credentials at runtime and override config before the facade singleton is resolved (middleware or a factory), otherwise you'll hold stale credentials:

config(['sisp.pos_id' => $tenant->sisp_pos_id]);

Reconciliation as a scheduled command. The package can ask SISP about a single pending transaction, but it can't compare your whole day against their statement. Keep the daily job: pull the SISP export, diff against transactions still non-terminal, and alert on anything older than a business day.

Testing: Http::fake() for the outbound call if the package uses the Laravel client; for the callback, post a sandbox payload you captured (both success and refusal variants — they use different fingerprint formulas) and assert the resulting status/invoice state. Test the 429 path by hammering the cancellation route. For signed result links use $this->get(URL::signedRoute(...)) rather than string-building the URL. Don't reimplement any fingerprint in tests.

Gotchas and Tips

The default reference is now unpredictable — this is the upgrade-breaking change. If v2 code implicitly used the package's merchant reference as your order identifier, v3 silently breaks reconciliation: every payment gets a fresh opaque value that maps to nothing. Pass ->reference() explicitly, derived from your own data, and backfill a column for it before deploying. This is also why the unique index on your reference matters more than ever.

Amount units are thousandths. The refund guard rejects "amounts too large to express in thousandths", which tells you the wire format is an integer scaled by 1000, not cents. Store money as an integer in one unit and convert at exactly one boundary; mixing cents (×100) with thousandths (×1000) undercharges or overcharges by 10×. Assert against a sandbox response, not intuition.

Success and error callbacks use different fingerprint formulas. v3 introduced a separate error-fingerprint formula for refused responses (and for sandbox error payloads), and stopped classifying refused-but-valid callbacks as tampering. Never write if (! $package->verify(...)) abort(403); and assume that covers both branches — use the package's verification, and treat "invalid fingerprint" and "valid fingerprint, refusal" as distinct outcomes in your logs and metrics. Invalid fingerprints are rejected without writing anything, so a missing row is expected for tampering, not a bug.

Rate limits will bite during retry storms. The callback route is rate limited; the cancellation branch is throttled per IP and covers both route spellings. A gateway retrying a burst, or several customers behind one NAT, can produce 429s that look like outages. Check the limiter config in config/sisp.php, log 429s explicitly, and remember a 429 to SISP usually means "retry later" — make sure the transaction stays reachable.

Signed result links fail closed. The result page link is path-signed and refuses unsignable amounts. Symptoms: 403 on the return page, or a missing return URL in support tickets. Don't proxy/rewrite the query string — the signature covers it. If you sit behind a proxy, make sure the app sees the right scheme/host so URL::signedRoute verification agrees.

CSRF and the shipped callback route. The package registers the callback, so it should already be excluded — but if you add your own post-payment route, exclude it in bootstrap/app.php ($middleware->validateCsrfTokens(except: [...])) or VerifyCsrfToken::$except on Laravel 10. Symptom is a 419 in the logs and an order stuck at pending.

Encryption is load-bearing in v3. Refund ledger entries and other attributes are encrypted, and transaction change logs redact them. Encrypted attributes can become undecryptable — the prune commands explicitly "survive undecryptable payloads" and mark them, which means this happens in the wild. Rotating APP_KEY without a re-encryption plan, or sharing a database across environments with different keys, turns rows into permanent garbage. Test key rotation in staging before you need it.

The refunds migration copies data and is resumable/idempotent. Safe to re-run if it dies mid-flight, and it keeps the foreign key indexed on rollback — but it's a data copy, so run it in a quiet window and watch the runtime on large tables. Back up before migrating to 3.0.

Renamed refund operation. The refund entry point was renamed to describe what it does (validation and idempotency landed in the same pass). Grep your codebase for the v2 name; there's no deprecation shim in a major.

Console option validation is strict now. --older-than must be a whole number ≥ 1 day; numeric options reject non-integers. Scheduled commands that passed "7d" or 0 will start failing loudly — which is the point, but check your Kernel/scheduler before deploying.

Config caching in production. If you call env() inside your own config files it returns null under config:cache. Read all SISP settings through config('sisp.*') only.

Local development can't receive the callback. You need a public HTTPS URL. Use ngrok/expose/valet share and update SISP_CALLBACK_URL; a mismatch between the registered callback and the actual route yields a dead redirect with no error on the SISP side. Also restart the tunnel URL when it changes — signed links generated against the wrong host will 403.

Log the raw payloads, redact the secrets. Dedicated sisp log channel, daily rotation. Log the full callback body for support tickets; never log the POS auth code, the encryption key, or PAN-shaped fields.

Credentials never belong in git. SISP_POS_AUTH_CODE is a terminal shared secret. Secrets manager, rotate with SISP if it touches a commit. The merchant session matters too — v3 requires it on cancellation, so a missing session is a real failure mode, not a no-op.

Pin the version and review the diff. This is a major with security-relevant hardening across the callback path; treat ^3.0 as 3.0.x until you've read 3.1's notes. Publishing config and migrations is normal; patching vendor/ is not — fork and point Composer at a VCS repository instead.

Extension points worth knowing: the published config (all tunables, including rate limits and retention), the package events (hook business logic without touching the callback), the package models (transactions/invoices/refunds — read, don't write directly except through the provided operations), the console commands (schedulable as-is), and any contract bound in the container you can swap. If you need behaviour the events don't expose, listening + your own projection is safer than overriding the callback controller.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style