DSC-2FA login fix + Individual-only link pickers + Title Case names

- DSC-as-2FA login: UI's claimAndValidate() now calls a new
  AuthenticationService.applyTwoFactorToken(token, liveSeconds) instead of
  validateOTP(). The /v1/login/dsc/{rn}/claim response is already a minted
  TFAccessToken — POSTing it to /twofactor/validate fails because that path
  expects an in-memory OTPRequest. applyTwoFactorToken runs only the
  post-success bookkeeping (interceptor.setTwoFactorAccessToken, setCredentials,
  Authentication Success alert that drives router.navigate(['/']), and
  sessionStorage persist of {token, validFrom, validTo}). Fixes the
  OTPTokenInvalidException users hit after a successful DSC verify+claim.

- Individual-only filter for link-client pickers: ClientTextSearch gains a
  legalFormType field; SearchingClientRepository accepts an Integer legalForm
  value and adds cb.equal(r.get("legalForm"), ...) to the spec.
  ClientSearchService.parseLegalForm tolerates INDIVIDUAL/PERSON/1 and
  NON_INDIVIDUAL/ENTITY/2; unknown = no filter so a typo doesn't blank results.
  UI: clients.service.ts searchByText gains a 7th optional arg; six link
  pickers (guardian, nominee, family-member across create flow and post-create
  tabs) now pass 'INDIVIDUAL'. General client list and signatory picker are
  intentionally unchanged.

- Title-case client names on persistence: ClientWritePlatformServiceJpa
  RepositoryImpl now runs first/middle/last/full name fields through
  toTitleCase() on both create and update paths. "aditya kumar vyas",
  "ADITYA KUMAR VYAS", "Aditya Kumar Vyas" all persist as "Aditya Kumar Vyas".
  Whitespace-split title case; preserves hyphens/apostrophes inside words
  without re-capitalising after them so clerks verifying against ID documents
  see predictable output.

- Migration 0296: unique constraint to prevent duplicate nominee per account
  type (the underlying repository + service changes also persisted in this
  commit complete the enforcement path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-15 11:50:04 +05:30
co-authored by Claude Opus 4.7
parent e1d7db9b7c
commit 6558ea6998
32 changed files with 89504 additions and 213424 deletions
@@ -43,4 +43,21 @@ public interface ClientNomineeRepository extends JpaRepository<ClientNominee, Lo
@Query("SELECT COUNT(n) FROM ClientNominee n WHERE n.accountNo = :accountNo AND n.accountType = :accountType AND n.isDeleted = false")
long countByAccountNoAndAccountType(@Param("accountNo") String accountNo, @Param("accountType") String accountType);
/**
* Counts non-deleted nominee rows that link {@code nomineeClientId} as a nominee under
* {@code clientId} for the given {@code accountType}. Backs the rule that the same person
* may not be linked as a nominee more than once per (account owner, account type).
* {@code excludeNomineeId} is set on update so a row doesn't conflict with itself.
*/
@Query("SELECT COUNT(n) FROM ClientNominee n "
+ "WHERE n.client.id = :clientId "
+ "AND n.nomineeClient.id = :nomineeClientId "
+ "AND n.accountType = :accountType "
+ "AND n.isDeleted = false "
+ "AND (:excludeNomineeId IS NULL OR n.id <> :excludeNomineeId)")
long countActiveByClientNomineeAndAccountType(@Param("clientId") Long clientId,
@Param("nomineeClientId") Long nomineeClientId,
@Param("accountType") String accountType,
@Param("excludeNomineeId") Long excludeNomineeId);
}
@@ -23,5 +23,10 @@ import org.springframework.data.domain.Pageable;
public interface SearchingClientRepository {
Page<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy, boolean activeOnly);
/**
* @param legalFormValue null = no filter; 1 = Individual only; 2 = Non-Individual only.
* Driven by the legal-form enum on Client (LegalForm.PERSON / .ENTITY).
*/
Page<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy, boolean activeOnly,
Integer legalFormValue);
}
@@ -52,7 +52,8 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
private final CriteriaQueryFactory criteriaQueryFactory;
@Override
public Page<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy, boolean activeOnly) {
public Page<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy, boolean activeOnly,
Integer legalFormValue) {
/*
* this whole thing can be replaced with Spring Data JPA 3+ with a findBy(Specification, Pageable) call but at
* this point the upgrade is too costly
@@ -85,6 +86,13 @@ public class SearchingClientRepositoryImpl implements SearchingClientRepository
predicates.add(cb.equal(r.get("status"), ClientStatus.ACTIVE.getValue()));
}
// Restrict to one legal form when requested. Used by guardian/nominee/family-member
// pickers so non-individual entities (companies, partnerships) never appear as
// selectable parties — they can never legally hold those relationships.
if (legalFormValue != null) {
predicates.add(cb.equal(r.get("legalForm"), legalFormValue));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
criteriaQueryFactory.applySpecificationToCriteria(root, spec, query);