Adds a fourth supervisory level between apex and bank — HO -> RO -> Bank -> Branch -> Enterprise — as 25 offices with a state mapping, plus /nabard/ regional-offices and its bank drill. An RO is Tier.APEX carrying a regionalOfficeId, not a tier of its own, so it inherits mayReadBorrowers() == false rather than restating it. The office is resolved branch -> district -> state -> office and never stored on the bank: six of eight partner banks are state-bound RRBs, so a bank-level column would look right today and break the moment an SFB opens a second-state branch. The two new endpoints scoped correctly from the start while the eight pre-existing apex reads ignored the office claim entirely — the supervisory boundary was decorative, though no borrower leaked. Every read now passes through ApexService.scopeDistricts(tenant), one choke point rather than eight guards. All six offices sum exactly to the national 263 units / 8 banks / 342 alerts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
174 lines
11 KiB
Java
174 lines
11 KiB
Java
package com.lincfusion.udyamlinc.seed;
|
|
|
|
import com.lincfusion.udyamlinc.auth.UdyamUser;
|
|
import com.lincfusion.udyamlinc.auth.UdyamUserRepository;
|
|
import com.lincfusion.udyamlinc.institution.*;
|
|
import com.lincfusion.udyamlinc.master.DistrictRepository;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
|
|
/**
|
|
* NABARD's Regional Offices and the states each supervises.
|
|
*
|
|
* <p>Twenty-five offices covering twenty-six states. Two of them supervise more
|
|
* than one state — Chandigarh takes Punjab and Haryana, Mumbai takes Maharashtra
|
|
* and Goa — which is the whole reason the mapping is a table rather than a column.
|
|
*
|
|
* <p>Only six offices have a portfolio in the pilot: Bhopal, Guwahati, Patna,
|
|
* Mumbai, Bengaluru and Jaipur. The other nineteen are seeded anyway. An empty
|
|
* office is a true statement — NABARD supervises that state and this pilot has no
|
|
* book there yet — whereas a missing office would make the national structure
|
|
* look like it stops at the pilot's edge, and would strand any bank later opened
|
|
* outside those six states.
|
|
*
|
|
* <p>Upserted per code, never guarded by {@code count() > 0}: the guard is why
|
|
* anything added after a database was first populated silently never arrives.
|
|
*/
|
|
@Component
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
public class RegionalOfficeSeeder {
|
|
|
|
private final RegionalOfficeRepository officeRepository;
|
|
private final RegionalOfficeStateRepository officeStateRepository;
|
|
private final InstitutionRepository institutionRepository;
|
|
private final DistrictRepository districtRepository;
|
|
private final UdyamUserRepository userRepository;
|
|
private final PasswordEncoder passwordEncoder;
|
|
|
|
/** code, display name, city, office type, and the states it covers. */
|
|
private record OfficeSpec(String code, String name, String nameHi, String city,
|
|
String officeType, List<String> states) {
|
|
}
|
|
|
|
private static OfficeSpec ro(String code, String name, String nameHi, String city, String... states) {
|
|
return new OfficeSpec(code, name, nameHi, city, "REGIONAL_OFFICE", List.of(states));
|
|
}
|
|
|
|
private static final List<OfficeSpec> OFFICES = List.of(
|
|
// Andhra Pradesh is listed as "Amaravati / Hyderabad" in the source
|
|
// table while Telangana maps to Hyderabad. Split here rather than
|
|
// merged: one office covering both would silently roll two states'
|
|
// books together, and unpicking that later is worse than being wrong
|
|
// about a building.
|
|
ro("AMARAVATI", "Andhra Pradesh Regional Office", "आंध्र प्रदेश क्षेत्रीय कार्यालय", "Amaravati", "Andhra Pradesh"),
|
|
ro("GUWAHATI", "Assam Regional Office", "असम क्षेत्रीय कार्यालय", "Guwahati", "Assam"),
|
|
ro("PATNA", "Bihar Regional Office", "बिहार क्षेत्रीय कार्यालय", "Patna", "Bihar"),
|
|
ro("RAIPUR", "Chhattisgarh Regional Office", "छत्तीसगढ़ क्षेत्रीय कार्यालय", "Raipur", "Chhattisgarh"),
|
|
ro("AHMEDABAD", "Gujarat Regional Office", "गुजरात क्षेत्रीय कार्यालय", "Ahmedabad", "Gujarat"),
|
|
// One office, two states.
|
|
ro("CHANDIGARH", "Punjab & Haryana Regional Office", "पंजाब एवं हरियाणा क्षेत्रीय कार्यालय", "Chandigarh", "Punjab", "Haryana"),
|
|
ro("SHIMLA", "Himachal Pradesh Regional Office", "हिमाचल प्रदेश क्षेत्रीय कार्यालय", "Shimla", "Himachal Pradesh"),
|
|
ro("RANCHI", "Jharkhand Regional Office", "झारखंड क्षेत्रीय कार्यालय", "Ranchi", "Jharkhand"),
|
|
ro("BENGALURU", "Karnataka Regional Office", "कर्नाटक क्षेत्रीय कार्यालय", "Bengaluru", "Karnataka"),
|
|
ro("THIRUVANANTHAPURAM", "Kerala Regional Office", "केरल क्षेत्रीय कार्यालय", "Thiruvananthapuram", "Kerala"),
|
|
ro("BHOPAL", "Madhya Pradesh Regional Office", "मध्य प्रदेश क्षेत्रीय कार्यालय", "Bhopal", "Madhya Pradesh"),
|
|
// Shares a city with Head Office, which is why the UI must show the
|
|
// office name and not just "Mumbai".
|
|
ro("MUMBAI", "Maharashtra Regional Office", "महाराष्ट्र क्षेत्रीय कार्यालय", "Mumbai", "Maharashtra", "Goa"),
|
|
ro("IMPHAL", "Manipur Regional Office", "मणिपुर क्षेत्रीय कार्यालय", "Imphal", "Manipur"),
|
|
ro("SHILLONG", "Meghalaya Regional Office", "मेघालय क्षेत्रीय कार्यालय", "Shillong", "Meghalaya"),
|
|
ro("AIZAWL", "Mizoram Regional Office", "मिज़ोरम क्षेत्रीय कार्यालय", "Aizawl", "Mizoram"),
|
|
ro("KOHIMA", "Nagaland Regional Office", "नागालैंड क्षेत्रीय कार्यालय", "Kohima", "Nagaland"),
|
|
ro("BHUBANESWAR", "Odisha Regional Office", "ओडिशा क्षेत्रीय कार्यालय", "Bhubaneswar", "Odisha"),
|
|
ro("JAIPUR", "Rajasthan Regional Office", "राजस्थान क्षेत्रीय कार्यालय", "Jaipur", "Rajasthan"),
|
|
ro("GANGTOK", "Sikkim Regional Office", "सिक्किम क्षेत्रीय कार्यालय", "Gangtok", "Sikkim"),
|
|
ro("CHENNAI", "Tamil Nadu Regional Office", "तमिलनाडु क्षेत्रीय कार्यालय", "Chennai", "Tamil Nadu"),
|
|
ro("HYDERABAD", "Telangana Regional Office", "तेलंगाना क्षेत्रीय कार्यालय", "Hyderabad", "Telangana"),
|
|
ro("AGARTALA", "Tripura Regional Office", "त्रिपुरा क्षेत्रीय कार्यालय", "Agartala", "Tripura"),
|
|
// A Local Office in NABARD's own structure, not a Regional Office.
|
|
new OfficeSpec("LUCKNOW", "Uttar Pradesh Local Office", "उत्तर प्रदेश स्थानीय कार्यालय",
|
|
"Lucknow", "LOCAL_OFFICE", List.of("Uttar Pradesh")),
|
|
ro("DEHRADUN", "Uttarakhand Regional Office", "उत्तराखंड क्षेत्रीय कार्यालय", "Dehradun", "Uttarakhand"),
|
|
ro("KOLKATA", "West Bengal Regional Office", "पश्चिम बंगाल क्षेत्रीय कार्यालय", "Kolkata", "West Bengal"));
|
|
|
|
/**
|
|
* One RO login per office that actually supervises a portfolio.
|
|
*
|
|
* <p>Only the six with a book, because a login to an office with nothing in
|
|
* it demonstrates nothing and adds five empty consoles to the demo. The other
|
|
* nineteen offices still exist as data — they just have nobody to sign in as
|
|
* until a bank opens there.
|
|
*/
|
|
private void seedOfficeUsers() {
|
|
int written = 0;
|
|
for (RegionalOffice office : officeRepository.findByActiveTrueOrderByNameAsc()) {
|
|
// Skip offices with no supervised branches — resolved through the
|
|
// state mapping, the same path the tenancy filter uses.
|
|
boolean hasPortfolio = officeStateRepository.findByRegionalOfficeId(office.getId()).stream()
|
|
.anyMatch(s -> districtRepository.findByActiveTrue().stream()
|
|
.anyMatch(d -> d.getState().equals(s.getState())));
|
|
if (!hasPortfolio) continue;
|
|
|
|
String username = office.getCity().toLowerCase().replace(" ", "") + ".ro";
|
|
if (userRepository.findByUsernameIgnoreCase(username).isPresent()) continue;
|
|
|
|
userRepository.save(UdyamUser.builder()
|
|
.username(username)
|
|
.fullName(office.getName())
|
|
.password(passwordEncoder.encode(UdyamDataSeeder.DEFAULT_OFFICER_PASSWORD))
|
|
.role("RO_ADMIN")
|
|
// NABARD scope, not a scope of its own — the office is a
|
|
// narrowing of the apex tier, and the no-borrower rule comes
|
|
// with it rather than being granted again.
|
|
.scope("NABARD")
|
|
.institutionId(apexInstitutionId())
|
|
.regionalOfficeId(office.getId())
|
|
.branch(office.getCity())
|
|
.active(true)
|
|
.build());
|
|
written++;
|
|
}
|
|
if (written > 0) log.info("Seeded {} regional-office logins (password {})",
|
|
written, UdyamDataSeeder.DEFAULT_OFFICER_PASSWORD);
|
|
}
|
|
|
|
private Long apexInstitutionId() {
|
|
return institutionRepository.findByCode("NABARD").map(Institution::getId).orElse(null);
|
|
}
|
|
|
|
public void seed() {
|
|
int offices = 0, mappings = 0;
|
|
|
|
for (OfficeSpec spec : OFFICES) {
|
|
Optional<RegionalOffice> existing = officeRepository.findByCode(spec.code());
|
|
RegionalOffice office = existing.orElse(null);
|
|
if (office == null) {
|
|
office = officeRepository.save(RegionalOffice.builder()
|
|
.code(spec.code()).name(spec.name()).nameHi(spec.nameHi())
|
|
.city(spec.city()).officeType(spec.officeType()).active(true)
|
|
.build());
|
|
offices++;
|
|
}
|
|
|
|
for (String state : spec.states()) {
|
|
Optional<RegionalOfficeState> mapped = officeStateRepository.findByState(state);
|
|
if (mapped.isPresent()) {
|
|
// A state already claimed by a different office is a genuine
|
|
// conflict, not something to silently reassign — the unique
|
|
// index on state exists to make it impossible, and this says
|
|
// so out loud if the plan ever contradicts itself.
|
|
if (!mapped.get().getRegionalOfficeId().equals(office.getId())) {
|
|
log.warn("State {} is already mapped to office id {} — not reassigning to {}",
|
|
state, mapped.get().getRegionalOfficeId(), spec.code());
|
|
}
|
|
continue;
|
|
}
|
|
officeStateRepository.save(RegionalOfficeState.builder()
|
|
.regionalOfficeId(office.getId()).state(state).build());
|
|
mappings++;
|
|
}
|
|
}
|
|
|
|
if (offices > 0) log.info("Seeded {} NABARD regional offices", offices);
|
|
if (mappings > 0) log.info("Seeded {} office-state mappings", mappings);
|
|
|
|
seedOfficeUsers();
|
|
}
|
|
}
|