Appearance
Architecture & extension points
For developers
Namespace Kommandhub\FlutterwaveSW\ → src/. Plugin class Kommandhub\FlutterwaveSW\KommandhubFlutterwaveSW. Feature-first modules; Shopware entry points stay thin and delegate to services.
The one rule: major units on the wire
Flutterwave v3 charges exactly the amount it is sent — ₦100.00 is sent as 100. This is the opposite of Paystack.
- Amounts sent to Flutterwave (initialize, refund) are in major units. Never scale them.
- Amounts compared (verification, refundable balance) go through
Util\FlutterwaveCurrencyHelper, which converts both sides to minor units first so float error cannot hide a mismatch, and respects per-currency decimals (RWF/UGX 0, KWD 3).
Mixing the two is the easiest way to introduce a 100× overcharge.
Modules
| Module | Responsibility |
|---|---|
Checkout/Payment | FlutterwavePaymentHandler (thin) → PaymentProcessor, FinalizeProcessor, RefundProcessor; refund helpers RefundEligibilityResolver, RefundAmountCalculator, FlutterwaveRefundLedger (live refund history). |
Checkout/Cart/Validation | TransactionCartValidator — blocks checkout without a secret key for the active mode. |
Service | PayloadBuilder, OrderTransactionService. |
Webhook | Controller, WebhookSignatureValidator (verif-hash), WebhookEventFactory, WebhookProcessor, WebhookDeduplicator, subscribers. |
Administration/Controller | RefundController. |
BankVerification | Storefront bank list / resolve / save. |
Client | FlutterwaveClient; resources Transaction, Refund, Bank, Bvn, Subaccount. |
Payment flow
text
pay() PaymentProcessor → POST /v3/payments
tx_ref = Shopware order transaction id, amount in major units,
customer email + name, customizations {title, logo, description},
redirect_url = Shopware return URL
→ redirect to Flutterwave's hosted link
finalize() ?status=cancelled → transaction cancelled, customer-cancelled exception
?transaction_id=<id> → GET /v3/transactions/<id>/verify
validate tx_ref, amount, currency
persist Flutterwave data as custom fields
successful → paid · failed → fail · other → reopenWebhooks
POST /flutterwave/webhook, storefront scope.
Authentication. Flutterwave does not sign the payload. It echoes the static secret hash you configured, in the verif-hash header. The plugin compares it with hash_equals against secretHash / secretHashSandbox for the active mode, read from the global configuration scope (WebhookProcessor passes no sales channel). A valid header proves the sender knows the secret; it says nothing about the body.
| Event | Handling |
|---|---|
charge.completed | tx_ref must be a UUID that matches a Shopware order transaction (anything else is acknowledged and ignored). Deduplicated per Flutterwave transaction ID (flutterwave_processed_events). Skipped if already paid. Otherwise only the transaction ID is handed to FinalizeProcessor, which re-fetches status, amount and currency from the API. |
refund.completed | Matched to the pending Shopware refund by externalReference (the Flutterwave refund ID stored when the refund was requested). Deduplicated. Status completed/successful completes the refund, failed fails it, anything else leaves it pending. |
Refund status comes from the payload
Unlike charge.completed, the refund.completed handler takes the refund status from the webhook body and does not re-fetch it from the Flutterwave API. The README describes every handler as re-verifying; for refunds that is not the case in 0.9.0-beta.1. Protection rests on the secret hash and on the refund ID having to match a refund this shop created. Listed on Known issues.
Responses: 200 {"status":"success"} handled or ignored · 400 malformed body · 403 missing/invalid hash or no hash configured · 500 unexpected error (Flutterwave retries). Flutterwave treats anything but 200 — including 204 and redirects — as a failed delivery.
Refund flow
text
POST /api/_action/flutterwave/refund (_acl flutterwave.refund)
RefundEligibilityResolver refundEnabled, state paid/partially_paid/partially_refunded,
has Flutterwave transaction id, currency known, minimum amount
FlutterwaveRefundLedger already refunded (minor units) — live from Flutterwave
RefundAmountCalculator min ≤ amount ≤ transaction − already refunded
POST /v3/transactions/<id>/refund (major units)
create Shopware capture + refund, pending, externalReference = Flutterwave refund id
refund.completed webhook → complete or fail that refund; transaction refunded / partially refundedData written
Order transaction custom fields (Util\FlutterwaveConstants): flutterwave_reference, flutterwave_transaction_id, flutterwave_payment_type, flutterwave_transaction_fee, flutterwave_amount_charged, flutterwave_amount_settled, flutterwave_currency, flutterwave_verified_at, flutterwave_customer, flutterwave_processed_events (webhook idempotency).
Customer custom fields: see Customer bank details.
No migrations or tables. The payment method (technical name kommandhub_flutterwave_payment) and custom fields are created by installers on install and update.
Events
The plugin dispatches its webhook events on Shopware's event dispatcher; you can listen to them:
Kommandhub\FlutterwaveSW\Webhook\Event\ChargeCompletedEventKommandhub\FlutterwaveSW\Webhook\Event\RefundCompletedEvent
Both fire after the verif-hash check. Treat their payload as untrusted provider data. There is no "payment finalized" event; listen to Shopware's state_enter.order_transaction.state.paid instead.
Known limitations
paymentOptions,sessionDurationandmaxRetryAttemptare read by no code.- The
logosetting stores a media ID, which is sent as-is incustomizations.logowhere Flutterwave expects a URL. - Split payments are not implemented (the
Subaccountclient resource exists but is unused). PaymentMethodBlockedError's constructor is forward-deprecated for Shopware 6.8; addressed when 6.8 support is added.
Development setup & testing
bash
git clone https://github.com/KommandHub/KommandhubFlutterwaveSW.git
cd KommandhubFlutterwaveSW
make up # Shopware + this plugin in container `shopware-flutterwave`
make shell # then: bin/console plugin:install --activate KommandhubFlutterwaveSW
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. - Payment, refund and webhook flows are tested manually with Flutterwave test credentials; 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.