feat(prices): PriceFetchControl component + consent modal + best-effort UX #158

Closed
opened 2026-04-27 00:15:36 +00:00 by maximus · 0 comments
Owner

Goal

Build the PriceFetchControl.tsx component (button + consent modal + spinner + attribution) used in the snapshot editor for priced categories.

Contract reference

docs/api-contract-prices.md §1 (UX best-effort label), Annexe B (i18n mapping). ADR 0011 (best-effort UX rules).

Fichiers concernés

  • src/components/balance/PriceFetchControl.tsx (new, ~150 lines)
  • src/components/balance/PriceFetchControl.test.tsx (new, ~120 lines)
  • src/components/balance/SnapshotEditPage.tsx (or équivalent — wire the new control next to each priced line)

Depends on

Scope

  • src/components/balance/PriceFetchControl.tsx:
    • Props: { symbol: string; date: string; categoryKind: 'simple' | 'priced'; assetType: 'stock' | 'crypto'; onPriceFetched: (price: number, currency: string) => void; }
    • Returns null if useIsPremium() === false (also if categoryKind !== 'priced')
    • For assetType === 'stock': badge "best-effort" + warning text via t('balance.priceFetching.bestEffortNotice') (premier usage de la session, dismissable)
    • For assetType === 'crypto': no warning
    • First click on a profile (consent absent in user_preferences for key price_fetching_consent) → render consent modal
    • Modal :
      • Title: t('balance.priceFetching.consent.title')
      • Body: t('balance.priceFetching.consent.body') (mentions privacy proxy + maximus-api)
      • Buttons: t('balance.priceFetching.consent.accept') / consent.decline
      • On accept: write user_preferences key price_fetching_consent = JSON.stringify({consented_at: new Date().toISOString(), version: 1}). Do NOT seed elsewhere.
    • On accept (and on subsequent clicks once consented): spinner during prices.fetchPrice(symbol, date) ; on success, attribution t('balance.priceFetching.attribution', {date: '...'}) displayed near the input ; on error, toast via t('balance.priceFetching.errors.<key>') per Annexe B
  • Wire into SnapshotEditPage for kind=priced lines (1 control per line, beside the unit_price input)
  • Vitest + React Testing Library tests:
    • hidden if not premium
    • hidden if kind !== 'priced'
    • warning visible only for stock categories
    • first click opens consent modal
    • accept consent → modal closes, fetch runs
    • decline consent → modal closes, no fetch
    • second click (after consent) → no modal, fetch runs
    • error → toast appears, manual unit_price input remains active

Critères d'acceptation

  • All vitest tests green (≥ 8 tests)
  • Manual smoke test: snapshot editor with a stock category shows badge + warning ; with a crypto category, no badge
  • Consent modal appears once per profile per consent state ; persists in user_preferences
  • Le champ de saisie manuelle reste actif sur tous chemins d'erreur (jamais bloqué)

Décisions prises ce soir

  • Namespace i18n confirmé : balance.priceFetching.*
  • L'ergonomie dépend d'un mix de props (assetType, categoryKind) + state (consent, premium)
  • Best-effort warning montré une fois par session (dismiss en mémoire, pas persisté)

Spec source

docs/api-contract-prices.md, ADR 0011

## Goal Build the `PriceFetchControl.tsx` component (button + consent modal + spinner + attribution) used in the snapshot editor for priced categories. ## Contract reference `docs/api-contract-prices.md` §1 (UX best-effort label), Annexe B (i18n mapping). ADR 0011 (best-effort UX rules). ## Fichiers concernés - `src/components/balance/PriceFetchControl.tsx` (new, ~150 lines) - `src/components/balance/PriceFetchControl.test.tsx` (new, ~120 lines) - `src/components/balance/SnapshotEditPage.tsx` (or équivalent — wire the new control next to each priced line) ## Depends on - #156, #157 ## Scope - [ ] `src/components/balance/PriceFetchControl.tsx`: - Props: `{ symbol: string; date: string; categoryKind: 'simple' | 'priced'; assetType: 'stock' | 'crypto'; onPriceFetched: (price: number, currency: string) => void; }` - Returns `null` if `useIsPremium() === false` (also if `categoryKind !== 'priced'`) - For `assetType === 'stock'`: badge "best-effort" + warning text via `t('balance.priceFetching.bestEffortNotice')` (premier usage de la session, dismissable) - For `assetType === 'crypto'`: no warning - First click on a profile (consent absent in `user_preferences` for key `price_fetching_consent`) → render consent modal - Modal : - Title: `t('balance.priceFetching.consent.title')` - Body: `t('balance.priceFetching.consent.body')` (mentions privacy proxy + maximus-api) - Buttons: `t('balance.priceFetching.consent.accept')` / `consent.decline` - On accept: write `user_preferences` key `price_fetching_consent = JSON.stringify({consented_at: new Date().toISOString(), version: 1})`. **Do NOT seed** elsewhere. - On accept (and on subsequent clicks once consented): spinner during `prices.fetchPrice(symbol, date)` ; on success, attribution `t('balance.priceFetching.attribution', {date: '...'})` displayed near the input ; on error, toast via `t('balance.priceFetching.errors.<key>')` per Annexe B - [ ] Wire into `SnapshotEditPage` for `kind=priced` lines (1 control per line, beside the unit_price input) - [ ] Vitest + React Testing Library tests: - hidden if not premium - hidden if `kind !== 'priced'` - warning visible only for stock categories - first click opens consent modal - accept consent → modal closes, fetch runs - decline consent → modal closes, no fetch - second click (after consent) → no modal, fetch runs - error → toast appears, manual unit_price input remains active ## Critères d'acceptation - [ ] All vitest tests green (≥ 8 tests) - [ ] Manual smoke test: snapshot editor with a stock category shows badge + warning ; with a crypto category, no badge - [ ] Consent modal appears once per profile per consent state ; persists in user_preferences - [ ] Le champ de saisie manuelle reste actif sur tous chemins d'erreur (jamais bloqué) ## Décisions prises ce soir - Namespace i18n confirmé : `balance.priceFetching.*` - L'ergonomie dépend d'un mix de props (`assetType`, `categoryKind`) + state (consent, premium) - Best-effort warning montré une fois par session (dismiss en mémoire, pas persisté) ## Spec source `docs/api-contract-prices.md`, ADR 0011
maximus added this to the spec-price-fetching milestone 2026-04-27 00:15:36 +00:00
maximus added the
status:ready
type:feature
source:human
labels 2026-04-27 00:15:36 +00:00
maximus modified the milestone from spec-price-fetching to overnight-2026-04-27-prices 2026-04-27 00:32:02 +00:00
maximus added
status:in-progress
and removed
status:ready
source:human
labels 2026-04-27 12:36:52 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#158
No description provided.