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 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 10:50:16 +05:30
co-authored by Claude Sonnet 5
parent bc30241950
commit 67214ec0f8
20 changed files with 771 additions and 45 deletions
@@ -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<EntitySignatureIdAdapter> 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();