PHPackages                             bpotmalnik/lunar-paynow - 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. bpotmalnik/lunar-paynow

ActiveLibrary[Payment Processing](/categories/payments)

bpotmalnik/lunar-paynow
=======================

PayNow payment driver for LunarPHP

v1.1.0(1mo ago)00MITPHPPHP ^8.3

Since May 29Pushed 1mo agoCompare

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

READMEChangelog (2)Dependencies (11)Versions (3)Used By (0)

lunar-paynow
============

[](#lunar-paynow)

 [![Latest Stable Version](https://camo.githubusercontent.com/ed44777badf98e63dcaf171488e06e96c381525e7941b01d2d93bb64b8f41ab4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f62706f746d616c6e696b2f6c756e61722d7061796e6f77)](https://packagist.org/packages/bpotmalnik/lunar-paynow) [![PHP Version](https://camo.githubusercontent.com/e832ae72d86cbb927c9d0f1f89c3400b24e216fd696bf63ac96011f9ddf09cca/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f62706f746d616c6e696b2f6c756e61722d7061796e6f77)](https://packagist.org/packages/bpotmalnik/lunar-paynow) [![License](https://camo.githubusercontent.com/d5cda7dbaa844b186fbccbf587f5bc3e0e43d2f828e69b1f2774de2f19f11ca7/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f62706f746d616c6e696b2f6c756e61722d7061796e6f77)](https://packagist.org/packages/bpotmalnik/lunar-paynow)

[PayNow](https://paynow.pl/) ([docs](https://docs.paynow.pl/docs/reference/v3/api-reference)) v3 payment driver for [LunarPHP](https://lunarphp.io). Handles the full payment lifecycle — authorization, notifications, refunds, partial refunds, refund cancellation, and payment recovery — following Lunar's payment driver conventions.

- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Usage](#usage)
    - [Authorizing a payment](#authorizing-a-payment)
    - [Handling the redirect](#handling-the-redirect)
    - [Payment recovery](#payment-recovery)
    - [Refunding](#refunding)
    - [Cancelling a refund](#cancelling-a-refund)
- [Notifications](#notifications)
- [Events](#events)
- [Error messages](#error-messages)
- [Translation](#translation)
- [Testing in your application](#testing-in-your-application)
    - [FakePaynowClient](#fakepaynovclient)
    - [Simulating a webhook confirmation](#simulating-a-webhook-confirmation)
- [Package development](#package-development)
- [License](#license)

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 12 or 13
- LunarPHP 1.x

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

[](#installation)

Install the package via Composer:

```
composer require bpotmalnik/lunar-paynow
```

Publish the configuration file and migrations, then run the migrations:

```
php artisan vendor:publish --tag=lunar-paynow-config
php artisan vendor:publish --tag=lunar-paynow-migrations
php artisan migrate
```

Optionally publish the translation files if you want to customise the error messages:

```
php artisan vendor:publish --tag=lunar-paynow-lang
```

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

[](#configuration)

Add the following variables to your `.env` file:

```
PAYNOW_API_KEY=your-api-key
PAYNOW_SIGNATURE_KEY=your-signature-key
PAYNOW_SANDBOX=true
```

Both credentials are available in your PayNow merchant dashboard under **Integration** → **Keys**.

#### Notification URL

[](#notification-url)

PayNow sends server-to-server POST requests when a payment status changes. Configure the full URL in your merchant dashboard under **PoS settings** → **Notification URL**:

```
https://yoursite.com/paynow/notification

```

The route is registered automatically by the package. It operates outside the `web` middleware group — CSRF protection is replaced by HMAC-SHA256 signature verification on every inbound request.

You may change the path via the environment:

```
PAYNOW_NOTIFICATION_PATH=paynow/notification
```

#### Order status mapping

[](#order-status-mapping)

By default the package maps PayNow statuses to these Lunar order statuses. Override them to match your store's configuration:

```
PAYNOW_STATUS_CONFIRMED=payment-received
PAYNOW_STATUS_REJECTED=payment-failed
PAYNOW_STATUS_ABANDONED=payment-failed
PAYNOW_STATUS_EXPIRED=payment-failed
PAYNOW_STATUS_ERROR=payment-failed
```

Full configuration reference: [`config/lunar/paynow.php`](config/lunar/paynow.php).

Usage
-----

[](#usage)

### Authorizing a payment

[](#authorizing-a-payment)

Call `authorize()` from your checkout controller. The method creates a draft order from the cart (or reuses one if it already exists), calls the PayNow API, and returns a redirect URL.

```
use Lunar\Facades\Payments;

$result = Payments::driver('paynow')
    ->cart($cart)
    ->withData([
        'continue_url' => route('checkout.complete'),
    ])
    ->authorize();

if (! $result->success) {
    return back()->withErrors(['payment' => $result->message]);
}

return redirect($result->redirectUrl);
```

`$result->message` is always safe to display to the customer. For the detailed admin-level reason (e.g. "Signature key misconfigured"), use `$result->adminMessage()` in logs or the admin panel.

#### Optional withData keys

[](#optional-withdata-keys)

KeyDescription`continue_url`Where PayNow redirects the customer after the payment page. Overrides the PoS default.`description`Payment description shown on the PayNow page. Falls back to `PAYNOW_PAYMENT_DESCRIPTION`.`refund_reason`One of `RMA`, `REFUND_BEFORE_14`, `REFUND_AFTER_14`, `OTHER`. Used when calling `refund()`.### Handling the redirect

[](#handling-the-redirect)

After `authorize()` succeeds, redirect the customer to `$result->redirectUrl`. PayNow handles the payment and redirects them back to your `continue_url`. At this point the payment may not yet be confirmed — confirmation arrives asynchronously via the [notification endpoint](#notifications).

A typical `continue_url` handler simply polls the order status:

```
public function complete(Order $order)
{
    if ($order->placed_at) {
        return view('checkout.success', ['order' => $order]);
    }

    // Payment still pending — show a waiting page or poll via JS.
    return view('checkout.pending', ['order' => $order]);
}
```

### Payment recovery

[](#payment-recovery)

PayNow allows customers to retry a payment that failed with a `PENDING`, `REJECTED`, or `ERROR` status. The recovered payment shares the same `externalId` as the original so PayNow can link the attempts on its side.

```
use Bpotmalnik\LunarPaynow\Models\PaynowPayment;

$failedPayment = PaynowPayment::findOrFail($id);

$result = Payments::driver('paynow')
    ->recoverFrom($failedPayment)
    ->withData(['continue_url' => route('checkout.complete')])
    ->authorize();

if (! $result->success) {
    return back()->withErrors(['payment' => $result->message]);
}

return redirect($result->redirectUrl);
```

Recovery must be enabled in the PayNow merchant panel. Calling `recoverFrom()` on a payment that is not in a recoverable status returns a failure response immediately without hitting the API.

### Refunding

[](#refunding)

Refunds are initiated from the Lunar admin panel via the standard capture transaction interface. To trigger one programmatically, pass the capture `Transaction` and an amount in grosze (smallest currency unit):

```
use Lunar\Facades\Payments;

$result = Payments::driver('paynow')
    ->refund($captureTransaction, 5000); // 50.00 PLN

if (! $result->success) {
    // $result->message contains the admin-translated reason.
}
```

To include a refund reason recognised by PayNow:

```
$result = Payments::driver('paynow')
    ->withData(['refund_reason' => 'RMA'])
    ->refund($captureTransaction, 5000);
```

Valid reasons: `RMA`, `REFUND_BEFORE_14`, `REFUND_AFTER_14`, `OTHER`.

The package validates that:

- The source payment is `CONFIRMED`.
- The requested amount does not exceed the unrefunded balance (taking partial refunds already made into account).
- The payment is not older than six months (`REFUND_POSSIBILITY_EXPIRED`).

All amounts are integers in the smallest currency unit. `10000` = 100.00 PLN.

### Cancelling a refund

[](#cancelling-a-refund)

PayNow supports cancelling a refund that is still in `NEW` status (the *awaiting refunds* feature, used when your merchant balance is temporarily insufficient).

```
use Bpotmalnik\LunarPaynow\Models\PaynowRefund;
use Lunar\Facades\Payments;

$refund = PaynowRefund::where('refund_id', $refundId)->firstOrFail();

$result = Payments::driver('paynow')->cancelRefund($refund);

if (! $result->success) {
    // $result->message explains why (e.g. status is no longer NEW).
}
```

Cancellation is only possible while the refund status is `NEW`. The package rejects the request locally and does not call the API if the status has already advanced.

Notifications
-------------

[](#notifications)

PayNow sends a POST request to your notification URL whenever a payment status changes to a terminal state (`CONFIRMED`, `REJECTED`, `ABANDONED`, `EXPIRED`, or `ERROR`).

The package handles this automatically:

- Verifies the `Signature` header using HMAC-SHA256 before processing anything.
- Uses a database transaction with `lockForUpdate` to prevent duplicate processing if PayNow sends the same notification twice.
- On `CONFIRMED`: marks the intent transaction as successful, creates a `capture` transaction, sets `placed_at` on the order, updates the order status, and fires `PaymentConfirmed`.
- On failure statuses: marks the intent transaction as failed, updates the order status, and fires `PaymentFailed`.

Non-terminal statuses (`NEW`, `PENDING`) are acknowledged with `200` and ignored — the notification is only fully processed once a final state is reached.

Events
------

[](#events)

Listen for these events to react to payment outcomes in your application:

```
use Bpotmalnik\LunarPaynow\Events\PaymentConfirmed;
use Bpotmalnik\LunarPaynow\Events\PaymentFailed;

// In a service provider or EventServiceProvider:
Event::listen(PaymentConfirmed::class, function (PaymentConfirmed $event) {
    $event->order;        // Lunar\Models\Order
    $event->paynowPayment; // Bpotmalnik\LunarPaynow\Models\PaynowPayment
});

Event::listen(PaymentFailed::class, function (PaymentFailed $event) {
    $event->order;
    $event->paynowPayment;
});
```

Error messages
--------------

[](#error-messages)

`$result->message` from `authorize()` is always **customer-safe** — it maps to a localised string the customer can act on, or falls back to a generic message for errors that should not be surfaced (configuration problems, merchant balance issues, etc.).

For the detailed admin-level reason, use `$result->adminMessage()` or access `$result->errorType` directly:

```
$result = Payments::driver('paynow')->cart($cart)->authorize();

if (! $result->success) {
    // Show to customer:
    return back()->withErrors(['payment' => $result->message]);

    // Log for the developer/admin:
    Log::error($result->adminMessage(), [
        'error_type' => $result->errorType?->value,
        'order'      => $result->orderId,
    ]);
}
```

The `refund()` and `cancelRefund()` methods are admin operations — their `$result->message` is the admin-translated string and is displayed directly in the Lunar admin panel.

#### Customer-safe errors

[](#customer-safe-errors)

These are shown to the customer verbatim; all others fall back to a generic message:

PayNow errorExample`SYSTEM_TEMPORARILY_UNAVAILABLE`"Payment service is temporarily unavailable."`PAYMENT_AMOUNT_TOO_SMALL`"The payment amount is too small."`PAYMENT_AMOUNT_TOO_LARGE`"The payment amount is too large."`PAYMENT_METHOD_NOT_AVAILABLE`"The selected payment method is not available."`AUTHORIZATION_CODE_EXPIRED`"Your BLIK code has expired."`AUTHORIZATION_CODE_INVALID`"Invalid BLIK code."`AUTHORIZATION_CODE_USED`"This BLIK code has already been used."Errors such as `VERIFICATION_FAILED`, `INSUFFICIENT_BALANCE_FUNDS`, and `REFUND_POSSIBILITY_EXPIRED` are admin-only — the customer receives the generic fallback.

Translation
-----------

[](#translation)

The package ships with English and Polish translations. Polish is the default language for the PayNow market, but all strings follow Laravel's standard translation resolution — the active application locale is used automatically.

The language files are split into two sections:

- `errors.admin.*` — detailed messages for the Lunar admin panel and application logs.
- `errors.customer.*` — short, user-friendly messages safe for browser display.

To customise any string, publish the translations and edit the files under `lang/vendor/lunar-paynow/`:

```
php artisan vendor:publish --tag=lunar-paynow-lang
```

Testing in your application
---------------------------

[](#testing-in-your-application)

The package ships with a `FakePaynowClient` that lets you write feature tests for the full checkout flow without making real HTTP calls to PayNow or needing valid API credentials.

### FakePaynowClient

[](#fakepaynowclient)

`Bpotmalnik\LunarPaynow\Testing\FakePaynowClient` implements `PaynowClientContract` and can be swapped into the container before each test:

```
use Bpotmalnik\LunarPaynow\Contracts\PaynowClientContract;
use Bpotmalnik\LunarPaynow\Testing\FakePaynowClient;

beforeEach(function () {
    $fake = new FakePaynowClient;
    $this->app->instance(PaynowClientContract::class, $fake);
    $this->paynow = $fake;
});
```

The fake behaves as follows:

MethodBehaviour`createPayment()`Returns `['paymentId' => $this->paymentId, 'status' => 'NEW', 'redirectUrl' => $this->redirectUrl]``verifyNotificationSignature()`Always returns `true` — any signature header is accepted`getPaymentStatus()`Returns `['paymentId' => $this->paymentId, 'status' => 'CONFIRMED']``createRefund()`Returns a minimal successful refund payload`cancelRefund()` / `getRefundStatus()`No-ops / return stubsTwo public properties let you reference the deterministic values in assertions:

```
$fake->paymentId;    // 'fake-paynow-id-00001'
$fake->redirectUrl;  // 'https://paynow.pl/fake-redirect'
```

You can override them before the test to simulate different scenarios:

```
$fake = new FakePaynowClient;
$fake->paymentId = 'custom-id-for-this-test';
```

### Simulating a webhook confirmation

[](#simulating-a-webhook-confirmation)

Because `verifyNotificationSignature()` always returns `true`, you can POST a fake webhook notification directly in your test without a valid HMAC signature:

```
$body = json_encode(['paymentId' => $this->paynow->paymentId, 'status' => 'CONFIRMED']);

$this->call(
    'POST',
    route('paynow.notification'),
    [],
    [],
    [],
    ['HTTP_SIGNATURE' => 'fake-sig', 'CONTENT_TYPE' => 'application/json'],
    $body,
);
```

This triggers the full notification handling path: the `PaynowPayment` record is updated, a `capture` transaction is created, `placed_at` is set on the order, the order status transitions to `payment-received`, and the `PaymentConfirmed` event is fired — exactly as in production.

Package development
-------------------

[](#package-development)

```
composer test
```

`composer test` runs the full quality suite in order: Pint lint check, PHPStan, unit tests, feature tests.

Individual commands:

```
composer lint          # fix code style with Pint

composer test:lint     # check style without fixing
composer test:types    # PHPStan static analysis
composer test:unit
composer test:feature
```

License
-------

[](#license)

lunar-paynow is open-sourced software licensed under the [MIT license](LICENSE.md).

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance89

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

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 ~1 days

Total

2

Last Release

55d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/20015942?v=4)[Bart Potmalnik](/maintainers/bpotmalnik)[@bpotmalnik](https://github.com/bpotmalnik)

---

Top Contributors

[![bpotmalnik](https://avatars.githubusercontent.com/u/20015942?v=4)](https://github.com/bpotmalnik "bpotmalnik (10 commits)")

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/bpotmalnik-lunar-paynow/health.svg)

```
[![Health](https://phpackages.com/badges/bpotmalnik-lunar-paynow/health.svg)](https://phpackages.com/packages/bpotmalnik-lunar-paynow)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

77922.3M186](/packages/laravel-mcp)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[fleetbase/core-api

Core Framework and Resources for Fleetbase API

1235.9k21](/packages/fleetbase-core-api)

PHPackages © 2026

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