Skip to content

Conversation

@coodos
Copy link
Contributor

@coodos coodos commented Dec 15, 2025

Description of change

Issue Number

Type of change

  • Breaking (any change that would cause existing functionality to not work as expected)
  • New (a change which implements a new feature)
  • Update (a change which updates existing functionality)
  • Fix (a change which fixes an issue)
  • Docs (changes to the documentation)
  • Chore (refactoring, build scripts or anything else that isn't user-facing)

How the change has been tested

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for managing currency accounts within group contexts with appropriate admin authorization.
    • Introduced numeric input field for configuring maximum negative balance limits with enhanced validation.
  • Improvements

    • Strengthened validation for negative balance constraints during currency creation and updates to catch invalid values earlier.
    • Enhanced error messaging for invalid balance configurations.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 15, 2025

Walkthrough

Backend validation layers for maxNegativeBalance are strengthened across CurrencyController and CurrencyService with normalization and range checks. LedgerController integrates GroupService to support group-scoped account initialization with admin authorization. Frontend modals are updated to accept and propagate account context (user or group), adjust balances queries and mutations accordingly, and provide UI input for maxNegativeBalance configuration.

Changes

Cohort / File(s) Summary
Backend Validation & Service Layer
platforms/eCurrency-api/src/controllers/CurrencyController.ts, platforms/eCurrency-api/src/services/CurrencyService.ts
Adds strict normalization and validation for maxNegativeBalance across create and update flows: enforces non-positive values, caps lower bound at -1,000,000,000, errors on NaN, and clears value when allowNegative is false. Validation runs in controller before service call and again in service before persistence.
Group Context & Authorization
platforms/eCurrency-api/src/controllers/LedgerController.ts
Integrates GroupService; extends account initialization to accept optional accountId and accountType from request body, derive finalAccountType/finalAccountId, validate accountType, and enforce group admin authorization via groupService before proceeding.
Frontend Group Context Integration
platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx, platforms/eCurrency/client/src/pages/dashboard.tsx
Adds optional accountContext prop (type: "user" | "group") to AddCurrencyAccountModal; builds balances query key and initialize mutation payload dynamically based on account context, and passes accountType/accountId parameters in group contexts. Dashboard passes accountContext when invoking modal.
Frontend maxNegativeBalance Input & UI
platforms/eCurrency/client/src/components/currency/create-currency-modal.tsx
Extends create currency form to include maxNegativeBalance state and validation: parses numeric input, enforces non-negative parsing (error if negative value entered), caps at MAX_NEGATIVE_LIMIT (1,000,000,000), derives negative magnitude for payload, and resets input when allowNegative toggles off.
Frontend Max Negative Balance UX Refactor
platforms/eCurrency/client/src/pages/currency-detail.tsx
Replaces range-input UI with numeric input constrained by MAX_NEGATIVE_LIMIT; adds validation to cap magnitude input, refactors save flow to compute and store as negative value, removes range-based display, updates instructional text, and adds "Clear" button alongside "Save".

Sequence Diagram

sequenceDiagram
    actor Client
    participant LedgerController
    participant GroupService
    participant LedgerService
    
    Client->>LedgerController: POST /initialize-account<br/>{accountType, accountId, ...}
    
    alt accountType is GROUP
        LedgerController->>GroupService: isGroupAdmin(userId, groupId)
        alt Admin Check Passes
            GroupService-->>LedgerController: true
            LedgerController->>LedgerService: initializeAccount(finalAccountId, GROUP)
            LedgerService-->>LedgerController: Success
            LedgerController-->>Client: 200 OK
        else Admin Check Fails
            GroupService-->>LedgerController: false
            LedgerController-->>Client: 403 Forbidden
        end
    else accountType is USER (or default)
        LedgerController->>LedgerService: initializeAccount(userId, USER)
        LedgerService-->>LedgerController: Success
        LedgerController-->>Client: 200 OK
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • LedgerController group authorization logic: Verify GroupService integration, authorization checks, and account context derivation logic for correctness.
  • maxNegativeBalance validation consistency: Ensure validation rules (bounds, non-positive requirement) are applied consistently across CurrencyController, CurrencyService, and frontend forms.
  • Frontend state management: Cross-check add-currency-account-modal's dynamic query key construction and initialize mutation payload building when accountContext is present vs. absent.
  • Currency detail page: Verify numeric input parsing, magnitude-to-negative conversion, and "Clear" vs. "Save" button state transitions.

Possibly related PRs

Suggested reviewers

  • xPathin
  • sosweetham

Poem

🐰 Groups now frolic in ledgers so fair,
With balances bounded with utmost care,
Admins guard gates while negatives stay
Capped at a billion—no overplay!
Hop-hop, the validation's complete. ✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is entirely a blank template with no actual details filled in—no issue number, type of change selected, testing information, or any substantive content provided. Complete all required sections: add issue number, select and specify type of change, describe how changes were tested, and check off relevant items from the change checklist with explanations.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title mentions 'fix' and 'UX improvements' but is vague about what bugs were fixed and which UX improvements were made, making it unclear to reviewers. Replace with a more specific title describing the main changes, such as 'feat: Add group account support and normalize currency validation' or 'feat: Implement group ledger context and max negative balance validation'.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/ecurrency-bugs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
platforms/eCurrency/client/src/components/currency/create-currency-modal.tsx (1)

25-47: Consider adding error handling for mutation failures.

The mutation lacks an onError handler. If the server returns a validation error (e.g., duplicate currency name, invalid group), users won't see feedback. The local error state is available but only used for client-side validation.

   const createMutation = useMutation({
     mutationFn: async (data: {
       name: string;
       description?: string;
       groupId: string;
       allowNegative: boolean;
       maxNegativeBalance: number | null;
     }) => {
       const response = await apiClient.post("/api/currencies", data);
       return response.data;
     },
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ["currencies"] });
       queryClient.invalidateQueries({ queryKey: ["balances"] });
       setName("");
       setDescription("");
       setGroupId("");
       setAllowNegative(false);
       setMaxNegativeInput("");
       setError(null);
       onOpenChange(false);
     },
+    onError: (error: any) => {
+      const message = error?.response?.data?.error || error?.message || "Failed to create currency";
+      setError(message);
+    },
   });
platforms/eCurrency-api/src/controllers/LedgerController.ts (1)

109-113: Address the TODO: Missing group admin authorization for transfers.

The TODO at line 110 indicates a security gap. Since GroupService is now integrated, consider completing this authorization check to prevent unauthorized transfers from group accounts.

             // Verify user has permission to transfer from this account
             if (finalFromAccountType === AccountType.GROUP) {
-                // TODO: Verify user is admin of the group
+                const isAdmin = await this.groupService.isGroupAdmin(finalFromAccountId, req.user.id);
+                if (!isAdmin) {
+                    return res.status(403).json({ error: "Only group admins can transfer from group accounts" });
+                }
             } else if (finalFromAccountId !== req.user.id) {
                 return res.status(403).json({ error: "You can only transfer from your own account" });
             }
platforms/eCurrency-api/src/controllers/CurrencyController.ts (2)

24-43: Clarify allowNegative coercion and possibly skip maxNegativeBalance validation when negatives are disabled

The normalization/validation flow looks solid overall, but two details are worth tightening:

  • const allowNegativeFlag = Boolean(allowNegative); will treat any non-empty string (including "false") or non-zero number as true. If the API is ever called by anything other than your current frontend, this can easily misinterpret intent. It’s often safer to require a real boolean and/or explicitly check allowNegative === true (or a small whitelist of accepted values) instead of generic Boolean(...).
  • When allowNegativeFlag is false, you eventually force normalizedMaxNegative = null, but only after fully parsing and validating the inbound maxNegativeBalance. That means a client sending allowNegative: false with a (say) positive maxNegativeBalance gets a 400, even though the value will be ignored anyway. Consider short‑circuiting: if !allowNegativeFlag, ignore any provided maxNegativeBalance (or at least skip the zero/negative/limit checks) so callers don’t get confusing validation errors for a field that’s effectively disabled.

188-195: Validation mirrors create flow; consider centralizing the limit constant

The added guards here (non‑positive, >= −1_000_000_000) are consistent with the createCurrency validation, which is good for behavior parity. Since -1_000_000_000 is now a business limit used in multiple places (and likely in the service layer too), consider lifting it into a shared constant (e.g., in the service or a config/constants module) to avoid divergence if the limit ever changes.

platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx (2)

35-45: Group-aware balances query behavior is correct; consider optional early return when context is missing

The group-specific parameterization:

  • Uses params.accountType = "group"; params.accountId = accountContext.id; only when isGroupContext && accountContext?.id, falling back to the authenticated user for the default path, which matches the likely backend contract.
  • Keeps params empty for user context, so you don’t accidentally change existing semantics.

If you ever expect the modal to be opened only with a valid context, you could optionally add a guard (e.g., returning an empty list or throwing) when isGroupContext is true but accountContext?.id is missing, so misconfiguration fails loudly instead of silently calling the user‑scoped endpoint.


64-76: Mutation closure pattern is fine; the extra !currencyId guard is redundant

The mutation is correctly switched to a zero‑argument mutationFn and invoked via initializeMutation.mutate() in the submit handler, avoiding accidentally passing the submit event into the mutation.

Given:

  • The submit handler already checks if (currencyId) before calling mutate().
  • The submit button is disabled when !currencyId or there are no available currencies.

the inner guard:

if (!currencyId) return;

is effectively unreachable and can be dropped to simplify the mutation function. The rest of the payload construction, including conditional accountType/accountId for group context and the matching invalidateQueries({ queryKey: balancesQueryKey }), looks solid.

Also applies to: 108-108

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd226ce and 8b22abb.

📒 Files selected for processing (7)
  • platforms/eCurrency-api/src/controllers/CurrencyController.ts (2 hunks)
  • platforms/eCurrency-api/src/controllers/LedgerController.ts (2 hunks)
  • platforms/eCurrency-api/src/services/CurrencyService.ts (2 hunks)
  • platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx (4 hunks)
  • platforms/eCurrency/client/src/components/currency/create-currency-modal.tsx (5 hunks)
  • platforms/eCurrency/client/src/pages/currency-detail.tsx (4 hunks)
  • platforms/eCurrency/client/src/pages/dashboard.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
platforms/eCurrency/client/src/pages/dashboard.tsx (1)
platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx (1)
  • AddCurrencyAccountModal (18-199)
platforms/eCurrency-api/src/controllers/LedgerController.ts (2)
platforms/eCurrency-api/src/services/LedgerService.ts (1)
  • LedgerService (9-334)
platforms/eCurrency-api/src/services/GroupService.ts (1)
  • GroupService (6-237)
platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx (1)
platforms/eCurrency/client/src/lib/apiClient.ts (1)
  • apiClient (5-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (7)
platforms/eCurrency-api/src/services/CurrencyService.ts (2)

37-47: LGTM! Validation logic for maxNegativeBalance in createCurrency is well-structured.

The validation correctly ensures:

  1. allowNegative must be true when setting a max negative balance
  2. Value must be zero or negative
  3. Value respects the lower bound of -1 billion

This aligns with the client-side MAX_NEGATIVE_LIMIT constant and provides proper server-side enforcement.


125-127: LGTM! Lower-bound guard completes the validation chain.

The update path now has parity with the create path for the -1 billion limit.

platforms/eCurrency/client/src/pages/currency-detail.tsx (2)

25-25: LGTM! Client-side validation aligns with server-side constraints.

The MAX_NEGATIVE_LIMIT of 1 billion matches the server-side limit in CurrencyService.ts. The magnitude-to-negative conversion logic is correct.

Also applies to: 211-216


328-361: LGTM! UI improvements for max negative balance configuration.

The numeric input approach is clearer than the previous slider-based UI. The responsive layout and button grouping are well-structured.

platforms/eCurrency/client/src/components/currency/create-currency-modal.tsx (2)

67-95: LGTM! Validation logic is consistent with currency-detail.tsx.

The form submission correctly:

  1. Clears previous errors before validation
  2. Validates magnitude is non-negative and within limits
  3. Converts to negative value for the payload
  4. Resets maxNegativeInput when allowNegative is false

175-192: LGTM! Conditional UI for max negative balance input.

The input is appropriately gated by allowNegative and provides clear guidance with the placeholder and helper text.

platforms/eCurrency-api/src/controllers/LedgerController.ts (1)

298-327: LGTM! Group account initialization with proper authorization.

The logic correctly:

  1. Derives finalAccountType with a sensible default
  2. Requires accountId for GROUP accounts
  3. Verifies admin permissions via groupService.isGroupAdmin

The isGroupAdmin(finalAccountId, req.user.id) call at line 318 correctly passes the group ID as the first parameter and user ID as the second, matching the service signature shown in the relevant code snippets.

Comment on lines +7 to 16
interface AccountContext {
type: "user" | "group";
id: string;
}

interface AddCurrencyAccountModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
accountContext?: AccountContext | null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for other "balances" query key usages
rg -n '["'"'"'"]balances' --type ts --type tsx -C 2

Repository: MetaState-Prototype-Project/prototype

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Search for balancesQueryKey variable
rg -n 'balancesQueryKey' --type ts --type tsx -C 2

Repository: MetaState-Prototype-Project/prototype

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Search for AccountContext type definition/usage
rg -n 'AccountContext' --type ts --type tsx -C 2

Repository: MetaState-Prototype-Project/prototype

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Look for useQuery calls with balances
rg -n 'useQuery.*balances|balances.*useQuery' --type ts --type tsx -C 3

Repository: MetaState-Prototype-Project/prototype

Length of output: 108


🏁 Script executed:

#!/bin/bash
# First, find the specific file mentioned in the review
fd -e tsx -e ts | grep -i "add-currency-account-modal"

Repository: MetaState-Prototype-Project/prototype

Length of output: 162


🏁 Script executed:

#!/bin/bash
# Search for balances without specifying tsx type
rg 'balancesQueryKey' -C 2

Repository: MetaState-Prototype-Project/prototype

Length of output: 1840


🏁 Script executed:

#!/bin/bash
# Search for AccountContext usage
rg 'AccountContext' -C 2

Repository: MetaState-Prototype-Project/prototype

Length of output: 10028


🏁 Script executed:

#!/bin/bash
# Broader search for balances query patterns
rg '"balances"' -C 2

Repository: MetaState-Prototype-Project/prototype

Length of output: 3451


Fix query key structure mismatch and centralize AccountContext type

The query key inconsistency is a real issue. The balancesQueryKey in this modal uses a decomposed tuple structure ["balances", accountContext?.type, accountContext?.id], but other components (dashboard.tsx, transfer-modal.tsx) query balances with ["balances", accountContext] (the entire object as a single parameter). React Query won't match these keys, so:

  • Invalidating with balancesQueryKey here won't invalidate the balances query in the dashboard or transfer modal.
  • The modal's mutation success won't cascade cache updates to other screens showing balance data.

Additionally, when accountContext is undefined, the key becomes ["balances", undefined, undefined], creating yet another cache entry.

Also: The AccountContext type is defined locally and duplicated inline (as { type: "user" | "group"; id: string }) across at least 5 files (dashboard, currency-detail, user-menu-dropdown, account-context-switcher). Extract it to a shared types file to prevent divergence.

🤖 Prompt for AI Agents
In
platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx
around lines 7 to 16, the modal constructs a mismatched React Query key and
duplicates the AccountContext type: change balancesQueryKey usage to match the
rest of the app by using the whole accountContext object as the second tuple
element (e.g. ["balances", accountContext]) instead of decomposing into type/id,
and ensure you normalize undefined to null (use accountContext ?? null) so you
don't create ["balances", undefined] entries; also extract the local
AccountContext interface into a shared types file (e.g.
platforms/eCurrency/client/src/types/account-context.ts), export it, and replace
local inline type definitions across dashboard.tsx, transfer-modal.tsx,
currency-detail, user-menu-dropdown, account-context-switcher and this modal
with an import of the centralized AccountContext type so all components use the
exact same type.

Comment on lines +331 to +335
<AddCurrencyAccountModal
open={addAccountOpen}
onOpenChange={setAddAccountOpen}
accountContext={accountContext}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n 'queryKey.*balances' --type=ts -C2

Repository: MetaState-Prototype-Project/prototype

Length of output: 4044


🏁 Script executed:

cat -n platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx | head -50

Repository: MetaState-Prototype-Project/prototype

Length of output: 2228


🏁 Script executed:

sed -n '50,100p' platforms/eCurrency/client/src/pages/dashboard.tsx

Repository: MetaState-Prototype-Project/prototype

Length of output: 2048


🏁 Script executed:

sed -n '70,80p' platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx

Repository: MetaState-Prototype-Project/prototype

Length of output: 404


Align query key structures between dashboard and AddCurrencyAccountModal to prevent stale data.

The accountContext prop is correctly passed to the modal. However, there is a confirmed cache key mismatch that will prevent proper query invalidation:

  • Dashboard (line 96): queryKey: ["balances", accountContext] where accountContext is an object
  • Modal (line 24): balancesQueryKey = ["balances", accountContext?.type, accountContext?.id] spreads the object

React Query won't recognize these as the same key, so when the modal invalidates balances on success (line 75), the dashboard's query won't be refreshed, leaving stale balance data. Align the query key structure across both components—either use the object approach in the modal or use the spread approach in the dashboard.

🤖 Prompt for AI Agents
In platforms/eCurrency/client/src/pages/dashboard.tsx around lines 331 and
earlier at line ~96, the dashboard uses queryKey: ["balances", accountContext]
while AddCurrencyAccountModal (line ~24 and invalidation at ~75) uses
["balances", accountContext?.type, accountContext?.id]; update the dashboard to
construct the same key shape as the modal (e.g., ["balances",
accountContext?.type, accountContext?.id]) so React Query recognizes the same
cache entry and invalidations from the modal refresh the dashboard; ensure any
places that invalidate or refetch balances use the identical key structure.

@coodos coodos merged commit 41741b5 into main Dec 15, 2025
4 checks passed
@coodos coodos deleted the fix/ecurrency-bugs branch December 15, 2025 10:50
@coderabbitai coderabbitai bot mentioned this pull request Dec 15, 2025
6 tasks
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.

2 participants