# Multiple Bangladesh payment gateways

Prepared on 16 October 2026 from the Donation and Settings modules, public Blade checkout forms, accounting synchronization trait, database schema, and provider documentation. The installed framework is Laravel 13.35.0 with Inertia 3.5.1. The inspection findings below describe the starting point; the first release has now been implemented as described in the status notes.

## Implementation status

The first release implements ShurjoPay and SSLCommerz behind a shared checkout and verification boundary. Settings → Payment Gateways controls enablement, default, display order, and receiving accounts. Both public entry points use those settings, and new donations retain the canonical gateway name from creation. Verified income still passes exclusively through `SyncsDonationTransaction`.

The public forms now use gateway radio buttons and omit the additional customer-details section at the owner's request. Both campaign and homepage quick donations proceed directly to checkout. The server uses submitted or known donor details first, then gateway-only defaults for missing values: anonymous name, a configured merchant email, unspecified address/city, postcode `0000`, and Bangladesh. `PAYMENT_CUSTOMER_EMAIL` overrides the fallback email; otherwise the configured mail sender is used. Defaults are never saved into donor profiles. Merchant sandbox acceptance of these fallback values still needs verification.

Provider callback/IPN routes verify server-side before posting income. Existing ShurjoPay callback aliases remain available. Recovery runs every five minutes through the registered `donations:recover-gateway-payments --limit=100` command, with an authorized staff recheck action. Limited recovery batches rotate unresolved records so an older pending payment cannot indefinitely block later ones. Distinct additional verified captures get their own receipt and a staff review flag.

The additive gateway migration and missing-default seeder have been applied locally. Existing account selections are preserved; there is no historical backfill or account-balance reset. For another environment, run migrations and `php artisan db:seed --class='Modules\Settings\Database\Seeders\PaymentGatewaySettingsSeeder' --no-interaction`, then reset the permission cache. Grant `payment-settings-edit` to staff who manage gateways.

Before enabling SSLCommerz, configure `SSLCOMMERZ_ENVIRONMENT` (`sandbox` or `live`), `SSLCOMMERZ_STORE_ID`, and `SSLCOMMERZ_STORE_PASSWORD`, refresh the configuration cache if used, and select an active receiving account. Credentials remain server-side. Existing ShurjoPay environment variables remain supported. Production accepts live gateway configuration only. Keep the scheduler and notification queue running.

Automated coverage uses HTTP fakes for provider checkout, callback verification, identity/amount checks, duplicate notifications, second captures, recovery, Settings authorization, and existing accounting synchronization. A focused test with two independent MySQL workers verified one receipt and one balance increase for simultaneous completion. Public form rendering and required-field behavior were also checked in the browser. Actual merchant sandbox/live payments still require merchant-approved testing; no real charge was made during implementation.

aamarPay, direct bKash/Nagad, refunds, and settlement reconciliation remain later phases. The broader Settings tests contain an existing analytics-script assertion failure unrelated to payment integration.

## 1. Required behavior

- Settings controls which implemented gateways accept new public donations.
- Both campaign checkout and homepage quick donation display the available gateways and submit the selected gateway to the server.
- A public donation stores its gateway's canonical name in `payment_method` **when the pending donation is created**, before contacting the provider. Success, cancellation, failure, and delayed notifications retain that name.
- The underlying payment channel is separate. Paying through SSLCommerz using bKash still means `payment_method = SSLCommerz`, with `payment_channel = bKash` if the provider supplies it.
- Verified paid donations continue to synchronize through `SyncsDonationTransaction`. Gateway drivers never create accounting transactions or adjust account balances directly.
- Disabling a gateway prevents new checkout admissions while allowing existing payments to finish verification. It does not reverse donations or remove history.

| Submitted gateway key | Stored `payment_method` | Example separate channel |
| --- | --- | --- |
| `shurjopay` | `ShurjoPay` | Visa, bKash |
| `sslcommerz` | `SSLCommerz` | Visa, bKash, internet banking |
| `aamarpay` | `aamarPay` | Provider-reported channel |
| `bkash` | `bKash` | Provider-reported channel, if applicable |

Gateway keys and canonical names come from application code. Never accept the stored name, receiving account, expected amount, merchant identity, or verified status from callback/browser input.

## 2. Current implementation findings

| Area | Current behavior | Required change |
| --- | --- | --- |
| Public controller | `SiteDonationController::store()` and `quickDonate()` inject `ShurjopayService` directly. | Resolve the selected gateway through a shared registry and checkout service. |
| Campaign form | `donation-fund-show.blade.php` contains one hardcoded ShurjoPay radio button. | Render a shared gateway selector from a sanitized server-side availability list. |
| Quick donation | `resources-site/views/components/donation-card.blade.php` has no gateway choice. | Add the same selector and an additional-details step when needed. |
| Input validation | `StoreDonationRequest` does not validate `payment_gateway`; quick checkout has separate inline rules. | Share checkout validation and enforce current enabled/ready gateways server-side. |
| Method attribution | New pending donations omit `payment_method`; ShurjoPay verification sets it to the returned `method`. | Store the gateway name immediately; put the returned channel in a separate field. |
| Provider metadata | Donation uses `sp_order_id`, `sp_code`, and `gateway_response`. | Introduce generic metadata while retaining legacy ShurjoPay fields and callback compatibility. |
| Accounting | `SyncsDonationTransaction` synchronizes paid model changes and has a ShurjoPay account fallback. | Assign the selected gateway's receiving account before checkout; preserve that assignment through verification. |
| Settings | Groups are allowlisted in the controller, request, and Vue form. | Add a `payments` group, component, validation, defaults, and explicit authorization. |
| Settings confidentiality | `SettingService::getAll()` caches all settings; `Setting` uses an activity trait that logs all changed attributes. | Keep gateway secrets outside generic settings in this phase. `is_public = false` alone would not solve secret caching/logging. |
| Notifications | ShurjoPay completion already locks the donation and dispatches `DonationPaid` after commit. | Centralize completion so every gateway follows the same rule. |
| Callback transport | Existing callback is GET; IPN is POST but reads only query parameters. Only `/donations/ipn` is CSRF-exempt. | Add provider-specific handlers with documented HTTP methods and exact CSRF exemptions for provider POST endpoints. |

The current admin method-label helper recognizes ShurjoPay aliases. It still needs the new canonical names, SSLCommerz/aamarPay labels, filters, and consistent export/notification presentation.

## 3. Rollout scope

1. **First release:** shared gateway foundation, existing ShurjoPay driver, SSLCommerz hosted checkout, Settings controls, both public entry points, gateway-name persistence, verification/recovery, and tests.
2. **Second release:** aamarPay using the same checkout/verification contract.
3. **Later drivers:** direct bKash and optionally Nagad, subject to merchant access and confirmation of the current official API contract. Do not show an enabled option for an unimplemented driver.

Hosted checkout keeps card entry on the provider's page. Laravel's installed HTTP client is sufficient for the first release; no new package dependency is required. Any later SDK needs a separate compatibility/dependency review.

Automatic charging of recurring commitments, provider refunds, settlement reconciliation, and the previously removed cash-book framework are outside this plan. A recurring installment paid through checkout uses the same completion operation and resolves its schedule once.

## 4. Settings and credentials

Add **Settings → Payment Gateways** within the existing Settings module. Store non-secret preferences in the existing settings table under `payments`:

| Preference | Behavior |
| --- | --- |
| Gateway enabled | Independent switch for each implemented provider. |
| Default gateway | Must be available; use it for initial selection on both public forms. |
| Display order | Deterministic ordering, with gateway key as a tie-breaker. |
| Receiving account | Active accounting account assigned to new donations using this gateway. |

Show read-only environment and configuration readiness indicators. Enabling requires complete credentials and an active receiving account. Seed new preferences only when absent; rerunning seeders must not overwrite the administrator's choices.

Concretely, store `payments.default_gateway` as a text setting and `payments.providers` as a JSON setting keyed by registered gateway slug, containing only `enabled`, `sort_order`, and `account_id`. Validate both the provider keys and their allowed nested fields; users cannot supply a driver class or API URL. This fits the existing typed-setting and group-form mechanisms.

Preserve the existing ShurjoPay configuration and the selected **shurjoPay Collections** account during transition. SSLCommerz and other new providers start disabled until configured and mapped to an account. Different gateways may deliberately use the same account, but never inherit ShurjoPay's account merely because their mapping is missing.

Credentials remain in environment/configuration files initially: ShurjoPay username/password/prefix and SSLCommerz store ID/password, with explicit sandbox/live profiles and fixed provider endpoint allowlists. Settings gets only sanitized metadata such as `configured: true`. Do not return credentials, token values, passwords, full checkout-session URLs, or raw verification payloads as Inertia props.

Add `payment-settings-edit` authorization following the existing group-specific authorization approach. Enforce it on viewing sensitive configuration and saving changes; hiding the Settings tab is insufficient. Audit enabled/default/account changes without recording secrets. Clear the existing settings cache after save; public render and admission checks must see updated preferences across workers.

If credential editing through Settings is later requested, design encrypted private storage, masked/write-only inputs, retention/rotation, and explicit cache/activity-log exclusions first. It is not needed to implement enable/disable switches.

## 5. Shared payment boundary

Keep payment code inside `Modules/Donation` and follow its existing service/provider structure:

- `Contracts/PaymentGateway`: provider identity, configuration readiness, checkout initiation, authoritative verification/query, and required checkout fields.
- `Services/PaymentGatewayManager`: an allowlisted registry; resolves drivers and derives public availability from settings plus readiness.
- `Services/DonationCheckoutService`: validates admission, creates the pending donation and checkout reference, pins the receiving account and provider context, then starts checkout.
- `Services/CompleteDonationPayment`: consumes a normalized verified result, checks identity/amount/currency, locks the donation, updates it once, resolves a recurring installment if applicable, and dispatches `DonationPaid` after commit.
- Provider drivers for ShurjoPay, SSLCommerz, and subsequently aamarPay. They translate provider-specific request/response fields rather than implement accounting or donor creation.
- Provider-specific callback/IPN controllers. They parse transport details and pass verified results into the shared completion operation.

Use small typed checkout/verification result objects. A normalized result identifies the merchant/environment, our order, provider reference, exact BDT gross amount, channel, provider payment time, and verified outcome. Drivers must preserve meaningful distinctions between declined, cancelled, pending, high-risk/review, and unknown results.

Keep external HTTP calls outside long database locks. After verification returns, acquire the row lock and recheck the current state before saving. Bind drivers explicitly in the module provider. Register any recovery command explicitly; module commands are not auto-discovered here.

```mermaid
flowchart LR
    Settings[Settings availability and account mapping] --> Registry[Gateway registry]
    Forms[Campaign and quick donation forms] --> Checkout[Shared checkout]
    Registry --> Checkout
    Checkout --> Drivers[Provider driver]
    Drivers --> Verification[Authoritative verification]
    Verification --> Completion[Shared payment completion]
    Completion --> Donation[Donation marked paid]
    Donation --> Trait[Existing synchronization trait]
    Trait --> Transactions[Income transaction and account balance]
```

## 6. Donation data and transaction synchronization

Use the existing donation as the checkout record for this release: **one provider session per donation**. Avoid a new generic payment-attempt subsystem until multiple attempts per donation are actually required.

Add nullable fields through an additive migration:

| Field | Purpose |
| --- | --- |
| `payment_gateway` | Stable provider key; null for manual donations. |
| `payment_channel` | Optional channel returned by the provider. |
| `gateway_environment` | Sandbox/live context pinned at initiation. |
| `gateway_merchant_scope` | Non-secret merchant identity/fingerprint for verification and duplicate detection. |
| `gateway_reference` | Provider's canonical verified payment reference. |
| `checkout_key` | Unique application submission key to prevent duplicate sessions from repeated form submissions. |
| `verified_at` | Last successful authoritative verification timestamp. |
| `gateway_context` | Private, allowlisted session/status/recovery metadata; hidden from serialization. |

Keep `payment_method`, `order_id`, `account_id`, `paid_at`, and `gateway_response` where needed. Minimize/redact stored payloads; never persist full card details or authentication tokens. Put a unique constraint on the non-null verified reference scoped by gateway, environment, and merchant. Provider IDs from different merchants or test environments must not collide.

Use an order identifier that meets every selected provider's limits. SSLCommerz documents a 30-character transaction ID limit; the current 36-character UUID should not be sent unchanged. A 26-character ULID is a suitable application-generated reference, subject to each driver's documented restrictions.

On creation, set the gateway key, canonical `payment_method`, expected gross amount/currency, and receiving account. On verification, preserve those facts and save the channel separately. The trait remains the only code that creates/updates the linked income transaction. Existing accounting screens can obtain gateway details through `transaction.donation`; no duplicate payment-method column is required on transactions.

Keep manual methods such as Cash, Cheque, and Bank Transfer available for manual entry. Public gateway callbacks must never overwrite a paid record with a later failure/cancellation. Soft-deleted donations require an explicit review/restore decision; a callback must not silently restore deleted income.

## 7. Public checkout behavior

Extract a shared Blade gateway selector used by campaign and homepage forms. Show only implemented, enabled, configured gateways with valid receiving accounts. Preserve English/Bangla labels, keyboard accessibility, selected values after validation errors, mobile layout, and current light/dark styles.

- **One available gateway:** preselect it and show its name.
- **Several available:** show a choice with the configured default selected.
- **No available gateways:** show a translated online-payment-unavailable message and disable checkout while keeping campaign content accessible.
- **Stale page:** reject a gateway disabled before admission, retain donor input, and show the current alternatives.

Validate `payment_gateway` in both public endpoints against the server's current registry. Unknown, disabled, misconfigured, or unmapped gateways fail before donor/donation/session creation. Client-provided `payment_method` is ignored; derive it from the registry.

Each driver declares required customer fields and amount limits. SSLCommerz documents name, email, phone, and address-related fields, while the current quick form collects a phone/email-style identifier and amount. For providers requiring more details, send quick donors through a short completion step before creating the remote session. Collect genuine required information rather than inventing email addresses, postcodes, or donor addresses. Keep the quicker existing flow for providers that support it.

Use the provider's documented non-shipping donation/product classification. Confirm mandatory fields and contracted limits in sandbox; do not assume all providers accept the application's full amount range.

## 8. Provider-specific work

**ShurjoPay:** wrap/refactor the existing service; keep token → checkout → verification behavior. Namespace cached tokens by environment, merchant, and credential version instead of the current global `shurjopay:token` key. Validate verified order, amount, currency, and merchant context. Stop assigning the returned `method` to `payment_method`. Retain the current callback/IPN URLs as compatibility aliases for existing sessions. [Official API guide](https://web2.shurjopay.com.bd/frontend-developers/direct-integration-with-api).

**SSLCommerz:** implement server-side session creation and redirect to the returned hosted checkout URL. Add success, fail, cancel, and IPN handlers. The official documentation requires server-side order validation; browser redirects or IPN bodies alone are insufficient. Interpret `VALID`/`VALIDATED` as provider verification outcomes only after checking our order, expected gross amount, BDT currency, and merchant context. Treat risk flags through an explicit review policy; do not mistake a high-risk result for a simple declined payment or silently discard received money. `store_amount` is the net merchant amount, not the donor's gross donation amount. [Official integration guide](https://sandbox-gw.sslcommerz.com/docs).

The guide currently documents BDT 10–500,000 for initiation and a 30-character transaction ID. Implement driver limits from the merchant's supported contract and reject unsupported amounts before checkout. Production and sandbox use separate credentials/endpoints. [Official initiation parameters](https://developer.sslcommerz.com/doc/v4/).

Keep fraud-review flags separate from financial status: an authoritatively confirmed captured payment still needs an income receipt, even if staff must review its risk flag. A review flag does not authorize refunding money or erasing that receipt.

**aamarPay:** add hosted initiation and authenticated transaction lookup by the merchant transaction identifier. Verify provider success code, amount, currency, and transaction identity through its transaction search API rather than trusting the redirect. Its documentation separates sandbox/live environments. [Official overview](https://aamarpay.readme.io/reference/overview), [transaction search API](https://aamarpay.readme.io/reference/search-transaction).

**Direct bKash / Nagad:** treat these as separate drivers and merchant integrations, even if a customer can already use their wallet through an aggregator. Confirm merchant product/version, credential requirements, verification, retry/timeout semantics, callbacks, limits, and allowed donation use before implementation. Detailed bKash pages were not accessible through the research tool during this inspection; exact endpoint contracts are intentionally deferred to the merchant's official documentation. Do not infer Nagad API behavior from unofficial packages. [bKash developer portal](https://developer.bka.sh/docs).

## 9. Verification, disabling, and recovery

1. Validate gateway availability at a defined admission point and save the immutable checkout context.
2. Claim initiation once using the submission key/order. A browser refresh or duplicate POST returns the existing known session rather than creating another charge.
3. Parse provider callbacks without relying on a Laravel login session. Exempt only exact provider-controlled POST callback/IPN routes from CSRF, and apply the provider's documented authenticity checks plus authoritative verification.
4. Verify the order, merchant/environment, exact amount, currency, and provider outcome server-side. Normalize harmless trailing decimal zeroes; reject real sub-poisha amounts and amount mismatches.
5. Lock and complete the donation once. Updating the donation to paid invokes the trait and transaction/account changes within the same database transaction.
6. Deliver confirmation and advance any applicable recurring schedule once after successful commit. Replayed notifications must not repeat these effects.

Disabling removes the gateway from new checkout admission. Existing sessions still use their pinned merchant/environment/account context and can become paid. Retain old verification configuration until outstanding sessions are resolved; changing merchant identity/environment must not redirect old sessions to new credentials. Existing balances must not move because an administrator changes the gateway's future receiving account.

A cancellation redirect is not proof that no money was taken. Success verification wins over stale failure/cancellation. If verification times out or the database save fails, leave the payment recoverable; do not start another remote charge automatically. Add a small scheduled recovery command to query unresolved sessions with bounded retries, plus an authorized recheck action. Use safe HTTP retries only where the operation is idempotent; checkout POST retries require provider-supported duplicate protection.

Changing provider or deliberately starting again creates a fresh donation/session after the old state is checked. A second distinct verified capture must not be silently ignored as a replay: give it a separate linked donation record/income receipt and flag it for staff review. This preserves the trait's one-transaction-per-donation rule without losing actual receipts. Refund execution remains outside this phase.

Sandbox flows run only against isolated local/staging data. Production availability requires live configuration, so test gateway successes cannot inflate real donation reports or balances. Use trusted, validated checkout redirect hosts and fixed API endpoints; never follow arbitrary callback-supplied URLs. Redact authentication data and donor/payment details in logs, and return translated, non-sensitive errors to donors.

## 10. Compatibility and deployment

- Use additive migrations. Preserve current donations, transaction links, amounts, accounts, and legacy ShurjoPay columns.
- The gateway-name rule applies to new public donations immediately. Do not rewrite old `payment_method` values automatically. Derive legacy ShurjoPay attribution only where `sp_order_id` or other verified evidence proves it; any historical conversion needs a separate reviewed decision.
- Keep old callbacks working during transition. New checkouts use provider-specific named routes and original sessions retain compatible verification.
- Update donation lists, filters, detail views, notifications, reports, exports, and method-label helpers to show the canonical gateway name. Manual method choices stay separate from public gateway choices.
- Supply new Settings links/forms through Wayfinder where Vue calls backend routes. Keep public Blade links generated with named Laravel routes.
- Run schema changes, seed missing non-secret preferences/permission, clear relevant caches, configure live callback/IPN URLs, and verify the normal scheduler/queue setup before enabling a new gateway.
- No database reset, historical reclassification, provider activation, credential submission, deployment, or real charge is part of planning.

## 11. Implementation order and file map

| Step | Main files/areas | Completion criterion |
| --- | --- | --- |
| 1. Foundation | Donation contract, driver registry/result objects, checkout/completion services, additive Donation migration, module provider | Existing ShurjoPay works through the shared boundary with unchanged accounting totals. |
| 2. Settings | Settings controller/request/seeder/ACL, `PaymentGatewaysGroup.vue`, Settings form/index group maps | Authorized administrators can enable/disable, order, select default, and map accounts; invalid configurations cannot activate. |
| 3. Public forms | `SiteDonationController`, checkout requests, campaign form, homepage donation-card component, shared selector | Both entry points use the same availability rules and persist the gateway name before redirect. |
| 4. SSLCommerz | Driver, provider callback/IPN controller, routes, exact CSRF exceptions, configuration | Hosted checkout and server-verified completion work in sandbox; duplicate events post income once. |
| 5. Recovery and presentation | Registered recovery command/schedule, staff recheck, Donation UI filters/labels, translated messages and notification/export presentation | Delayed/disabled-provider payments recover correctly, and gateway attribution agrees across screens. |
| 6. Additional providers | aamarPay driver first; direct wallet drivers after merchant-contract confirmation | Each provider passes the same contract and end-to-end acceptance tests before it can be enabled. |

## 12. Test and acceptance matrix

Use Pest feature tests, Laravel HTTP fakes, factories, and the existing trait tests. Provider calls are faked precisely; automated tests must not make real payments.

- Settings permission on GET and save; unauthorized direct requests fail. Unknown provider keys, invalid defaults, inactive accounts, and missing credentials cannot activate checkout.
- Both forms show zero/one/several available gateways correctly in Bangla/English and current themes. Cached settings refresh after changes. A stale disabled-gateway submission creates no donation or provider session.
- Pending, failed, cancelled, and paid public donations all retain the selected gateway name. SSLCommerz via bKash remains `payment_method = SSLCommerz`; its channel stays separate.
- Per-driver required donor fields, minimum/maximum amounts, exact currency/amount matching, correct order-ID length, and provider reference scoping.
- Success through callback, IPN without browser return, out-of-order notifications, duplicate/replayed events, unknown references, invalid signatures, high-risk results, and callback transport/CSRF behavior.
- Two simultaneous success notifications result in one donation transaction, one balance increase, one confirmation, and one recurring schedule advance. Include a focused MySQL concurrency test.
- Disabled gateway after initiation still verifies; changing default/account does not move an existing donation. Merchant/environment context cannot be crossed.
- Timeout after initiation, malformed responses, verification outages, rollback during trait synchronization, and later recovery do not create duplicate remote sessions or duplicate income.
- Soft-deleted donations are not silently restored by callbacks. A distinct second capture is handled separately from a repeated notification of the first.
- Existing ShurjoPay callbacks and manual donation create/edit/delete/restore still work. Legacy history is not rewritten. Financial screen/PDF/Excel totals still count synchronized transactions once.
- No credentials or token/session material in public props, settings cache, logs, or error messages. Sandbox success cannot post into production accounts.

Before live activation, complete a merchant-approved sandbox checklist for each provider and verify its registered callback/IPN URLs. Required merchant inputs are SSLCommerz sandbox/live credentials, receiving account, applicable customer-field/amount requirements, and corresponding credentials/contracts for later providers.
