kommandhub / paystack-sw
Integrate the Paystack payment gateway into Shopware 6 to accept secure payments via credit cards, bank transfers, and mobile money. Expand your business across Africa with ease.
Package info
github.com/KommandHub/KommandhubPaystackSW
Type:shopware-platform-plugin
pkg:composer/kommandhub/paystack-sw
Requires
- shopware/core: ~6.6.0 || ~6.7.0
- shopware/storefront: ~6.6.0 || ~6.7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- rregeer/phpunit-coverage-check: ^0.3.1
- shopware/dev-tools: ^1.5
This package is auto-updated.
Last update: 2026-07-15 07:35:40 UTC
README
A production-grade Shopware 6 payment plugin that integrates the Paystack payment gateway, enabling merchants across Africa to accept secure online payments through cards, bank transfers, USSD, and mobile money.
Developed by Kommandhub Limited.
This document is the technical reference for developers contributing to the plugin. If you only want to install and configure it on a live shop, the Installation and Configuration sections are enough.
Table of Contents
- Project Overview
- Key Features
- Supported Payment Channels
- Architecture Overview
- Technology Stack
- Directory Structure
- System Requirements
- Installation
- Local Development Setup
- Docker & Docker Compose
- Makefile Commands
- Configuration Options
- Build & Asset Compilation
- Testing
- Code Quality
- Data Handling
- Logging & Debugging
- Security Considerations
- Performance Considerations
- Deployment
- CI/CD
- Version Compatibility
- Troubleshooting
- Contributing
- Coding Standards
- Roadmap
- License
- Support
Project Overview
The plugin adds Paystack as a native Shopware 6 payment method. It handles the full payment lifecycle:
- Initialize a Paystack transaction and redirect the customer to Paystack's hosted checkout.
- Finalize the payment on return by verifying the transaction against the Paystack API — matching status, amount, and currency before the order is marked paid.
- Reconcile payments asynchronously via signed webhooks (for cases where the customer never returns from the redirect).
- Refund orders (full or partial) from the Administration, guarded by a dedicated permission and a server-side over-refund check.
It also provides customer bank-account verification (via Paystack's account-resolution endpoint) and correct money handling for the currencies Paystack supports, including zero-decimal (e.g. XOF, RWF) and three-decimal (e.g. KWD) currencies.
The plugin does not create currencies or languages in your shop. Amounts are converted correctly for whichever currency an order uses; adding a currency (e.g. XOF) or a language to Shopware remains a normal shop configuration step.
Key Features
- Native Shopware 6 payment method with Paystack hosted checkout.
- Payment verification that validates status + amount + currency (the return reference is attacker-controllable, so all three are checked).
- Asynchronous reconciliation through signed webhooks:
charge.success,refund.pending,refund.processed. - Full and partial refunds from the order detail page, with a server-side over-refund cap.
- A dedicated
paystack.refundadmin permission, assignable to roles (depends on the order editor permission). - Customer bank-account verification in the account area.
- Correct amount handling for every currency Paystack supports, including 0-decimal (e.g. XOF, RWF) and 3-decimal (e.g. KWD) currencies — amounts always cross the Paystack boundary in the right minor unit.
- Plugin interface translated into English, German and French (administration + storefront).
- Optional Paystack split payments (subaccount / charges bearer).
- Configurable, level-filtered logging with a sandbox/live mode toggle.
Supported Payment Channels
Availability depends on your Paystack account and configuration:
- Debit & credit cards
- Bank transfers
- USSD
- QR codes
- Mobile money (Ghana, Kenya, and others)
Architecture Overview
The plugin uses a feature-first layout that mirrors Shopware's own plugins (for example, SwagPayPal). Each top-level directory under src/ is a bounded feature; inside it, code is organized into flat, Symfony-idiomatic folders (Service, Subscriber, Handler, Struct, Event, Enum, Controller). There is no Application/Domain/Infrastructure layering — a top-level module is the boundary, and every class has one obvious home.
Guiding principles
- Shopware entry points (payment handler, controllers, subscribers) stay thin and delegate to services.
- A change stays inside its feature module; cross-module access goes through a service, not by deep-linking another module's internals.
- New Paystack API calls go through a typed
Client/Resource/class, never raw HTTP. - Money is always in minor units at the Paystack boundary and converted only through
PaystackCurrencyHelper.
Payment flow
Customer selects Paystack
│
▼
PaymentProcessor.initialize() ──► Paystack transaction created, reference persisted
│
▼
Redirect to Paystack hosted checkout
│
▼
Customer pays ──► redirect back ──► FinalizeProcessor.verify()
│ (status == success AND amount AND currency match)
▼
Order transaction marked "paid"
If the customer never returns, the `charge.success` webhook reconciles the order
using the reference stored at initialize time (idempotent: it no-ops if already paid).
Refund flow
Admin opens the order → Paystack tab → Refund
│ (client gate: paystack.refund permission + refundable balance)
▼
RefundController.refund() ──► server gate: _acl paystack.refund, min & max (over-refund) checks
│
▼
Paystack refund created ──► refund.pending / refund.processed webhooks update Shopware refund records
Technology Stack
| Layer | Technology |
|---|---|
| Language | PHP 8.2+ (8.3 in the dev container) |
| Framework | Shopware 6 (shopware/core, shopware/storefront), Symfony |
| Admin UI | Vue, Shopware Administration, Meteor Component Library (mt-*) |
| Admin build | Vite (output committed under Resources/public/) |
| Storefront | Twig + vanilla JS plugin (output under Resources/app/storefront/dist/) |
| Testing | PHPUnit 11 |
| Static analysis | PHPStan (level 9) |
| Code style | PHP-CS-Fixer |
| Local environment | Docker + Docker Compose (dockware/shopware) |
| CI | GitHub Actions |
Directory Structure
KommandhubPaystackSW/
├── src/
│ ├── KommandhubPaystackSW.php # Plugin base class (lifecycle hooks)
│ ├── Administration/
│ │ └── Controller/RefundController.php # Admin refund API endpoint
│ ├── BankVerification/
│ │ ├── Controller/ # Storefront account-verification endpoints
│ │ └── Service/ # Bank validation
│ ├── Checkout/
│ │ ├── Payment/
│ │ │ ├── Handler/ # Shopware payment handler (pay/finalize/refund)
│ │ │ ├── Service/ # PaymentProcessor, FinalizeProcessor,
│ │ │ │ # RefundProcessor, RefundAggregator,
│ │ │ │ # Transaction{Verification,Metadata}Processor,
│ │ │ │ # PayloadBuilder, OrderTransactionService, …
│ │ │ ├── Struct/ # DTOs (e.g. PaystackInitializationResponse)
│ │ │ ├── Event/ # PaymentFinalizedEvent
│ │ │ └── Enum/ # PaystackTransactionStatus
│ │ └── Cart/ # CartValidator + Error/
│ ├── Client/ # Paystack REST client
│ │ ├── PaystackClient.php
│ │ ├── Http/ # HttpClientInterface + PaystackHttpClient
│ │ └── Resource/ # One typed class per Paystack endpoint
│ ├── DataAbstractionLayer/ # Order-transaction reader/writer gateways
│ ├── Webhook/
│ │ ├── Controller/ # Storefront webhook endpoint
│ │ ├── Service/ # Signature validator, event factory, processor
│ │ ├── Subscriber/ # ChargeSuccessSubscriber, WebhookSubscriber
│ │ └── Event/ # WebhookEvent + charge/refund events
│ ├── Setting/Service/Config.php # Typed access to system configuration
│ ├── Logging/ConfigurableLogger.php # Level-filtered logger
│ ├── Exception/ # Domain exceptions
│ ├── Util/ # PaystackConstants, PaystackCurrencyHelper
│ ├── Installer/ # Payment method + custom fields installers
│ └── Resources/
│ ├── config/ # services.yml, routes.yml, config.xml
│ ├── snippet/ # Storefront snippets
│ ├── views/ # Twig templates
│ ├── app/administration/ # Vue admin source (src/) + built assets (public/)
│ └── app/storefront/ # Storefront JS source + dist
├── tests/
│ ├── Unit/ # Mirrors src/, no Shopware kernel
│ └── Integration/ # Controller/plugin tests
├── .github/workflows/php.yml # CI pipeline
├── composer.json
├── phpstan.dist.neon
├── phpunit.dist.xml
├── .php-cs-fixer.dist.php
├── docker-compose.yml
├── Makefile
├── CHANGELOG_en-GB.md
└── CLAUDE.md # Contributor conventions (read this too)
Assets under
Resources/public/andResources/app/storefront/dist/are generated. Never hand-edit them — change the source inResources/app/**/src/and rebuild.
System Requirements
- Shopware:
~6.6.0or~6.7.0 - PHP:
8.2+ - Composer: 2.x
- Paystack account: https://dashboard.paystack.com/#/signup
- For local development: Docker and Docker Compose
- For admin/storefront asset builds: Node.js (provided inside the dev container)
Installation
Via Composer (recommended)
composer require kommandhub/paystack-sw bin/console plugin:refresh bin/console plugin:install --activate KommandhubPaystackSW bin/console cache:clear
Manual upload
- Build a ZIP containing at least
src/andcomposer.json. - In the Administration, go to Extensions → My Extensions → Upload Extension.
- Install and activate Paystack Payment.
After installation, assign the Paystack Payment method to your sales channel under Settings → Shop → Payment Methods.
Local Development Setup
The repository ships a Docker Compose stack based on dockware that mounts this plugin into a full Shopware install.
# 1. Clone the repository git clone <repository-url> KommandhubPaystackSW cd KommandhubPaystackSW # 2. Start the stack (builds the container and prepares the shop) make up # 3. Install and activate the plugin inside the container make shell bin/console plugin:refresh bin/console plugin:install --activate KommandhubPaystackSW bin/console cache:clear exit
The plugin directory is mounted at /var/www/html/custom/static-plugins/KommandhubPaystackSW; .git/, node_modules/, and vendor/ are excluded from the mount. Changes to source files on the host are reflected immediately in the container.
Default dockware credentials:
- Admin: user
admin, passwordshopware - Database: user
root, passwordroot, databaseshopware
Docker & Docker Compose
The stack is defined in docker-compose.yml:
- Image:
dockware/shopware:6.7.8.0 - Container:
kommandhub-paystack-plugin - PHP: 8.3 (
XDEBUG_ENABLEDtoggle available) - Persistent volume:
database(MySQL data)
make up # build + start + prepare make down # stop and REMOVE volumes (wipes the database) make shell # open a shell in the container
No host port is published by default. To reach the Administration from your browser, add a mapping such as
ports: ["80:80"]to theshopwareservice and reload withdocker compose up -d. Do not usemake restartfor this — it runsdocker compose down -v, which deletes the database volume and the installed shop.
Makefile Commands
All targets run the underlying tools inside the running container.
| Command | Description |
|---|---|
make up |
Build, start, and prepare the Shopware container |
make down |
Stop the container and remove volumes (wipes the DB) |
make build |
Rebuild the container image |
make restart |
down + up (wipes the DB) |
make shell |
Open a bash shell in the container |
make plugin-list |
List installed plugins |
make test |
Run PHPUnit. Filter with make test FILTER="--filter SomeTest" |
make test-coverage |
Run PHPUnit with a text coverage report |
make cs |
PHP-CS-Fixer dry run (no changes) |
make cs-fix |
PHP-CS-Fixer, applying fixes |
make analyse |
PHPStan static analysis on src/ |
make fixture-load |
Load test fixtures |
make resync |
Sync the test config into the shop root |
make prepare |
Full project preparation (run by make up) |
Before committing, run:
make cs-fix && make analyse && make test
Configuration Options
Configure the plugin under Extensions → My Extensions → Paystack → Configuration. Options are stored in Shopware's system configuration under the KommandhubPaystackSW.config.* domain and read through Setting\Service\Config.
| Key | Type | Purpose |
|---|---|---|
apiSecretKey |
password | Live secret key (sk_live_...) |
enableSandbox |
bool | Use the sandbox/test key instead of live |
apiSecretKeySandbox |
password | Test secret key (sk_test_...) |
enableSplitPayment |
bool | Enable Paystack split payments |
subaccountCode |
text | Split payment subaccount code |
splitCode |
text | Split payment split code |
splitPaymentTransactionCharge |
int | Flat transaction charge for split payments |
paystackChargesBearer |
select | Who bears fees: Account (merchant) or Subaccount |
metaData |
multi-select | Extra order/customer data sent to Paystack |
collectBankData |
bool | Collect customer bank data in the account area |
showBvnField |
bool | Show the BVN field |
requireBvn |
bool | Make the BVN field required |
refundEnabled |
bool | Allow refunds from the Administration |
minimumRefundAmount |
int | Minimum refund amount, in minor units |
enableDebugging |
bool | Enable verbose logging |
logLevels |
multi-select | PSR-3 levels to log when debugging is enabled |
Secret keys are stored in Shopware's system configuration, never in code. There is no public key setting — Paystack initialization is server-to-server using the secret key.
Build & Asset Compilation
PHP has no build step beyond composer install. Frontend assets do, and their compiled output is committed to the repository.
Administration (Vue → Vite), output to Resources/public/administration/:
make shell ./bin/build-administration.sh
Storefront, output to Resources/app/storefront/dist/:
make shell ./bin/build-storefront.sh
Rebuild the relevant bundle whenever you change source under Resources/app/administration/src/ or Resources/app/storefront/src/, and commit the regenerated assets together with the source. Keeping route names, service IDs, and custom-field keys stable means most PHP changes require no asset rebuild.
Testing
Tests live under tests/ and mirror src/. Each behavior change should ship with a test.
make test # full suite make test FILTER="--filter RefundControllerTest" make test-coverage # text coverage report
- Unit tests (
tests/Unit/) use plainPHPUnit\Framework\TestCasewith mocks and require no Shopware kernel. These run in CI. - Integration tests (
tests/Integration/) exercise controllers and the plugin lifecycle. Those tagged#[Group('kernel')]need a booted Shopware kernel and database, so they run locally viamake testrather than in the lightweight CI job. - End-to-end: there is no automated E2E suite yet. The payment, refund and webhook flows are validated manually against a running shop with a Paystack test key. Automating this is on the roadmap.
Pure business logic in the admin (Resources/app/administration/src/service/refund-calculator.js) has a companion Jest-style spec (refund-calculator.spec.js) for a full Shopware admin test runner.
Code Quality
| Tool | Command | Config |
|---|---|---|
| PHPStan (level 9) | make analyse |
phpstan.dist.neon |
| PHP-CS-Fixer | make cs / make cs-fix |
.php-cs-fixer.dist.php |
| PHPUnit | make test |
phpunit.dist.xml |
CI enforces PHP lint, PHPStan, code style, unit + mockable integration tests, and a 100% line-coverage threshold. Kernel-dependent tests are excluded from CI.
Data Handling
The plugin ships no schema migrations — it does not alter the database schema. All setup is performed through Shopware's plugin lifecycle by dedicated installers:
- Payment method: created and kept in sync by
Installer\PaymentMethodInstaller. - Custom fields (Paystack reference, amount, currency, fee, etc.): created by
Installer\CustomFieldsInstallerduring install/update.
These run on install()/update() and are reverted appropriately on uninstall (custom fields are removed only when the user does not keep plugin data).
The plugin's
update()lifecycle hook re-runs the installers, which migrates the stored payment-methodhandlerIdentifierif the handler class moves between versions. Without this, a plugin update (as opposed to a fresh install) would leave a dangling handler identifier and break checkout.
Logging & Debugging
Logging goes through Logging\ConfigurableLogger, wired to the paystack_channel Monolog channel (writes to var/log/).
- Set
enableDebuggingand picklogLevelsto control verbose output. - Error, critical, alert, and emergency messages are always written, regardless of the debugging toggle, so production keeps a trail of failures (webhook signature rejections, verification/refund errors).
- Enable Xdebug in the container by setting
XDEBUG_ENABLED=1indocker-compose.ymland restarting.
Security Considerations
- Webhook authenticity:
WebhookSignatureValidatorverifies thex-paystack-signatureHMAC over the raw request body usinghash_equals(timing-safe) and rejects missing signatures or a missing secret. - Payment verification: an order is marked paid only when the verified transaction matches status AND amount AND currency. The return
referencecomes from an attacker-controllable callback, so it is never trusted on its own. - Refund authorization: the refund endpoint requires the dedicated
paystack.refundprivilege (route_acl), and the admin action is gated by the same permission. - Over-refund protection: the server recomputes the refundable balance (captures/transaction total minus completed and in-progress refunds) and rejects amounts above it and below the configured minimum. The client-side bound is a UX aid only.
- Secrets: API keys live in Shopware's system configuration, not in the codebase or in URLs.
- Money handling: amounts cross the Paystack boundary in minor units via
PaystackCurrencyHelper, which knows per-currency decimals — never multiply by 100 inline.
Performance Considerations
- Order/transaction reads use targeted DAL criteria with only the associations they need.
- The admin detail view guards against concurrent capture/refund loads and loads plugin configuration reactively rather than on a fixed lifecycle tick.
- Refund math is a pure, memoizable computation isolated in
refund-calculator.js.
Deployment
- Ship the plugin via Composer (
composer require kommandhub/paystack-sw) or an Administration upload. - Run:
bin/console plugin:refresh bin/console plugin:update KommandhubPaystackSW # migrates handler identifier + custom fields bin/console cache:clear - Ensure compiled Administration and Storefront assets are built and committed as part of the release.
- Configure live secret keys and disable sandbox mode.
- Grant the Paystack → Process Paystack refunds permission to the roles that should be able to issue refunds (the built-in admin role already has all privileges).
Validate a release build for the Shopware Store with:
shopware-cli extension validate . --full --store-compliance
CI/CD
GitHub Actions (.github/workflows/php.yml) runs on pushes to main/develop and on pull requests:
- Validate
composer.json. - Install dependencies.
- Lint all PHP files.
- PHPStan (level 9).
- PHP-CS-Fixer (dry run).
- PHPUnit unit tests with clover coverage.
- Enforce a minimum coverage threshold (85%) via
coverage-check.
Integration tests are excluded from this job because they need a booted Shopware kernel; run them locally with make test.
Version Compatibility
| Plugin | Shopware | PHP |
|---|---|---|
0.9.0-beta.x |
6.6 and 6.7 | 8.2+ |
A single plugin release supports both Shopware 6.6 and 6.7 (shopware/core: ~6.6.0 || ~6.7.0).
Pre-1.0 status: the plugin is on a
0.xversion while it completes sandbox/staging validation (see Roadmap). Namespaces and the public API may still change before1.0.0. Once testing is complete and all critical issues are resolved, this will be promoted to1.0.0and submitted to the Shopware Store.
Troubleshooting
Order status not updating after payment
- Confirm the webhook endpoint is reachable from Paystack and the secret key matches the mode (test vs live).
- Check
var/log/for verification or signature errors.
Plugin not visible in the Administration
bin/console plugin:refresh
Stale Administration UI or DI errors after changes
bin/console cache:clear
Refund action not shown
- Ensure
refundEnabledis on, the role has thepaystack.refundpermission, and the transaction still has a refundable balance.
PHPStan can't find Shopware\Storefront\... during shopware-cli validation
shopware/storefrontmust be declared inrequire(it is). Do not pointscanDirectoriesat the host'svendor/— let Composer install and autoload it.
Contributing
- Create a feature branch off
develop. - Keep changes inside the relevant feature module; reach across modules through services.
- Add or update tests alongside behavior changes (mirror the
src/path undertests/). - Rebuild admin/storefront assets if you touched their source, and commit the output.
- Run the full local gate before opening a PR:
make cs-fix && make analyse && make test
- Use Conventional Commits for commit messages (
feat:,fix:,refactor:,test:, …). - Open a pull request against
develop.
Please also read CLAUDE.md for the module boundaries and project-specific conventions.
Coding Standards
- PHP style: PSR-12, enforced by PHP-CS-Fixer (
.php-cs-fixer.dist.php). - Static analysis: PHPStan level 9 must pass with no new errors.
- Architecture: feature-first modules; one obvious home per class; thin Shopware entry points delegating to services.
- Paystack API: add new calls as typed classes under
Client/Resource/, never as raw HTTP. - Money: always convert through
Util\PaystackCurrencyHelper; amounts are minor units at the Paystack boundary. - Custom-field keys: defined only in
Util\PaystackConstants(paystack_referenceis the webhook lookup key). - Dependency injection: services are autowired via the
../../*glob inservices.yml. Symfony does not auto-alias an interface to its single implementation — when you add a constructor-injected*Interface, add an explicitalias:entry.
Roadmap
- Complete the migration of Administration components to the Meteor Component Library (
sw-*→mt-*), including data grids and modals. - Make the storefront bank-verification feature optional for headless setups (decouple from
StorefrontController). - Trim unused Paystack API resource classes to the endpoints the plugin actually uses.
- Add automated end-to-end coverage. Planned in layers, cheapest first:
- Paystack sandbox contract tests — call the real API with an
sk_test_key to prove the client matches Paystack's contract and that amounts survive the round trip in minor units. - Webhook endpoint tests — POST signed/unsigned payloads over HTTP (Paystack signs with HMAC-SHA512 of the raw body using the secret key, in
x-paystack-signature). - Browser tests — storefront checkout through Paystack's hosted page and the Administration refund flow.
Notes for whoever picks this up: gate the suite behind an env var so it skips by default, refuse to run against a live key, exclude it from CI, and remember that real webhook delivery needs a public tunnel and the tunnel hostname registered as a Shopware sales-channel domain (otherwise Shopware answers
400before the controller runs).
- Paystack sandbox contract tests — call the real API with an
License
Licensed under the MIT License. See LICENSE for details.
Support
- Email: info@kommandhub.com
- Website: https://kommandhub.com
- Support: https://kommandhub.com/support
