Merge branch 'feature/account-charges' of https://git.zeptic.in/CBSNexora/JAVA-Backend-CBS-MAIN into feature/account-charges
This commit is contained in:
+8
@@ -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) {
|
||||
|
||||
+63
-2
@@ -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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+24
@@ -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);
|
||||
|
||||
+37
-14
@@ -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);
|
||||
|
||||
|
||||
+37
@@ -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));
|
||||
|
||||
+1
@@ -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, ");
|
||||
|
||||
Reference in New Issue
Block a user