PHPackages                             laraditz/tng-ewallet - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. laraditz/tng-ewallet

ActiveLibrary[Payment Processing](/categories/payments)

laraditz/tng-ewallet
====================

Laravel SDK for Touch 'n Go's Mini Program OpenAPI (Cashier Payment, Agreement Payment, refunds, user info, and messaging).

1.1.0(1mo ago)034↓70%MITPHPPHP ^8.1

Since Jul 10Pushed 3w agoCompare

[ Source](https://github.com/laraditz/tng-ewallet)[ Packagist](https://packagist.org/packages/laraditz/tng-ewallet)[ RSS](/packages/laraditz-tng-ewallet/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (9)Versions (6)Used By (0)

Laravel Touch 'n Go e-wallet
============================

[](#laravel-touch-n-go-e-wallet)

[![Latest Version on Packagist](https://camo.githubusercontent.com/7f9c72989afa8146f0d943aa57160d58619ef6ca6c8f804b308e903e8b08dc53/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6c6172616469747a2f746e672d6577616c6c65742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/laraditz/tng-ewallet)[![Total Downloads](https://camo.githubusercontent.com/866f840f62bd2cc5b8fe4eb6055e154eb1fa01a7ceb404b2bc7ccaca89ffb408/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6c6172616469747a2f746e672d6577616c6c65742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/laraditz/tng-ewallet)[![License](https://camo.githubusercontent.com/e6636bceb28377ddfd72ca8aa748eb6c29b10fd474d34daa893858357efb385a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6c6172616469747a2f746e672d6577616c6c65743f7374796c653d666c61742d737175617265)](./LICENSE.md)

A Laravel SDK for Touch 'n Go's Mini Program OpenAPI — RSA256 request signing, response/webhook signature verification, and a persistence layer covering every call, access token, payment, refund, and inbound notification.

Requires PHP 8.1+ and Laravel 11.0+.

Installation
------------

[](#installation)

```
composer require laraditz/tng-ewallet
```

Publish the config file and the migrations:

```
php artisan vendor:publish --tag=tng-ewallet-config
php artisan vendor:publish --tag=tng-ewallet-migrations
```

Then run them:

```
php artisan migrate
```

Re-running `vendor:publish --tag=tng-ewallet-migrations` is safe — it checks your app's `database/migrations` directory and only publishes files that aren't already there (matched by filename, ignoring the timestamp prefix), so upgrading the package never creates duplicate migrations.

If you'll use the **Agreement Payment** flow (anything involving stored access tokens), also generate a dedicated encryption key now, before you go any further:

```
php artisan tng-ewallet:generate-key
```

This sets `TNG_ENCRYPTION_KEY` in your `.env` — independent of your app's `APP_KEY`, used to encrypt access/refresh tokens at rest. **Set it once and never change it afterwards** — there's no migration path if it's rotated or lost, and every stored token would stop working. A Cashier Payment-only integration never needs this.

RSA keys
--------

[](#rsa-keys)

TNG's signing scheme involves **two independent RSA keypairs**, not one:

- **Yours** — generated by you. You keep the private half and use it to sign every outbound request; TNG never sees it. You give TNG the public half out-of-band (during API onboarding/registration) so they can verify requests genuinely came from you.
- **TNG's** — generated and held by TNG. They give you their public key so you can verify their responses and inbound `notifyPayment` webhooks weren't tampered with. You never see TNG's private key, and TNG never sees yours.

Generate your own keypair:

```
openssl genrsa -out storage/tng/private.pem 2048
openssl rsa -in storage/tng/private.pem -pubout -out storage/tng/public.pem
```

- `storage/tng/private.pem` is what `TNG_PRIVATE_KEY_PATH` points at (matches the default below) — keep it secret, never commit it.
- Submit the contents of `storage/tng/public.pem` to TNG during onboarding. This file's job is done once submitted — the SDK itself never reads it.
- TNG will separately give you *their* public key. Save it as `storage/tng/tng_public.pem` (matches `TNG_PUBLIC_KEY_PATH`'s default) — this is the file the SDK actually reads at runtime, to verify TNG's responses.

Add `storage/tng/*.pem` to your app's `.gitignore` so neither key is ever committed.

Configuration
-------------

[](#configuration)

Set these in your `.env`:

VariableDescriptionDefault`TNG_SANDBOX`Use TNG's sandbox host instead of production`true``TNG_CLIENT_ID`Your TNG-assigned Client-Id*(required)*`TNG_PARTNER_ID`Your TNG-assigned partner ID*(required)*`TNG_PRIVATE_KEY_PATH`Path to *your* RSA private key (PEM), used to sign every outbound request — see [RSA keys](#rsa-keys)`storage_path('tng/private.pem')``TNG_PUBLIC_KEY_PATH`Path to *TNG's* RSA public key (PEM), used to verify responses and inbound notifications — see [RSA keys](#rsa-keys)`storage_path('tng/tng_public.pem')``TNG_KEY_VERSION`Key version sent in the `Signature` header`1``TNG_VERIFY_RESPONSE_SIGNATURE`Verify every response's signature before returning data`true``TNG_TIMEOUT`HTTP timeout in seconds`30``TNG_NOTIFY_PATH`Path the inbound `notifyPayment` webhook is registered at`/tng-ewallet/notify``TNG_RETURN_PATH`Path the Cashier Payment return page is registered at — see [Return page](docs/payment.md#return-page)`/tng-ewallet/return``TNG_DEFAULT_RETURN_URL`Fallback "Back" destination on the return page when `pay()` wasn't given a `customerReturnUrl` — see [Return page](docs/payment.md#return-page)`config('app.url')``TNG_ENCRYPTION_KEY`Dedicated key for encrypting sensitive stored data (independent of `APP_KEY`) — see [Installation](#installation)*(required if using the Agreement Payment / access-token flow)*`client_id`, `partner_id`, and `private_key_path` are required — a missing value throws `ConfigurationException` before any HTTP call is made. `encryption_key` is only required if you use the Agreement Payment flow (anything involving access tokens) — a Cashier Payment-only integration never needs to set it.

`partner_id` is automatically included in every call that needs it (`prepare`, `pay`, `inquiryPayment`, `refund`, `inquiryRefund`) — you never need to pass it yourself, though an explicit `partnerId` in your own call data always takes precedence if you do.

API Reference
-------------

[](#api-reference)

Every wrapped call is a POST under `config('tng-ewallet.api_path')` (default `/acl/api`), reached through the `Tng` facade. Every response DTO shares `resultStatus`/`resultCode`/`resultMessage` plus `isSuccessful()`/`isAccepted()`/`isFailed()`/`isUnknown()`/`raw()`/`toArray()` — see [Handling U and A results](#handling-u-unknown-and-a-accepted-results) below.

ResourceMethodTNG endpointDescriptionDocsAuthorization[`prepare()`](#authorization-prepare)`/v1/authorizations/prepare`Start the Agreement Payment binding flow[docs/authorization.md](docs/authorization.md#prepare)Authorization[`applyToken()`](#agreement-payment-stored-credential-auto-debit)`/v1/authorizations/applyToken`Exchange an authCode (or refresh token) for an access token[docs/authorization.md](docs/authorization.md#applytoken)Authorization[`cancelToken()`](#agreement-payment-stored-credential-auto-debit)`/v1/authorizations/cancelToken`Revoke a binding[docs/authorization.md](docs/authorization.md#canceltoken)Payment[`pay()`](#cashier-payment-redirect-checkout)`/v1/payments/pay`Create a Cashier or Agreement payment[docs/payment.md](docs/payment.md#pay)Payment[`inquiry()`](#payment-inquiry)`/v1/payments/inquiryPayment`Check the real status of a payment[docs/payment.md](docs/payment.md#inquiry)Refund[`create()`](#refund-create)`/v1/payments/refund`Refund all or part of a payment[docs/refund.md](docs/refund.md#create)Refund[`inquiry()`](#refund-inquiry)`/v1/payments/inquiryRefund`Check the real status of a refund[docs/refund.md](docs/refund.md#inquiry)User[`inquiryByAccessToken()`](#user-info-and-messaging)`/v1/customers/user/inquiryUserInfoByAccessToken`Fetch a user's profile info[docs/user-and-messaging.md](docs/user-and-messaging.md#user-inquirybyaccesstoken)Message[`sendByAccessToken()`](#user-info-and-messaging)`/v2/customers/message/sendByAccessToken`Send a message to a user[docs/user-and-messaging.md](docs/user-and-messaging.md#message-sendbyaccesstoken)Client (escape hatch)`client()->post($uri, $data)`anyRaw signed POST for endpoints not wrapped above—`pay()`, `applyToken()`, `cancelToken()`, `inquiryByAccessToken()`, and `sendByAccessToken()` each have a full working example further down, in the [Cashier Payment](#cashier-payment-redirect-checkout), [Agreement Payment](#agreement-payment-stored-credential-auto-debit), and [User info and messaging](#user-info-and-messaging) sections. The remaining calls are shown below; full parameter lists, response DTO fields, and side-effect notes for every method are in [docs/](docs/).

#### `authorization()->prepare()`

[](#authorization-prepare)

```
$prepare = Tng::authorization()->prepare([
    'referenceClientId' => 'your-mini-program-client-id',
]);

// Redirect / hand $prepare->authURL to your Mini Program frontend.
```

#### `payment()->inquiry()`

[](#payment-inquiry)

```
$status = Tng::payment()->inquiry(['paymentRequestId' => $paymentRequestId]);

if ($status->isSuccessful()) {
    // Safe to mark the order paid.
} elseif ($status->isFailed()) {
    // $status->paymentFailReason explains why.
}
```

#### `refund()->create()`

[](#refund-create)

```
$refund = Tng::refund()->create([
    'refundRequestId' => (string) Str::uuid(),
    'paymentRequestId' => $paymentRequestId,
    'refundAmount' => ['currency' => 'MYR', 'value' => '10000'],
    'refundReason' => 'Customer requested cancellation',
]);
```

#### `refund()->inquiry()`

[](#refund-inquiry)

```
$status = Tng::refund()->inquiry(['refundRequestId' => $refundRequestId]);

$status->refundStatus; // PROCESSING | SUCCESS | FAIL
```

Cashier Payment (redirect checkout)
-----------------------------------

[](#cashier-payment-redirect-checkout)

This is the documented golden path: create a payment, redirect the user to TNG's hosted cashier page, then receive the result asynchronously via the webhook.

```
use Laraditz\TngEwallet\Facades\Tng;

$response = Tng::payment()->pay([
    // partnerId, paymentNotifyUrl, paymentReturnUrl, and envInfo are filled in automatically — see below.
    'paymentRequestId' => (string) Str::uuid(), // your own unique ID — the SDK never generates one for you
    'paymentOrderTitle' => 'Order #1234',
    'productCode' => '51051000101000100001', // Cashier Payment product code
    'paymentAmount' => ['currency' => 'MYR', 'value' => '10000'], // smallest currency unit
    'paymentFactor' => ['isCashierPayment' => true],
    'customerReturnUrl' => route('checkout.thanks'), // optional — see "Return page" below
]);

if ($response->isAccepted()) {
    // The normal Cashier Payment path — redirect the user to finish payment.
    return redirect()->away($response->actionForm->redirectionUrl);
}

if ($response->isFailed()) {
    // $response->resultCode / $response->resultMessage explain why.
}

if ($response->isUnknown()) {
    // See "Handling U (Unknown) results" below — do not treat as final.
}
```

`pay()` defaults `paymentNotifyUrl` to this package's own auto-registered webhook route (`config('tng-ewallet.notify_path')`, default `/tng-ewallet/notify`) — that's what TNG calls when the payment reaches a final state, and it's what the middleware/controller/job pipeline below is built to receive. Listen for the result in your own listener:

```
use Laraditz\TngEwallet\Events\PaymentNotified;

class HandlePaymentNotified implements ShouldQueue // recommended, though not required — see the afterResponse() note below
{
    public function handle(PaymentNotified $event): void
    {
        $payload = $event->payload; // the raw, already-verified notifyPayment body

        // $payload['paymentResult']['resultStatus'] is 'S' or 'F'.
        // Persist your own order state here — this package never touches
        // your application's domain model, only its own audit tables.
    }
}
```

> **Don't override `paymentNotifyUrl` unless you're prepared to handle the webhook yourself.** You *can* pass your own `paymentNotifyUrl` and it will be used instead of the package default — but this package's signature-verifying middleware, controller, and `PaymentNotified` event only run for requests that hit *its own* registered route. If you point TNG at a different URL, none of that pipeline applies to that call: you're responsible for verifying the inbound signature, returning the mandatory ack, and processing the notification entirely on your own. Leave `paymentNotifyUrl` unset unless you have a specific reason to bypass this package's webhook handling.

`pay()` similarly defaults `paymentReturnUrl` to this package's own return route (`config('tng-ewallet.return_path')`, default `/tng-ewallet/return`) — that's where TNG redirects the customer's browser once the hosted cashier page finishes. This package owns that whole page: it looks up the payment, checks its live status via `inquiry()`, and shows a status page with a "Back" link. Pass `customerReturnUrl` (as in the example above) to control where that Back link points — otherwise it falls back to `config('tng-ewallet.default_return_url')`. See [docs/payment.md#return-page](docs/payment.md#return-page) for the full behavior, including the three states it can render.

Agreement Payment (stored-credential auto-debit)
------------------------------------------------

[](#agreement-payment-stored-credential-auto-debit)

Used once a user has bound their account to your app and authorized recurring/one-off charges without re-entering payment details each time. Three steps: bind, apply for a token, then pay with it.

```
use Laraditz\TngEwallet\Facades\Tng;

// 1. Prepare — kicks off the binding flow, returns a URL for the user to authorize.
$prepare = Tng::authorization()->prepare([
    'referenceClientId' => 'your-mini-program-client-id',
]);
// Redirect / hand $prepare->authURL to your Mini Program frontend.

// 2. Apply for an access token — after the user authorizes, you'll receive an authCode.
$token = Tng::authorization()->applyToken([
    'grantType' => 'AUTHORIZATION_CODE',
    'authCode' => $authCodeFromMiniProgram,
]);
// $token->accessToken / $token->customerId are now persisted in tng_ewallet_access_tokens.

// 3. Pay using the access token — no cashier redirect needed.
$response = Tng::payment()->pay([
    // partnerId, paymentNotifyUrl, and envInfo are filled in automatically — see above.
    'paymentRequestId' => (string) Str::uuid(),
    'paymentOrderTitle' => 'Order #1234',
    'productCode' => '51051000101000100031', // Agreement Payment product code
    'paymentAmount' => ['currency' => 'MYR', 'value' => '10000'],
    'paymentFactor' => ['isAgreementPay' => true],
    'paymentAuthCode' => $token->accessToken,
]);
```

Refreshing an expired access token uses the same `applyToken()` call with `grantType: REFRESH_TOKEN` — each call creates a **new** `tng_ewallet_access_tokens` row rather than overwriting the old one, so rotation history is preserved.

To revoke a binding:

```
Tng::authorization()->cancelToken(['accessToken' => $token->accessToken]);
```

User info and messaging
-----------------------

[](#user-info-and-messaging)

Given an access token from the Agreement Payment flow above:

```
$userInfo = Tng::user()->inquiryByAccessToken(['accessToken' => $token->accessToken]);
$userInfo->userInfo; // raw array, e.g. ['userId' => '...']

Tng::message()->sendByAccessToken([
    'accessToken' => $token->accessToken,
    'message' => 'Your order has shipped!',
]);
```

Handling `U` (Unknown) and `A` (Accepted) results
-------------------------------------------------

[](#handling-u-unknown-and-a-accepted-results)

Every response DTO exposes `isSuccessful()` (`S`), `isAccepted()` (`A`), `isFailed()` (`F`), and `isUnknown()` (`U`). Two of these need explicit caller attention beyond a simple if/else:

**`isAccepted()` on `pay()` is not a failure** — it's TNG's normal Cashier Payment success path. `PayResponse::isAccepted()` plus `$response->actionForm->redirectionUrl` is the documented golden-path branch (see the Cashier Payment example above). Treating `A` as an error will break the most common call in this SDK.

**`isUnknown()` must never be treated as final.** Per TNG's own docs, a `U` result means an unknown exception occurred on the wallet's side — the SDK does **not** auto-retry or auto-inquire on your behalf. Specifically for `pay()`:

- A `U` result must never be independently refunded or re-charged offline — the payment may still complete on TNG's side after you've observed `U`.
- Use `Tng::payment()->inquiry(['paymentRequestId' => $id])` to check the real status once you're ready, rather than assuming failure or success.
- The same caution applies to `Tng::refund()->create()` — a `U` refund result must not be treated as failed and retried, since the original refund may still be processing.

Operational notes
-----------------

[](#operational-notes)

**The webhook ack is guaranteed to be sent before `PaymentNotified` fires**, via Laravel's `dispatch(...)->afterResponse()` — not a queue. This relies on `fastcgi_finish_request()`, which is standard under PHP-FPM (the overwhelming majority of production Laravel deployments). Under non-FPM SAPIs (`php artisan serve`, some Octane configurations), verify this ordering holds in your own environment before relying on it in production.

**There is no retry if your own `PaymentNotified` listener throws**, and none from TNG either — by the time the job runs, TNG has already received a successful ack and will not redeliver. Make listeners resilient (catch your own exceptions, log failures, reconcile via `Tng::payment()->inquiry()` if needed) rather than assuming the event always completes cleanly.

**Listeners must be idempotent.** TNG retries the notification until it receives an `S` ack; each delivery gets its own `tng_ewallet_notifications` row (the package does not de-duplicate), so the same `PaymentNotified` event can fire more than once for the same payment. Design your listener so processing the same payload twice is safe.

**`notify_path` must match exactly** what you configured as `paymentNotifyUrl` when calling `pay()` — including scheme, host, and any trailing slash. A mismatch (e.g. a reverse proxy that rewrites paths) causes legitimate notifications to be rejected with 401, not silently accepted; there's no way for this package to detect a misconfigured path itself.

**Add your own rate limiting** (via your reverse proxy, CDN, or a `throttle:` middleware sized to your expected TNG callback volume) if the notify endpoint needs protection beyond signature verification — the package intentionally ships with none, since a wrong hardcoded limit could reject legitimate TNG traffic.

**Rotate your TNG public key file atomically** (write to a temp file, then `rename()`) rather than editing it in place — the key is read fresh from disk on every verification, and a torn read during a non-atomic rewrite would cause verification failures, not a security gap (verification fails closed either way).

Security
--------

[](#security)

- Access and refresh tokens are encrypted at rest using `TNG_ENCRYPTION_KEY`, generated during [Installation](#installation) above. Exclude it from any blanket "rotate all secrets" tooling or policy — there's no migration path if it's rotated or lost.
- Credentials are automatically stripped from logged request/response data before it's stored, so a database backup or read replica doesn't expose them.
- This package's tables are a full audit trail of your payment activity — every call, success or failure, is recorded. Apply the same database-level access controls you'd use for any PII/financial-data store.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance94

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

4

Last Release

37d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1203676?v=4)[Raditz Farhan](/maintainers/raditzfarhan)[@raditzfarhan](https://github.com/raditzfarhan)

---

Top Contributors

[![raditzfarhan](https://avatars.githubusercontent.com/u/1203676?v=4)](https://github.com/raditzfarhan "raditzfarhan (179 commits)")

---

Tags

laravelpaymentewallettngtouch n go

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/laraditz-tng-ewallet/health.svg)

```
[![Health](https://phpackages.com/badges/laraditz-tng-ewallet/health.svg)](https://phpackages.com/packages/laraditz-tng-ewallet)
```

###  Alternatives

[linkxtr/laravel-qrcode

A clean, modern, and easy-to-use QR code generator for Laravel

3827.1k](/packages/linkxtr-laravel-qrcode)[sebdesign/laravel-viva-payments

A Laravel package for integrating the Viva Payments gateway

4952.7k](/packages/sebdesign-laravel-viva-payments)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
