Skip to content

feat(multi-currency): add currency conversion metadata storage and display#367

Open
neogenz wants to merge 8 commits intopreviewfrom
neogenz/yeosu
Open

feat(multi-currency): add currency conversion metadata storage and display#367
neogenz wants to merge 8 commits intopreviewfrom
neogenz/yeosu

Conversation

@neogenz
Copy link
Owner

@neogenz neogenz commented Mar 10, 2026

Summary

• Add multi-currency support with automatic exchange rate conversion
• Store original amount, currency, and exchange rate metadata on transactions and budget lines
• Display conversion details via new CurrencyConversionBadge component on detail views
• Add currency settings view on iOS with EUR/CHF selection
• Implement currency conversion service across all platforms

Changes

Backend
• New currency service with exchange rate conversion
• Add originalAmount, originalCurrency, targetCurrency, exchangeRate columns to database
• Update budget line and transaction mappers to handle conversion metadata
• Export conversion data in Excel exports

Frontend (Angular)
• New CurrencyConversionBadge UI component with tooltip showing conversion details
• New CurrencyConverterService for client-side currency conversions
• Integrate badges into budget detail panel, allocated transactions dialog, and bottom sheet
• Add i18n support for conversion tooltips

iOS (SwiftUI)
• New CurrencyConversionBadge with popover showing original amount and exchange rate
• CurrencyConversionService actor for currency conversion
• CurrencyAmountPicker component for selecting input currency
• CurrencySettingView for EUR/CHF selection in account settings
• Integrate badges into edit sheets (transactions, budget lines, templates)

Testing
• Add 6 unit tests for CurrencyConversionBadge component
• Add currency converter service tests
• Add currency pipe tests

Type Safety
• Update shared Zod schemas with currency metadata fields
• Add error codes for currency conversion failures

@vercel
Copy link

vercel bot commented Mar 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pulpe-frontend Ready Ready Preview, Comment Mar 18, 2026 1:47pm
pulpe-landing Ready Ready Preview, Comment Mar 18, 2026 1:47pm

@supabase
Copy link

supabase bot commented Mar 10, 2026

Updates to Preview Branch (neogenz/yeosu) ↗︎

Deployments Status Updated
Database Wed, 18 Mar 2026 13:47:06 UTC
Services Wed, 18 Mar 2026 13:47:06 UTC
APIs Wed, 18 Mar 2026 13:47:06 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Wed, 18 Mar 2026 13:47:07 UTC
Migrations Wed, 18 Mar 2026 13:47:07 UTC
Seeding Wed, 18 Mar 2026 13:47:08 UTC
Edge Functions Wed, 18 Mar 2026 13:47:09 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@claude
Copy link
Contributor

claude bot commented Mar 10, 2026

Review PR 367 - Resume: 1 bloquant, 1 important, 1 suggestion - Bloquant: app-currency.pipe.ts:10 pure:true avec signal interne, montants ne se rafraichissent pas au changement de devise (95%) - Important: edit-transactions-dialog.ts:502 metadonnees de conversion perdues a la sauvegarde des lignes template (93%) - Suggestion: error-definitions.ts:603 CURRENCY_RATE_FETCH_FAILED string litterale au lieu API_ERROR_CODES.XXX (90%) - Points positifs: chiffrement original_amount correct, helper decryptAmountFields propre, migration SQL retro-compatible, cache TTL frontend, tests backend complets - Verdict: CHANGES REQUESTED

@github-actions
Copy link

github-actions bot commented Mar 10, 2026

🎭 Playwright E2E Tests

162 tests  ±0   162 ✅ ±0   8m 59s ⏱️ +13s
 36 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit a703615. ± Comparison against base commit f91f88a.

♻️ This comment has been updated with latest results.

@claude
Copy link
Contributor

claude bot commented Mar 13, 2026

Review PR 367

Resume : 1 bloquant - 1 important - 1 suggestion

Bloquants

  • backend-nest/src/modules/budget-template/budget-template.service.ts:L1778original_amount jamais chiffre dans le bulk insert (confiance: 97%)

Le chemin single-insert chiffre correctement original_amount via prepareAmountData. La branche bulk (creation de budget depuis un template) chiffre les amount en masse via prepareAmountsData mais oublie les original_amount. Resultat : lignes creees en bulk avec une devise source -> original_amount = NULL en base -> badge de conversion absent pour ces lignes.

Importants

  • backend-nest/src/modules/currency/currency.service.ts:L67supportedCurrencySchema.parse() peut lever une ZodError brute (confiance: 92%)

overrideExchangeRate est appele depuis trois services dans un contexte metier. Une ZodError non-transformee ne produira pas la reponse d erreur standard BusinessException. En pratique attenue par la validation DTO en amont, mais safeParse + BusinessException serait coherent avec les patterns du projet.

Suggestions

  • frontend/projects/webapp/src/app/feature/complete-profile/complete-profile-page.ts:L149Chaine "Devise" hardcodee (confiance: 100%)

La cle settings.currencyLabel existe deja dans fr.json. Violation de la regle Transloco du projet : ne jamais hardcoder de chaines francaises dans les templates.

Points positifs

  • Architecture du CurrencyModule propre : controller, service, module, createInfoLoggerProvider, AuthGuard tous en place.
  • Cache in-memory TTL 24h backend / 5min frontend pour Frankfurter : bonne strategie pour eviter les appels repetes.
  • Migration 20260310120000_rekey_rpc_add_original_amount_columns.sql couvre original_amount dans la RPC de re-keying — pas de perte de donnees apres changement de PIN. Bonne anticipation.
  • Chiffrement de original_amount correctement applique dans les paths create/update de budget-line, transaction et template-line (hors bulk).
  • mapCurrencyMetadataToApi valide les devises via safeParse avant de les retourner — donnees corrompues en base ne crashent pas l API.
  • Bonne couverture de tests sur CurrencyService (5 cas) et CurrencyConversionBadge (6 cas).

Verdict : CHANGES REQUESTED

Comment on lines +26 to +39
return `Comptabilisé ${roundedConsumed} sur ${roundedEnvelope} ${currency} (enveloppe)`;
}
return messages.withinEnvelope(roundedEnvelope);
return `Comptabilisé ${roundedEnvelope} ${currency} (enveloppe)`;
}

export function computeTransactionSnackbarMessage(
transactionId: string,
transactions: Transaction[],
messages: TransactionSnackbarMessages,
currency: SupportedCurrency,
): string | null {
const transaction = transactions.find((tx) => tx.id === transactionId);
if (!transaction || transaction.checkedAt == null) return null;

return messages.checked(Math.round(Math.abs(transaction.amount)));
return `Comptabilisé ${Math.round(Math.abs(transaction.amount))} ${currency}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT] Régression i18n — messages snackbar hardcodés en français (confiance: 95%)

La règle projet (.claude/rules/03-frameworks-and-libraries/transloco-i18n.md) interdit explicitement les strings français hardcodées dans les fichiers TypeScript : "NEVER hardcode French strings in templates or TS files — always use transloco keys".

L'ancienne architecture était correcte : budget-details-page.ts injectait TranslocoService et passait des factories de messages. Le refactor retire cette intégration et remplace les keys transloco par des strings littérales, rendant les messages non-traduisibles.

Les clés fr.json budget.snackbar.envelopeOver, budget.snackbar.envelopeWithin et budget.snackbar.transactionChecked sont désormais code mort.

Fix suggéré — réintroduire le service transloco ou conserver le pattern factory :

// Dans budget-details-page.ts, au lieu de passer juste `currency`:
computeEnvelopeSnackbarMessage(
  budgetLineId,
  details.budgetLines,
  details.transactions,
  {
    overEnvelope: (consumed, envelope) =>
      this.#transloco.translate('budget.snackbar.envelopeOver', { consumed, envelope }),
    withinEnvelope: (envelope) =>
      this.#transloco.translate('budget.snackbar.envelopeWithin', { envelope }),
  },
);

import { CURRENCY_CONFIG, DEFAULT_DIGITS_INFO } from './currency-config';

@Pipe({
name: 'appCurrency',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] pure: false inutile pour ce pipe déterministe (confiance: 90%)

Le pipe ne dépend que de ses arguments (value, currency, digitsInfo) et de CURRENCY_CONFIG (constante statique). Il n'a pas d'état interne mutable ni de side effects. pure: false force Angular à réévaluer le pipe à chaque cycle de change detection, y compris quand les inputs n'ont pas changé.

Avec l'utilisation intensive dans les cartes du budget grid, cela génère de nombreux appels inutiles de transform().

Suggested change
name: 'appCurrency',
pure: true,

@@ -117,7 +117,7 @@ import { TranslocoPipe } from '@jsverse/transloco';
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] Incohérence : CurrencyPipe natif au lieu de AppCurrencyPipe (confiance: 80%)

Toute la PR migre les composants vers AppCurrencyPipe (budget-grid, budget-table, allocated-transactions-dialog, etc.), mais template-list-item.ts conserve le pipe Angular natif avec des inputs manuels currency() et locale().

AppCurrencyPipe encapsule exactement ce pattern (locale issu de CURRENCY_CONFIG), ce qui évite l'erreur de passer une locale incohérente avec la devise. En l'état, rien n'empêche un futur appelant de passer currency='EUR' et locale='de-CH', produisant un formatage incorrect.

Remplacer par | appCurrency: currency() pour cohérence avec le reste de la codebase.

@claude
Copy link
Contributor

claude bot commented Mar 13, 2026

Review PR 367 - Resume: 0 bloquant, 1 important, 2 suggestions

IMPORTANT - budget-details-snackbar.utils.ts L26-39 - Regression i18n: messages snackbar hardcodes en francais (confiance 95%)

Les regles projet interdisent les strings francais hardcodes dans les fichiers TypeScript. L ancienne architecture passait des factories transloco depuis budget-details-page.ts. Le refactor bypass transloco et hardcode les messages. Les cles fr.json budget.snackbar.envelopeOver, budget.snackbar.envelopeWithin, budget.snackbar.transactionChecked sont desormais code mort.

SUGGESTION - app-currency.pipe.ts L9 - pure: false inutile (confiance 90%)

Pipe deterministe, pure: false force une reevaluation a chaque cycle de change detection. Changer en pure: true.

SUGGESTION - template-list-item.ts L117 - Utilise encore CurrencyPipe natif au lieu d AppCurrencyPipe (confiance 80%)

Seul composant de la PR a ne pas avoir migre vers AppCurrencyPipe. Incoherence et risque de passer une locale incompatible avec la devise.


Points positifs :

  • Architecture backend solide : CurrencyModule isole, overrideExchangeRate cote serveur anti-manipulation client
  • Chiffrement original_amount complet : rekey RPC etendu, ENCRYPTION.md mis a jour
  • Bonne couverture de tests : CurrencyService, CurrencyConverterService, AppCurrencyPipe
  • Suppression des as unknown as dans budget.service.ts via decryptAmountFields generique

Verdict : CHANGES REQUESTED

neogenz added 3 commits March 18, 2026 08:36
…splay

- Backend: New currency module with exchange rate service and metadata mapper
- DB: Add originalAmount, originalCurrency, targetCurrency, exchangeRate columns to budget_lines and transactions
- Frontend: CurrencyConversionBadge component with tooltip showing conversion details
- iOS: CurrencyConversionBadge with popover, currency settings view, and conversion service
- Integrate badges into detail views and edit sheets across all platforms
- i18n: Add currency conversion tooltip translations
Addresses critical type safety, i18n, and code quality issues across frontend and backend.

**Type Safety (3)**:
- Replace unsafe SupportedCurrency type casts with Zod validation (currency.service.ts)
- Fix missing pipe arguments in templates (transactions-table.ts, search-transactions-dialog.ts)
- Add type annotation for null literals in tests (currency.service.test.ts)
- Remove invalid isChecked tests (web form doesn't have this control; iOS-only feature)

**i18n Localization (15)**:
- Centralize 40+ hardcoded French strings via transloco keys (fr.json)
- Update template, budget, currentMonth sections with 30+ new keys
- Add TranslocoPipe imports and provideTranslocoForTest() to specs

**Code Quality (3)**:
- Add error handling for currency conversion loop
- Remove unnecessary JSDoc comments and static computed() wrappers
- Extract magic number 1000 to AXIS_ABBREVIATION_THRESHOLD constant

All tests pass; pnpm quality passes with 0 type errors.
- Set appCurrency pipe to pure: false (reads external signal)
- Propagate conversion metadata in edit-transactions-dialog
- Encrypt original_amount in bulk insert path
- Use safeParse + BusinessException in CurrencyService
- Replace hardcoded "Devise" with Transloco key
neogenz added 3 commits March 18, 2026 08:38
…n transaction forms

- Add module-level Map caches for Intl.NumberFormat in budget-action-menu.ts and dashboard-next-month.ts to prevent re-allocation in computed()
- Restore isChecked form control, mat-slide-toggle, and checkedAt mapping in both allocated transaction creation forms (dialog + bottom sheet)
- Add missing i18n keys: budget.forecastCheckedToggle and transactionForm.checkedToggle
- Add checkedAt toggle test coverage to both allocated transaction spec files
- All 1689 tests passing
> {
if (!lines.length || !lines.some((l) => l.amount))
return lines.map((l) => ({ ...l, amount: 0 }));
return lines.map((l) => ({ ...l, amount: 0, original_amount: null }));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT] Perte silencieuse de original_amount dans le chemin de retour anticipé (confidence: 90%)

La condition !lines.some((l) => l.amount) évalue uniquement la présence du champ amount. Si toutes les lignes ont un amount null (ex. erreur de chiffrement en amont, ou ligne créée sans montant), le retour anticipé force original_amount: null pour toutes les lignes, effaçant les données de conversion multi-devises potentiellement stockées.

La correction devrait également tester original_amount pour décider si le déchiffrement est nécessaire :

Suggested change
return lines.map((l) => ({ ...l, amount: 0, original_amount: null }));
if (!lines.length || (!lines.some((l) => l.amount) && !lines.some((l) => l.original_amount)))
return lines.map((l) => ({ ...l, amount: 0, original_amount: null }));


if (!baseResult.success || !targetResult.success) {
throw new BusinessException(
ERROR_DEFINITIONS.CURRENCY_RATE_FETCH_FAILED,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT] Code d'erreur HTTP sémantiquement incorrect pour une erreur de validation (confidence: 85%)

Quand originalCurrency ou targetCurrency échoue au safeParse de Zod, l'exception levée utilise CURRENCY_RATE_FETCH_FAILED (HTTP 503 Service Unavailable). Or il s'agit ici d'un problème de validation des données en entrée, pas d'une indisponibilité du service externe — le 503 est donc trompeur pour le client.

En pratique, les DTOs du contrôleur (via createZodDto(budgetLineCreateSchema)) rejettent les devises invalides avant d'atteindre ce code, donc ce chemin n'est jamais déclenché aujourd'hui. Mais si un appel direct au service contournait la validation (test, futur refactoring), le client recevrait un 503 pour ce qui est une erreur 400/422. Il faudrait un error code dédié VALIDATION_FAILED ou CURRENCY_INVALID avec HttpStatus.BAD_REQUEST.

const encryptedOriginalAmounts = await Promise.all(
overriddenCreates.map(async (line) => {
if (line.originalAmount == null) return undefined;
const prepared = await this.encryptionService.prepareAmountData(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] Inefficacité : N appels prepareAmountData au lieu de encryptOptionalAmount (confidence: 85%)

Dans le chemin d'insertion bulk, les original_amount sont chiffrés en appelant prepareAmountData individuellement pour chaque ligne (ce qui déclenche potentiellement N récupérations du DEK). La méthode encryptOptionalAmount existe précisément pour ce cas et gère la valeur null/undefined en une seule invocation. Le remplacement simplifie le code et s'aligne avec l'approche utilisée dans createTemplateLine :

Suggested change
const prepared = await this.encryptionService.prepareAmountData(
return this.encryptionService.encryptOptionalAmount(
line.originalAmount,
user.id,
user.clientKey,
);

@claude
Copy link
Contributor

claude bot commented Mar 18, 2026

Review PR #367

Résumé : 0 bloquant — 2 important — 1 suggestion


Bloquant

Aucun.


Important

  • backend-nest/src/modules/budget-template/budget-template.service.ts:L95 — Perte silencieuse de original_amount dans le retour anticipé de #decryptTemplateLines. La condition !lines.some((l) => l.amount) n'évalue que amount : si toutes les lignes ont un amount null mais un original_amount chiffré, le retour anticipé force original_amount: null pour toutes les lignes, effaçant les données de conversion. (confidence: 90%)

  • backend-nest/src/modules/currency/currency.service.ts:L75 — Mauvais code HTTP pour une erreur de validation dans overrideExchangeRate. Une devise invalide au safeParse lève une BusinessException avec CURRENCY_RATE_FETCH_FAILED (503 Service Unavailable), alors qu'il s'agit d'une erreur de validation d'entrée (400/422). En pratique, la validation Zod du DTO contrôleur empêche ce chemin d'être atteint, mais la sémantique est incorrecte et peut induire en erreur lors de tests ou refactoring futurs. (confidence: 85%)


Suggestions

  • backend-nest/src/modules/budget-template/budget-template.service.ts:L1772 — Dans le chemin d'insertion bulk, les original_amount sont chiffrés en appelant prepareAmountData individuellement pour chaque ligne. La méthode encryptOptionalAmount existe pour ce cas précis, est déjà utilisée dans createTemplateLine, et évite la duplication de logique. (confidence: 85%)

Points positifs

  • Architecture multi-devises cohérente et bien propagée sur les trois couches (backend, frontend Angular, iOS SwiftUI) en une seule PR.
  • Le chiffrement de original_amount est correctement intégré dans le flux de re-key (rekey_rpc) — donnée critique qui aurait pu être oubliée et causer une perte de données lors d'un changement de PIN.
  • La refactorisation de #decryptRowWithDEK en méthode générique dans BudgetLineService élimine efficacement la duplication de code de déchiffrement, et le pattern est cohérent avec decryptAmountFields dans BudgetService.
  • CurrencyService côté backend : cache in-memory avec TTL 24h, timeout sur fetch (5s via AbortSignal), erreurs bien enveloppées en BusinessException — robustesse correcte pour un service externe.
  • La méthode overrideExchangeRate surcharge systématiquement le taux fourni par le client avec le taux serveur, ce qui empêche toute manipulation du taux de change côté client — bonne pratique de sécurité.
  • Tests unitaires présents pour CurrencyService, CurrencyConverterService et AppCurrencyPipe.

Verdict : CHANGES REQUESTED

CHF was formatted with de-CH locale (prefix: "CHF 400") but tests
and product intent require fr-CH (suffix: "400 CHF"). Changed locale
in both iOS Formatters and frontend currency-config. Updated unit
test assertions for narrow non-breaking space grouping separator.

if (roundedConsumed > roundedEnvelope) {
return messages.overEnvelope(roundedConsumed, roundedEnvelope);
return `Comptabilisé ${roundedConsumed} sur ${roundedEnvelope} ${currency} (enveloppe)`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOQUANT] Chaînes françaises hardcodées en violation de la règle transloco (confiance: 95%)

Cette fonction utilitaire retourne maintenant des template literals avec du texte français hardcodé, alors que :

  1. La règle du projet (transloco-i18n.md) interdit formellement de hardcoder des chaînes françaises
  2. Les clés transloco correspondantes existent déjà dans fr.json : budget.snackbar.envelopeOver, budget.snackbar.envelopeWithin, budget.snackbar.transactionChecked
  3. L'ancien design (callbacks messages) permettait d'utiliser transloco depuis le composant appelant — c'était la bonne approche

Impact : toute modification future des libellés devra chasser des chaînes hardcodées dans le code TS, et l'internationalisation est impossible.

Correction suggérée : réintroduire les callbacks de message ou passer un TranslocoService en paramètre, en s'appuyant sur les clés existantes dans fr.json.

throw new BusinessException(
ERROR_DEFINITIONS.CURRENCY_RATE_FETCH_FAILED,
{ base: dto.originalCurrency, target: dto.targetCurrency },
{ operation: 'overrideExchangeRate' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT] Code d'erreur HTTP incorrect pour une currency non supportée (confiance: 88%)

Quand originalCurrency ou targetCurrency n'est pas dans supportedCurrencySchema, le code lève CURRENCY_RATE_FETCH_FAILED qui retourne un 503 SERVICE_UNAVAILABLE. Or il s'agit d'une erreur de validation client (mauvaise devise), pas d'un échec de service externe.

De plus, ces valeurs proviennent des DTOs BudgetLineCreate / TransactionCreate qui valident déjà les currencies via supportedCurrencySchema.optional(). Ce cas ne peut se produire que si quelqu'un contourne la validation — c'est typiquement un 400/422, pas un 503.

Un client recevant 503 va interpréter ça comme une indisponibilité temporaire et potentiellement retenter, alors qu'un retry ne changera rien.

Correction : utiliser ERROR_DEFINITIONS.VALIDATION_FAILED (400) à la place, ou simplement supprimer cette branche puisque les DTOs garantissent déjà la validité des valeurs.

let response: Response;
try {
response = await fetch(url, { signal: AbortSignal.timeout(5000) });
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] Context de logging incomplet pour le suivi des erreurs (confiance: 85%)

Selon la règle error-handling-backend.md, le paramètre loggingContext doit inclure userId pour permettre le suivi des erreurs par utilisateur. Les 3 instances de BusinessException dans #fetchRate passent { operation: 'getRate' } sans userId — ce qui est cohérent puisque #fetchRate n'a pas accès à l'utilisateur.

Cependant, overrideExchangeRate est appelé avec le contexte du DTO utilisateur — il serait utile de propager un userId optionnel jusqu'à getRate/#fetchRate pour enrichir les logs.

C'est une suggestion mineure pour la traçabilité future, pas bloquant.

const currency = userSettings.currency;
const showCurrencySelector = userSettings.showCurrencySelector;
const inputCurrency = signal<SupportedCurrency>(currency());
const conversionError = signal(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] inputCurrency initialisé une seule fois sans réactivité (confiance: 82%)

inputCurrency est initialisé avec currency() au moment de l'injection, mais ne se met pas à jour si currency() change en session (ex: l'utilisateur change sa devise dans les settings pendant que le formulaire est ouvert). L'utilisation de linkedSignal permettrait de synchroniser automatiquement :

Suggested change
const conversionError = signal(false);
const inputCurrency = linkedSignal(() => currency());

C'est un cas de bord peu probable en pratique (le dialog se ferme en changeant de page), mais le pattern linkedSignal est plus correct sémantiquement pour un signal dérivé réinitialisable.

@claude
Copy link
Contributor

claude bot commented Mar 18, 2026

Review PR 367 - 1 bloquant, 1 important, 2 suggestions. Voir les commentaires inline pour les details. Verdict: CHANGES REQUESTED

@claude
Copy link
Contributor

claude bot commented Mar 18, 2026

Voir les commentaires inline pour le detail de chaque finding. Resume: 1 bloquant (snackbar utils - chaines FR hardcodees), 1 important (currency service - HTTP 503 vs 400), 2 suggestions (logging userId, linkedSignal). Verdict: CHANGES REQUESTED

…ting

- iOS: update DecimalCurrencyFormattingTests to expect CHF prefix (fr_CH)
- E2E: update normalizeSwissNumber to handle narrow no-break space (\u202F)
- E2E: update amount assertions from period to comma decimal separator
- E2E: update toContainText from apostrophe to narrow no-break space

if (roundedConsumed > roundedEnvelope) {
return messages.overEnvelope(roundedConsumed, roundedEnvelope);
return `Comptabilisé ${roundedConsumed} sur ${roundedEnvelope} ${currency} (enveloppe)`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Important] Chaînes françaises codées en dur contournant le système i18n (confiance : 92%)

Les templates de traduction budget.snackbar.envelopeOver, envelopeWithin et transactionChecked existent déjà dans fr.json et acceptent des paramètres. Ce refactoring les a remplacés par des interpolations de chaînes directes en TypeScript, violant la règle du projet : NEVER hardcode French strings (règle Transloco dans .claude/rules/).

Impact : si les clés de traduction sont mises à jour ou que d'autres langues sont ajoutées, ce fichier ne sera pas synchronisé.

Correction : passer une fonction de traduction ou le TranslocoService en paramètre plutôt que le currency brut, pour déléguer la formulation aux clés existantes.


if (!baseResult.success || !targetResult.success) {
throw new BusinessException(
ERROR_DEFINITIONS.CURRENCY_RATE_FETCH_FAILED,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Important] Code d'erreur sémantiquement incorrect pour un échec de validation (confiance : 90%)

Lorsque les devises passées ne sont pas dans supportedCurrencySchema, c'est une erreur de validation client (400 Bad Request), mais CURRENCY_RATE_FETCH_FAILED est défini avec httpStatus: HttpStatus.SERVICE_UNAVAILABLE (503). Le client recevra un 503 pour une requête invalide qu'il contrôle entièrement, ce qui est trompeur.

Correction : utiliser ERROR_DEFINITIONS.VALIDATION_FAILED (400) pour ce cas, et réserver CURRENCY_RATE_FETCH_FAILED (503) uniquement aux vraies erreurs réseau/Frankfurter.

checked_at: createBudgetLineDto.checkedAt ?? null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
original_currency: createBudgetLineDto.originalCurrency ?? null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Duplication de la logique de mapping des champs devise entre le service et le mapper (confiance : 85%)

prepareBudgetLineData() écrit original_currency, target_currency, exchange_rate directement — alors que budget-line.mappers.ts expose déjà mapCurrencyMetadataToDb() à cet effet. Le service duplique ce que le mapper fait, à la différence du code toInsert dans budget-line.mappers.ts qui utilise ...mapCurrencyMetadataToDb(createDto).

Ce n'est pas bloquant (les deux chemins donnent le bon résultat), mais la centralisation dans le mapper existant éviterait de diverger si les champs changent.

@claude
Copy link
Contributor

claude bot commented Mar 18, 2026

Review PR #367

Résumé : 0 bloquant — 2 importants — 1 suggestion

Bloquant

(aucun)

Important

  • frontend/.../budget-details-snackbar.utils.ts:L26 — Chaînes françaises codées en dur contournant Transloco : les templates existants (budget.snackbar.envelopeOver, envelopeWithin, transactionChecked) ont été remplacés par des interpolations directes. Viole la règle du projet « NEVER hardcode French strings ». (confiance : 92%)
  • backend-nest/.../currency.service.ts:L75 — Mauvais code HTTP pour une erreur de validation : une devise non supportée (erreur client 400) retourne SERVICE_UNAVAILABLE (503) car le code réutilise CURRENCY_RATE_FETCH_FAILED. Trompeur pour le client et les outils de monitoring. (confiance : 90%)

Suggestion

  • backend-nest/.../budget-line.service.ts:L181 — Logique de mapping devise dupliquée dans le service alors que mapCurrencyMetadataToDb() existe déjà dans le mapper. (confiance : 85%)

Points positifs

  • Architecture multi-devise bien pensée : le backend override toujours le taux de change côté serveur (via currencyService.overrideExchangeRate), empêchant la manipulation côté client — c'est le bon pattern de sécurité.
  • L'intégration du re-chiffrement (rekey) pour original_amount dans la migration SQL et dans EncryptionService est complète et cohérente avec docs/ENCRYPTION.md.
  • Le CurrencyService backend dispose d'un cache TTL 24h avec gestion d'expiration et timeout 5s sur fetch — robuste.
  • La PR est bien couverte par des tests unitaires (service, pipe, converter, mapper).
  • Le basculement de de-CH vers fr-CH pour le formatage CHF, et la mise à jour des séparateurs Unicode dans les tests E2E (U+202F narrow no-break space), est correctement propagé.

Verdict : CHANGES REQUESTED

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