Skip to content

Commit 4f2af28

Browse files
committed
docs: audit and update multi-currency-guide.md
1 parent 58fcf0f commit 4f2af28

1 file changed

Lines changed: 88 additions & 41 deletions

File tree

Lines changed: 88 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,102 @@
1-
# Multi-Currency Pricing Guide
1+
# Multi-Currency Guide
22

3-
The Book Store application supports multi-currency pricing, allowing for explicit price control across different regions and a tailored shopping experience for international users.
3+
BookStore uses explicit per-currency pricing. Prices are authored and stored for each currency code (for example, USD/EUR/GBP), and the UI chooses which stored value to display.
44

5-
## Architectural Philosophy
5+
## Current Implementation Summary
66

7-
We chose **Explicit Pricing** over dynamic conversion for several strategic reasons:
8-
9-
* **Psychological Pricing**: Regional markets have different price thresholds. A book priced at $19.99 is better represented as €17.99 or £14.99 rather than an unoptimized conversion like €18.43.
10-
* **Stability**: Item prices in the database remain constant, shielding the application from volatile exchange rate fluctuations during the checkout process.
11-
* **Performance**: Since prices are denormalized into the `BookSearchProjection`, the UI can display them instantly without contacting an external Pricing API.
12-
13-
## Interesting Facts & Technical Insights
14-
15-
### 1. Architectural Symmetry
16-
The multi-currency system is designed to mirror the **Localization Pattern** used throughout the codebase.
17-
- Just as we use a `Dictionary<string, string>` for translated text (e.g., `Title`), we use a `Dictionary<string, decimal>` for `Prices`.
18-
- The same validation logic ensures that the `DefaultCurrency` (configured in `appsettings.json`) is always present, just like the default language translation.
19-
20-
### 2. Intelligent Category-Aware Seeding
21-
The `DatabaseSeeder` doesn't just assign random numbers. It implements a realistic pricing strategy based on book categories:
22-
- **Classics**: Priced defensively ($7–$13) as they are often in the public domain or have lower licensing costs.
23-
- **History & Professional**: Priced higher ($20–$41) to reflect specialized content value.
24-
- **Sci-Fi & Fantasy**: Mid-tier pricing ($15–$31).
25-
26-
### 3. Psychological Pricing Endings
27-
To mimic real-world retail behavior, the seeder applies "charm pricing" logic. Every generated price is terminated with a psychological ending:
28-
- **.99**: The most common "sale" or retail ending.
29-
- **.49**: Suggests a value-oriented price.
30-
- **.95**: Often used in premium or "boutique" listings.
31-
32-
### 4. Blazor Prerendering Safety
33-
Accessing `localStorage` for user preferences (like currency) can break Blazor Server applications during the static prerendering phase (where no browser/JS context exists).
34-
We solved this by:
35-
1. Initializing the `CurrencyService` in the `OnAfterRenderAsync` lifecycle method.
36-
2. Using an initialization guard to prevent redundant calls.
37-
3. Ensuring the UI reactively updates via the `OnCurrencyChanged` event once the browser-side initialization completes.
7+
- Prices are stored as a dictionary keyed by ISO currency code.
8+
- API configuration exposes supported currencies and default currency.
9+
- The web UI lets users pick a currency and persists that choice in local storage.
10+
- Price filtering supports a selected currency.
11+
- Discounts are applied per stored currency price.
12+
- Runtime foreign exchange conversion is not implemented.
3813

3914
## Configuration
4015

41-
Available currencies are defined in the `ApiService` via `appsettings.json`:
16+
Currency configuration is defined in ApiService settings:
4217

4318
```json
4419
"Currency": {
45-
"DefaultCurrency": "USD",
20+
"DefaultCurrency": "GBP",
4621
"SupportedCurrencies": ["USD", "EUR", "GBP"]
4722
}
4823
```
4924

50-
## UI Implementation
25+
`CurrencyOptions` validates:
26+
27+
- `DefaultCurrency` is required and must be a 3-character code.
28+
- `SupportedCurrencies` must contain at least one value.
29+
- `DefaultCurrency` must be present in `SupportedCurrencies`.
30+
31+
The API exposes this through `GET /api/config/currency`.
32+
33+
## How Prices Are Stored
34+
35+
### Write model (events and aggregate)
36+
37+
- Create/update commands carry `IReadOnlyDictionary<string, decimal>? Prices`.
38+
- `BookHandlers` validates:
39+
1. Default currency price is present.
40+
2. All provided currencies are in configured supported currencies.
41+
- `BookAggregate` stores `Dictionary<string, decimal> Prices` and validates non-empty dictionary, 3-character currency codes, and non-negative values.
42+
- `BookAdded` and `BookUpdated` events persist the full dictionary.
43+
44+
### Read model
45+
46+
`BookSearchProjection` stores:
47+
48+
- `Prices` (base prices by currency)
49+
- `CurrentPrices` (effective prices by currency after discount factor)
50+
51+
`CurrentPrices` is recalculated from `Prices` using the active discount percentage. This is discount calculation, not cross-currency conversion.
52+
53+
## DTOs and API Shape
54+
55+
Shared DTOs expose multi-currency prices:
56+
57+
- `BookDto.Prices` and `AdminBookDto.Prices`: dictionary by currency.
58+
- `BookDto.CurrentPrices` and `AdminBookDto.CurrentPrices`: list of `PriceEntry` values representing effective prices (for example, after discount).
59+
- Shopping cart item DTOs also expose a per-currency price dictionary.
60+
61+
## Currency Selection and Display in Web
62+
63+
`CurrencyService` is the client-side source of truth for selected currency:
64+
65+
- Default selected currency in the service is `GBP`.
66+
- Selection is persisted under local storage key `selected_currency`.
67+
- `OnCurrencyChanged` notifies UI components.
68+
- `FormatPrice` formats known currencies (USD, EUR, GBP) using invariant numeric formatting and symbols.
69+
70+
`CurrencySelector` component:
71+
72+
- Is shown in the main app bar.
73+
- Loads supported currencies from `GET /api/config/currency`.
74+
- Uses a local fallback list (`USD`, `EUR`, `GBP`) if config fetch fails.
75+
76+
Pages such as home, book details, and cart render prices by looking up `CurrencyService.CurrentCurrency` in each book/cart `Prices` dictionary.
77+
78+
## Filtering by Currency
79+
80+
Book search supports currency-aware filtering:
81+
82+
- If only `Currency` is provided, results are filtered to books that have that currency in `Prices`.
83+
- If `MinPrice` and/or `MaxPrice` are provided with `Currency`, filtering is applied to matching entries in `CurrentPrices`.
84+
- Without a `Currency`, price range filtering applies across any `CurrentPrices` entry.
85+
86+
## Seeding Behavior
87+
88+
Database seeding currently generates USD, EUR, and GBP prices per book using category-based ranges and fixed multipliers plus charm endings (`.49`, `.95`, `.99`).
89+
90+
This seeding logic is only for initial/sample data generation and is not used for runtime conversion.
91+
92+
## Not Implemented (Important)
93+
94+
- No runtime FX/exchange-rate service exists.
95+
- No background synchronization of prices from external currency providers exists.
96+
- Currency switching does not convert values; it selects another stored value from the price dictionary.
97+
98+
## Practical Implications
5199

52-
The `CurrencyService` in the Web project acts as the single source of truth:
53-
- **State Management**: It tracks `CurrentCurrency`.
54-
- **Persistence**: It keeps the choice in `localStorage`.
55-
- **Formatting**: It provides `FormatPrice(prices)` which uses `CultureInfo.InvariantCulture` to ensure consistent formatting (using dots as decimal separators) across all server locales.
100+
- New currencies require both configuration updates and persisted per-book prices.
101+
- If a selected currency is missing in a specific dictionary, UI formatting returns `N/A` for that item.
102+
- For consistent UX, admin workflows should continue supplying prices for all supported currencies.

0 commit comments

Comments
 (0)