Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
48f49de
COST-7252: Implement constant currency Phase 1
ELK4N4 Apr 15, 2026
c6a7729
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 Apr 16, 2026
f6600d2
COST-7252: Fix CI failures and address review feedback
ELK4N4 Apr 16, 2026
28b86a6
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 Apr 16, 2026
f275159
COST-7252: Fix pre-commit failures (unused imports + black formatting)
ELK4N4 Apr 16, 2026
9a317c1
Fix CI: TruncMonth(OuterRef) crash, test auth, flake8 F821/C901
ELK4N4 Apr 16, 2026
54cada1
Fix black formatting in exchange_rate_annotations.py
ELK4N4 Apr 16, 2026
2044fe1
Fix FieldError: use costmodelmap (not cost_model_map) for reverse FK …
ELK4N4 Apr 16, 2026
a46bb11
COST-7252: Fix nested OuterRef for OCP exchange rate subquery
ELK4N4 Apr 16, 2026
584ae4b
Fix TruncMonth(OuterRef) by setting usage_start output_field
ELK4N4 Apr 16, 2026
67d6836
Fix CostModel reverse relation name in OCP exchange rate subquery
ELK4N4 Apr 16, 2026
813b09c
fix: remove unsupported OuterRef output_field for Django F()
ELK4N4 Apr 16, 2026
fd55d03
Fix exchange rate subquery crashes in Django 5.2
ELK4N4 Apr 16, 2026
a4c0c5a
Fix MonthlyExchangeRate query missing tenant schema context
ELK4N4 Apr 16, 2026
4f5fce9
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 Apr 19, 2026
6f9a863
Remove dead code from constant-currency branch
ELK4N4 Apr 19, 2026
9c73a8a
lint
ELK4N4 Apr 19, 2026
2cb453c
Add unique constraint on StaticExchangeRate to prevent duplicate entries
ELK4N4 Apr 19, 2026
8b4570d
Make build_monthly_rate_annotation private
ELK4N4 Apr 19, 2026
baba247
Refactor _build_monthly_rate_annotation to accept a generic expression
ELK4N4 Apr 19, 2026
2659306
Rename base_currency_expr to base_currency and simplify docstring
ELK4N4 Apr 19, 2026
51b4a22
Move currency settings URL routes to settings group
ELK4N4 Apr 19, 2026
17a99dc
Rename exchange-rate-pairs URL to static-exchange-rates
ELK4N4 Apr 19, 2026
878fba1
Make EnabledCurrency the single source of truth for available currencies
ELK4N4 Apr 19, 2026
7a7839c
Filter currency/ endpoint by EnabledCurrency; remove available-curren…
ELK4N4 Apr 19, 2026
5bd48ac
Move static-exchange-rates under settings/currency/ URL path
ELK4N4 Apr 19, 2026
a8c534c
Add logger to PriceListSerializer
ELK4N4 Apr 19, 2026
1613415
Remove hardcoded currency list; use EnabledCurrency + pycountry
ELK4N4 Apr 19, 2026
aa411e0
Use babel for currency name + symbol; drop pycountry
ELK4N4 Apr 19, 2026
316a3dd
Store only currency_name; compute symbol and description at response …
ELK4N4 Apr 19, 2026
e70da89
Store only currency_code in EnabledCurrency; compute all display meta…
ELK4N4 Apr 19, 2026
8880e58
Rename EnabledCurrency to CurrencyConfig
ELK4N4 Apr 20, 2026
3c041e9
Fix misleading log message in currency config endpoint
ELK4N4 Apr 21, 2026
e110c23
Extract CurrencyField to deduplicate validate_currency across seriali…
ELK4N4 Apr 21, 2026
ea89a29
Rename currency config endpoint path from enabled-currencies to config
ELK4N4 Apr 21, 2026
54cf1cf
revert
ELK4N4 Apr 21, 2026
83ebcf8
Use CurrencyField and enabled currencies in cost model serializers an…
ELK4N4 Apr 23, 2026
4875bfb
Simplify exchange_rates_applied access in ReportQueryHandler
ELK4N4 Apr 23, 2026
a1e238c
Move imports to module level and remove redundant guard in _get_excha…
ELK4N4 Apr 23, 2026
039849a
Remove unnecessary None guards in _get_exchange_rates_applied
ELK4N4 Apr 23, 2026
8089c7c
Simplify _get_exchange_rates_applied to one entry per month
ELK4N4 Apr 26, 2026
5979803
Revert MIG profiles implicit group_by for filter[limit]
ELK4N4 Apr 26, 2026
e8313d9
Use DateHelper.month_end instead of manual calendar.monthrange in exc…
ELK4N4 Apr 26, 2026
f18fef1
Merge upstream/main into COST-7252/constant-currency-phase1
ELK4N4 Apr 26, 2026
4c0de87
COST-7252: Skip Others bucket for MIG profile ranking and fix GROUP B…
ELK4N4 Apr 26, 2026
7308642
Revert MIG-related changes from constant-currency branch
ELK4N4 Apr 26, 2026
da45e1d
Move ExchangeRateDictionary import to module level
ELK4N4 Apr 26, 2026
ca74b37
COST-7252: Refactor exchange rate helpers for clarity
ELK4N4 Apr 26, 2026
ea6bfa0
COST-7252: Replace manual get_queryset with DjangoFilterBackend in St…
ELK4N4 Apr 26, 2026
d0aaede
COST-7252: Remove redundant never_cache wrappers from StaticExchangeR…
ELK4N4 Apr 26, 2026
84034ed
COST-7252: Use perform_destroy hook instead of overriding destroy in …
ELK4N4 Apr 26, 2026
a8f058f
COST-7252: Extract exchange rate helpers into static_exchange_rate_ut…
ELK4N4 Apr 26, 2026
2296f7e
COST-7252: Validate static exchange rate currencies against ISO 4217 …
ELK4N4 Apr 26, 2026
95581fe
COST-7252: Remove ensure_currencies_enabled from static exchange rate…
ELK4N4 Apr 26, 2026
c7d3118
COST-7252: Inline schema_name access following project convention
ELK4N4 Apr 26, 2026
a146485
COST-7252: Rename upsert_monthly_rates to upsert_static_monthly_rates…
ELK4N4 Apr 26, 2026
2a99c7c
COST-7252: Remove dead ensure_currencies_enabled function from utils
ELK4N4 Apr 26, 2026
f241684
COST-7252: Use set-based ISO 4217 lookup and inline currency validation
ELK4N4 Apr 27, 2026
b7cfde8
COST-7252: Replace CurrencyConfig with EnabledCurrency and use Babel …
ELK4N4 Apr 27, 2026
c7a198c
COST-7252: Rename currency config endpoint to settings/currency/enabled/
ELK4N4 Apr 27, 2026
5c577ac
COST-7252: Rename EnabledCurrencyConfigView to EnabledCurrencyView
ELK4N4 Apr 27, 2026
14a64cb
COST-7252: Move EnabledCurrencySerializer to settings/serializers.py
ELK4N4 Apr 27, 2026
e80100c
COST-7252: Replace bulk currency enablement with single-currency toggle
ELK4N4 Apr 28, 2026
61c3404
COST-7252: Consolidate currency endpoints under settings/currency/exc…
ELK4N4 Apr 28, 2026
c416fb2
COST-7252: Sync constant-currency architecture docs with new URL stru…
ELK4N4 Apr 28, 2026
a401aa2
COST-7252: Remove version field from StaticExchangeRate
ELK4N4 Apr 28, 2026
7a904d9
COST-7252: Fix currency endpoint docs to match implementation
ELK4N4 Apr 28, 2026
b62ad98
COST-7252: Add enabled_only flag to CurrencyField
ELK4N4 Apr 28, 2026
1aef5ba
COST-7252: Update Pipfile.lock to include Babel dependency
ELK4N4 Apr 28, 2026
4166e1e
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 Apr 28, 2026
cd56221
COST-7252: Restore dev/containers/minio/.gitignore
ELK4N4 Apr 28, 2026
e51bb78
COST-7252: Sync migrations with model changes
ELK4N4 Apr 28, 2026
13506eb
COST-7252: Remove unnecessary blank=True from ExchangeRates.currency_…
ELK4N4 Apr 28, 2026
55cfb39
COST-7252: Fix pre-commit violations (black formatting, unused imports)
ELK4N4 Apr 28, 2026
ddf9f79
COST-7252: Remove static-rate enablement bypass from architecture docs
ELK4N4 Apr 28, 2026
37bdbe3
COST-7252: Fix black formatting for exchange_rate_annotations
ELK4N4 Apr 28, 2026
7640313
COST-7252: Add migration seed, exchange rate validation, and costs-as…
ELK4N4 Apr 28, 2026
3f212a5
COST-7252: Filter exchange_rates_applied to currencies present in rep…
ELK4N4 Apr 29, 2026
a732a5a
COST-7252: Capitalize Babel package name in Pipfile
ELK4N4 Apr 29, 2026
c39512a
COST-7252: Regenerate Pipfile.lock with Python 3.11
ELK4N4 Apr 29, 2026
86d0b5b
Revert Pipfile.lock to main branch version
ELK4N4 Apr 29, 2026
d1a0a0d
Downgrade tox to 4.11.4 to resolve platformdirs conflict
ELK4N4 Apr 29, 2026
ccd9863
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 Apr 29, 2026
b989ff7
revert Pipfile.lock
ELK4N4 Apr 29, 2026
4160b2a
revert Pipfile.lock
ELK4N4 Apr 29, 2026
81fd01f
revert Pipfile.lock
ELK4N4 Apr 29, 2026
637a696
revert Pipfile.lock
ELK4N4 Apr 29, 2026
fe2a812
installing Babel
ELK4N4 Apr 29, 2026
ab94e05
Group exchange rates by base_currency in settings list endpoint
ELK4N4 Apr 29, 2026
bb4fac1
Move enabled-currency endpoint to settings/currency/enabled-currencies/
ELK4N4 Apr 29, 2026
25d8d1f
Seed EnabledCurrency in test DB and filter unsupported currencies
ELK4N4 Apr 29, 2026
ae35f2c
add to in model
ELK4N4 Apr 29, 2026
c47b465
Fix tenant context for EnabledCurrency in tests
ELK4N4 Apr 30, 2026
0396c3c
Add missing migration for ExchangeRates.currency_type blank=True
ELK4N4 Apr 30, 2026
82b6343
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 Apr 30, 2026
87d4671
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 Apr 30, 2026
e95190e
Merge branch 'main' into COST-7252/constant-currency-phase1
myersCody Apr 30, 2026
b652370
Update constant-currency docs: fix enablement URL and clarify costs-a…
ELK4N4 Apr 30, 2026
b24bca8
[COST-7252] Add bidirectional inverse for static exchange rates
ELK4N4 Apr 30, 2026
4c236d6
Merge branch 'main' into COST-7252/constant-currency-phase1
bacciotti May 5, 2026
b1106be
Merge branch 'main' into COST-7252/constant-currency-phase1
bacciotti May 6, 2026
f040247
Merge branch 'main' into COST-7252/constant-currency-phase1
bacciotti May 7, 2026
c1bf96d
[COST-7252] Fix static exchange rate update to clean up old rates on …
ELK4N4 May 7, 2026
2ff0317
[COST-7252] Require authentication for currency endpoint
ELK4N4 May 7, 2026
2003b9b
[COST-7252] Seed EnabledCurrency in migration instead of test runner
ELK4N4 May 7, 2026
c62525d
[COST-7252] Add is_authenticated property to User model
ELK4N4 May 7, 2026
3b4cd5f
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 May 8, 2026
fed5507
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 May 11, 2026
3b3d517
[COST-7252] Skip currencies with invalid exchange rate values
ELK4N4 May 11, 2026
fe1f7a4
squash migrations into one
ELK4N4 May 11, 2026
fbe8de8
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 May 12, 2026
a6b922b
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 May 13, 2026
c9defdc
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 May 17, 2026
5c9fcb0
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 May 18, 2026
1da21f0
Conditionalize exchange rate error message based on CURRENCY_URL
ELK4N4 May 18, 2026
f069542
removed self.target_currency
ELK4N4 May 18, 2026
2bb4ecf
Warn when enabling a currency without a dynamic exchange rate
ELK4N4 May 18, 2026
ba70bbb
Fix pre-commit formatting in exchange_rate_annotations.py
ELK4N4 May 18, 2026
1210532
Fix exchange rate error message logic
ELK4N4 May 19, 2026
58d9cd0
Warn admin when enabling a currency with no exchange rates
ELK4N4 May 20, 2026
f078f46
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 May 20, 2026
1d3bbdd
Merge branch 'main' into COST-7252/constant-currency-phase1
ELK4N4 May 20, 2026
0b52cbc
Add has_dynamic_rate flag to currency info response
ELK4N4 May 25, 2026
78c07f5
Eliminate N+1 queries in currency info by batching dynamic rate lookup
ELK4N4 May 25, 2026
c21e1f3
Simplify get_daily_currency_rates early-return logic
ELK4N4 May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pandas = "<3.0"
scipy = ">=1.16"
boto3 = "*"
sqlalchemy = ">=2.0.0"
Babel = "*"

[dev-packages]
argh = ">=0.26.2"
Expand Down
1,705 changes: 623 additions & 1,082 deletions Pipfile.lock

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions docs/architecture/api-settings-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,80 @@ PUT /settings/aws_category_keys/disable/

---

## Currency Enablement

### **Purpose**
Control which ISO 4217 currencies are available for selection across the tenant. Only enabled currencies can be used in account settings, cost models, and report filters.

### **Endpoints**

#### List Enabled Currencies
```
GET /currency/
```

Returns only the currencies that an administrator has enabled via the `EnabledCurrency` table. Metadata (name, symbol, description) is computed at response time via babel.

**Query Parameters:**
- `limit` (integer) - Results per page
- `offset` (integer) - Pagination offset

**Response:**
```json
{
"meta": {
"count": 2
},
"data": [
{
"code": "EUR",
"name": "Euro",
"symbol": "€",
"description": "EUR (€) - Euro"
},
{
"code": "USD",
"name": "US Dollar",
"symbol": "$",
"description": "USD ($) - US Dollar"
}
]
}
```

#### Enable a Currency
```
POST /settings/currency/exchange_rate/<code>/enable/
```

#### Disable a Currency
```
DELETE /settings/currency/exchange_rate/<code>/enable/
```

Enable or disable a single currency by its ISO 4217 code in the URL path.

**Path Parameters:**
- `code` (string) - ISO 4217 currency code (case-insensitive, normalized to uppercase)

**Response:** `204 No Content`

**Error Responses:**

**Invalid Currency Code (400 Bad Request):**
```json
{
"code": ["Invalid ISO 4217 currency code: INVALID"]
}
```

**Behavior:**
- `POST` — idempotently creates an `EnabledCurrency` row for the code
- `DELETE` — idempotently deletes the `EnabledCurrency` row if it exists
- Currency code in the URL is case-insensitive (`/settings/currency/exchange_rate/usd/enable/` enables `USD`)

---

## Cost Groups (OpenShift)

### **Purpose**
Expand Down
67 changes: 35 additions & 32 deletions docs/architecture/constant-currency/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,25 +126,22 @@ again.
**Problem**: Should all currencies returned by the exchange rate API be
immediately available for use, or should an administrator explicitly enable them?

**Resolution**: Explicit enablement. Currencies fetched from the dynamic exchange
rate API arrive in Cost Management as **disabled** by default (stored in the
`EnabledCurrency` table with `enabled=False`). An administrator must explicitly
enable currencies through the Settings UI before they appear in the target
currency dropdown.
**Resolution**: Explicit enablement. The full list of known currencies comes from
Babel's ISO 4217 registry. Only currencies that an administrator has explicitly
enabled are stored in the `EnabledCurrency` table. An administrator must enable
currencies through the Settings API (`POST settings/currency/enabled-currencies/{code}/`) before
they appear in the target currency dropdown.

All currencies are always stored in `MonthlyExchangeRate` regardless of their
enabled status — the `enabled` flag only controls dropdown visibility, not
data storage. This ensures the underlying data is complete and
enabled status — the `EnabledCurrency` table only controls dropdown visibility,
not data storage. This ensures the underlying data is complete and
ready when an administrator enables a currency.

**Rationale**: Explicit enablement gives administrators control over which
currencies appear in their UI. In on-premise environments, customers may only
need a small subset of the ~170 currencies available from the API. Showing all
currencies by default would clutter the dropdown.
need a small subset of the ~300 ISO 4217 currencies. Showing all currencies by
default would clutter the dropdown.

**Exception**: Static exchange rate pairs always make their currencies available
in the dropdown, regardless of `EnabledCurrency` status. If an administrator
defines a `USD→EUR` static rate, both `USD` and `EUR` are immediately available.

### IQ-6: Rate resolution without `CURRENCY_URL` — RESOLVED

Expand All @@ -153,15 +150,17 @@ configured (e.g., airgapped or disconnected deployments)?

**Resolution**: The system does not require `CURRENCY_URL` to function. Rate
resolution follows a simple priority: **static rates first, dynamic rates as
fallback, error if neither exists** for a given currency pair. When
`CURRENCY_URL` is empty or unset:
fallback**. When `CURRENCY_URL` is empty or unset:

- The daily Celery task skips the API fetch step (no dynamic rates are fetched)
- Static exchange rates defined via the CRUD API work normally
- If dynamic rates were previously fetched (before the URL was removed), they
remain available as fallback
- If no rate exists for a given pair (static or dynamic), the API returns an
actionable error
- If `MonthlyExchangeRate` is completely empty (no rates configured at all),
the feature is inactive — no currencies are enabled, so the user cannot
select a target currency and costs are returned as-is in their original
bill currency
- If rates exist but not for a given pair, the API returns an actionable error

The `CURRENCY_URL` setting is documented with the production API URL
(`open.er-api.com`) as a reference example. Only the free tier of the Open
Expand All @@ -170,7 +169,9 @@ Exchange Rates API is supported in this design.
**Rationale**: The system should work with whatever data is available rather
than treating the absence of `CURRENCY_URL` as a special mode. Customers can
define their own exchange rates via the CRUD API regardless of whether dynamic
rates are being fetched.
rates are being fetched. Deployments that never configure exchange rates
continue to work exactly as before — costs are returned in their original
currency with no conversion.

### IQ-7: No-rate corner case — RESOLVED

Expand Down Expand Up @@ -254,28 +255,27 @@ graph LR
API["open.er-api.com<br/>(or custom URL)"] -->|"daily fetch<br/>(skipped if no URL)"| CT["Celery Task:<br/>get_daily_currency_rates"]
CT -->|upsert| ER["ExchangeRates<br/>(public schema)"]
CT -->|rebuild| ERD["ExchangeRateDictionary<br/>(public schema)"]
CT -->|"discover currencies<br/>create as disabled"| EC["EnabledCurrency<br/>(tenant schema)<br/>enabled/disabled per currency"]
CT -->|"no currency discovery"| EC["EnabledCurrency<br/>(tenant schema)<br/>admin-managed"]
CT -->|"Writer 1: per-tenant<br/>skip static pairs<br/>all currencies"| MER["MonthlyExchangeRate<br/>(tenant schema)<br/>single source of truth"]
MER -->|"all months:<br/>per-month rates"| QH["QueryHandler<br/>Subquery annotation"]
QH -->|"per-month rates +<br/>rate metadata"| REPORT["Report Response<br/>+ exchange_rates_applied"]
QH -->|"no rate? →<br/>actionable error"| ERR["Error: no exchange rate<br/>available"]
QH -->|"no rate +<br/>feature active? →<br/>actionable error"| ERR["Error: no exchange rate<br/>available"]
ADMIN["CM Admin"] -->|"enable/disable<br/>currencies"| EC
USER["Price List Admin"] -->|CRUD| SER["Serializer"]
SER -->|"write canonical<br/>rate record"| STATIC["StaticExchangeRate<br/>(tenant schema)"]
SER -->|"Writer 2: upsert<br/>rate_type=static"| MER
EC -->|"dropdown filter:<br/>enabled dynamic ∪<br/>static rate currencies"| DD["Target Currency<br/>Dropdown"]
STATIC -->|"static currencies<br/>always available"| DD
EC -->|"dropdown filter:<br/>enabled currencies only"| DD["Target Currency<br/>Dropdown"]
```

**Key changes**:

1. **Single source of truth**: `MonthlyExchangeRate` stores rates for all months (current and past); query handlers read from this one table
2. **Two writers**: Celery task writes dynamic rates daily for the current month; CRUD serializer writes static rates for affected months
3. **Rate resolution**: All months read from `MonthlyExchangeRate`; M2 migration seeds current-month data at deployment; pre-deployment months fall back to earliest available rate; error if no rate exists at all for a currency pair
3. **Rate resolution**: All months read from `MonthlyExchangeRate`; M2 migration seeds current-month data at deployment; pre-deployment months fall back to earliest available rate. When `MonthlyExchangeRate` is empty (feature not configured), no currencies are enabled and costs are returned as-is; when rows exist but not for the target currency, an actionable error is returned
4. Report responses include rate provenance metadata
5. **Currency enablement**: Dynamic currencies arrive as disabled; administrator enables them via Settings to make them visible in the dropdown (all currencies are always stored)
6. **Dropdown visibility**: Target currency dropdown shows only the union of enabled dynamic currencies and static rate currencies (disabled currencies are stored but hidden from the dropdown)
7. **No-rate error**: If user selects a currency with no conversion path from the bill currency, an actionable error is returned
6. **Dropdown visibility**: Target currency dropdown shows only currencies that an administrator has explicitly enabled (static rate currencies still require enablement)
7. **No-rate handling**: If `MonthlyExchangeRate` is empty, the feature is inactive — no currencies are enabled, so costs are returned as-is. If rows exist but not for the selected currency, an actionable error is returned

---

Expand All @@ -289,14 +289,13 @@ graph LR
| 4 | **No multi-hop conversion** | No chain conversion (e.g., USD→EUR→CNY) to avoid prioritization complexity |
| 5 | **Bidirectional implicit inverse** | USD→EUR at 0.87 implies EUR→USD = 1/0.87 unless explicitly defined |
| 6 | **Natural month boundaries** | Start/end dates must align to first/last day of month; no mid-month validity periods |
| 7 | **Simple integer versioning** | Auto-increment on `StaticExchangeRate.version`; Phase 2 adds full audit history |
| 8 | **Automatic finalized month locking** | Dynamic rows overwritten daily during current month; untouched after month ends |
| 9 | **Forward-only with current-month seed** | M2 migration seeds current-month data from `ExchangeRateDictionary`; pre-deployment months fall back to earliest available rate |
| 10 | **Per-pair rows, not JSON blob** | Enables `unique_together` constraint, simpler queries, cleaner ORM integration |
| 11 | **Explicit currency enablement** | Dynamic currencies arrive disabled; administrator enables them in Settings to control which currencies appear in the dropdown. All currencies are always stored regardless of enabled status. |
| 12 | **Configurable exchange rate URL** | `CURRENCY_URL` is a variable; empty value skips dynamic rate fetching. System works with whatever rates are available (static first, dynamic fallback, error if neither). Documentation references `open.er-api.com` (free tier) as the production example |
| 13 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection |
| 14 | **Static rates bypass enablement** | Currencies in static exchange rate pairs are always available in dropdowns regardless of `EnabledCurrency` status |
| 7 | **Automatic finalized month locking** | Dynamic rows overwritten daily during current month; untouched after month ends |
| 8 | **Forward-only with current-month seed** | M2 migration seeds current-month data from `ExchangeRateDictionary`; pre-deployment months fall back to earliest available rate |
| 9 | **Per-pair rows, not JSON blob** | Enables `unique_together` constraint, simpler queries, cleaner ORM integration |
| 10 | **Explicit currency enablement** | Dynamic currencies arrive disabled; administrator enables them in Settings to control which currencies appear in the dropdown. All currencies are always stored regardless of enabled status. |
| 11 | **Configurable exchange rate URL** | `CURRENCY_URL` is a variable; empty value skips dynamic rate fetching. System works with whatever rates are available (static first, dynamic fallback). Documentation references `open.er-api.com` (free tier) as the production example |
| 12 | **Show-then-error for no-rate currencies** | Available currencies appear in dropdown even without a conversion path from the bill currency; actionable error returned on selection. When `MonthlyExchangeRate` is empty (no rates configured at all), the feature is inactive — no currencies are enabled and costs are returned as-is |
| 13 | **Enablement is always required for reports** | Static exchange rate currencies must still be explicitly enabled to appear in the report dropdown. The settings admin page shows them regardless for management purposes. |

---

Expand All @@ -313,3 +312,7 @@ graph LR
| v1.6 | 2026-03-30 | Removed `ExchangeRateDictionary` fallback from query handler. M2 seeds current-month data. Decision #9 updated. |
| v1.7 | 2026-04-12 | Updated data flow diagram: query handler uses `Subquery` annotation instead of `Case`/`When`. |
| v1.8 | 2026-04-13 | Synced pre-deployment month references: fall back to earliest available rate (aligns with pipeline-changes.md v2.1). |
| v1.9 | 2026-04-28 | Updated currency enablement URL reference to `settings/currency/enabled-currencies/{code}/`. |
| v2.0 | 2026-04-28 | Removed static-rate enablement bypass (decision #13). Report dropdown governed solely by `EnabledCurrency`; settings admin page shows static rates regardless. |
| v2.1 | 2026-04-28 | Added "costs as-is" behavior: when `MonthlyExchangeRate` is empty, feature is inactive, validation skipped, costs returned in original currency. Updated IQ-6, decision #12, data flow key changes. |
| v2.2 | 2026-04-30 | Fixed currency enablement URL to `settings/currency/enabled-currencies/{code}/`. Clarified "costs as-is": no currencies enabled means serializer blocks currency selection; costs returned as-is. |
Loading
Loading