Merge branch 'feature/account-charges' of https://git.zeptic.in/CBSNexora/JAVA-Backend-CBS-MAIN into feature/account-charges

This commit is contained in:
Jaanu423
2026-05-21 21:00:05 +05:30
6 changed files with 170 additions and 16 deletions
@@ -184,6 +184,14 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
return constitutionId;
}
public CodeValue getConstitution() {
return this.constitution;
}
public LocalDate getDateOfRegistration() {
return this.dateOfRegistration;
}
public Long mainBusinessLineId() {
Long mainBusinessLineId = null;
if (this.mainBusinessLine != null) {
@@ -121,6 +121,11 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
final String rawResponse = post(url, requestBody.toString());
final JsonObject response = parseJson(rawResponse);
if (response == null) {
return KycVerificationResult.failure(
"Aadhaar OTP generation failed — vendor returned non-JSON: " + vendorBodySnippet(rawResponse),
rawResponse);
}
if (isSuccess(response)) {
// LincPay nests reference_id inside "data" object
String refNumber = null;
@@ -177,6 +182,11 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
final String rawResponse = post(url, requestBody.toString());
final JsonObject response = parseJson(rawResponse);
if (response == null) {
return KycVerificationResult.failure(
"Aadhaar OTP verification failed — vendor returned non-JSON: " + vendorBodySnippet(rawResponse),
rawResponse);
}
if (isSuccess(response)) {
LOG.info("LincPay Aadhaar OTP verified successfully for refNumber: {}", refNumber);
return KycVerificationResult.success(refNumber, rawResponse);
@@ -220,6 +230,12 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
LOG.info("LincPay PAN raw response: {}", rawResponse);
final JsonObject response = parseJson(rawResponse);
if (response == null) {
// Vendor returned non-JSON (e.g. "Invalid DOB format. Required format is dd/MM/yyyy")
return KycVerificationResult.failure(
"PAN verification failed — vendor returned non-JSON: " + vendorBodySnippet(rawResponse),
rawResponse);
}
if (isSuccess(response)) {
LOG.info("LincPay PAN verified successfully for PAN: {}", panNumber);
return KycVerificationResult.success("PAN-" + panNumber, rawResponse);
@@ -259,6 +275,11 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
final String rawResponse = post(url, requestBody.toString());
final JsonObject response = parseJson(rawResponse);
if (response == null) {
return KycVerificationResult.failure(
"GSTIN verification failed — vendor returned non-JSON: " + vendorBodySnippet(rawResponse),
rawResponse);
}
if (isSuccess(response)) {
LOG.info("LincPay GSTIN verified successfully for GSTIN: {}", gstinNumber);
return KycVerificationResult.success("GSTN-" + gstinNumber, rawResponse);
@@ -300,6 +321,11 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
LOG.info("LincPay CIN raw response: {}", rawResponse);
final JsonObject response = parseJson(rawResponse);
if (response == null) {
return KycVerificationResult.failure(
"CIN verification failed — vendor returned non-JSON: " + vendorBodySnippet(rawResponse),
rawResponse);
}
if (isSuccess(response)) {
LOG.info("LincPay CIN verified successfully for CIN: {}", cinNumber);
return KycVerificationResult.success("CIN-" + cinNumber, rawResponse);
@@ -341,6 +367,11 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
LOG.info("LincPay DIN raw response: {}", rawResponse);
final JsonObject response = parseJson(rawResponse);
if (response == null) {
return KycVerificationResult.failure(
"DIN verification failed — vendor returned non-JSON: " + vendorBodySnippet(rawResponse),
rawResponse);
}
if (isSuccess(response)) {
LOG.info("LincPay DIN verified successfully for DIN: {}", dinNumber);
return KycVerificationResult.success("DIN-" + dinNumber, rawResponse);
@@ -374,12 +405,42 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
return response.getBody();
}
/**
* Parse LincPay's response defensively. The vendor sometimes returns plain text instead of
* JSON (e.g. "Invalid DOB format. Required format is dd/MM/yyyy" when the input is missing
* a field, or an HTML error page when the upstream service is down). In those cases we
* used to throw MalformedJsonException at column 10 with no clue what the body actually
* contained — now we return null and let the caller surface the raw text.
*/
private JsonObject parseJson(final String raw) {
if (raw == null || raw.isBlank()) {
return new JsonObject();
}
final JsonElement parsed = JsonParser.parseString(raw);
return parsed.isJsonObject() ? parsed.getAsJsonObject() : new JsonObject();
final String trimmed = raw.trim();
final char first = trimmed.charAt(0);
if (first != '{' && first != '[') {
// Not even shaped like JSON — vendor returned plain text or HTML.
LOG.warn("LincPay returned non-JSON body (first 200 chars): {}",
trimmed.substring(0, Math.min(200, trimmed.length())));
return null;
}
try {
final JsonElement parsed = JsonParser.parseString(trimmed);
return parsed.isJsonObject() ? parsed.getAsJsonObject() : new JsonObject();
} catch (final Exception e) {
LOG.warn("LincPay JSON parse failed: {} | raw body: {}", e.getMessage(),
trimmed.substring(0, Math.min(200, trimmed.length())));
return null;
}
}
/** Build a clean failure message that includes the actual vendor text (truncated) for ops debugging. */
private String vendorBodySnippet(final String raw) {
if (raw == null) {
return "(empty body)";
}
final String t = raw.trim();
return t.isEmpty() ? "(empty body)" : t.substring(0, Math.min(200, t.length()));
}
/**
@@ -112,6 +112,25 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
}
}
/**
* Family members are an individual-client concept (mother/father/spouse etc.). Non-individual
* entities — companies, trusts, LLPs, even proprietorships — don't have "family" semantics;
* any relational role they need is handled via nominees + authorized signatories instead.
* Refuse the add/update so we don't end up with stray rows on entity clients.
*/
private void assertCanHaveFamilyMembers(final Client client) {
if (client == null || client.getLegalForm() == null) {
return;
}
if (org.apache.fineract.portfolio.client.domain.LegalForm.fromInt(client.getLegalForm()).isEntity()) {
throw new PlatformApiDataValidationException(java.util.List.of(
ApiParameterError.parameterError(
"error.msg.client.family.member.not.allowed.for.non.individual",
"Family members are not allowed for non-individual clients.",
"legalForm", client.getLegalForm())));
}
}
@Override
public CommandProcessingResult addFamilyMember(final long clientId, final JsonCommand command) {
@@ -139,6 +158,7 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
apiJsonDeserializer.validateForCreate(clientId, command.json());
Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
assertCanHaveFamilyMembers(client);
firstName = command.stringValueOfParameterNamed("firstName");
middleName = command.stringValueOfParameterNamed("middleName");
lastName = command.stringValueOfParameterNamed("lastName");
@@ -203,6 +223,8 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
@Override
public CommandProcessingResult addClientFamilyMember(final Client client, final JsonCommand command) {
assertCanHaveFamilyMembers(client);
Long relationshipId = null;
CodeValue relationship = null;
CodeValue maritalStatus = null;
@@ -394,6 +416,8 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
*/
ClientFamilyMembers clientFamilyMember = clientFamilyRepository.getReferenceById(familyMemberId);
// Reject updates if the parent client is non-individual — same rule as add.
assertCanHaveFamilyMembers(clientFamilyMember.getClient());
// Client
// client=clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
@@ -43,7 +43,10 @@ import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientKyc;
import org.apache.fineract.portfolio.client.domain.ClientKycRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientKycStatus;
import org.apache.fineract.portfolio.client.domain.ClientNonPerson;
import org.apache.fineract.portfolio.client.domain.ClientNonPersonRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.LegalForm;
import org.apache.fineract.portfolio.client.exception.ClientKycAlreadyExistsException;
import org.apache.fineract.portfolio.client.exception.ClientKycInvalidStateTransitionException;
import org.apache.fineract.portfolio.client.kyc.KycVerificationResult;
@@ -64,6 +67,7 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
private final PlatformSecurityContext context;
private final ClientRepositoryWrapper clientRepository;
private final ClientNonPersonRepositoryWrapper clientNonPersonRepository;
private final ClientKycRepositoryWrapper kycRepositoryWrapper;
private final ClientKycCommandFromApiJsonDeserializer deserializer;
private final KycVerificationService kycVerificationService;
@@ -344,21 +348,40 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
"PAN number is not set on this KYC record. Update the KYC record first.");
}
final String dob = client.dateOfBirth() != null
? client.dateOfBirth().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
: null;
// Build name without salutation: firstname + middlename (if not blank) + lastname
final StringBuilder panName = new StringBuilder();
if (client.getFirstname() != null && !client.getFirstname().isBlank()) {
panName.append(client.getFirstname().trim());
// Non-individual clients store their identity in different fields than individuals:
// firstname/middlename/lastname ← individual columns; NULL for entities
// fullname ← the entity name lives here
// dateOfBirth ← only set for individuals
// ClientNonPerson.dateOfRegistration ← the incorporation date for entities
// LincPay's /validate-pan rejects empty/missing DOB with a PLAIN-TEXT "Invalid DOB format"
// response (not JSON) — Gson then throws MalformedJsonException at column 10. Build the
// right (name, dob) pair per legal form so we always send LincPay the data it expects.
final String clientNameForPan;
final String dob;
if (client.getLegalForm() != null && LegalForm.fromInt(client.getLegalForm()).isEntity()) {
clientNameForPan = client.getFullname() != null ? client.getFullname().trim() : "";
final ClientNonPerson np = this.clientNonPersonRepository.findOneByClientId(client.getId());
final java.time.LocalDate regDate = (np != null) ? np.getDateOfRegistration() : null;
dob = regDate != null
? regDate.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
: null;
} else {
dob = client.dateOfBirth() != null
? client.dateOfBirth().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
: null;
// Build name without salutation: firstname + middlename (if not blank) + lastname
final StringBuilder panName = new StringBuilder();
if (client.getFirstname() != null && !client.getFirstname().isBlank()) {
panName.append(client.getFirstname().trim());
}
if (client.getMiddlename() != null && !client.getMiddlename().isBlank()) {
panName.append(" ").append(client.getMiddlename().trim());
}
if (client.getLastname() != null && !client.getLastname().isBlank()) {
panName.append(" ").append(client.getLastname().trim());
}
clientNameForPan = panName.toString().trim();
}
if (client.getMiddlename() != null && !client.getMiddlename().isBlank()) {
panName.append(" ").append(client.getMiddlename().trim());
}
if (client.getLastname() != null && !client.getLastname().isBlank()) {
panName.append(" ").append(client.getLastname().trim());
}
final String clientNameForPan = panName.toString().trim();
LOG.info("verifyPan: calling LincPay with panNumber='{}', name='{}', dob='{}'",
kyc.getPanNumber(), clientNameForPan, dob);
@@ -28,14 +28,19 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientNominee;
import org.apache.fineract.portfolio.client.domain.ClientNomineeRepository;
import org.apache.fineract.portfolio.client.domain.ClientNonPerson;
import org.apache.fineract.portfolio.client.domain.ClientNonPersonRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.LegalForm;
import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
@@ -58,12 +63,42 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
private final CodeValueRepositoryWrapper codeValueRepository;
private final SavingsAccountRepository savingsAccountRepository;
private final LoanRepository loanRepository;
private final ClientNonPersonRepositoryWrapper clientNonPersonRepository;
/**
* Nominees apply to individual clients and to non-individual proprietorships only. Other
* non-individual constitutions (company, LLP, trust, society, HUF, partnership, etc.) don't
* carry a nomination concept — succession is governed by their constitutional documents and
* authorized signatories. Refuse the add/update so we don't end up with stray rows.
*/
private void assertCanHaveNominees(final Client client) {
if (client == null || client.getLegalForm() == null) {
return;
}
if (LegalForm.fromInt(client.getLegalForm()).isPerson()) {
return; // individual — always allowed
}
// Non-individual: allow only if constitution is proprietorship.
final ClientNonPerson nonPerson = this.clientNonPersonRepository.findOneByClientId(client.getId());
final String constitutionLabel = (nonPerson != null && nonPerson.getConstitution() != null)
? nonPerson.getConstitution().getLabel() : null;
final String normalized = constitutionLabel == null ? "" : constitutionLabel.toUpperCase().replace(" ", "");
if (!"PROPRIETORSHIP".equals(normalized) && !"PROPRIETOR".equals(normalized)) {
throw new PlatformApiDataValidationException(java.util.List.of(
ApiParameterError.parameterError(
"error.msg.client.nominee.not.allowed.for.non.individual",
"Nominees are only allowed for individual clients and proprietorship entities. "
+ "Constitution: " + (constitutionLabel != null ? constitutionLabel : "(none)"),
"legalForm", client.getLegalForm())));
}
}
@Override
@Transactional
public CommandProcessingResult addNominee(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
assertCanHaveNominees(client);
// ── Account linkage (MANDATORY) ──
final String accountNo = command.stringValueOfParameterNamed("accountNo");
@@ -142,6 +177,8 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
@Transactional
public CommandProcessingResult updateNominee(final Long clientId, final Long nomineeId, final JsonCommand command) {
this.context.authenticatedUser();
// Same constitution rule as add — refuse updates on entities other than proprietorship.
assertCanHaveNominees(this.clientRepository.findOneWithNotFoundDetection(clientId));
final ClientNominee nominee = nomineeRepository.findByClientIdAndId(clientId, nomineeId)
.orElseThrow(() -> new ClientNotFoundException(nomineeId));
@@ -718,6 +718,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
builder.append("c.nationality as nationality, ");
builder.append("c.is_minor as isMinor, ");
builder.append("c.guardian_client_id as guardianClientId, ");
builder.append("gc.display_name as guardianClientDisplayName, ");
builder.append("c.guardian_relation as guardianRelation, ");
builder.append("c.alternate_mobile_no as alternateMobileNo, ");
builder.append("c.dip_verified as dipVerified, ");