|
1 | | -# Multi-Currency Pricing Guide |
| 1 | +# Multi-Currency Guide |
2 | 2 |
|
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. |
4 | 4 |
|
5 | | -## Architectural Philosophy |
| 5 | +## Current Implementation Summary |
6 | 6 |
|
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. |
38 | 13 |
|
39 | 14 | ## Configuration |
40 | 15 |
|
41 | | -Available currencies are defined in the `ApiService` via `appsettings.json`: |
| 16 | +Currency configuration is defined in ApiService settings: |
42 | 17 |
|
43 | 18 | ```json |
44 | 19 | "Currency": { |
45 | | - "DefaultCurrency": "USD", |
| 20 | + "DefaultCurrency": "GBP", |
46 | 21 | "SupportedCurrencies": ["USD", "EUR", "GBP"] |
47 | 22 | } |
48 | 23 | ``` |
49 | 24 |
|
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 |
51 | 99 |
|
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