Appearance
Architecture & extension points
For developers
Namespace Kommandhub\PaystackSW\ → src/. Plugin class Kommandhub\PaystackSW\KommandhubPaystackSW.
Modules
| Module | Responsibility |
|---|---|
Checkout/Payment/Handler | PaystackPaymentHandler — Shopware's payment handler (pay, finalize, refund); delegates to services. |
Checkout/Payment/Service | PaymentProcessor (initialize), FinalizeProcessor (verify + mark paid), RefundProcessor, RefundAggregator, TransactionVerificationProcessor, TransactionMetadataProcessor, PayloadBuilder, TransactionService, OrderTransactionService. |
Checkout/Cart | CartValidator — blocks checkout when the secret key for the active mode is missing. |
Webhook | Controller, WebhookSignatureValidator, WebhookEventFactory, WebhookProcessor, RefundInitializeService, subscribers. |
Administration/Controller | RefundController — the refund API used by the order tab. |
BankVerification | Storefront controller for bank list, account resolution and saving bank details. |
Client | PaystackClient with one typed class per Paystack endpoint under Client/Resource/. |
DataAbstractionLayer | Reader/writer gateways for order transactions and refunds. |
Installer | PaymentMethodInstaller, CustomFieldsInstaller. |
Util | PaystackConstants (custom-field keys), PaystackCurrencyHelper (minor units), OrderCurrencyResolver. |
Payment flow
text
PaystackPaymentHandler::pay()
└─ PaymentProcessor
├─ PayloadBuilder amount in minor units, currency, email, callback_url,
│ metadata, optional split parameters
├─ POST /transaction/initialize
├─ persist custom field paystack_reference on the order transaction
└─ redirect to authorization_url
customer returns ?reference=…
PaystackPaymentHandler::finalize()
└─ FinalizeProcessor
├─ already "paid"? → no-op
├─ TransactionVerificationProcessor
│ GET /transaction/verify/{reference}
│ assert status == success AND amount (minor units) AND currency match
├─ TransactionMetadataProcessor persist Paystack data as custom fields
├─ OrderTransactionStateHandler::paid()
└─ dispatch PaymentFinalizedEventA missing reference on return is treated as a customer cancellation; a failed verification call interrupts finalization so Shopware shows the payment-failed page.
Webhook reconciliation. charge.success is looked up by paystack_reference (stored at initialization) and run through the same FinalizeProcessor, so the redirect and the webhook cannot diverge. Both are idempotent: a transaction already paid is skipped.
Refund flow
text
Admin → POST /api/_action/paystack/refund
├─ _acl paystack.refund, refundEnabled, transaction state paid / partially_paid / partially_refunded
├─ min (minimumRefundAmount) and max (captures − completed/in-progress refunds) in minor units
└─ POST /refund at Paystack
refund.pending → RefundInitializeService: create capture + refund in Shopware
(externalReference = "<refund id>-<transaction_reference>", deduplicated,
bounded by the transaction total)
refund.processed → find refund by externalReference (create it if pending never arrived),
check amount matches, then Shopware's PaymentRefundProcessor
→ PaystackPaymentHandler::refund() → RefundProcessor
→ refund completed; transaction refunded / partially refundedData written
Order transaction custom fields (keys in PaystackConstants):
| Key | Content |
|---|---|
paystack_reference | Paystack reference. Written at initialization; the webhook lookup key. |
paystack_transaction_id | Paystack transaction ID |
paystack_payment_type | Channel (card, bank_transfer, ussd…) |
paystack_transaction_fee | Fee, converted from minor units |
paystack_amount | Amount charged, converted from minor units |
paystack_currency | Currency reported by Paystack |
paystack_verified_at | Verification timestamp |
Customer custom fields — the bank-details set kommandhub_paystack_fieldset, see Customer bank details.
The plugin ships no migrations and no tables of its own. The payment method (technical name kommandhub_paystack_payment) and the custom-field set are created by installers on install() and re-applied on update().
Events you can subscribe to
| Event | When | Payload |
|---|---|---|
Kommandhub\PaystackSW\Checkout\Payment\Event\PaymentFinalizedEvent | After a payment was verified and the transaction set to paid (redirect or webhook) | getOrder(), getOrderTransaction(), getPaymentTransactionStruct(), getContext() |
Kommandhub\PaystackSW\Webhook\Event\ChargeSuccessEvent | A signed charge.success webhook arrived | getData() (Paystack data object), getContext() |
Kommandhub\PaystackSW\Webhook\Event\RefundPendingEvent | A signed refund.pending webhook arrived | same |
Kommandhub\PaystackSW\Webhook\Event\RefundProcessedEvent | A signed refund.processed webhook arrived | same |
php
use Kommandhub\PaystackSW\Checkout\Payment\Event\PaymentFinalizedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
final class NotifyWarehouseOnPayment
{
#[AsEventListener]
public function __invoke(PaymentFinalizedEvent $event): void
{
$orderNumber = $event->getOrder()->getOrderNumber();
// …
}
}The webhook events fire after signature verification but before the plugin's own subscribers have finished; treat getData() as provider data, not as confirmed state. Other Paystack webhook event types are acknowledged and logged, not dispatched.
Replaceable services
The finalizer depends on two interfaces, aliased in services.yml, which you can decorate:
Checkout\Payment\Service\TransactionVerificationProcessorInterface— verification rules.Checkout\Payment\Service\TransactionMetadataProcessorInterface— what is persisted after verification.
Keep the status + amount + currency checks if you decorate verification: the reference comes from an attacker-controllable query string.
Money
Paystack expects minor units (kobo, pesewas). All conversions go through Util\PaystackCurrencyHelper::toMinorUnit() / fromMinorUnit(), which know each currency's decimals (NGN 2, XOF/RWF 0, KWD 3). Never multiply by 100 directly.
Security notes
- Webhook signature: HMAC-SHA512 of the raw body with the secret key, compared with
hash_equals; missing header or missing key →403. The key is read from the global configuration scope. - Verification checks status, amount and currency before marking paid.
- Refund endpoint: route
_aclpaystack.refund, server-side min/max, currency resolved from the order (fails closed when unknown). - Webhook refund amounts are checked against the Shopware refund and the transaction total.
Development setup & testing
bash
git clone https://github.com/KommandHub/KommandhubPaystackSW.git
cd KommandhubPaystackSW
make up # Shopware + this plugin in container `kommandhub-paystack-plugin`
make shell # then: bin/console plugin:install --activate KommandhubPaystackSW
make cs-fix && make analyse && make test # the gate to pass before every commit
make test FILTER=SomeTest
make test-coverage
make validate-plugin # Shopware Store compliance (shopware-cli, inside the container)
make zip # release ZIP into build/tests/Unitmirrorssrc/and needs no kernel; tests that boot Shopware carry#[Group('kernel')]and are excluded in CI.- CI (GitHub Actions,
.github/workflows/php.yml) runs composer validate, PHP lint, PHPStan level 9, php-cs-fixer and PHPUnit, and enforces 100 % line coverage. - Webhook and payment flows are tested manually against a Paystack test key; there is no automated end-to-end suite yet. For local webhook testing use a tunnel and add its hostname as a domain on a sales channel.
make downdeletes the stack's database volume; the stack publishes no host port by default (addports: ["80:80"]to reach the shop in a browser). Shared tooling: Development environment.