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

Knet Laravel Package

asciisd/knet

Laravel package for integrating KNET payments into PHP applications. It simplifies payment initiation, callback handling, and response verification, giving developers a straightforward way to accept Kuwaiti debit card payments.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and scaffold

composer require asciisd/knet
php artisan knet:install   # publishes config, runs migrations, validates setup
php artisan knet:check     # validates credentials before you write any code

Run knet:check first. It's the cheapest way to discover you've been given a Tran Portal ID that only works against the test host.

2. Configure .env (the package reads config, but these are the knobs that matter):

KNET_TRANSPORT_ID=
KNET_TRANSPORT_PASSWORD=
KNET_RESOURCE_KEY=
KNET_REDIRECT_URL=/payment/success   # a PATH, not a full URL
KNET_CURRENCY=414                    # KWD
KNET_LANGUAGE=EN                     # or AR
KNET_DEBUG=false                     # false → kpay.com.kw, true → kpaytest.com.kw

3. Add the trait to your billable model. For most apps that's User:

use Asciisd\Knet\HasKnet;

class User extends Authenticatable
{
    use HasKnet;
}

This gives you pay(), payWithKfast(), refund(), kfastToken(), and a knetTransactions() relation. If your payer isn't User, skip the trait and inject the service instead.

4. Where to look first. Read three things and nothing else: the KnetTransaction model (it is your payment record — result, paid, refunded, udf1..5), the Events directory (all your business logic hangs off these), and Contracts/ (the interfaces tell you what the package actually does). The service class is a pure delegator, so reading it tells you nothing about behaviour.

5. First use case — a redirect checkout. Don't build a full checkout. Build the smallest possible loop: pay, redirect, listen, log.

public function pay(Request $request)
{
    $transaction = $request->user()->pay(10.000, [
        'udf1' => (string) $request->order_id,
    ]);

    return redirect($transaction->url);
}

Then register a single listener for KnetPaymentSucceeded that writes a line to the log. Once you've seen a real callback land and decrypted correctly, everything else is variations on this.


Implementation Patterns

Fulfil in listeners, never in the return controller

The user's browser arrives at KNET_REDIRECT_URL via the package's /knet/handle POST handler. That request is not proof of payment — it's a redirect. Fulfilment belongs in a listener on KnetPaymentSucceeded, which fires from the encrypted callback path:

Event::listen(KnetPaymentSucceeded::class, function (KnetPaymentSucceeded $event) {
    DB::transaction(function () use ($event) {
        $order = Order::where('id', $event->transaction->udf1)
            ->lockForUpdate()
            ->firstOrFail();

        if ($order->status === 'paid') {
            return; // idempotency guard — see Gotchas
        }

        $order->update(['status' => 'paid', 'knet_trackid' => $event->transaction->trackid]);
    });
});

Keep udf1 as a correlation key only

UDF fields are short, opaque, and travel through the acquirer's systems. Put an order reference in udf1 and nothing else. Don't put JSON, emails, or anything user-facing in udf2..udf5 — store rich state locally against trackid and look it up:

$transaction = app(TransactionRepository::class)->findByTrackId($trackid);

Inject the narrowest interface you can

KnetPaymentService implements all three payment interfaces. Type-hint only what you use:

public function checkout(CreatesPayments $payments) { ... }
public function refundAction(RefundsPayments $refunds) { ... }

This isn't ceremony — it makes the surface you depend on explicit, and it means a future swap of the package only breaks the classes that genuinely use it. In tests you can bind a fake implementation of CreatesPayments without touching the gateway.

Reconcile pending transactions on a schedule

Callbacks get lost: users close tabs, networks drop, the acquirer retries into a 500. Anything left sitting at INITIATED/PENDING past a threshold should be reconciled:

KnetTransaction::where('result', 'INITIATED')
    ->where('created_at', '<', now()->subMinutes(30))
    ->each(fn ($t) => $paymentService->inquireAndUpdateTransaction($t));

Run this on a schedule, and again from your return-page controller so the user sees the correct status even when the callback never arrived.

Build refunds as a deliberate, one-shot operation

$user->refund($transaction, 5.000); // partial
$user->refund($transaction);        // full

Refunds create a second KnetTransaction row (action = 2, original_transaction_id pointing back). Your admin UI should display the refund record and the original side by side, not treat the refund as an in-place mutation.

Use the enum for presentation, not for control flow

PaymentStatus::from($transaction->result) plus ->presenter() gives you styleColor(), textColor(), imageUrl() — handy for a status badge component. But guard business logic with the model helpers (isCaptured(), hasStatus()), which are more stable than string matching on result.


Gotchas and Tips

Amounts are strings in the database and three decimals in your code. 10.000, 0.250, 25.500. Passing 10.5 or 10 into pay() risks producing 10.5 in the amt column and a rejected or mispriced transaction. Wrap it: number_format($amount, 3, '.', ''). KNET is KWD-only for practical purposes — currency 414 — so don't try to reuse this for multi-currency.

isRefunded() is true after a partial refund. The docs are explicit that the original transaction is flagged refunded = true and refund_amount is recorded. Combined with isRefundable() returning false once refunded, this means you get exactly one refund per transaction, and you cannot distinguish full from partial via isRefunded() alone — check refund_amount against rawAmount(). If your business needs staged partial refunds, this package will fight you and you'll be calling the gateway directly.

Callbacks are POST and idempotent-by-nobody. KNET will retry, and your return handler plus the callback listener can both run. Wrap fulfilment in a transaction with a status check (as above). A unique index on trackid is table stakes; a processed_at guard on your order is the real protection.

The callback middleware decrypts once and stuffs the payload into request attributes. Don't try to read trandata from $request->input() inside your own code — by then it's been handled. If you need the raw payload, listen to KnetResponseReceived / KnetResponseHandled (both carry array $payload). If you call Knet::ignoreRoutes() to host /knet/response yourself, you must reimplement that middleware — including AES-128-CBC decryption with KNET_RESOURCE_KEY — and you will get it wrong on the first attempt.

KNET_REDIRECT_URL is a path, not a URL. Set it to /payment/success; the package redirects relative to your app. A full https:// value is a common copy-paste failure that silently sends users to https://yoursite.com/https://yoursite.com/....

KNET_DEBUG=false is production. Flipping it to true points at kpaytest.com.kw. If credentials work in knet:check but the redirect page 404s or shows a merchant error, check this flag before anything else. Ship false and add a config assertion in a non-production service provider check.

Track IDs must be globally unique. The package auto-generates one if you omit trackid, so only supply your own if you have a reason. If you do, use UUIDs — a colliding trackid fails the transaction with a cryptic gateway error, and a reused one can make findByTrackId() return the wrong record.

KFAST needs banker approval. payWithKfast() will fail if KFAST isn't enabled on your terminal, and the failure looks like a generic decline. The auto-token (user ID 42 → "00000042") is validated to exactly 8 numeric digits by PaymentRequest — if you override kfastToken(), pad and assert, or you get a validation exception at payment time rather than a clear message.

Custom routes vs. the return page. The package registers POST /knet/response, POST /knet/handle, and GET|POST /knet/error under KNET_PATH. Your KNET_REDIRECT_URL route is yours to define — and since the user arrives there in the same browser session, session flash and auth()->user() still work. Use that: store the trackid in session at payment creation, then verify ownership on the return page rather than trusting a query string.

KnetReceiptSeen is in the events table with no documented payload. Treat it as internal/experimental. Don't build on it.

Migration and model customisation: Knet::ignoreMigrations() if you need your own schema (you then own the columns — match them exactly); KnetTransaction::useCustomerModel() if payers aren't User. Both belong in AppServiceProvider, not in a controller.

Vendor-risk assessment: 14 stars, single-maintainer, MIT, last release March 2025. The architecture is genuinely good — SOLID interfaces, contracts, events — which is exactly what makes it safe to adopt if you keep the dependency at the edges. Put CreatesPayments/RefundsPayments type-hints in your own thin PaymentGateway wrapper, keep listener logic free of package classes beyond KnetTransaction, and a future fork or replacement is a weekend rather than a rewrite. The thing you cannot abstract away cheaply is the knet_transactions schema — design your order/ledger tables to be independent of it, referencing trackid as a plain foreign key string.

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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log