diff --git a/src/components/AccountCard/index.tsx b/src/components/AccountCard/index.tsx index b71c66a..09da386 100644 --- a/src/components/AccountCard/index.tsx +++ b/src/components/AccountCard/index.tsx @@ -220,14 +220,14 @@ const AccountCardComponent = ({ {accountHolder ?? account.ifsc} - + {/* {account.mmid ? 'MMID' : 'IFSC'} {account.mmid ?? account.ifsc} - + */} ); diff --git a/src/modules/home/screens/AccountDetailsScreen.tsx b/src/modules/home/screens/AccountDetailsScreen.tsx index 5933be3..3b9aa9e 100644 --- a/src/modules/home/screens/AccountDetailsScreen.tsx +++ b/src/modules/home/screens/AccountDetailsScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useRef } from 'react'; +import React, { useCallback, useMemo, useState, useRef } from 'react'; import { StyleSheet, Text, View, Dimensions, Animated } from 'react-native'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import Ionicons from '@react-native-vector-icons/ionicons'; @@ -13,7 +13,11 @@ import { useTheme, fontFamily } from '../../../theme'; import { FeatureKey } from '../../../constants/featureKeys'; import { useFeature } from '../../../features'; import { formatDate } from '../../../utils/formatters'; -import { accountTypeLabel, supportsBalanceEnquiry } from '../utils/accountRules'; +import { + accountTypeLabel, + isEverydayAccount, + supportsBalanceEnquiry, +} from '../utils/accountRules'; import { useLazyGetAccountBalanceQuery } from '../services/accountsApi'; import { describeError, isRetryableError } from '../../../services/api/cbsErrors'; import type { ApiError } from '../../../types/api'; @@ -50,7 +54,20 @@ export default function AccountDetailsScreen({ route, navigation }: Props) { const theme = useTheme(); const canToggleBalance = useFeature(FeatureKey.HOME_ACCOUNTS_BALANCE_TOGGLE); // Accounts come from the bank on every mount and are never cached. - const { accounts } = useAccounts(); + const { accounts: allAccounts } = useAccounts(); + + /** + * Savings and current accounts only, matching the home screen's carousel. + * + * A fixed or recurring deposit has no place here: every action this screen + * offers — mini statement, e-passbook, re-KYC, balance — is one the bank + * refuses on a deposit, and the "Account information" grid would show an + * MMID that is permanently "Not allocated" because deposits cannot take part + * in IMPS. Their terms live on the deposit screens instead. + * + * Filtered on type alone, so a dormant savings account still appears. + */ + const accounts = useMemo(() => allAccounts.filter(isEverydayAccount), [allAccounts]); const customerName = useAppSelector(selectCustomerName); const profile = useAppSelector((state) => state.user.profile); const [selectedNumber, setSelectedNumber] = useState(route.params?.accountNumber); diff --git a/src/modules/home/utils/accountRules.test.ts b/src/modules/home/utils/accountRules.test.ts index 5aaa93e..1e40630 100644 --- a/src/modules/home/utils/accountRules.test.ts +++ b/src/modules/home/utils/accountRules.test.ts @@ -1,7 +1,9 @@ import { accountTypeLabel, + isEverydayAccount, isOperativeAccount, maskAccountNumber, + supportsBalanceEnquiry, } from './accountRules'; import type { CbsAccount } from '../../../types/cbs'; @@ -82,6 +84,41 @@ describe('isOperativeAccount', () => { ); }); +describe('isEverydayAccount', () => { + it('keeps the two everyday accounts and drops both deposits', () => { + expect(LIVE_ACCOUNTS.filter(isEverydayAccount).map((a) => a.accountType)).toEqual([ + 'CURRENT', + 'SAVINGS', + ]); + }); + + /** + * The difference from `isOperativeAccount`, and the reason both exist. A + * dormant savings account cannot be debited, but it is still the customer's + * money and still belongs on a screen that lists what they hold. + */ + it.each(['DORMANT', 'BLOCKED', 'CLOSED'] as const)('still keeps a %s savings account', (status) => { + const account: CbsAccount = { + accountNumber: '50108103544967', + accountType: 'SAVINGS', + status, + ifsc: 'SIM0000001', + mmid: '1072220', + }; + + expect(isEverydayAccount(account)).toBe(true); + expect(isOperativeAccount(account)).toBe(false); + }); + + // The balance endpoint serves exactly the everyday set — a deposit's balance + // lives on GET /accounts/deposits/detail instead. + it('matches which accounts the balance endpoint will answer for', () => { + for (const account of LIVE_ACCOUNTS) { + expect(supportsBalanceEnquiry(account)).toBe(isEverydayAccount(account)); + } + }); +}); + describe('accountTypeLabel', () => { it('turns the API shape into something readable for customers', () => { expect(accountTypeLabel('FIXED_DEPOSIT')).toBe('Deposit account'); diff --git a/src/modules/home/utils/accountRules.ts b/src/modules/home/utils/accountRules.ts index 06bc19b..ac13eaf 100644 --- a/src/modules/home/utils/accountRules.ts +++ b/src/modules/home/utils/accountRules.ts @@ -23,18 +23,33 @@ export const isOperativeAccount = (account: CbsAccount) => account.status === 'ACTIVE' && OPERATIVE_ACCOUNT_TYPES.includes(account.accountType); /** - * True when `GET /accounts/balance` will answer for this account. + * True when this is an everyday account rather than a deposit. * * Type only, with no status check — unlike `isOperativeAccount` above. A - * dormant savings account cannot be the debit side of a transfer but it still - * has a balance, and refusing to show it would be the app inventing a - * restriction the bank does not have. A deposit is excluded because the - * endpoint genuinely does not serve one: its balance lives on - * `GET /accounts/deposits/detail`. + * dormant savings account cannot be the debit side of a transfer, but it is + * still the customer's money and still belongs anywhere their accounts are + * shown. Excluding it would be the app inventing a restriction the bank does + * not have. + * + * This is the rule for "which accounts does a day-to-day screen show": a + * deposit sitting beside them would offer a row of actions — mini statement, + * MMID, cheque book — that the bank refuses on a deposit. Deposits have their + * own screens, which show their terms instead. */ -export const supportsBalanceEnquiry = (account: CbsAccount) => +export const isEverydayAccount = (account: CbsAccount) => OPERATIVE_ACCOUNT_TYPES.includes(account.accountType); +/** + * True when `GET /accounts/balance` will answer for this account. + * + * The same set as `isEverydayAccount` today, and kept under its own name + * because it states a fact about the endpoint rather than about the account: a + * deposit's balance is not missing, it lives on + * `GET /accounts/deposits/detail`. If the bank ever prices deposits here, this + * moves and `isEverydayAccount` does not. + */ +export const supportsBalanceEnquiry = isEverydayAccount; + /** * Masked form of a full account number, in the shape the rest of the app uses. * diff --git a/src/types/cbs.ts b/src/types/cbs.ts index f258e52..ace79c8 100644 --- a/src/types/cbs.ts +++ b/src/types/cbs.ts @@ -235,8 +235,9 @@ export type CbsAccountStatus = 'ACTIVE' | 'DORMANT' | 'BLOCKED' | 'CLOSED'; /** * An account as `GET /accounts` returns it. * - * Note what is *not* here: there is no balance field, because no endpoint in - * the CBS API returns one. Anything showing a balance is not sourced from here. + * Note what is *not* here: there is no balance field. `GET /accounts/balance` + * answers that separately, as `CbsAccountBalance` below — so anything showing + * a balance is not sourced from this type. * * The account number is unmasked — this is the only endpoint that returns one — * so it must never be written to disk, logged, or rendered in full where a