Skip to content

Multi-Currency ​

Flow ID: CF-33 | Module(s): eshop, Currency domain | Complexity: Medium Last Updated: 2026-09-29

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_code at creation (default EUR)
  • Frontend: Vuex getSelectedCurrency getter triggers reactive price recalculation
  • Feeds: ?curr= URL parameter for XML currency conversion

API Reference ​

REST Endpoints ​

MethodPathAuthDescription
GET/rest/currency/currencyGuestList currencies (filter by code, active, symbol)
GET/rest/currency/currency/itemGuestGet single currency by filter (e.g. filter[code]=EUR)
GET/rest/currency/currency/{id}GuestGet single currency
POST/rest/currency/currencyBackendCreate currency
POST/rest/currency/currency/{id}BackendUpdate currency
DELETE/rest/currency/currency/{id}BackendDelete 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 322-327 for GET index/item/show + prefixed variants, lines 1391-1396 for POST store/update, DELETE destroy + prefixed variants) 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 PatternControllerMethod
POST /setCustomerSessionCurrencyIdWebrun.phpsetCustomerSessionCurrencyId()

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:

  1. Loads the default currency from registry: getDefaultCurrency() resolves ESHOP.DEFAULT_CURRENCY to a currency entity.
  2. Fetches all active currencies and indexes them by ID: getResultObjectAsIndexedArray($this->currencies_model->getActiveCurrencies(), 'id').
  3. Checks for a ?curr= query parameter via currencyFromQueryString() — if present and the code matches an active currency, stores its ID in session.
  4. During initializeCustomerSessionData(), if no currencyId exists in session, defaults to getDefaultCurrencyId() (the registry value).
  5. Sets $this->currentCurrency from 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:

  1. Request is AJAX (is_ajax_request())
  2. Posted currencyId is a numeric digit string (ctype_digit)
  3. 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:

  1. Calls CurrencyRepository::matchOne(new Filter('code', $cartCurrencyCode)) to look up the currency matching the cart's stored currency_code.
  2. If the cart code is absent or unmatched, falls back to matchOne(new Filter('active', 1)) — the first active currency.
  3. Throws RuntimeException if 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.symbol

Components 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 ​

ColumnTypeDescription
idint (PK, AI)Currency identifier
codevarcharISO 4217 code (e.g., EUR, USD, GBP)
numvarcharISO 4217 numeric code (e.g., 978 for EUR)
symbolvarcharDisplay symbol (e.g., $)
ratedecimalExchange rate relative to base currency (base = 1.0)
activetinyintWhether currency is available on storefront

shop_order (currency columns) ​

ColumnTypeDescription
order_currencyvarcharISO 4217 code at time of order (e.g., EUR)
currency_idintFK to currencies.id at order time
currency_ratedecimalExchange rate snapshot at order time

Domain Layer ​

ComponentPath
Servicesrc/Domains/Currency/Currency/Service.php
WriteServicesrc/Domains/Currency/Currency/WriteService.php
Validatorsrc/Domains/Currency/Currency/Validator.php
WriteDatasrc/Domains/Currency/Currency/WriteData.php
ListRequestsrc/Domains/Currency/Currency/ListRequest.php
Entitysrc/Domains/Currency/Currency/Repository/Entity.php
Repositorysrc/Domains/Currency/Currency/Repository/Repository.php
RepositoryConfiguratorsrc/Domains/Currency/Currency/Repository/RepositoryConfigurator.php
WriteRepositorysrc/Domains/Currency/Currency/Repository/WriteRepository.php
REST Controllersrc/Rest/Currency/Controllers/Currency.php
Resourcesrc/Rest/Currency/Resources/Currency/Resource.php
Collectionsrc/Rest/Currency/Resources/Currency/Collection.php
Domain DIsrc/Domains/Currency/container.php
REST DIsrc/Rest/Currency/container.php

Legacy Model ​

ModelPathKey Methods
Adv_currencies_modelecommercen/eshop/models/Adv_currencies_model.phpgetActiveCurrencies(), getRecords(), getRecord(), add(), edit()

Helper Functions ​

All defined in ecommercen/helpers/shopmodule_helper.php:

FunctionPurpose
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 ​

GroupKeyPurposeSet In
ESHOPDEFAULT_CURRENCYID 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:2156 for the save, :2267-2269 for the view data: dropdown list + current selection).

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:674-677

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 ​

ModelOverride In
Adv_currencies_modelapplication/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) returns formatNumber($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 the currencies table).
  • If a customer's session has no currencyId, the front controller defaults to getDefaultCurrencyId().
  • The ?curr=CODE query 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 ​

  1. Order currency snapshot — RESOLVED (fix #169, 2026-04-24). PlaceOrderService::resolveOrderCurrency() now looks up the cart's currency_code against the currencies table at order-placement time and snapshots currency_id, currency_rate, and order_currency on the order row. Falls back to the first active currency when the code can't be matched; throws RuntimeException if no active currency is configured. Citation: src/Domains/Checkout/PlaceOrderService.php (see resolveOrderCurrency() and the order-data block).

  2. Currency WriteService Validator contains no specific validation rules — all writes are unchecked at the domain validation layer. Field presence is enforced only by WriteData constructor 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 ​

FileTypeCoverage
tests/Unit/Domains/Currency/Currency/ServiceTest.phpUnitService layer
tests/Integration/Domains/Currency/Currency/ServiceTest.phpIntegrationService layer (DB)
tests/Integration/Domains/Currency/Currency/RepositoryTest.phpIntegrationRepository layer (DB)
tests/Unit/Rest/Currency/Resources/Currency/ResourceTest.phpUnitREST Resource transformer
tests/Unit/Rest/Currency/Resources/Currency/CollectionTest.phpUnitREST Collection transformer

No dedicated tests cover the currency-switching AJAX flow (Webrun::setCustomerSessionCurrencyId()), the front-controller initialization path, or feed currency resolution.