Hide fixed and recurring deposits on the account details screen
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 showed an MMID that is permanently "Not allocated" because deposits cannot take part in IMPS. The home screen already filters its carousel the same way; this brings the screen it links to into line. Filtered on type alone, not through `isOperativeAccount`, so a dormant savings account still appears — it cannot be debited but it is still the customer's money. Adds `isEverydayAccount` to carry that rule, with `supportsBalanceEnquiry` now defined in terms of it: the same set today, kept under its own name because it states a fact about the endpoint rather than about the account. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
86f9647437
commit
4eeababec8
@@ -220,14 +220,14 @@ const AccountCardComponent = ({
|
||||
{accountHolder ?? account.ifsc}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.footerItem, styles.footerItemEnd]}>
|
||||
{/* <View style={[styles.footerItem, styles.footerItemEnd]}>
|
||||
<Text style={[styles.footerLabel, { color: theme.colors.onBrandMuted }]}>
|
||||
{account.mmid ? 'MMID' : 'IFSC'}
|
||||
</Text>
|
||||
<Text style={[styles.footerValue, { color: theme.colors.onBrandPrimary }]}>
|
||||
{account.mmid ?? account.ifsc}
|
||||
</Text>
|
||||
</View>
|
||||
</View> */}
|
||||
</View>
|
||||
</LinearGradient>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user