Client uniqueness pre-save check + seed all Indian states & UTs

Pre-save validator (ClientWritePlatformServiceJpaRepositoryImpl.validateClientUniqueness)
rejects duplicate mobile / email / PAN / Aadhaar (masked) / GSTIN before saveAndFlush
and before the LincPay KYC API call. Avoids burning vendor credits on identity-duplicate
records and gives the UI a clean field-level 403 instead of waiting for the DB constraint.

Splits the new repository finders into "create" and "excluding-self" variants so a
nullable Long parameter never reaches PostgreSQL's parser (same bigint <> varchar trap
that broke the family-member POST earlier).

Also seeds the full set of 28 Indian States + 8 UTs as STATE code_values (migration
0297). Previously only MAHARASHTRA / MADHYA_PRADESH / DELHI existed and the names were
UPPER_SNAKE_CASE; the postal PIN-lookup service returns "Madhya Pradesh" so any state
other than Maharashtra/Delhi silently failed to resolve to m_address.state_province_id.
Renames the 3 existing rows in place (preserves their ids/FKs) and inserts the missing
33 with idempotent preConditions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-20 12:31:54 +05:30
co-authored by Claude Opus 4.7
parent 026e5fb875
commit 8b4351013e
5 changed files with 446 additions and 0 deletions
@@ -35,4 +35,31 @@ public interface ClientKycRepository extends JpaRepository<ClientKyc, Long> {
@Query("SELECT k FROM ClientKyc k WHERE k.kycStatus = :status")
List<ClientKyc> findAllByStatus(@Param("status") Integer status);
// ── Pre-KYC uniqueness probes ───────────────────────────────────────────
// Why: stop a second client from being created with the same PAN / Aadhaar / GSTIN BEFORE the
// external KYC API is called, so we don't burn vendor calls on duplicate-identity records. PAN
// and GSTIN match exact upper-cased; Aadhaar is compared masked-form (only last 4 are stored).
// Two variants per field — one for create (no row to exclude), one for update (exclude self) —
// to avoid PostgreSQL's bigint <> varchar parse error on a null Long parameter.
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k WHERE UPPER(k.panNumber) = UPPER(:panNumber)")
boolean existsByPanNumber(@Param("panNumber") String panNumber);
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k "
+ "WHERE UPPER(k.panNumber) = UPPER(:panNumber) AND k.client.id <> :excludeClientId")
boolean existsByPanNumberExcluding(@Param("panNumber") String panNumber, @Param("excludeClientId") Long excludeClientId);
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k WHERE k.aadhaarNumberMasked = :aadhaarMasked")
boolean existsByAadhaarMasked(@Param("aadhaarMasked") String aadhaarMasked);
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k "
+ "WHERE k.aadhaarNumberMasked = :aadhaarMasked AND k.client.id <> :excludeClientId")
boolean existsByAadhaarMaskedExcluding(@Param("aadhaarMasked") String aadhaarMasked, @Param("excludeClientId") Long excludeClientId);
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k WHERE UPPER(k.gstinNumber) = UPPER(:gstinNumber)")
boolean existsByGstinNumber(@Param("gstinNumber") String gstinNumber);
@Query("SELECT CASE WHEN COUNT(k) > 0 THEN true ELSE false END FROM ClientKyc k "
+ "WHERE UPPER(k.gstinNumber) = UPPER(:gstinNumber) AND k.client.id <> :excludeClientId")
boolean existsByGstinNumberExcluding(@Param("gstinNumber") String gstinNumber, @Param("excludeClientId") Long excludeClientId);
}
@@ -61,4 +61,25 @@ public interface ClientRepository extends JpaRepository<Client, Long>, JpaSpecif
*/
@Query("SELECT COUNT(c) FROM Client c WHERE c.cifNumber LIKE CONCAT(:prefix, '%')")
long countByCifNumberPrefix(@Param("prefix") String prefix);
// ── Pre-save uniqueness probes ───────────────────────────────────────────
// Why: DB has UNIQUE on mobile_no / email_address, but we want to fail fast with a clear
// field-level error BEFORE saveAndFlush — and importantly before the external KYC API call.
// Two variants on each: create-time has no row to exclude; update-time excludes the row being
// edited so it doesn't conflict with itself. Splitting avoids the bigint <> varchar JPQL bug
// PostgreSQL throws when a null Long parameter is parsed as varchar at prepare-time.
@Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM Client c WHERE c.mobileNo = :mobileNo")
boolean existsByMobileNo(@Param("mobileNo") String mobileNo);
@Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM Client c "
+ "WHERE c.mobileNo = :mobileNo AND c.id <> :excludeClientId")
boolean existsByMobileNoExcluding(@Param("mobileNo") String mobileNo, @Param("excludeClientId") Long excludeClientId);
@Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM Client c "
+ "WHERE LOWER(c.emailAddress) = LOWER(:emailAddress)")
boolean existsByEmailAddress(@Param("emailAddress") String emailAddress);
@Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM Client c "
+ "WHERE LOWER(c.emailAddress) = LOWER(:emailAddress) AND c.id <> :excludeClientId")
boolean existsByEmailAddressExcluding(@Param("emailAddress") String emailAddress, @Param("excludeClientId") Long excludeClientId);
}