From 67214ec0f8c9c81d9d8c62d8e2e9c5ece099a3d2 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Tue, 28 Jul 2026 10:50:16 +0530 Subject: [PATCH] fix(clients): stale signature bug, CKYC fixes, monthly turnover/annual income, senior citizen/PWD classification - Add m_client.signature_id (single-slot pointer, mirrors image_id) plus ClientSignatureIdAdapter/EntitySignatureIdAdapter so re-uploading a signature updates the existing m_document row in place instead of leaving duplicate rows behind that readers picked inconsistently. - Fix CkycBatchBuilderServiceImpl address-proof classification to read the client identifier's actual m_code (KYCAddressProofType vs KYCIdProofType) instead of a hardcoded string set that disagreed with the CKYC tab and silently dropped Aadhaar-as-address-proof identifiers. - Organize the CKYC ZIP into per-artefact-type folders (PHOTO/SIGNATURE/ID_PROOF/ADDRESS_PROOF). - Fix CkycBatchBuilderServiceImpl.createBatch() using the daily batch sequence number as the ZIP document's parent_entity_id instead of the real m_ckyc_batch.id, which 404'd on download whenever the two diverged (i.e. almost always). - Add monthlyTurnover and annualIncome fields alongside the existing annualTurnover/monthlyIncome. - Add isSeniorCitizen, isPwd, and pwdCategory (proper m_code_value-backed PwdCategory code, not a free-text column) to m_client, following the same pattern as occupation/religion/marital status. - Add Diploma as its own Education Level (with diploma-specific DegreeQualification values) and Graduate as a level distinct from Undergraduate. Co-Authored-By: Claude Sonnet 5 --- .../adapter/EntitySignatureIdAdapter.java | 56 +++++++++ .../adapter/ClientSignatureIdAdapter.java | 66 +++++++++++ .../client/api/ClientApiConstants.java | 12 +- .../portfolio/client/data/ClientData.java | 21 ++++ .../portfolio/client/domain/Client.java | 27 +++++ .../client/domain/ClientRepository.java | 8 ++ .../DocumentWritePlatformServiceImpl.java | 28 ++++- .../service/CkycBatchBuilderServiceImpl.java | 91 ++++++++------ .../data/ClientApiCollectionConstants.java | 10 +- .../portfolio/client/mapper/ClientMapper.java | 11 ++ .../ClientReadPlatformServiceImpl.java | 38 ++++++ ...ClientTemplateReadPlatformServiceImpl.java | 4 + ...WritePlatformServiceJpaRepositoryImpl.java | 62 ++++++++++ .../db/changelog/tenant/changelog-tenant.xml | 6 + .../parts/0421_add_signature_id_to_client.xml | 73 ++++++++++++ .../0422_add_monthly_turnover_to_client.xml | 32 +++++ .../0423_add_annual_income_to_client.xml | 32 +++++ .../0424_add_senior_citizen_pwd_to_client.xml | 111 ++++++++++++++++++ .../0425_add_diploma_degree_qualification.xml | 83 +++++++++++++ .../0426_add_graduate_education_level.xml | 45 +++++++ 20 files changed, 771 insertions(+), 45 deletions(-) create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/adapter/EntitySignatureIdAdapter.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/client/adapter/ClientSignatureIdAdapter.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0421_add_signature_id_to_client.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0422_add_monthly_turnover_to_client.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0423_add_annual_income_to_client.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0424_add_senior_citizen_pwd_to_client.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0425_add_diploma_degree_qualification.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0426_add_graduate_education_level.xml diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/adapter/EntitySignatureIdAdapter.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/adapter/EntitySignatureIdAdapter.java new file mode 100644 index 000000000..4f6ec710a --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/documentmanagement/adapter/EntitySignatureIdAdapter.java @@ -0,0 +1,56 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.documentmanagement.adapter; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Optional; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Decouples the entity "current signature" pointer from the generic document service, the same + * way {@link EntityImageIdAdapter} decouples the "current photo" pointer — lets + * DocumentWritePlatformServiceImpl (fineract-document module) give single-slot upsert semantics to + * signature uploads without depending on portfolio-module entities directly. + */ +public interface EntitySignatureIdAdapter { + + /** @param documentName the m_document.name of the document being created/removed */ + boolean accept(String entityType, String documentName); + + Optional get(Long entityId); + + Optional set(Long entityId, Long signatureId); + + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class SignatureIdResult implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long id; + private String displayName; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/adapter/ClientSignatureIdAdapter.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/adapter/ClientSignatureIdAdapter.java new file mode 100644 index 000000000..9b7aa0674 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/adapter/ClientSignatureIdAdapter.java @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.adapter; + +import static java.util.Objects.nonNull; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.documentmanagement.adapter.EntitySignatureIdAdapter; +import org.apache.fineract.portfolio.client.domain.ClientRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Component +class ClientSignatureIdAdapter implements EntitySignatureIdAdapter { + + private static final String ENTITY_TYPE = "clients"; + private static final String DOCUMENT_NAME = "clientSignature"; + + private final ClientRepository repository; + + @Override + public boolean accept(String entityType, String documentName) { + return ENTITY_TYPE.equalsIgnoreCase(entityType) && DOCUMENT_NAME.equalsIgnoreCase(documentName); + } + + @Override + @Transactional(readOnly = true) + public Optional get(Long entityId) { + return repository.findById(entityId).filter(client -> nonNull(client.getSignatureId())) + .map(client -> SignatureIdResult.builder().id(client.getSignatureId()).displayName(client.getDisplayName()).build()); + } + + @Override + @Transactional + public Optional set(Long entityId, Long signatureId) { + final var result = get(entityId); + + if (signatureId == null) { + repository.removeSignatureId(entityId); + } else { + repository.updateByIdAndSignatureId(entityId, signatureId); + } + + return result; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java index 6a92d6bab..eabe2eb9a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java @@ -63,6 +63,7 @@ public class ClientApiConstants { public static final String CLIENT_CLASSIFICATION = "ClientClassification"; public static final String OCCUPATION_CATEGORY = "OccupationCategory"; + public static final String PWD_CATEGORY = "PwdCategory"; public static final String OCCUPATION = "Occupation"; public static final String CLIENT_NON_PERSON_CONSTITUTION = "Constitution"; @@ -147,7 +148,13 @@ public class ClientApiConstants { public static final String religionParamName = "religion"; public static final String socialCategoryParamName = "socialCategory"; public static final String monthlyIncomeParamName = "monthlyIncome"; + public static final String annualIncomeParamName = "annualIncome"; public static final String annualTurnoverParamName = "annualTurnover"; + public static final String monthlyTurnoverParamName = "monthlyTurnover"; + public static final String isSeniorCitizenParamName = "isSeniorCitizen"; + public static final String isPwdParamName = "isPwd"; + public static final String pwdCategoryIdParamName = "pwdCategoryId"; + public static final String pwdCategoryParamName = "pwdCategory"; public static final String agricultureIncomeParamName = "agricultureIncome"; public static final String nonAgricultureIncomeParamName = "nonAgricultureIncome"; public static final String ageYearsParamName = "ageYears"; @@ -257,8 +264,9 @@ public class ClientApiConstants { staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName, salutationParamName, occupationCategoryParamName, occupationParamName, maritalStatusParamName, educationLevelParamName, degreeQualificationParamName, - religionParamName, socialCategoryParamName, monthlyIncomeParamName, - annualTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName, + religionParamName, socialCategoryParamName, monthlyIncomeParamName, annualIncomeParamName, + annualTurnoverParamName, monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName, + isSeniorCitizenParamName, isPwdParamName, pwdCategoryParamName, ageYearsParamName, ageMonthsParamName, ageDaysParamName, nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianClientDisplayNameParamName, guardianRelationParamName, dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName, diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java index c6a691408..3f1a05f6d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java @@ -82,7 +82,13 @@ public final class ClientData implements Comparable, Serializable { private CodeValueData religion; private CodeValueData socialCategory; private BigDecimal monthlyIncome; + private BigDecimal annualIncome; private BigDecimal annualTurnover; + private BigDecimal monthlyTurnover; + private Boolean isSeniorCitizen; + private Boolean isPwd; + private CodeValueData pwdCategory; + private Collection pwdCategoryOptions; private BigDecimal agricultureIncome; private BigDecimal nonAgricultureIncome; private Integer ageYears; @@ -406,7 +412,12 @@ public final class ClientData implements Comparable, Serializable { data.religion = clientData.religion; data.socialCategory = clientData.socialCategory; data.monthlyIncome = clientData.monthlyIncome; + data.annualIncome = clientData.annualIncome; data.annualTurnover = clientData.annualTurnover; + data.monthlyTurnover = clientData.monthlyTurnover; + data.isSeniorCitizen = clientData.isSeniorCitizen; + data.isPwd = clientData.isPwd; + data.pwdCategory = clientData.pwdCategory; data.agricultureIncome = clientData.agricultureIncome; data.nonAgricultureIncome = clientData.nonAgricultureIncome; data.ageYears = clientData.ageYears; @@ -462,7 +473,12 @@ public final class ClientData implements Comparable, Serializable { data.religion = clientData.religion; data.socialCategory = clientData.socialCategory; data.monthlyIncome = clientData.monthlyIncome; + data.annualIncome = clientData.annualIncome; data.annualTurnover = clientData.annualTurnover; + data.monthlyTurnover = clientData.monthlyTurnover; + data.isSeniorCitizen = clientData.isSeniorCitizen; + data.isPwd = clientData.isPwd; + data.pwdCategory = clientData.pwdCategory; data.agricultureIncome = clientData.agricultureIncome; data.nonAgricultureIncome = clientData.nonAgricultureIncome; data.ageYears = clientData.ageYears; @@ -509,7 +525,12 @@ public final class ClientData implements Comparable, Serializable { data.religion = clientData.religion; data.socialCategory = clientData.socialCategory; data.monthlyIncome = clientData.monthlyIncome; + data.annualIncome = clientData.annualIncome; data.annualTurnover = clientData.annualTurnover; + data.monthlyTurnover = clientData.monthlyTurnover; + data.isSeniorCitizen = clientData.isSeniorCitizen; + data.isPwd = clientData.isPwd; + data.pwdCategory = clientData.pwdCategory; data.agricultureIncome = clientData.agricultureIncome; data.nonAgricultureIncome = clientData.nonAgricultureIncome; data.ageYears = clientData.ageYears; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java index 8692c6098..a96c6629a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java @@ -77,6 +77,9 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "image_id") private Long imageId; + @Column(name = "signature_id") + private Long signatureId; + @Column(name = "status_enum", nullable = false) private Integer status; @@ -231,9 +234,25 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "monthly_income") private java.math.BigDecimal monthlyIncome; + @Column(name = "annual_income") + private java.math.BigDecimal annualIncome; + @Column(name = "annual_turnover") private java.math.BigDecimal annualTurnover; + @Column(name = "monthly_turnover") + private java.math.BigDecimal monthlyTurnover; + + @Column(name = "is_senior_citizen") + private Boolean isSeniorCitizen; + + @Column(name = "is_pwd") + private Boolean isPwd; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pwd_category_cv_id") + private CodeValue pwdCategory; + @Column(name = "agriculture_income") private java.math.BigDecimal agricultureIncome; @@ -832,6 +851,14 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom { return this.socialCategory != null ? this.socialCategory.getId() : null; } + public Long pwdCategoryId() { + return this.pwdCategory != null ? this.pwdCategory.getId() : null; + } + + public void updatePwdCategory(CodeValue pwdCategory) { + this.pwdCategory = pwdCategory; + } + public void updateSocialCategory(CodeValue socialCategory) { this.socialCategory = socialCategory; } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java index 47b662553..fee2a4bf1 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java @@ -55,6 +55,14 @@ public interface ClientRepository extends JpaRepository, JpaSpecif @Query("UPDATE Client client SET client.imageId = null WHERE client.id = :clientId") void removeImageId(@Param("clientId") Long clientId); + @Modifying + @Query("UPDATE Client client SET client.signatureId = :signatureId WHERE client.id = :clientId") + void updateByIdAndSignatureId(@Param("clientId") Long clientId, @Param("signatureId") Long signatureId); + + @Modifying + @Query("UPDATE Client client SET client.signatureId = null WHERE client.id = :clientId") + void removeSignatureId(@Param("clientId") Long clientId); + /** * Count clients whose cif_number begins with the given prefix. Used by the CIF number generator * (TTYYYYNNNNNN format) to compute the next sequence within a given (type, year) bucket. diff --git a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceImpl.java b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceImpl.java index 576781d6c..8775f4acd 100644 --- a/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceImpl.java +++ b/fineract-document/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceImpl.java @@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.documentmanagement.service; import static java.util.Objects.requireNonNull; import jakarta.validation.Valid; +import java.util.List; import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -31,6 +32,7 @@ import org.apache.commons.lang3.Strings; import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.exception.ErrorHandler; +import org.apache.fineract.infrastructure.documentmanagement.adapter.EntitySignatureIdAdapter; import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest; import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse; import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest; @@ -59,6 +61,7 @@ public class DocumentWritePlatformServiceImpl implements DocumentWritePlatformSe private final ContentStoreService storeService; private final BusinessEventNotifierService businessEventNotifierService; private final FineractProperties properties; + private final List signatureIdAdapters; @Transactional @Override @@ -74,13 +77,30 @@ public class DocumentWritePlatformServiceImpl implements DocumentWritePlatformSe path = storeService.upload(path, request.getStream(), request.getType()); - final var doc = new Document().setParentEntityType(request.getEntityType()).setParentEntityId(request.getEntityId()) - .setName(Optional.ofNullable(request.getName()).orElse(request.getFileName())).setFileName(request.getFileName()) - .setSize(request.getSize()).setType(request.getType()).setDescription(request.getDescription()).setLocation(path) - .setStorageType(storeService.getType().getValue()); + final var docName = Optional.ofNullable(request.getName()).orElse(request.getFileName()); + + // Some document "slots" (e.g. a client's signature) are single-slot, mirroring + // m_client.image_id: re-upload should replace the existing row in place rather than + // leave the old one behind as an orphan whose ordering readers then disagree about. + // Adapters recognize their own (entityType, name) convention and own the pointer. + final var signatureAdapter = signatureIdAdapters.stream() + .filter(adapter -> adapter.accept(request.getEntityType(), docName)).findFirst(); + + final var doc = signatureAdapter.flatMap(adapter -> adapter.get(request.getEntityId())) + .flatMap(result -> documentRepository.findById(result.getId())).map(existing -> { + // replacing an existing single-slot document: drop the old file, reuse the row + storeService.delete(existing.getLocation()); + return existing; + }).orElseGet(Document::new); + + doc.setParentEntityType(request.getEntityType()).setParentEntityId(request.getEntityId()).setName(docName) + .setFileName(request.getFileName()).setSize(request.getSize()).setType(request.getType()) + .setDescription(request.getDescription()).setLocation(path).setStorageType(storeService.getType().getValue()); documentRepository.save(doc); + signatureAdapter.ifPresent(adapter -> adapter.set(request.getEntityId(), doc.getId())); + businessEventNotifierService.notifyPostBusinessEvent(new DocumentCreatedBusinessEvent(documentMapper.map(doc))); return DocumentCreateResponse.builder().resourceId(doc.getId()).resourceIdentifier(request.getEntityType()).build(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycBatchBuilderServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycBatchBuilderServiceImpl.java index f90a9d1a0..274e7aa56 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycBatchBuilderServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/ckyc/service/CkycBatchBuilderServiceImpl.java @@ -32,7 +32,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import lombok.RequiredArgsConstructor; @@ -61,16 +60,22 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService { private static final DateTimeFormatter ZIP_NAME_TS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); /** - * Names of code values (from {@code KYCAddressProofType}) that unambiguously indicate an - * address proof. Aadhaar / Passport / DL / Voter ID can be both ID and address proof — we - * default them to ID (PI) unless the UI tags them explicitly. + * Code (m_code.code_name) under which an identifier's document type must be classified for it + * to count as an address proof. Matches the same m_code_value config that + * KycProofTypesApiResource (GET /kyc/proof-types) reads to populate the CKYC tab's + * "Address Proof Document" card — e.g. Aadhaar Card is seeded as a document type under BOTH + * KYCIdProofType and KYCAddressProofType (two separate m_client_identifier rows), so trusting + * the code a given identifier was actually tagged under is the only way this can agree with + * what the CKYC tab already shows as "available". */ - private static final Set ADDRESS_PROOF_ONLY = Set.of( - "UTILITY BILLS", "UTILITY BILL", "ELECTRICITY BILL", "WATER BILL", "GAS BILL", "INTERNET BILL", - "BANK ACCOUNT STATEMENT / PASSBOOK", "BANK STATEMENT", "POSTPAID MOBILE BILL", - "REGISTERED RENT AGREEMENT", "RENT AGREEMENT", "PROPERTY TAX RECEIPT", - "EMPLOYER LETTER (FOR SALARIED EMPLOYEES)", "EMPLOYER LETTER", - "PENSION PAYMENT ORDERS (FOR RETIREES)", "PENSION PAYMENT ORDER"); + private static final String ADDRESS_PROOF_CODE_NAME = "KYCAddressProofType"; + + // Top-level ZIP folders — one per artefact type, so every client's file of a given type lands + // in the same folder instead of all 4 types being interleaved flat at the ZIP root. + private static final String PHOTO_FOLDER = "PHOTO/"; + private static final String SIGNATURE_FOLDER = "SIGNATURE/"; + private static final String ID_PROOF_FOLDER = "ID_PROOF/"; + private static final String ADDRESS_PROOF_FOLDER = "ADDRESS_PROOF/"; private final JdbcTemplate jdbcTemplate; private final DocumentReadPlatformService documentReadPlatformService; @@ -111,13 +116,30 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService { final byte[] zipBytes = zipBytesOut.toByteArray(); final String checksum = "sha256:" + sha256Hex(zipBytes); - // Persist the ZIP as an m_document row (parent_entity_type='ckyc_batch', parent_entity_id=seq) - // Persistence happens BEFORE we know m_ckyc_batch.id, so we use the synthetic seq as a - // temporary parent_entity_id. The real FK is via m_ckyc_batch.document_id after save. + // Persist the batch registry row FIRST (without documentId) so we have its real, + // DB-generated id before creating the ZIP's m_document row. The document's + // parent_entity_id must be the actual m_ckyc_batch.id — using the daily `seq` here + // (as a stand-in for an id that doesn't exist yet) was wrong: seq resets every day + // while m_ckyc_batch.id is a global auto-increment, so GET + // /ckyc_batch/{id}/documents/{documentId}/attachment (which matches on the REAL batch + // id) 404'd for any batch whose seq didn't coincidentally equal its DB id. + final CkycBatch batch = new CkycBatch(); + batch.setBatchId(batchId); + batch.setFilename(filename); + batch.setChecksumSha256(checksum); + batch.setRecordCount(includedCount); + batch.setSizeBytes((long) zipBytes.length); + batch.setCreatedOnDate(today); + batch.setUploadedToCersai(false); + batch.setManifestJson(new Gson().toJson(manifest)); + + final CkycBatch saved = ckycBatchRepository.saveAndFlush(batch); + + // Now persist the ZIP as an m_document row keyed to the batch's real id. final var docResponse = documentWritePlatformService.createDocument( DocumentCreateRequest.builder() .entityType("ckyc_batch") - .entityId((long) seq) + .entityId(saved.getId()) .name(batchId) .fileName(filename) .description(StringUtils.defaultIfEmpty(remarks, "CERSAI CKYC batch ZIP")) @@ -126,19 +148,8 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService { .stream(new ByteArrayInputStream(zipBytes)) .build()); - // Persist the batch registry row - final CkycBatch batch = new CkycBatch(); - batch.setBatchId(batchId); - batch.setFilename(filename); - batch.setChecksumSha256(checksum); - batch.setRecordCount(includedCount); - batch.setSizeBytes((long) zipBytes.length); - batch.setDocumentId(docResponse.getResourceId() != null ? docResponse.getResourceId().longValue() : null); - batch.setCreatedOnDate(today); - batch.setUploadedToCersai(false); - batch.setManifestJson(new Gson().toJson(manifest)); - - final CkycBatch saved = ckycBatchRepository.saveAndFlush(batch); + saved.setDocumentId(docResponse.getResourceId() != null ? docResponse.getResourceId().longValue() : null); + ckycBatchRepository.saveAndFlush(saved); log.info("CKYC batch created: id={}, batchId={}, filename={}, recordCount={}/{}, bytes={}", saved.getId(), batchId, filename, includedCount, clientIds.size(), zipBytes.length); @@ -184,32 +195,40 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService { entry.setCifNumber(cifNumber); entry.setDisplayName(displayName); - // Photo — packaged as "-PI-PHOTO." using m_client.image_id → m_image + // Photo — packaged as "PHOTO/-PI-PHOTO." using m_client.image_id → m_image. + // Every artefact type gets its own top-level ZIP folder, so a batch of N clients produces + // one PHOTO/ folder containing all N client photos (and likewise for the other 3 types), + // rather than every client's files interleaved flat at the ZIP root. final Map photo = new LinkedHashMap<>(); photo.put("present", imageId != null); entry.setPhoto(photo); if (imageId != null && cifNumber != null) { - packageClientImage(zip, clientId, cifNumber + "-PI-PHOTO", photo); + packageClientImage(zip, clientId, PHOTO_FOLDER + cifNumber + "-PI-PHOTO", photo); } - // Signature — packaged as "-PI-SIGNATURE." + // Signature — packaged as "SIGNATURE/-PI-SIGNATURE." final Long signatureDocId = findSignatureDocId(clientId); final Map signature = new LinkedHashMap<>(); signature.put("present", signatureDocId != null); entry.setSignature(signature); if (signatureDocId != null && cifNumber != null) { packageFile(zip, "clients", clientId, signatureDocId, - cifNumber + "-PI-SIGNATURE", signature); + SIGNATURE_FOLDER + cifNumber + "-PI-SIGNATURE", signature); } - // ID proof + Address proof: iterate m_client_identifier, classify by code value name. + // ID proof + Address proof: iterate m_client_identifier, classify by the m_code the + // identifier's document type actually belongs to (KYCIdProofType vs KYCAddressProofType) — + // the same source of truth KycProofTypesApiResource uses for the CKYC tab, so a document + // the client sees as "Address Proof: Available" is never silently dropped here. // For each identifier, find the attached m_document (one-to-one, latest wins). // NOTE: the `active` column in m_client_identifier stores the status code (200 = active) // not a 1/0 boolean — so we filter on `status = 200` (ClientIdentifierStatus.ACTIVE). final List> identifiers = jdbcTemplate.queryForList( - "select ci.id as identifierId, ci.document_key as docKey, cv.code_value as codeValue " + "select ci.id as identifierId, ci.document_key as docKey, cv.code_value as codeValue, " + + " c.code_name as codeName " + " from m_client_identifier ci " + " join m_code_value cv on cv.id = ci.document_type_id " + + " join m_code c on c.id = cv.code_id " + " where ci.client_id = ? and (ci.status = 200 or ci.status is null)", clientId); @@ -221,8 +240,9 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService { for (final Map idRow : identifiers) { final Long identifierId = ((Number) idRow.get("identifierId")).longValue(); final String codeValue = (String) idRow.get("codeValue"); + final String codeName = (String) idRow.get("codeName"); final String docKey = (String) idRow.get("docKey"); - final boolean isAddress = ADDRESS_PROOF_ONLY.contains(codeValue == null ? "" : codeValue.toUpperCase(Locale.ROOT)); + final boolean isAddress = ADDRESS_PROOF_CODE_NAME.equalsIgnoreCase(codeName); final String role = isAddress ? "PA" : "PI"; final Long attachedDocId = findLatestDocForIdentifier(identifierId); @@ -242,8 +262,9 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService { bucket.put("key", docKey); if (cifNumber != null) { + final String folder = isAddress ? ADDRESS_PROOF_FOLDER : ID_PROOF_FOLDER; packageFile(zip, "client_identifiers", identifierId, attachedDocId, - cifNumber + "-" + role + "-" + shortCode, bucket); + folder + cifNumber + "-" + role + "-" + shortCode, bucket); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java index c68bd0996..28dcd60e0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientApiCollectionConstants.java @@ -32,8 +32,9 @@ public class ClientApiCollectionConstants extends ClientApiConstants { dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName, occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName, educationLevelIdParamName, degreeQualificationIdParamName, - religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualTurnoverParamName, - agricultureIncomeParamName, nonAgricultureIncomeParamName, + religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualIncomeParamName, annualTurnoverParamName, + monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName, + isSeniorCitizenParamName, isPwdParamName, pwdCategoryIdParamName, ageYearsParamName, ageMonthsParamName, ageDaysParamName, nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName, dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName, @@ -55,8 +56,9 @@ public class ClientApiCollectionConstants extends ClientApiConstants { dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName, occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName, educationLevelIdParamName, degreeQualificationIdParamName, - religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualTurnoverParamName, - agricultureIncomeParamName, nonAgricultureIncomeParamName, + religionIdParamName, socialCategoryIdParamName, monthlyIncomeParamName, annualIncomeParamName, annualTurnoverParamName, + monthlyTurnoverParamName, agricultureIncomeParamName, nonAgricultureIncomeParamName, + isSeniorCitizenParamName, isPwdParamName, pwdCategoryIdParamName, ageYearsParamName, ageMonthsParamName, ageDaysParamName, nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName, dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/mapper/ClientMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/mapper/ClientMapper.java index 38aadc990..26104ad1b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/mapper/ClientMapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/mapper/ClientMapper.java @@ -65,12 +65,14 @@ public interface ClientMapper { @Mapping(target = "degreeQualification", source = "source", qualifiedByName = "degreeQualificationCode") @Mapping(target = "religion", source = "source", qualifiedByName = "religionCode") @Mapping(target = "socialCategory", source = "source", qualifiedByName = "socialCategoryCode") + @Mapping(target = "pwdCategory", source = "source", qualifiedByName = "pwdCategoryCode") @Mapping(target = "occupationCategoryOptions", ignore = true) @Mapping(target = "occupationOptions", ignore = true) @Mapping(target = "educationLevelOptions", ignore = true) @Mapping(target = "degreeQualificationOptions", ignore = true) @Mapping(target = "religionOptions", ignore = true) @Mapping(target = "socialCategoryOptions", ignore = true) + @Mapping(target = "pwdCategoryOptions", ignore = true) @Mapping(target = "legalForm", source = "source", qualifiedByName = "clientLegalFormEnum") @Mapping(target = "isStaff", source = "source", qualifiedByName = "clientIsStaff") @Mapping(target = "ageYears", ignore = true) @@ -196,6 +198,15 @@ public interface ClientMapper { return CodeValueData.instance(code.getId(), code.getLabel()); } + @Named("pwdCategoryCode") + default CodeValueData pwdCategoryCode(Client client) { + final CodeValue code = client.getPwdCategory(); + if (code == null) { + return null; + } + return CodeValueData.instance(code.getId(), code.getLabel()); + } + @Named("clientSubStatusCode") default CodeValueData clientSubStatusCode(Client client) { final CodeValue code = client.getSubStatus(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java index d1bd60de7..66a2495f5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java @@ -321,7 +321,13 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService sqlBuilder.append("c.occupation_cv_id as occupationId, "); sqlBuilder.append("cvOccupation.code_value as occupationValue, "); sqlBuilder.append("c.monthly_income as monthlyIncome, "); + sqlBuilder.append("c.annual_income as annualIncome, "); sqlBuilder.append("c.annual_turnover as annualTurnover, "); + sqlBuilder.append("c.monthly_turnover as monthlyTurnover, "); + sqlBuilder.append("c.is_senior_citizen as isSeniorCitizen, "); + sqlBuilder.append("c.is_pwd as isPwd, "); + sqlBuilder.append("c.pwd_category_cv_id as pwdCategoryId, "); + sqlBuilder.append("cvPwdCategory.code_value as pwdCategoryValue, "); sqlBuilder.append("c.nationality as nationality, "); sqlBuilder.append("c.is_minor as isMinor, "); sqlBuilder.append("c.guardian_client_id as guardianClientId, "); @@ -383,6 +389,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService sqlBuilder.append("left join m_code_value cvclassification on cvclassification.id = c.client_classification_cv_id "); sqlBuilder.append("left join m_code_value cvSubStatus on cvSubStatus.id = c.sub_status "); sqlBuilder.append("left join m_code_value cvOccupationCategory on cvOccupationCategory.id = c.occupation_category_cv_id "); + sqlBuilder.append("left join m_code_value cvPwdCategory on cvPwdCategory.id = c.pwd_category_cv_id "); sqlBuilder.append("left join m_code_value cvOccupation on cvOccupation.id = c.occupation_cv_id "); sqlBuilder.append("left join m_client gc on gc.id = c.guardian_client_id "); sqlBuilder.append("left join m_code_value cvConstitution on cvConstitution.id = cnp.constitution_cv_id "); @@ -482,7 +489,14 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final CodeValueData occupation = CodeValueData.instance(occupationId, occupationValue); final BigDecimal monthlyIncome1 = rs.getBigDecimal("monthlyIncome"); + final BigDecimal annualIncome1 = rs.getBigDecimal("annualIncome"); final BigDecimal annualTurnover1 = rs.getBigDecimal("annualTurnover"); + final BigDecimal monthlyTurnover1 = rs.getBigDecimal("monthlyTurnover"); + final Boolean isSeniorCitizen1 = rs.getBoolean("isSeniorCitizen"); + final Boolean isPwd1 = rs.getBoolean("isPwd"); + final Long pwdCategoryId1 = JdbcSupport.getLong(rs, "pwdCategoryId"); + final String pwdCategoryValue1 = rs.getString("pwdCategoryValue"); + final CodeValueData pwdCategory1 = CodeValueData.instance(pwdCategoryId1, pwdCategoryValue1); Integer ageYears1 = null; Integer ageMonths1 = null; @@ -520,7 +534,12 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService savingsProductName, savingsAccountId, clienttype, classification, occupationCategory, occupation, legalForm, clientNonPerson, isStaff); clientData.setMonthlyIncome(monthlyIncome1); + clientData.setAnnualIncome(annualIncome1); clientData.setAnnualTurnover(annualTurnover1); + clientData.setMonthlyTurnover(monthlyTurnover1); + clientData.setIsSeniorCitizen(isSeniorCitizen1); + clientData.setIsPwd(isPwd1); + clientData.setPwdCategory(pwdCategory1); clientData.setAgeYears(ageYears1); clientData.setAgeMonths(ageMonths1); clientData.setAgeDays(ageDays1); @@ -714,7 +733,13 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService builder.append("c.occupation_cv_id as occupationId, "); builder.append("cvOccupation.code_value as occupationValue, "); builder.append("c.monthly_income as monthlyIncome, "); + builder.append("c.annual_income as annualIncome, "); builder.append("c.annual_turnover as annualTurnover, "); + builder.append("c.monthly_turnover as monthlyTurnover, "); + builder.append("c.is_senior_citizen as isSeniorCitizen, "); + builder.append("c.is_pwd as isPwd, "); + builder.append("c.pwd_category_cv_id as pwdCategoryId, "); + builder.append("cvPwdCategory.code_value as pwdCategoryValue, "); builder.append("c.nationality as nationality, "); builder.append("c.is_minor as isMinor, "); builder.append("c.guardian_client_id as guardianClientId, "); @@ -775,6 +800,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService builder.append("left join m_code_value cvclassification on cvclassification.id = c.client_classification_cv_id "); builder.append("left join m_code_value cvSubStatus on cvSubStatus.id = c.sub_status "); builder.append("left join m_code_value cvOccupationCategory on cvOccupationCategory.id = c.occupation_category_cv_id "); + builder.append("left join m_code_value cvPwdCategory on cvPwdCategory.id = c.pwd_category_cv_id "); builder.append("left join m_code_value cvOccupation on cvOccupation.id = c.occupation_cv_id "); builder.append("left join m_client gc on gc.id = c.guardian_client_id "); builder.append("left join m_code_value cvConstitution on cvConstitution.id = cnp.constitution_cv_id "); @@ -873,7 +899,14 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final CodeValueData occupation = CodeValueData.instance(occupationId, occupationValue); final BigDecimal monthlyIncome = rs.getBigDecimal("monthlyIncome"); + final BigDecimal annualIncome = rs.getBigDecimal("annualIncome"); final BigDecimal annualTurnover = rs.getBigDecimal("annualTurnover"); + final BigDecimal monthlyTurnover = rs.getBigDecimal("monthlyTurnover"); + final Boolean isSeniorCitizen = rs.getBoolean("isSeniorCitizen"); + final Boolean isPwd = rs.getBoolean("isPwd"); + final Long pwdCategoryId = JdbcSupport.getLong(rs, "pwdCategoryId"); + final String pwdCategoryValue = rs.getString("pwdCategoryValue"); + final CodeValueData pwdCategory = CodeValueData.instance(pwdCategoryId, pwdCategoryValue); Integer ageYears = null; Integer ageMonths = null; @@ -911,7 +944,12 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService savingsProductName, savingsAccountId, clienttype, classification, occupationCategory, occupation, legalForm, clientNonPerson, isStaff); clientData.setMonthlyIncome(monthlyIncome); + clientData.setAnnualIncome(annualIncome); clientData.setAnnualTurnover(annualTurnover); + clientData.setMonthlyTurnover(monthlyTurnover); + clientData.setIsSeniorCitizen(isSeniorCitizen); + clientData.setIsPwd(isPwd); + clientData.setPwdCategory(pwdCategory); clientData.setAgeYears(ageYears); clientData.setAgeMonths(ageMonths); clientData.setAgeDays(ageDays); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTemplateReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTemplateReadPlatformServiceImpl.java index 964d46cc4..ffeb3dc66 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTemplateReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTemplateReadPlatformServiceImpl.java @@ -126,6 +126,9 @@ public class ClientTemplateReadPlatformServiceImpl implements ClientTemplateRead final List socialCategoryOptions = new ArrayList<>( this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.SOCIAL_CATEGORY)); + final List pwdCategoryOptions = new ArrayList<>( + this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.PWD_CATEGORY)); + final List clientNonPersonConstitutionOptions = new ArrayList<>( this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.CLIENT_NON_PERSON_CONSTITUTION)); @@ -146,6 +149,7 @@ public class ClientTemplateReadPlatformServiceImpl implements ClientTemplateRead templateData.setDegreeQualificationOptions(degreeQualificationOptions); templateData.setReligionOptions(religionOptions); templateData.setSocialCategoryOptions(socialCategoryOptions); + templateData.setPwdCategoryOptions(pwdCategoryOptions); return templateData; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java index 3f40f928c..1c3d95ab9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java @@ -395,6 +395,13 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP .findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.SOCIAL_CATEGORY, socialCategoryId); } + CodeValue pwdCategory = null; + final Long pwdCategoryId = command.longValueOfParameterNamed(ClientApiConstants.pwdCategoryIdParamName); + if (pwdCategoryId != null) { + pwdCategory = this.codeValueRepository + .findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.PWD_CATEGORY, pwdCategoryId); + } + final Long savingsProductId = command.longValueOfParameterNamed(ClientApiConstants.savingsProductIdParamName); if (savingsProductId != null) { this.savingsProductRepository.findById(savingsProductId) @@ -481,11 +488,31 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP newClient.setMonthlyIncome(monthlyIncome); } + final java.math.BigDecimal annualIncome = command.bigDecimalValueOfParameterNamed(ClientApiConstants.annualIncomeParamName); + if (annualIncome != null) { + newClient.setAnnualIncome(annualIncome); + } + final java.math.BigDecimal annualTurnover = command.bigDecimalValueOfParameterNamed(ClientApiConstants.annualTurnoverParamName); if (annualTurnover != null) { newClient.setAnnualTurnover(annualTurnover); } + final java.math.BigDecimal monthlyTurnover = command.bigDecimalValueOfParameterNamed(ClientApiConstants.monthlyTurnoverParamName); + if (monthlyTurnover != null) { + newClient.setMonthlyTurnover(monthlyTurnover); + } + + final Boolean isSeniorCitizen = command.booleanObjectValueOfParameterNamed(ClientApiConstants.isSeniorCitizenParamName); + newClient.setIsSeniorCitizen(Boolean.TRUE.equals(isSeniorCitizen)); + + final Boolean isPwd = command.booleanObjectValueOfParameterNamed(ClientApiConstants.isPwdParamName); + newClient.setIsPwd(Boolean.TRUE.equals(isPwd)); + + if (pwdCategory != null) { + newClient.updatePwdCategory(pwdCategory); + } + final java.math.BigDecimal agricultureIncome = command.bigDecimalValueOfParameterNamed(ClientApiConstants.agricultureIncomeParamName); if (agricultureIncome != null) { newClient.setAgricultureIncome(agricultureIncome); @@ -964,12 +991,47 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP clientForUpdate.setMonthlyIncome(newValue); } + if (command.isChangeInBigDecimalParameterNamed(ClientApiConstants.annualIncomeParamName, clientForUpdate.getAnnualIncome())) { + final java.math.BigDecimal newValue = command.bigDecimalValueOfParameterNamed(ClientApiConstants.annualIncomeParamName); + changes.put(ClientApiConstants.annualIncomeParamName, newValue); + clientForUpdate.setAnnualIncome(newValue); + } + if (command.isChangeInBigDecimalParameterNamed(ClientApiConstants.annualTurnoverParamName, clientForUpdate.getAnnualTurnover())) { final java.math.BigDecimal newValue = command.bigDecimalValueOfParameterNamed(ClientApiConstants.annualTurnoverParamName); changes.put(ClientApiConstants.annualTurnoverParamName, newValue); clientForUpdate.setAnnualTurnover(newValue); } + if (command.isChangeInBigDecimalParameterNamed(ClientApiConstants.monthlyTurnoverParamName, clientForUpdate.getMonthlyTurnover())) { + final java.math.BigDecimal newValue = command.bigDecimalValueOfParameterNamed(ClientApiConstants.monthlyTurnoverParamName); + changes.put(ClientApiConstants.monthlyTurnoverParamName, newValue); + clientForUpdate.setMonthlyTurnover(newValue); + } + + if (command.isChangeInBooleanParameterNamed(ClientApiConstants.isSeniorCitizenParamName, clientForUpdate.getIsSeniorCitizen())) { + final Boolean newValue = command.booleanObjectValueOfParameterNamed(ClientApiConstants.isSeniorCitizenParamName); + changes.put(ClientApiConstants.isSeniorCitizenParamName, newValue); + clientForUpdate.setIsSeniorCitizen(Boolean.TRUE.equals(newValue)); + } + + if (command.isChangeInBooleanParameterNamed(ClientApiConstants.isPwdParamName, clientForUpdate.getIsPwd())) { + final Boolean newValue = command.booleanObjectValueOfParameterNamed(ClientApiConstants.isPwdParamName); + changes.put(ClientApiConstants.isPwdParamName, newValue); + clientForUpdate.setIsPwd(Boolean.TRUE.equals(newValue)); + } + + if (command.isChangeInLongParameterNamed(ClientApiConstants.pwdCategoryIdParamName, clientForUpdate.pwdCategoryId())) { + final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.pwdCategoryIdParamName); + changes.put(ClientApiConstants.pwdCategoryIdParamName, newValue); + CodeValue newCodeVal = null; + if (newValue != null) { + newCodeVal = this.codeValueRepository + .findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.PWD_CATEGORY, newValue); + } + clientForUpdate.updatePwdCategory(newCodeVal); + } + if (command.isChangeInBigDecimalParameterNamed(ClientApiConstants.agricultureIncomeParamName, clientForUpdate.getAgricultureIncome())) { final java.math.BigDecimal newValue = command.bigDecimalValueOfParameterNamed(ClientApiConstants.agricultureIncomeParamName); changes.put(ClientApiConstants.agricultureIncomeParamName, newValue); diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 3204abe52..528f94b47 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -441,6 +441,12 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE m_client c + SET signature_id = ( + SELECT MAX(d.id) FROM m_document d + WHERE d.parent_entity_type = 'clients' AND d.parent_entity_id = c.id AND d.name = 'clientSignature' + ) + WHERE EXISTS ( + SELECT 1 FROM m_document d + WHERE d.parent_entity_type = 'clients' AND d.parent_entity_id = c.id AND d.name = 'clientSignature' + ) + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0422_add_monthly_turnover_to_client.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0422_add_monthly_turnover_to_client.xml new file mode 100644 index 000000000..b14c60a87 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0422_add_monthly_turnover_to_client.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0423_add_annual_income_to_client.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0423_add_annual_income_to_client.xml new file mode 100644 index 000000000..2e0914705 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0423_add_annual_income_to_client.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0424_add_senior_citizen_pwd_to_client.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0424_add_senior_citizen_pwd_to_client.xml new file mode 100644 index 000000000..a5a014907 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0424_add_senior_citizen_pwd_to_client.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_code WHERE code_name = 'PwdCategory' + + + + + + + + + + SELECT COUNT(*) FROM m_code_value WHERE code_id = (SELECT id FROM m_code WHERE code_name = 'PwdCategory') + + + INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) + SELECT id, val, val, pos, true, false FROM m_code + CROSS JOIN (VALUES + ('Blindness', 1), + ('Low Vision', 2), + ('Leprosy Cured Persons', 3), + ('Hearing Impairment (Deaf and Hard of Hearing)', 4), + ('Locomotor Disability', 5), + ('Dwarfism', 6), + ('Intellectual Disability', 7), + ('Mental Illness', 8), + ('Autism Spectrum Disorder', 9), + ('Cerebral Palsy', 10), + ('Muscular Dystrophy', 11), + ('Chronic Neurological Conditions', 12), + ('Specific Learning Disabilities', 13), + ('Multiple Sclerosis', 14), + ('Speech and Language Disability', 15), + ('Thalassemia', 16), + ('Hemophilia', 17), + ('Sickle Cell Disease', 18), + ('Multiple Disabilities', 19), + ('Acid Attack Victim', 20), + ('Parkinson''s Disease', 21) + ) AS t(val, pos) + WHERE code_name = 'PwdCategory'; + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0425_add_diploma_degree_qualification.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0425_add_diploma_degree_qualification.xml new file mode 100644 index 000000000..6c5895868 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0425_add_diploma_degree_qualification.xml @@ -0,0 +1,83 @@ + + + + + + + + INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory) + SELECT c.id, 'Diploma', + (SELECT COALESCE(MAX(cv.order_position), 0) FROM m_code_value cv WHERE cv.code_id = c.id) + 1, + true, false + FROM m_code c + WHERE c.code_name = 'EducationLevel' + AND NOT EXISTS ( + SELECT 1 FROM m_code_value cv WHERE cv.code_id = c.id AND cv.code_value = 'Diploma' + ); + + + + + + INSERT INTO m_code_value (code_id, code_value, code_description, order_position, is_active, is_mandatory) + SELECT c.id, v.code_value, 'Diploma', + (SELECT COALESCE(MAX(cv.order_position), 0) FROM m_code_value cv WHERE cv.code_id = c.id) + v.rn, + true, false + FROM m_code c + CROSS JOIN ( + VALUES + ('Diploma in Mechanical Engineering', 1), + ('Diploma in Civil Engineering', 2), + ('Diploma in Electrical Engineering', 3), + ('Diploma in Electronics & Communication', 4), + ('Diploma in Computer Science / IT', 5), + ('Diploma in Automobile Engineering', 6), + ('ITI Certificate', 7), + ('Polytechnic Diploma (Other)', 8), + ('Other Diploma', 9) + ) AS v(code_value, rn) + WHERE c.code_name = 'DegreeQualification' + AND NOT EXISTS ( + SELECT 1 FROM m_code_value cv + WHERE cv.code_id = c.id AND cv.code_value = v.code_value AND cv.code_description = 'Diploma' + ); + + + + + + + UPDATE m_code_value cv + SET is_active = false + WHERE cv.code_id = (SELECT id FROM m_code WHERE code_name = 'DegreeQualification') + AND cv.code_value = 'Diploma' + AND cv.code_description = 'School'; + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0426_add_graduate_education_level.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0426_add_graduate_education_level.xml new file mode 100644 index 000000000..e49e862ce --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0426_add_graduate_education_level.xml @@ -0,0 +1,45 @@ + + + + + + + + INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory) + SELECT c.id, 'Graduate', + (SELECT COALESCE(MAX(cv.order_position), 0) FROM m_code_value cv WHERE cv.code_id = c.id) + 1, + true, false + FROM m_code c + WHERE c.code_name = 'EducationLevel' + AND NOT EXISTS ( + SELECT 1 FROM m_code_value cv WHERE cv.code_id = c.id AND cv.code_value = 'Graduate' + ); + + + +