Skip to content

Let each store owner set their own primary currency - #837

Closed
KrzysztofPajak wants to merge 2 commits into
developfrom
feature/per-store-primary-currency
Closed

KrzysztofPajak wants to merge 2 commits into
developfrom
feature/per-store-primary-currency

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Type: feature

Stacked on #836 — this branch builds on fix/store-without-available-currency. GitHub will retarget it to develop once #836 merges.

Issue

Product prices are stored in the primary store currency, and that currency was a single global setting (CurrencySettings.PrimaryStoreCurrencyId). In a multi-store installation where each store owner runs their own catalog, every store was forced to hold its prices in the same base currency. Store.DefaultCurrencyId only selects which currency a visitor sees, not the one prices are stored in, so there was no way to express "this store's prices are in PLN".

Solution

Store.PrimaryCurrencyId — a per-store override sitting next to the existing DefaultCurrencyId. CurrencyService.GetPrimaryStoreCurrency() prefers it and falls back to the global setting when it is empty or points at a currency that no longer exists. Every price conversion in the system already routes through that one method, so pricing, checkout and the storefront needed no changes.

The override deliberately does not live in a store-scoped CurrencySettings row. SettingService.LoadSetting falls back per object rather than per field, and three of that class's four fields (PrimaryExchangeRateCurrencyId, ActiveExchangeRateProviderSystemName, AutoUpdateEnabled) are genuinely system-wide. A store-scoped row would freeze them for that store, so a later change to the global exchange rate currency would never reach it — a silent rate failure.

Store owners set the currency from Store manager → Configuration → Currencies. Prices are not recalculated; the stored amounts are reinterpreted in the new currency. Because a product shared with another store would silently change meaning, IProductService.CountSharedProducts counts products visible both here and elsewhere, and the owner has to confirm when there are any.

Deleting, unpublishing or unmapping a currency now inspects every store's primary currency rather than only the global one — previously a store's primary currency could be deleted or unmapped from another panel.

Breaking changes

Two public contracts changed. Both are source-breaking for anyone who constructs these types directly; neither changes behaviour for existing installations.

  • ICurrencyService implementation CurrencyService takes an additional IContextAccessor constructor parameter.
  • IProductService gains Task<int> CountSharedProducts(string storeId). Any external implementation of the interface must add it.

No data migration is required: the new Store.PrimaryCurrencyId deserializes to empty on existing documents and the fallback covers it. New localization resources ship in Upgrade/en_240.xml, picked up by the existing 2.4 resource migration.

Testing

  1. Run the solution with at least two stores and a store manager account whose StaffStoreId is one of them.
  2. Sign in to Store manager and open Configuration → Currencies. The star badge marks the currency inherited from the global setting; that is the pre-change behaviour.
  3. Assign a second currency to the store (Assign store), then press Set as primary currency on it.
    • If any product in the store is also available in another store (Limited to stores unchecked, or mapped to more than one store), a confirmation dialog reports how many. Cancel it — nothing changes.
    • Confirm it — the star badge moves to the new currency.
  4. Open the storefront for that store and check a product page: the price number is unchanged but is now interpreted in the new currency, and conversion to other currencies follows its rate.
  5. Open the storefront for the other store and confirm its prices are untouched.
  6. Back in Store manager, try Unassign store on the new primary currency — it is rejected.
  7. In the main admin panel, try to delete or unpublish that currency — both are rejected because a store uses it as primary. Editing its store mapping to exclude that store is rejected too.
  8. On a store that never set a primary currency, confirm it still follows the global setting from Configuration → Currencies in the main admin panel.

Automated

Full solution builds. All 22 test projects pass (~3250 tests, 0 failures), including 21 new tests covering the fallback chain, shared-product counting, the confirmation flow, and the extended currency validations.

🤖 Generated with Claude Code

KrzysztofPajak and others added 2 commits September 12, 2026 15:15
A store whose published currencies are all limited to other stores made
WorkingCurrency throw "No currency could be loaded" from ContextMiddleware,
which took down every request for that store. A store manager could reach
that state alone: unset the default currency, then unassign each remaining
store-limited currency.

Fall back to the primary store currency instead of throwing, and refuse the
edits that would empty a store in the first place - in the store panel when
unassigning the last available currency, and in Main Admin when saving store
mappings that would leave any store without one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrGY1NnCzdkn7wUxbyHzsX
Product prices are stored in the primary store currency, which was a
single global setting. A store owner running their own catalog had no
way to hold prices in their own currency.

The store now carries its own Store.PrimaryCurrencyId and falls back to
the global CurrencySettings.PrimaryStoreCurrencyId when it is empty, so
existing installations keep behaving exactly as before.

The override deliberately does not live in a store-scoped CurrencySettings
row: LoadSetting falls back per object, not per field, and three of that
class's four fields (exchange rate currency, rate provider, auto update)
are genuinely system-wide. A store-scoped row would freeze them for that
store and silently break its rates the next time an administrator changed
the global value.

Prices are not recalculated when the currency changes - the stored amounts
are simply reinterpreted. Because a product shared with another store would
silently change meaning, the store owner has to confirm the change when
CountSharedProducts finds any.

Deleting, unpublishing or unmapping a currency now checks every store's
primary currency, not just the global one, so a store cannot be left with
a currency it no longer has access to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrGY1NnCzdkn7wUxbyHzsX
@KrzysztofPajak
KrzysztofPajak changed the base branch from fix/store-without-available-currency to develop September 13, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant