Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable public changes are documented here.

## 1.0.1 - 2026-09-27

### Added

- Added a gateway setting that selects SSLCommerz's current conversion rate or the configured WHMCS rate for non-BDT invoices.

### Changed

- Locked the original invoice amount and gateway processing amount separately for callback validation, reconciliation, and refunds.
- Documented that checkout converts the current outstanding invoice balance without adding a module-level surcharge or convenience fee.
- Updated the gateway configuration documentation and screenshot for the new conversion setting.

## 1.0.0 - 2026-09-14

First public production release.
Expand Down
30 changes: 22 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# SSLCommerz for WHMCS

[![Release](https://img.shields.io/badge/release-1.0.0-1565c0.svg)](CHANGELOG.md)
[![Release](https://img.shields.io/badge/release-1.0.1-1565c0.svg)](CHANGELOG.md)
[![PHP](https://img.shields.io/badge/PHP-7.4%20%7C%208.1--8.3-777bb4.svg)](https://www.php.net/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

A production-ready SSLCommerz v4 payment gateway for WHMCS, with secure checkout, callback/IPN settlement, multi-currency reconciliation, refunds, and an optional administrator transaction-lookup add-on.

Version **1.0.0** is the first public production release.
Version **1.0.1** adds a selectable foreign-currency conversion policy, allowing non-BDT invoices to use either SSLCommerz's current rate or the rate configured in WHMCS.

> This is an independent community integration. It is not an official product of, sponsored by, or affiliated with SSL Wireless/SSLCommerz or WHMCS.

Expand All @@ -16,6 +16,7 @@ Version **1.0.0** is the first public production release.
- Server-side invoice reload and short-lived HMAC checkout tokens
- Shared, idempotent browser callback and IPN payment processing
- Strict transaction, amount, currency, risk, and identity validation
- Selectable SSLCommerz or WHMCS foreign-currency conversion rates
- Safe USD/foreign-currency to BDT settlement reconciliation
- Full and partial refunds using the payment's captured exchange rate
- Searchable local transaction ledger, remote refresh, and refund status lookup
Expand All @@ -37,7 +38,7 @@ Version **1.0.0** is the first public production release.
2. Download the release ZIP and extract it.
3. Copy the extracted `modules` directory into the WHMCS installation root, merging it with the existing `modules` directory.
4. Open **Configuration > Apps & Integrations > Payments** and activate **SSLCommerz Payment Gateway**.
5. Enter the store ID and password, select the environment and checkout mode, then save.
5. Enter the store ID and password, select the environment, conversion policy, and checkout mode, then save.
6. Optionally activate **SSLCommerz Transaction Lookup** under **System Settings > Addon Modules** and grant access to the required administrator roles.
7. Complete a sandbox payment and verify both the invoice and **Billing > Gateway Log** before enabling live mode.

Expand All @@ -47,7 +48,14 @@ For upgrades, replace all gateway, callback, and add-on files together. Keep `le

## Configuration

Configure the credentials, Test Mode, checkout mode, invoice transaction-ID preference, and gateway display name from the WHMCS payment gateway page. Never commit or share live store credentials.
Configure the credentials, Test Mode, checkout mode, foreign-currency conversion source, invoice transaction-ID preference, and gateway display name from the WHMCS payment gateway page. Never commit or share live store credentials.

**Foreign Currency Conversion** provides two policies for non-BDT invoices:

- **SSLCommerz Current Rate** is the backward-compatible default. The module sends the invoice amount and currency, and SSLCommerz converts it to BDT using its current rate.
- **WHMCS Configured Rate** converts the invoice balance to BDT on the server using the rates under **System Settings > Currencies**, then sends the locked BDT amount to SSLCommerz. BDT must exist as a configured WHMCS currency with a valid rate.

Direct BDT invoices bypass conversion in both modes.

![SSLCommerz gateway settings in WHMCS](images/config.png)

Expand Down Expand Up @@ -88,7 +96,11 @@ Browser returns and IPNs use the same validation and settlement processor. Atomi

## Currency reconciliation

The module keeps the customer's invoice amount separate from the BDT amount settled by SSLCommerz. A `0.10 USD` invoice settled as `12.34 BDT` is displayed as those two distinct values, never as `12.34 USD`.
The module keeps the customer's invoice amount separate from the amount submitted for processing and the BDT amount settled by SSLCommerz. A `0.10 USD` invoice processed and settled as `12.34 BDT` is displayed as those two distinct values, never as `12.34 USD`.

The conversion policy and processing amount are captured when the session is created. Later changes to the gateway setting or WHMCS currency rates do not alter callback validation or refund calculations for that transaction.

Checkout uses the invoice's current outstanding balance, including WHMCS ledger adjustments, rather than only the original line-item total. The module does not add a conversion surcharge or convenience fee.

For non-BDT invoices, refunds reuse the BDT-per-invoice-currency rate captured during payment. Historical foreign-currency payments without a captured rate are refused because applying a current rate could over-refund. Refund totals are capped at the remaining captured BDT balance, and durable reservations keep concurrent attempts idempotent.

Expand All @@ -110,13 +122,14 @@ The administrator add-on provides a minimal dashboard for recent transactions, m

## Troubleshooting

- **Popup reports an error in communication:** verify all 1.0.0 files were uploaded together and clear the PHP opcode cache. The popup endpoint accepts SSLCommerz's live `cart_json` wrapper while authenticating the signed invoice token.
- **Popup reports an error in communication:** verify all 1.0.1 files were uploaded together and clear the PHP opcode cache. The popup endpoint accepts SSLCommerz's live `cart_json` wrapper while authenticating the signed invoice token.
- **New Hosted opens the legacy page:** confirm `new_hosted` is saved. Live New Hosted rewrites only SSLCommerz's exact approved live session host to `pay.sslcommerz.com`.
- **Unable to start payment:** inspect the newest `SESSION_FAILED` record in the lookup add-on or **Billing > Gateway Log**. Local, transport, and provider failures are logged while the customer sees a generic message.
- **WHMCS rate conversion is unavailable:** add and configure BDT under **System Settings > Currencies**, verify both source and BDT rates are positive, then retry from a freshly loaded invoice.
- **Invoice remains unpaid:** confirm the public HTTPS callback and IPN endpoints are reachable and that the response matches the original invoice currency amount and settled BDT amount.
- **Customer is logged out after return:** confirm the 1.0.0 callback files replaced older scripts and that no proxy or plug-in injects a session cookie into callback responses.
- **Customer is logged out after return:** confirm the 1.0.1 callback files replaced older scripts and that no proxy or plug-in injects a session cookie into callback responses.
- **Refund returns `REQUEST_FROM_INVALID_SOURCE`:** register the server's public outbound IP with SSLCommerz, then retry the same refund.
- **Lookup View reports an expired token:** clear the opcode cache. Version 1.0.0 uses WHMCS's native administrator CSRF token and has no custom rotating lookup token.
- **Lookup View reports an expired token:** clear the opcode cache. Version 1.0.1 uses WHMCS's native administrator CSRF token and has no custom rotating lookup token.

## Development

Expand Down Expand Up @@ -144,3 +157,4 @@ GitHub Actions runs multi-version PHP syntax checks and builds the installable Z
- Changes are recorded in [CHANGELOG.md](CHANGELOG.md).

Released under the [MIT License](LICENSE). Copyright © 2026 Extent Technologies Limited.
Developed and maintained by [MD Abu Bakkar](https://www.google.com/search?kgmid=/g/11zwqn5szs).
Binary file modified images/config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion modules/addons/sslcommerz_lookup/sslcommerz_lookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function sslcommerz_lookup_config()
return array(
'name' => 'SSLCommerz Transaction Lookup',
'description' => 'Search the local payment ledger, refresh transaction data through the v4 API, export CSV, and query refund status.',
'version' => '1.0.0',
'version' => '1.0.1',
'author' => 'Extent Technologies Limited',
'fields' => array(
'store_id' => array(
Expand Down
2 changes: 1 addition & 1 deletion modules/addons/sslcommerz_lookup/whmcs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schema": "1.0",
"type": "whmcs-addons",
"name": "sslcommerz_lookup",
"version": "1.0.0",
"version": "1.0.1",
"license": "MIT",
"category": "utilities",
"description": {
Expand Down
10 changes: 10 additions & 0 deletions modules/gateways/sslcommerz.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ function sslcommerz_config()
'store_id' => array('FriendlyName' => 'Store ID', 'Type' => 'text', 'Size' => '40'),
'store_password' => array('FriendlyName' => 'Store Password', 'Type' => 'password', 'Size' => '40'),
'testmode' => array('FriendlyName' => 'Test Mode', 'Type' => 'yesno', 'Description' => 'Enable SSLCommerz Sandbox mode.'),
'conversion_source' => array(
'FriendlyName' => 'Foreign Currency Conversion',
'Type' => 'dropdown',
'Options' => array(
'sslcommerz' => 'SSLCommerz Current Rate',
'whmcs' => 'WHMCS Configured Rate',
),
'Default' => 'sslcommerz',
'Description' => 'For non-BDT invoices, either let SSLCommerz convert the invoice currency or convert to BDT using the current WHMCS currency rates before checkout.',
),
'ui_mode' => array(
'FriendlyName' => 'Checkout UI Mode',
'Type' => 'dropdown',
Expand Down
19 changes: 18 additions & 1 deletion modules/gateways/sslcommerz/lib/Bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ public static function checkoutService(array $gateway)
));
$ledger = new SslCommerzLedger();
$loader = new SslCommerzWhmcsInvoiceLoader($systemUrl);
return new SslCommerzCheckoutService($api, $ledger, array($loader, 'load'), $gateway, $systemUrl);
return new SslCommerzCheckoutService(
$api,
$ledger,
array($loader, 'load'),
$gateway,
$systemUrl,
array($loader, 'convertCurrency')
);
}
}

Expand Down Expand Up @@ -119,4 +126,14 @@ public function load($invoiceId)
'returnurl' => $this->systemUrl . '/viewinvoice.php?id=' . (int) $invoice->invoiceid,
);
}

public function convertCurrency($amount, $fromCode, $toCode)
{
$from = \WHMCS\Billing\Currency::where('code', strtoupper((string)$fromCode))->first();
$to = \WHMCS\Billing\Currency::where('code', strtoupper((string)$toCode))->first();
if (!$from || !$to) {
throw new RuntimeException('WHMCS BDT conversion rate is unavailable.');
}
return \WHMCS\Billing\Currency::convertBetween($from, (float)$amount, $to);
}
}
45 changes: 38 additions & 7 deletions modules/gateways/sslcommerz/lib/CheckoutService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ class SslCommerzCheckoutService
private $invoiceLoader;
private $gateway;
private $systemUrl;
private $currencyConverter;
private $lastDiagnostic = array();

public function __construct($api, $ledger, callable $invoiceLoader, array $gateway, $systemUrl)
public function __construct($api, $ledger, callable $invoiceLoader, array $gateway, $systemUrl, callable $currencyConverter = null)
{
$this->api = $api;
$this->ledger = $ledger;
$this->invoiceLoader = $invoiceLoader;
$this->gateway = $gateway;
$this->systemUrl = rtrim((string) $systemUrl, '/');
$this->currencyConverter = $currencyConverter;
}

public function initiate($invoiceId)
Expand All @@ -36,18 +38,45 @@ public function initiate($invoiceId)

$tranId = SslCommerzSupport::transactionId($invoice['invoiceid']);
$callback = $this->systemUrl . '/modules/gateways/callback/sslcommerz.php';
$this->lastDiagnostic = array(
'invoice_id' => (int)$invoice['invoiceid'],
'tran_id' => $tranId,
'amount' => (float)$invoice['amount'],
'currency' => strtoupper((string)$invoice['currency']),
'conversion_source' => isset($this->gateway['conversion_source']) && $this->gateway['conversion_source'] === 'whmcs'
? 'whmcs' : 'sslcommerz',
);
try {
$amounts = SslCommerzPaymentRules::checkoutAmounts($this->gateway, $invoice, $this->currencyConverter);
} catch (Throwable $error) {
$message = $this->redactDiagnosticText($error->getMessage());
$this->lastDiagnostic['preflight_error'] = $message;
try {
$this->ledger->begin(array(
'invoice_id' => (int) $invoice['invoiceid'],
'tran_id' => $tranId,
'status' => 'session_failed',
'currency' => strtoupper((string) $invoice['currency']),
'invoice_amount' => (float) $invoice['amount'],
'raw_response' => json_encode(array('status'=>'PREFLIGHT_FAILED','failedreason'=>$message)),
));
} catch (Throwable $ignored) {
// Preserve the actionable conversion error if diagnostic persistence also fails.
}
throw $error;
}
$invoice = array_merge($invoice, $amounts);

$urls = array(
'success_url' => $callback,
'fail_url' => $callback,
'cancel_url' => $callback,
'ipn_url' => $this->systemUrl . '/modules/gateways/callback/sslcommerz_ipn.php',
'return_url' => isset($invoice['returnurl']) ? $invoice['returnurl'] : $this->systemUrl . '/viewinvoice.php?id=' . (int) $invoice['invoiceid'],
);
$this->lastDiagnostic = array(
'invoice_id' => (int)$invoice['invoiceid'],
'tran_id' => $tranId,
'amount' => (float)$invoice['amount'],
'currency' => strtoupper((string)$invoice['currency']),
$this->lastDiagnostic = array_merge($this->lastDiagnostic, array(
'processing_amount' => (float)$invoice['processing_amount'],
'processing_currency' => strtoupper((string)$invoice['processing_currency']),
'checkout_mode' => SslCommerzSupport::checkoutMode(isset($this->gateway['ui_mode']) ? $this->gateway['ui_mode'] : ''),
'test_mode' => isset($this->gateway['testmode']) && $this->gateway['testmode'] === 'on',
'customer_fields_present' => array(
Expand All @@ -60,14 +89,16 @@ public function initiate($invoiceId)
'phone' => isset($invoice['phone']) && trim((string)$invoice['phone']) !== '',
),
'callback_host' => parse_url($callback, PHP_URL_HOST),
);
));

$this->ledger->begin(array(
'invoice_id' => (int) $invoice['invoiceid'],
'tran_id' => $tranId,
'status' => 'initiated',
'currency' => strtoupper((string) $invoice['currency']),
'invoice_amount' => (float) $invoice['amount'],
'processing_currency' => strtoupper((string) $invoice['processing_currency']),
'processing_amount' => (float) $invoice['processing_amount'],
));

try {
Expand Down
Loading
Loading