Appearance
Multi-Currency
Flow ID: CF-33 | Module(s): eshop, Currency domain | Complexity: Medium Last Updated: 2026-04-26
Business Context
Currencies stored with ISO 4217 code, symbol, and exchange rate. Base currency (from registry DEFAULT_CURRENCY) has rate=1.0. Other currencies multiply prices at display time only — all database storage is in base currency.
Key business behaviors:
- Default currency cannot be deactivated
- Cart stores
currency_codeat creation (default EUR) - Frontend: Vuex
getSelectedCurrencygetter triggers reactive price recalculation - Feeds:
?curr=URL parameter for XML currency conversion
API Reference
REST Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/currency/currency | Guest | List currencies (filter by code, active, symbol) |
| GET | /rest/currency/currency/item | Guest | Get single currency by filter (e.g. filter[code]=EUR) |
| GET | /rest/currency/currency/{id} | Guest | Get single currency |
| POST | /rest/currency/currency | Backend | Create currency |
| POST | /rest/currency/currency/{id} | Backend | Update currency |
| DELETE | /rest/currency/currency/{id} | Backend | Delete currency |
Available filters: id (exact), code (exact), active (exact), symbol (partial) Available sorts: id, code, rate, active
Routes are defined in application/config/rest_routes.php (lines 292-297, 1296-1302) with both bare and language-prefixed variants ((\w{2})/rest/currency/currency/...). The item route is registered before the (:num) show route to avoid wildcard conflicts.
Legacy Storefront (AJAX)
| URL Pattern | Controller | Method |
|---|---|---|
POST /setCustomerSessionCurrencyId | Webrun.php | setCustomerSessionCurrencyId() |
Code Flow
Step 1: Storefront Initialization
File: ecommercen/core/Adv_front_controller.php — constructor (line 96-116)
On every storefront page load the front controller:
- Loads the default currency from registry:
getDefaultCurrency()resolvesESHOP.DEFAULT_CURRENCYto a currency entity. - Fetches all active currencies and indexes them by ID:
getResultObjectAsIndexedArray($this->currencies_model->getActiveCurrencies(), 'id'). - Checks for a
?curr=query parameter viacurrencyFromQueryString()— if present and the code matches an active currency, stores its ID in session. - During
initializeCustomerSessionData(), if nocurrencyIdexists in session, defaults togetDefaultCurrencyId()(the registry value). - Sets
$this->currentCurrencyfrom the active currencies array using the session currency ID.
Step 2: Currency Data Passed to Vue
File: ecommercen/core/Adv_front_controller.php — renderVueJson() (line 756-758)
The current currency object is injected into the Vue initial state as selectedCurrency:
php
if (!isset($this->render['jsonState']['selectedCurrency'])) {
$this->render['jsonState']['selectedCurrency'] = $this->activeCurrencies[$this->cSData['currencyId']];
}The activeCurrencies array is also passed to the Blade/PHP render context (line 608) for server-side templates.
Step 3: Vue Reactivity
File: assets/vue/data-components/InitialData.vue — mounts selectedCurrency into Vuex via setSelectedCurrency action.
Store files:
- State:
assets/vue/store/state.js—selectedCurrency: [](initialized empty, hydrated from server) - Getter:
assets/vue/store/getters.js—getSelectedCurrency: state => state.selectedCurrency - Utility getter:
getPriceUsingCurrency: (state) => (price) => (price * state.selectedCurrency.rate).toFixed(2) - Mutation:
assets/vue/store/mutations.js—setSelectedCurrency(state, selectedCurrency) - Action:
assets/vue/store/actions.js—setSelectedCurrency({ commit }, selectedCurrency)
Components read getSelectedCurrency and apply the rate at display time:
javascript
// AdvProductCard.vue
priceWithSign (priceString, priceSign) {
return (parseFloat(priceString) * this.getSelectedCurrency.rate).toFixed(2) + priceSign
}
getSign () {
return this.getSelectedCurrency.symbol
}Step 4: Currency Switching (AJAX)
File: application/views/main/components/footer/footer_js.php (line 477-488)
When a customer clicks a .setCurrencyButton element, jQuery posts the currencyId to the URL produced by site_url('setCustomerSessionCurrencyId'):
javascript
$(document).on('click', '.setCurrencyButton', function (event) {
var currencyId = event.target.getAttribute("data-id")
$.ajax({
type: "POST",
url: site_url('setCustomerSessionCurrencyId'),
dataType: "json",
data: {'currencyId': currencyId}
}).done(function (data) {
if (data.result == 'success') {
location.reload()
} else {
alert('<?= t(\'generic.error\'); ?>')
}
}).fail(function () {
alert('<?= t(\'generic.error\'); ?>')
})
})File: application/controllers/Webrun.php — setCustomerSessionCurrencyId() (line 272-293)
The endpoint validates:
- Request is AJAX (
is_ajax_request()) - Posted
currencyIdis a numeric digit string (ctype_digit) - Currency exists and is active (
currencies_model->getRecord(['id' => ..., 'active' => true]))
On success, sets currencyId in CI session via set_userdata() and returns {"result": "success"}. The page reloads and the front controller picks up the new session currency.
Step 5: Currency in Feed Generation
File: ecommercen/feeds/core/AdvXml.php — constructor (line 27-29)
Feed XML controllers accept ?curr=CODE to generate prices in a specific currency:
php
$currencyCode = $this->input->get('curr');
$this->activeCurrencies = getResultObjectAsIndexedArray($this->currencies_model->getActiveCurrencies(), 'code');
$this->currency = $this->activeCurrencies[$currencyCode] ?? getDefaultCurrency();Product URLs in multi-currency feeds include ?curr=CODE via siteUrlWithCurrency() so customers land on the storefront with that currency pre-selected.
Step 6: Currency at Order Placement
File: src/Domains/Checkout/PlaceOrderService.php (resolveOrderCurrency())
When an order is placed via the modern REST checkout, PlaceOrderService resolves the cart's currency from the database via the private resolveOrderCurrency(?string $cartCurrencyCode) method:
- Calls
CurrencyRepository::matchOne(new Filter('code', $cartCurrencyCode))to look up the currency matching the cart's storedcurrency_code. - If the cart code is absent or unmatched, falls back to
matchOne(new Filter('active', 1))— the first active currency. - Throws
RuntimeExceptionif no active currency is found at all (misconfigured shop).
The resolved CurrencyEntity populates all three currency columns on the order row: order_currency (ISO code), currency_id (DB primary key), and currency_rate (exchange rate snapshot). This ensures the order record accurately reflects the rate in effect at placement time, not a hardcoded fallback.
Frontend Components
javascript
displayPrice = basePrice * selectedCurrency.rate
displaySymbol = selectedCurrency.symbolComponents that apply selectedCurrency.rate at render time: AdvCartProductRow.vue, CheckoutPage.vue, AdvProductCard.vue, AdvCartProductList.vue, AdvCartProductBundleDisplay.vue, CheckoutGiftPackaging.vue, AfterAddToCartModal.vue, GiftCardPage.vue, AdvOrderTransportersExternal.vue, AdvOrderTransportersMultiple.vue
The wishlist page (assets/main/js/components/wishlist-actions.js) reads selectedCurrency from window.advAppContext.vueData.initialData.selectedCurrency for GA4 tracking events.
Data Model
currencies
| Column | Type | Description |
|---|---|---|
id | int (PK, AI) | Currency identifier |
code | varchar | ISO 4217 code (e.g., EUR, USD, GBP) |
num | varchar | ISO 4217 numeric code (e.g., 978 for EUR) |
symbol | varchar | Display symbol (e.g., $) |
rate | decimal | Exchange rate relative to base currency (base = 1.0) |
active | tinyint | Whether currency is available on storefront |
shop_order (currency columns)
| Column | Type | Description |
|---|---|---|
order_currency | varchar | ISO 4217 code at time of order (e.g., EUR) |
currency_id | int | FK to currencies.id at order time |
currency_rate | decimal | Exchange rate snapshot at order time |
Domain Layer
| Component | Path |
|---|---|
| Service | src/Domains/Currency/Currency/Service.php |
| WriteService | src/Domains/Currency/Currency/WriteService.php |
| Validator | src/Domains/Currency/Currency/Validator.php |
| WriteData | src/Domains/Currency/Currency/WriteData.php |
| ListRequest | src/Domains/Currency/Currency/ListRequest.php |
| Entity | src/Domains/Currency/Currency/Repository/Entity.php |
| Repository | src/Domains/Currency/Currency/Repository/Repository.php |
| RepositoryConfigurator | src/Domains/Currency/Currency/Repository/RepositoryConfigurator.php |
| WriteRepository | src/Domains/Currency/Currency/Repository/WriteRepository.php |
| REST Controller | src/Rest/Currency/Controllers/Currency.php |
| Resource | src/Rest/Currency/Resources/Currency/Resource.php |
| Collection | src/Rest/Currency/Resources/Currency/Collection.php |
| Domain DI | src/Domains/Currency/container.php |
| REST DI | src/Rest/Currency/container.php |
Legacy Model
| Model | Path | Key Methods |
|---|---|---|
Adv_currencies_model | ecommercen/eshop/models/Adv_currencies_model.php | getActiveCurrencies(), getRecords(), getRecord(), add(), edit() |
Helper Functions
All defined in ecommercen/helpers/shopmodule_helper.php:
| Function | Purpose |
|---|---|
getDefaultCurrency() | Returns the currency object for ESHOP.DEFAULT_CURRENCY (cached via PSCache) |
getDefaultCurrencyId() | Returns the registry value ESHOP.DEFAULT_CURRENCY |
getSelectedCurrencyData() | Returns currency object for the session's currencyId (falls back to default) |
getCurrencyData($id) | Returns currency object by ID (cached) |
getActiveCurrencies() | Returns all active currencies from the model |
getActiveCurrenciesForDropdown() | Returns [id => "CODE (symbol)"] array for HTML selects |
applyCurrencyRate($amount, $rate) | formatNumber($amount * $rate) |
applyCurrency($amount, $currency) | formatNumber($amount * $currency->rate) . $currency->symbol |
Configuration
Registry Keys
| Group | Key | Purpose | Set In |
|---|---|---|---|
ESHOP | DEFAULT_CURRENCY | ID of the base currency (rate=1.0). Set during initial seed. | Adv_settings.php > Other Settings |
The default currency is selected from the active currencies dropdown in the admin settings panel (ecommercen/settings/controllers/Adv_settings.php, line 1832/1916).
Admin Menu
Currencies admin is registered in application/config/admin_menu.php under the SETTINGS group:
php
[
'route' => 'currencies_admin',
'icon' => t('admin.menu.currencies.icon'),
'label' => t('admin.menu.currencies.label'),
'roles' => [AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN],
'group' => 'SETTINGS'
]REST Policies
Defined in application/config/rest_policies.php:
php
Currency::class => [
'defaults' => ['auth' => 'backend'],
'methods' => [
'index' => ['auth' => 'guest'],
'show' => ['auth' => 'guest'],
'item' => ['auth' => 'guest'],
],
],Read endpoints (index, show, item) are public (guest). Write endpoints (store, update, destroy) require backend JWT authentication.
Admin Route
application/config/routes.php:666-669
php
$route['currencies_admin'] = 'eshop/currencies_admin/index';
$route['currencies_admin/(.+)'] = 'eshop/currencies_admin/$1';
$route['(\w{2})/currencies_admin'] = 'eshop/currencies_admin/index';
$route['(\w{2})/currencies_admin/(.+)'] = 'eshop/currencies_admin/$2';Client Extension Points
Admin Controller Override
The admin CRUD controller uses the standard thin-wrapper pattern:
File: application/modules/eshop/controllers/Currencies_admin.php
php
class Currencies_admin extends Adv_currencies_admin
{
//
}Client repos can override this file at application/modules/eshop/controllers/Currencies_admin.php to add custom validation, fields, or behavior (e.g., restricting which currencies can be created).
DI Container Overrides (Client Repos)
Override Domain layer services via custom/Domains/container.php:
php
// Override the RepositoryConfigurator to add custom relations
$services->set(\Custom\Domains\Currency\Currency\Repository\RepositoryConfigurator::class);
$services->alias(
\Advisable\Domains\Currency\Currency\Repository\RepositoryConfigurator::class,
\Custom\Domains\Currency\Currency\Repository\RepositoryConfigurator::class
);
// Override WriteService to add custom validation logic
$services->set(\Custom\Domains\Currency\Currency\WriteService::class);
$services->alias(
\Advisable\Domains\Currency\Currency\WriteService::class,
\Custom\Domains\Currency\Currency\WriteService::class
);Model Override
| Model | Override In |
|---|---|
Adv_currencies_model | application/modules/eshop/models/Adv_currencies_model.php |
Storefront Currency Switcher
The jQuery handler in footer_js.php targets .setCurrencyButton elements. Client themes can customize the switcher UI by placing currency buttons with data-id attributes in their template. The activeCurrencies array is available in the PHP render context.
Business Rules
Default Currency Protection
The default currency (identified by ESHOP.DEFAULT_CURRENCY registry value) cannot be deactivated. In Adv_currencies_admin::edit() (line 81-83), if the edited currency is the default, the active flag is forced to true:
php
if ($this->defaultCurrency->id == $id) {
$currency['active'] = true;
}Rate Application Formula
All prices in the database are stored in the base currency. Conversion is display-only:
displayPrice = basePrice * selectedCurrency.rate- PHP helper:
applyCurrencyRate(float $amount, float $rate)returnsformatNumber($amount * $rate) - PHP helper with symbol:
applyCurrency(float $amount, object $currency)returns formatted price + symbol - Vue getter:
getPriceUsingCurrency(price)returns(price * state.selectedCurrency.rate).toFixed(2)
The base currency always has rate = 1.0, so multiplication is a no-op for the default.
Active Toggle
Only currencies with active = 1 are:
- Available on the storefront currency switcher (
getActiveCurrencies()) - Selectable via the session switch endpoint (validated in
Webrun::setCustomerSessionCurrencyId()) - Available in feed generation (
AdvXml::__construct())
Deactivating a currency does not affect existing orders that were placed with that currency — the rate is snapshotted at order time.
Session Persistence
- Currency selection is stored in CI session as
currencyId(the integer primary key from thecurrenciestable). - If a customer's session has no
currencyId, the front controller defaults togetDefaultCurrencyId(). - The
?curr=CODEquery parameter overrides the session currency (used by feed URLs and direct links).
Validation
The Validator class (src/Domains/Currency/Currency/Validator.php) defines validateForCreate() and validateForUpdate() methods, but currently contains no specific rules — validation is open. Required fields for creation are enforced by WriteData: code, num, symbol, rate.
Known Issues & Security Gaps
Order currency snapshot — RESOLVED (fix #169, 2026-04-24).
PlaceOrderService::resolveOrderCurrency()now looks up the cart'scurrency_codeagainst thecurrenciestable at order-placement time and snapshotscurrency_id,currency_rate, andorder_currencyon the order row. Falls back to the first active currency when the code can't be matched; throwsRuntimeExceptionif no active currency is configured. Citation:src/Domains/Checkout/PlaceOrderService.php(seeresolveOrderCurrency()and the order-data block).Currency WriteService Validator contains no specific validation rules — all writes are unchecked at the domain validation layer. Field presence is enforced only by
WriteDataconstructor typing. Any constraints beyond required-field presence (e.g., valid ISO 4217 code format, positive rate, unique code) are absent. Citation:src/Domains/Currency/Currency/Validator.php.
Tests
| File | Type | Coverage |
|---|---|---|
tests/Unit/Domains/Currency/Currency/ServiceTest.php | Unit | Service layer |
tests/Integration/Domains/Currency/Currency/ServiceTest.php | Integration | Service layer (DB) |
tests/Integration/Domains/Currency/Currency/RepositoryTest.php | Integration | Repository layer (DB) |
tests/Unit/Rest/Currency/Resources/Currency/ResourceTest.php | Unit | REST Resource transformer |
tests/Unit/Rest/Currency/Resources/Currency/CollectionTest.php | Unit | REST Collection transformer |
No dedicated tests cover the currency-switching AJAX flow (Webrun::setCustomerSessionCurrencyId()), the front-controller initialization path, or feed currency resolution.
Related Flows
- CF-01 Product Browsing — prices displayed in selected currency
- CF-02 Product Detail — product prices converted at display time
- CF-05 Cart — cart stores
currency_codeat creation - CF-08 Payment Processing — payment gateway receives currency code (e.g., PayPal
currency_code) - IN-01 Feed Generation —
?curr=parameter for multi-currency XML feeds - AD-36 Currency Management — admin CRUD for currencies