Compare commits
11
Commits
bc30241950
...
b1cfa996b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1cfa996b4 | ||
|
|
02541eed6c | ||
|
|
7c15fbb0c7 | ||
|
|
6e3f195de0 | ||
|
|
a65d801250 | ||
|
|
f45d5890ea | ||
|
|
3c58bb231f | ||
|
|
aabda18b69 | ||
|
|
0e5671b706 | ||
|
|
67214ec0f8 | ||
|
|
026d902199 |
-1
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.apache.fineract.accounting.journalentry;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
|
||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
||||
|
||||
+14
-14
@@ -277,14 +277,14 @@ public class ProductToGLAccountMappingHelper {
|
||||
}
|
||||
|
||||
final JsonArray chargeToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayFragmentName, element);
|
||||
/**
|
||||
/*
|
||||
* Variable stores a map representation of charges (key) and their associated income Id's (value) extracted from
|
||||
* the passed in Jsoncommand
|
||||
**/
|
||||
*/
|
||||
final Map<Long, Long> inputChargeToIncomeAccountMap = new HashMap<>();
|
||||
/***
|
||||
/*
|
||||
* Variable stores all charges which have already been mapped to Income Accounts in the system
|
||||
**/
|
||||
*/
|
||||
final Set<Long> existingCharges = new HashSet<>();
|
||||
if (chargeToIncomeAccountMappingArray != null) {
|
||||
if (changes != null) {
|
||||
@@ -301,13 +301,13 @@ public class ProductToGLAccountMappingHelper {
|
||||
// If input map is empty, delete all existing mappings
|
||||
if (inputChargeToIncomeAccountMap.size() == 0) {
|
||||
this.accountMappingRepository.deleteAll(existingChargeToIncomeAccountMappings);
|
||||
} /**
|
||||
} /*
|
||||
* Else, <br/>
|
||||
* update existing mappings OR <br/>
|
||||
* delete old mappings (which are already present, but not passed in as a part of Jsoncommand)<br/>
|
||||
* Create new mappings for charges that are passed in as a part of the Jsoncommand but not already present
|
||||
*
|
||||
**/
|
||||
*/
|
||||
else {
|
||||
for (final ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) {
|
||||
final Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId();
|
||||
@@ -356,14 +356,14 @@ public class ProductToGLAccountMappingHelper {
|
||||
.findAllPaymentTypeToFundSourceMappings(productId, portfolioProductType.getValue());
|
||||
final JsonArray paymentChannelMappingArray = this.fromApiJsonHelper
|
||||
.extractJsonArrayNamed(LoanProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element);
|
||||
/**
|
||||
/*
|
||||
* Variable stores a map representation of Payment channels (key) and their fund sources (value) extracted from
|
||||
* the passed in Jsoncommand
|
||||
**/
|
||||
*/
|
||||
final Map<Long, Long> inputPaymentChannelFundSourceMap = new HashMap<>();
|
||||
/***
|
||||
/*
|
||||
* Variable stores all payment types which have already been mapped to Fund Sources in the system
|
||||
**/
|
||||
*/
|
||||
final Set<Long> existingPaymentTypes = new HashSet<>();
|
||||
if (paymentChannelMappingArray != null) {
|
||||
if (changes != null) {
|
||||
@@ -381,14 +381,14 @@ public class ProductToGLAccountMappingHelper {
|
||||
// If input map is empty, delete all existing mappings
|
||||
if (inputPaymentChannelFundSourceMap.isEmpty()) {
|
||||
this.accountMappingRepository.deleteAll(existingPaymentChannelToFundSourceMappings);
|
||||
} /**
|
||||
} /*
|
||||
* Else, <br/>
|
||||
* update existing mappings OR <br/>
|
||||
* delete old mappings (which re already present, but not passed in as a part of Jsoncommand)<br/>
|
||||
* Create new mappings for payment types that are passed in as a part of the Jsoncommand but not already
|
||||
* present
|
||||
*
|
||||
**/
|
||||
*/
|
||||
else {
|
||||
for (final ProductToGLAccountMapping existingPaymentChannelToFundSourceMapping : existingPaymentChannelToFundSourceMappings) {
|
||||
final Long currentPaymentChannelId = existingPaymentChannelToFundSourceMapping.getPaymentType().getId();
|
||||
@@ -584,9 +584,9 @@ public class ProductToGLAccountMappingHelper {
|
||||
// based on input condition
|
||||
|
||||
GLAccount glAccount;
|
||||
/**
|
||||
/*
|
||||
* Both CASH and Accrual placeholders have the same value for income from Interest and penalties
|
||||
**/
|
||||
*/
|
||||
CashAccountsForLoan placeHolderAccountType;
|
||||
if (isPenalty) {
|
||||
glAccount = getAccountByIdAndType(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), GLAccountType.INCOME,
|
||||
|
||||
+2
-2
@@ -181,12 +181,12 @@ public class TellerApiResource {
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Create Cashiers", description = """
|
||||
Mandatory Fields:\s
|
||||
Mandatory Fields:
|
||||
Cashier/staff, Fromm Date, To Date, Full Day or From time and To time
|
||||
|
||||
|
||||
|
||||
Optional Fields:\s
|
||||
Optional Fields:
|
||||
Description/Notes""")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersRequest.class)))
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersResponse.class)))
|
||||
|
||||
+56
@@ -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<SignatureIdResult> get(Long entityId);
|
||||
|
||||
Optional<SignatureIdResult> 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;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Simulator-side audit log for CFCFRMS (I4C/MHA) events.
|
||||
@@ -69,7 +70,7 @@ public class CfcfrmsSimulatorLog extends AbstractAuditableWithUTCDateTimeCustom<
|
||||
this.eventType = eventType;
|
||||
this.payload = payload;
|
||||
this.responsePayload = responsePayload;
|
||||
this.eventTimestamp = OffsetDateTime.now();
|
||||
this.eventTimestamp = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public CfcfrmsSimulatorLog(final String cfcfrmsRef, final String ticketId, final String bankIfsc,
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Audit trail for every status change on a CFCFRMS complaint ticket.
|
||||
@@ -54,7 +55,7 @@ public class ComplaintAuditLog extends AbstractAuditableWithUTCDateTimeCustom<Lo
|
||||
this.event = event;
|
||||
this.description = description;
|
||||
this.actor = actor;
|
||||
this.eventTimestamp = OffsetDateTime.now();
|
||||
this.eventTimestamp = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public ComplaintAuditLog(final String ticketId, final String event, final String description,
|
||||
|
||||
+3
-2
@@ -26,6 +26,7 @@ import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* CFCFRMS Complaint Ticket — represents a cyber fraud complaint pushed by MHA/I4C to the bank.
|
||||
@@ -121,7 +122,7 @@ public class ComplaintTicket extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
ticket.status = ComplaintStatus.RECEIVED;
|
||||
ticket.assignedOfficer = assignedOfficer;
|
||||
ticket.statePoliceRef = statePoliceRef;
|
||||
ticket.receivedAt = OffsetDateTime.now();
|
||||
ticket.receivedAt = DateUtils.getOffsetDateTimeOfTenant();
|
||||
return ticket;
|
||||
}
|
||||
|
||||
@@ -132,7 +133,7 @@ public class ComplaintTicket extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
this.status = newStatus;
|
||||
this.actionTaken = actionNote;
|
||||
if (newStatus == ComplaintStatus.RESOLVED || newStatus == ComplaintStatus.REJECTED) {
|
||||
this.resolvedAt = OffsetDateTime.now();
|
||||
this.resolvedAt = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+66
@@ -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<SignatureIdResult> 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<SignatureIdResult> set(Long entityId, Long signatureId) {
|
||||
final var result = get(entityId);
|
||||
|
||||
if (signatureId == null) {
|
||||
repository.removeSignatureId(entityId);
|
||||
} else {
|
||||
repository.updateByIdAndSignatureId(entityId, signatureId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -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,
|
||||
|
||||
@@ -82,7 +82,13 @@ public final class ClientData implements Comparable<ClientData>, 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<CodeValueData> pwdCategoryOptions;
|
||||
private BigDecimal agricultureIncome;
|
||||
private BigDecimal nonAgricultureIncome;
|
||||
private Integer ageYears;
|
||||
@@ -400,13 +406,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
||||
data.degreeQualificationOptions = templateData.degreeQualificationOptions;
|
||||
data.religionOptions = templateData.religionOptions;
|
||||
data.socialCategoryOptions = templateData.socialCategoryOptions;
|
||||
data.pwdCategoryOptions = templateData.pwdCategoryOptions;
|
||||
data.maritalStatus = clientData.maritalStatus;
|
||||
data.educationLevel = clientData.educationLevel;
|
||||
data.degreeQualification = clientData.degreeQualification;
|
||||
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;
|
||||
@@ -456,13 +468,19 @@ public final class ClientData implements Comparable<ClientData>, Serializable {
|
||||
data.degreeQualificationOptions = clientData.degreeQualificationOptions;
|
||||
data.religionOptions = clientData.religionOptions;
|
||||
data.socialCategoryOptions = clientData.socialCategoryOptions;
|
||||
data.pwdCategoryOptions = clientData.pwdCategoryOptions;
|
||||
data.maritalStatus = clientData.maritalStatus;
|
||||
data.educationLevel = clientData.educationLevel;
|
||||
data.degreeQualification = clientData.degreeQualification;
|
||||
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 +527,12 @@ public final class ClientData implements Comparable<ClientData>, 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;
|
||||
|
||||
@@ -77,6 +77,9 @@ public class Client extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
@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<Long> {
|
||||
@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<Long> {
|
||||
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;
|
||||
}
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@ import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Form 60 declaration as per Rule 114B of Income Tax Rules, 1962.
|
||||
@@ -87,7 +88,7 @@ public class ClientForm60 extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
form.estimatedTotalIncome = command.bigDecimalValueOfParameterNamed("estimatedTotalIncome");
|
||||
form.reasonForNoPan = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("reasonForNoPan"), null);
|
||||
form.declaration = command.booleanPrimitiveValueOfParameterNamed("declaration");
|
||||
form.submittedOn = LocalDate.now();
|
||||
form.submittedOn = DateUtils.getLocalDateOfTenant();
|
||||
return form;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-10
@@ -32,6 +32,7 @@ import java.util.Map;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Represents a client's KYC (Know Your Customer) record as mandated by the Reserve Bank of India
|
||||
@@ -260,7 +261,7 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.gstinVerified = false;
|
||||
this.cinVerified = false;
|
||||
this.dinVerified = false;
|
||||
this.initiatedOn = LocalDate.now();
|
||||
this.initiatedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) {
|
||||
@@ -456,13 +457,13 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
public void markCkycUploaded(final String ckycNumber, final String batchId) {
|
||||
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
|
||||
this.ckycBatchId = StringUtils.defaultIfEmpty(batchId, null);
|
||||
this.ckycUploadedOn = LocalDate.now();
|
||||
this.ckycUploadedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
/** Called after successful 3rd-party verification of all required documents. */
|
||||
public void markVerified(final String ckycNumber) {
|
||||
this.kycStatus = ClientKycStatus.VERIFIED.getValue();
|
||||
this.verifiedOn = LocalDate.now();
|
||||
this.verifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
|
||||
|
||||
final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory);
|
||||
@@ -474,14 +475,14 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
/** Called when verification fails. */
|
||||
public void markRejected(final String remarks) {
|
||||
this.kycStatus = ClientKycStatus.REJECTED.getValue();
|
||||
this.rejectedOn = LocalDate.now();
|
||||
this.rejectedOn = DateUtils.getLocalDateOfTenant();
|
||||
this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks);
|
||||
}
|
||||
|
||||
public void markPanVerified(final String fullName, final String firstName, final String lastName,
|
||||
final String aadhaarSeedingStatus, final String category, final String status) {
|
||||
this.panVerified = true;
|
||||
this.panVerifiedOn = LocalDate.now();
|
||||
this.panVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
this.panFullName = StringUtils.defaultIfEmpty(fullName, null);
|
||||
this.panFirstName = StringUtils.defaultIfEmpty(firstName, null);
|
||||
this.panLastName = StringUtils.defaultIfEmpty(lastName, null);
|
||||
@@ -507,28 +508,28 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
public void markAadhaarVerified() {
|
||||
this.aadhaarVerified = true;
|
||||
this.aadhaarVerifiedOn = LocalDate.now();
|
||||
this.aadhaarVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
public void markGstinVerified() {
|
||||
this.gstinVerified = true;
|
||||
this.gstinVerifiedOn = LocalDate.now();
|
||||
this.gstinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
public void markCinVerified() {
|
||||
this.cinVerified = true;
|
||||
this.cinVerifiedOn = LocalDate.now();
|
||||
this.cinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
public void markDinVerified() {
|
||||
this.dinVerified = true;
|
||||
this.dinVerifiedOn = LocalDate.now();
|
||||
this.dinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
}
|
||||
|
||||
public void markDinVerified(final String dinNumber, final String mcaResponseJson) {
|
||||
this.dinNumber = dinNumber;
|
||||
this.dinVerified = true;
|
||||
this.dinVerifiedOn = LocalDate.now();
|
||||
this.dinVerifiedOn = DateUtils.getLocalDateOfTenant();
|
||||
this.isDirector = true;
|
||||
this.dinMcaResponse = mcaResponseJson;
|
||||
}
|
||||
|
||||
+8
@@ -55,6 +55,14 @@ public interface ClientRepository extends JpaRepository<Client, Long>, 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.
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_client_risk_rule")
|
||||
public class ClientRiskRule {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "rule_name", nullable = false, length = 100)
|
||||
private String ruleName;
|
||||
|
||||
@Column(name = "legal_form_id")
|
||||
private Integer legalFormId;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "occupation_category_cv_id")
|
||||
private CodeValue occupationCategory;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "occupation_cv_id")
|
||||
private CodeValue occupation;
|
||||
|
||||
@Column(name = "min_monthly_income", precision = 19, scale = 6)
|
||||
private BigDecimal minMonthlyIncome;
|
||||
|
||||
@Column(name = "max_monthly_income", precision = 19, scale = 6)
|
||||
private BigDecimal maxMonthlyIncome;
|
||||
|
||||
@Column(name = "min_annual_income", precision = 19, scale = 6)
|
||||
private BigDecimal minAnnualIncome;
|
||||
|
||||
@Column(name = "max_annual_income", precision = 19, scale = 6)
|
||||
private BigDecimal maxAnnualIncome;
|
||||
|
||||
@Column(name = "min_annual_turnover", precision = 19, scale = 6)
|
||||
private BigDecimal minAnnualTurnover;
|
||||
|
||||
@Column(name = "max_annual_turnover", precision = 19, scale = 6)
|
||||
private BigDecimal maxAnnualTurnover;
|
||||
|
||||
@Column(name = "risk_category", nullable = false)
|
||||
private Integer riskCategory;
|
||||
|
||||
@Column(name = "reason", nullable = false, length = 255)
|
||||
private String reason;
|
||||
|
||||
@Column(name = "order_position", nullable = false)
|
||||
private Integer orderPosition;
|
||||
|
||||
protected ClientRiskRule() {}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getRuleName() {
|
||||
return ruleName;
|
||||
}
|
||||
|
||||
public Integer getLegalFormId() {
|
||||
return legalFormId;
|
||||
}
|
||||
|
||||
public CodeValue getOccupationCategory() {
|
||||
return occupationCategory;
|
||||
}
|
||||
|
||||
public CodeValue getOccupation() {
|
||||
return occupation;
|
||||
}
|
||||
|
||||
public BigDecimal getMinMonthlyIncome() {
|
||||
return minMonthlyIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxMonthlyIncome() {
|
||||
return maxMonthlyIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMinAnnualIncome() {
|
||||
return minAnnualIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxAnnualIncome() {
|
||||
return maxAnnualIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMinAnnualTurnover() {
|
||||
return minAnnualTurnover;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxAnnualTurnover() {
|
||||
return maxAnnualTurnover;
|
||||
}
|
||||
|
||||
public Integer getRiskCategory() {
|
||||
return riskCategory;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public Integer getOrderPosition() {
|
||||
return orderPosition;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
public interface ClientRiskRuleRepository extends JpaRepository<ClientRiskRule, Long> {
|
||||
|
||||
@Query("SELECT r FROM ClientRiskRule r ORDER BY r.orderPosition ASC")
|
||||
List<ClientRiskRule> findAllOrderedByOrderPosition();
|
||||
}
|
||||
+2
-1
@@ -24,6 +24,7 @@ import jakarta.persistence.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Activity log for each DIP / MNRL mobile number lookup request.
|
||||
@@ -78,6 +79,6 @@ public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
this.rawResponse = rawResponse;
|
||||
this.lookupSource = lookupSource;
|
||||
this.clientId = clientId;
|
||||
this.lookedUpOn = OffsetDateTime.now();
|
||||
this.lookedUpOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -18,7 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.sanctions.domain;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
@@ -40,4 +42,11 @@ public interface SanctionsEntryRepository extends JpaRepository<SanctionsEntry,
|
||||
void deleteAllByActiveTrue();
|
||||
|
||||
long countByActiveTrue();
|
||||
|
||||
/**
|
||||
* Timestamp the active sanctions list was last loaded into the database — i.e. the data
|
||||
* currency shown to callers so they know which list snapshot a screening ran against.
|
||||
*/
|
||||
@Query("SELECT MAX(s.createdDate) FROM SanctionsEntry s WHERE s.active = true")
|
||||
Optional<OffsetDateTime> findListLastLoadedOn();
|
||||
}
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Activity log for each sanctions screening request.
|
||||
@@ -73,7 +74,7 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
|
||||
this.screeningSource = screeningSource;
|
||||
this.clientId = clientId;
|
||||
this.ipAddress = ipAddress;
|
||||
this.screenedOn = OffsetDateTime.now();
|
||||
this.screenedOn = DateUtils.getOffsetDateTimeOfTenant();
|
||||
}
|
||||
|
||||
// ── Accessors ─────────────────────────────────────────────────────────────
|
||||
|
||||
+7
-6
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.search.data;
|
||||
|
||||
import java.util.Locale;
|
||||
import org.apache.fineract.portfolio.search.SearchConstants.SearchSupportedResources;
|
||||
|
||||
public class SearchConditions {
|
||||
@@ -37,17 +38,17 @@ public class SearchConditions {
|
||||
this.searchResource = searchResource;
|
||||
this.exactMatch = exactMatch;
|
||||
this.clientSearch = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.CLIENTS.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.CLIENTS.name().toLowerCase(Locale.ROOT));
|
||||
this.groupSearch = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.GROUPS.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.GROUPS.name().toLowerCase(Locale.ROOT));
|
||||
this.loanSeach = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.LOANS.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.LOANS.name().toLowerCase(Locale.ROOT));
|
||||
this.savingSeach = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.SAVINGS.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.SAVINGS.name().toLowerCase(Locale.ROOT));
|
||||
this.shareSeach = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.SHARES.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.SHARES.name().toLowerCase(Locale.ROOT));
|
||||
this.clientIdentifierSearch = null == searchResource
|
||||
|| searchResource.toLowerCase().contains(SearchSupportedResources.CLIENTIDENTIFIERS.name().toLowerCase());
|
||||
|| searchResource.toLowerCase(Locale.ROOT).contains(SearchSupportedResources.CLIENTIDENTIFIERS.name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public SearchConditions(final String searchQueryParam, final String searchResource, final Boolean clientSearch,
|
||||
|
||||
+1
-1
@@ -321,6 +321,6 @@ public class SearchUtil {
|
||||
|
||||
public String camelToSnake(final String camelStr) {
|
||||
return camelStr == null ? null
|
||||
: camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
|
||||
: camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ public interface BankUserDscRepository extends JpaRepository<BankUserDsc, Long>
|
||||
|
||||
/**
|
||||
* Login gate: returns true if the user has at least one usable DSC mapping that's also not
|
||||
* past its notAfter date. The {@code dscExpiryDate >= today} clause prevents 2FA-gating a
|
||||
* past its notAfter date. The {@code dscExpiryDate >= today} clause prevents 2FA-gating a
|
||||
* user who's enrolled but whose cert silently expired between log-ins — they'd otherwise be
|
||||
* stuck in a verification loop with a cert the helper would sign with but the server would
|
||||
* accept (the verify side filters too, but the gate is what kicks the user into the DSC flow
|
||||
|
||||
+4
-3
@@ -29,6 +29,7 @@ import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
@@ -452,7 +453,7 @@ public class CurrentAccountPendingEdit extends AbstractPersistableCustom<Long> {
|
||||
public CurrentAccountPendingEdit(final CurrentAccount currentAccount, final AppUser maker) {
|
||||
this.currentAccount = currentAccount;
|
||||
this.maker = maker;
|
||||
this.createdOn = LocalDateTime.now();
|
||||
this.createdOn = DateUtils.getLocalDateTimeOfTenant();
|
||||
this.status = STATUS_PENDING;
|
||||
this.pendingLockKey = lockKeyFor(currentAccount.getId());
|
||||
}
|
||||
@@ -582,14 +583,14 @@ public class CurrentAccountPendingEdit extends AbstractPersistableCustom<Long> {
|
||||
public void markApproved(final AppUser checker) {
|
||||
this.status = STATUS_APPROVED;
|
||||
this.checker = checker;
|
||||
this.checkedOn = LocalDateTime.now();
|
||||
this.checkedOn = DateUtils.getLocalDateTimeOfTenant();
|
||||
this.pendingLockKey = null;
|
||||
}
|
||||
|
||||
public void markRejected(final AppUser checker, final String reason) {
|
||||
this.status = STATUS_REJECTED;
|
||||
this.checker = checker;
|
||||
this.checkedOn = LocalDateTime.now();
|
||||
this.checkedOn = DateUtils.getLocalDateTimeOfTenant();
|
||||
this.rejectReason = reason;
|
||||
this.pendingLockKey = null;
|
||||
}
|
||||
|
||||
+3
-1
@@ -51,7 +51,9 @@ public class CurrentAccountNumberGenerator {
|
||||
if (val > maxVal) {
|
||||
maxVal = val;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// non-numeric suffix - skip when computing the max existing sequence
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-2
@@ -54,7 +54,6 @@ import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.MonthDay;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
@@ -76,7 +75,6 @@ import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductAccountingDataValidator;
|
||||
|
||||
-7
@@ -56,18 +56,11 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountT
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.customerSegmentParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.productCodeParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.glAccountIdParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.effectiveDateParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.effectiveToParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.endDateParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerRequiredParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareLinkingEnabledParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dormancyAlertParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.interestCalculateSelectorParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.averagePeriodParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoPostParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.frequencyFixDayParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.lastInterestDateParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enableLockinPeriodParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.balanceSwapOptionParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.msmeParamName;
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_current_product_deposit_limit_kyc")
|
||||
@@ -57,12 +58,12 @@ public class CurrentProductDepositLimitKyc extends AbstractPersistableCustom<Lon
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
protected CurrentProductDepositLimitKyc() {
|
||||
this.createdDate = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public CurrentProductDepositLimitKyc(CurrentProduct currentProduct) {
|
||||
this();
|
||||
this.currentProduct = currentProduct;
|
||||
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public CurrentProduct getCurrentProduct() { return currentProduct; }
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_current_product_facility_config")
|
||||
@@ -87,12 +88,12 @@ public class CurrentProductFacilityConfig extends AbstractPersistableCustom<Long
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
protected CurrentProductFacilityConfig() {
|
||||
this.createdDate = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public CurrentProductFacilityConfig(CurrentProduct currentProduct) {
|
||||
this();
|
||||
this.currentProduct = currentProduct;
|
||||
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public CurrentProduct getCurrentProduct() { return currentProduct; }
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_current_product_facility_limits")
|
||||
@@ -51,7 +52,6 @@ public class CurrentProductFacilityLimit extends AbstractPersistableCustom<Long>
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
protected CurrentProductFacilityLimit() {
|
||||
this.createdDate = LocalDateTime.now();
|
||||
}
|
||||
|
||||
private CurrentProductFacilityLimit(CurrentProduct currentProduct, String limitType,
|
||||
@@ -62,6 +62,7 @@ public class CurrentProductFacilityLimit extends AbstractPersistableCustom<Long>
|
||||
this.generalLimit = generalLimit;
|
||||
this.minorAccountLimit = minorAccountLimit;
|
||||
this.smallSavingLimit = smallSavingLimit;
|
||||
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
|
||||
}
|
||||
|
||||
public static CurrentProductFacilityLimit create(CurrentProduct currentProduct, String limitType,
|
||||
|
||||
-2
@@ -19,13 +19,11 @@
|
||||
package org.apache.fineract.portfolio.currentproduct.service;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
+6
-5
@@ -95,23 +95,24 @@ public class FileContentStoreService implements ContentStoreService {
|
||||
|
||||
final var target = Files.exists(getPath(safePath, false)) ? getPath(safePath, false) : getPath(safePath, true);
|
||||
|
||||
try (var in = is) {
|
||||
IOUtils.copy(in, new FileOutputStream(target.toString()));
|
||||
try (var in = is; var out = new FileOutputStream(target.toString())) {
|
||||
IOUtils.copy(in, out);
|
||||
} catch (Exception e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
|
||||
final var relativePath = getRelativePath(target);
|
||||
|
||||
try {
|
||||
postUploadContentPolicy
|
||||
.check(ContentPolicyContext.builder().path(path).inputStream(getInputStream(target)).mimeType(mimeType).build());
|
||||
try (var in = getInputStream(target)) {
|
||||
postUploadContentPolicy.check(ContentPolicyContext.builder().path(path).inputStream(in).mimeType(mimeType).build());
|
||||
} catch (ContentPolicyException e) {
|
||||
log.warn("Delete file because it didn't comply with the post-upload policy check: {} - {}", target, e.getMessage());
|
||||
|
||||
delete(relativePath.toString());
|
||||
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
throw new ContentStoreException(e);
|
||||
}
|
||||
|
||||
return relativePath.toString();
|
||||
|
||||
+24
-4
@@ -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();
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ import org.springframework.stereotype.Component;
|
||||
@Path("/v1/journalentries")
|
||||
@Component
|
||||
@Tag(name = "Journal Entries", description = """
|
||||
A journal entry refers to the logging of transactions against general ledger accounts. A journal entry may consist of several line items, each of which is either a "debit" or a "credit". The total amount of the debits must equal the total amount of the credits or the journal entry is said to be "unbalanced"\s
|
||||
A journal entry refers to the logging of transactions against general ledger accounts. A journal entry may consist of several line items, each of which is either a "debit" or a "credit". The total amount of the debits must equal the total amount of the credits or the journal entry is said to be "unbalanced"
|
||||
|
||||
A journal entry directly changes the account balances on the general ledger""")
|
||||
@RequiredArgsConstructor
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.email.service;
|
||||
import java.time.Year;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
public class EmailTemplateServiceImpl implements EmailTemplateService {
|
||||
@@ -129,7 +130,7 @@ public class EmailTemplateServiceImpl implements EmailTemplateService {
|
||||
+ escape(supportEmail) + "</a>"
|
||||
+ " </p>"
|
||||
+ " <p style=\"margin:0;font-size:12px;color:#94a3b8;\">"
|
||||
+ " © " + Year.now().getValue() + " " + escape(brandName) + ". All rights reserved."
|
||||
+ " © " + Year.now(DateUtils.getDateTimeZoneOfTenant()).getValue() + " " + escape(brandName) + ". All rights reserved."
|
||||
+ " </p>"
|
||||
+ " </td></tr>"
|
||||
+ " </table>"
|
||||
|
||||
+1
@@ -109,6 +109,7 @@ public class ImpsCryptoUtil {
|
||||
}
|
||||
|
||||
/** Decrypt inbound payload using the CBS private key. */
|
||||
@SuppressWarnings("StringSplitter")
|
||||
public String decryptJson(String encryptedPayload) {
|
||||
if (cbsPrivateKey == null) {
|
||||
throw new IllegalStateException("CBS private key not loaded — check fineract.imps.cbs.private-key-path");
|
||||
|
||||
+2
-1
@@ -34,6 +34,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* STUB implementation — returns hard-coded SUCCESS for every request and
|
||||
@@ -108,7 +109,7 @@ public class ImpsCbsServiceImpl implements ImpsCbsService {
|
||||
resp.setRefId(request.getRefId());
|
||||
resp.setUtr(request.getUtr());
|
||||
resp.setAmount(request.getAmount());
|
||||
resp.setTimeStamp(OffsetDateTime.now().format(ISO_OFFSET));
|
||||
resp.setTimeStamp(OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(ISO_OFFSET));
|
||||
resp.setStatus("SUCCESS");
|
||||
resp.setResponseCode("00");
|
||||
AccountData payeeAcc = Optional.ofNullable(request.getPayee()).map(PartyData::getAccount).orElse(null);
|
||||
|
||||
+2
-2
@@ -28,7 +28,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Credentials;
|
||||
import okhttp3.MediaType;
|
||||
@@ -42,6 +41,7 @@ import org.apache.fineract.infrastructure.openproject.data.OpenProjectProjectDat
|
||||
import org.apache.fineract.infrastructure.openproject.data.OpenProjectStatusSummaryData;
|
||||
import org.apache.fineract.infrastructure.openproject.data.OpenProjectWorkPackageData;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -164,7 +164,7 @@ public class OpenProjectService {
|
||||
}
|
||||
|
||||
// Set required custom field: "Last Follow-up Date" (customField8)
|
||||
body.addProperty("customField8", LocalDate.now().toString());
|
||||
body.addProperty("customField8", LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
|
||||
if (typeName != null && !typeName.isBlank()) {
|
||||
JsonObject links = new JsonObject();
|
||||
|
||||
+2
-1
@@ -47,6 +47,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -127,7 +128,7 @@ public class RbiFeedFetchService {
|
||||
NodeList items = doc.getElementsByTagName("item");
|
||||
log.debug("RBI FeedFetchService - Parsed {} items from RSS for category: {}", items.getLength(), category);
|
||||
List<RbiFeedItem> newItems = new ArrayList<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
for (int i = 0; i < items.getLength(); i++) {
|
||||
Element item = (Element) items.item(i);
|
||||
|
||||
+3
-1
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.security.otp.api;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -107,7 +109,7 @@ public class EmailLoginOtpApiResource {
|
||||
}
|
||||
final String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) {
|
||||
return fwd.split(",")[0].trim();
|
||||
return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
}
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
+3
-1
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.security.otp.api;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -107,7 +109,7 @@ public class LoginOtpApiResource {
|
||||
}
|
||||
final String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) {
|
||||
return fwd.split(",")[0].trim();
|
||||
return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
}
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
+2
-1
@@ -45,6 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
public class EmailLoginOtpServiceImpl implements EmailLoginOtpService {
|
||||
@@ -128,7 +129,7 @@ public class EmailLoginOtpServiceImpl implements EmailLoginOtpService {
|
||||
|
||||
// Render the branded template — same look and feel for OTP, welcome, password-reset, alerts.
|
||||
final String html = emailTemplateService.renderOtp(recipientName, otp, validityMinutes);
|
||||
final String subject = "Your OTP for Login - " + appName + " (" + LocalDateTime.now().format(SUBJECT_TIMESTAMP) + ")";
|
||||
final String subject = "Your OTP for Login - " + appName + " (" + LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(SUBJECT_TIMESTAMP) + ")";
|
||||
|
||||
final boolean sent = emailSenderService.sendHtml(email, subject, html);
|
||||
if (sent) {
|
||||
|
||||
-2
@@ -20,7 +20,6 @@ package org.apache.fineract.organisation.teller.service;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
@@ -44,7 +43,6 @@ import org.apache.fineract.organisation.teller.domain.CashierTransactionReposito
|
||||
import org.apache.fineract.organisation.teller.domain.CashierTxnType;
|
||||
import org.apache.fineract.organisation.teller.domain.Teller;
|
||||
import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper;
|
||||
import org.apache.fineract.organisation.teller.exception.CashierExistForTellerException;
|
||||
import org.apache.fineract.organisation.teller.exception.CashierNotFoundException;
|
||||
import org.apache.fineract.organisation.teller.exception.TellerAlreadyExistsException;
|
||||
import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer;
|
||||
|
||||
+12
-5
@@ -33,6 +33,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashexchange")
|
||||
@Component
|
||||
@@ -94,7 +95,7 @@ public class CashExchangeApiResource {
|
||||
header.setNote(note);
|
||||
header.setCustomerIdProof(customerIdProof);
|
||||
header.setIsReversed(false);
|
||||
header.setCreatedAt(LocalDateTime.now());
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
if (user != null) {
|
||||
@@ -107,7 +108,9 @@ public class CashExchangeApiResource {
|
||||
+ "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
|
||||
Long.class, user.getId());
|
||||
header.setCashierId(cashierId);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// resolving the current user must never block recording the exchange
|
||||
@@ -160,14 +163,18 @@ public class CashExchangeApiResource {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
reason = json.has("reason") && !json.get("reason").isJsonNull() ? json.get("reason").getAsString() : null;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
exchange.setIsReversed(true);
|
||||
exchange.setReversedAt(LocalDateTime.now());
|
||||
exchange.setReversedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
exchange.setReverseReason(reason);
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
if (user != null) exchange.setReversedBy(user.getId());
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
repository.save(exchange);
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("resourceId", id);
|
||||
|
||||
+9
-117
@@ -59,6 +59,9 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.base.Splitter;
|
||||
|
||||
@Path("/v1/cashier/deposit-requests")
|
||||
@Component
|
||||
@@ -364,7 +367,7 @@ public class CashierDepositRequestApiResource {
|
||||
final BigDecimal amount = new BigDecimal(record.get("amount").toString());
|
||||
final Object ptId = record.get("payment_type_id");
|
||||
final Object txnDateObj = record.get("txn_date");
|
||||
final String txnDateStr = txnDateObj != null ? txnDateObj.toString().split(" ")[0] : null;
|
||||
final String txnDateStr = txnDateObj != null ? Iterables.get(Splitter.on(' ').split(txnDateObj.toString()), 0) : null;
|
||||
final String description = (String) record.get("description");
|
||||
|
||||
final String storedAccountType = (String) record.get("account_type");
|
||||
@@ -513,7 +516,7 @@ public class CashierDepositRequestApiResource {
|
||||
final BigDecimal amount = new BigDecimal(record.get("amount").toString());
|
||||
final Object ptId = record.get("payment_type_id");
|
||||
final Object txnDateObj = record.get("txn_date");
|
||||
final String txnDateStr = txnDateObj != null ? txnDateObj.toString().split(" ")[0] : null;
|
||||
final String txnDateStr = txnDateObj != null ? Iterables.get(Splitter.on(' ').split(txnDateObj.toString()), 0) : null;
|
||||
final String description = (String) record.get("description");
|
||||
|
||||
if (savingsAccountId == null || !isCurrentAccountById(savingsAccountId)) {
|
||||
@@ -559,7 +562,7 @@ public class CashierDepositRequestApiResource {
|
||||
final Long savingsAccountId, final BigDecimal amount,
|
||||
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
final JsonObject payload = new JsonObject();
|
||||
@@ -587,7 +590,7 @@ public class CashierDepositRequestApiResource {
|
||||
final Long currentAccountId, final BigDecimal amount,
|
||||
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
final JsonObject payload = new JsonObject();
|
||||
@@ -644,7 +647,7 @@ public class CashierDepositRequestApiResource {
|
||||
final Object paymentTypeId, final String txnDateStr, final String note,
|
||||
final boolean isDeposit) {
|
||||
|
||||
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
// Deposit (CR): cashier receives cash → credit selected GL, debit cash vault (contra)
|
||||
@@ -686,67 +689,6 @@ public class CashierDepositRequestApiResource {
|
||||
return result.getTransactionId() != null ? result.getTransactionId() : null;
|
||||
}
|
||||
|
||||
private void saveGlDenomination(final String denominationData,
|
||||
final Map<?, ?> record, final String txnIdRaw) {
|
||||
final JsonObject denomMap;
|
||||
try {
|
||||
denomMap = gson.fromJson(denominationData, JsonObject.class);
|
||||
} catch (Exception e) {
|
||||
return;
|
||||
}
|
||||
if (denomMap == null || denomMap.size() == 0) return;
|
||||
|
||||
// Convert hex transaction ID to Long (same logic as CashierGlDenominationApiResource)
|
||||
Long txnId = null;
|
||||
if (txnIdRaw != null && !txnIdRaw.isBlank()) {
|
||||
try {
|
||||
txnId = Long.valueOf(txnIdRaw);
|
||||
} catch (NumberFormatException ignored) {
|
||||
try {
|
||||
final String hex = txnIdRaw.length() > 15 ? txnIdRaw.substring(txnIdRaw.length() - 15) : txnIdRaw;
|
||||
txnId = Long.parseUnsignedLong(hex, 16);
|
||||
} catch (NumberFormatException ignored2) {
|
||||
txnId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String accountNo = (String) record.get("account_no");
|
||||
final Object cashierIdObj = record.get("cashier_id");
|
||||
final Long cashierId = cashierIdObj != null ? Long.valueOf(cashierIdObj.toString()) : null;
|
||||
final String cashierUsername = (String) record.get("cashier_username");
|
||||
|
||||
for (final Map.Entry<String, com.google.gson.JsonElement> entry : denomMap.entrySet()) {
|
||||
final int count;
|
||||
try { count = entry.getValue().getAsInt(); } catch (Exception e) { continue; }
|
||||
if (count <= 0) continue;
|
||||
|
||||
final String[] parts = entry.getKey().split(":", 2);
|
||||
if (parts.length != 2) continue;
|
||||
final String kind = parts[0];
|
||||
final int value;
|
||||
try { value = Integer.parseInt(parts[1]); } catch (Exception e) { continue; }
|
||||
|
||||
final List<Long> ids = jdbcTemplate.queryForList(
|
||||
"SELECT id FROM m_denomination_master WHERE denomination = ? AND kind = ?"
|
||||
+ " AND (deleted IS NULL OR deleted = false) LIMIT 1",
|
||||
Long.class, value, kind);
|
||||
if (ids.isEmpty()) continue;
|
||||
final Long denomId = ids.get(0);
|
||||
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO m_savings_txn_denomination
|
||||
(txn_id, savings_account_id, account_no, client_id, denomination_id,
|
||||
denomination_value, count, total_amount, txn_date, txn_type, officer_id, officer_username)
|
||||
VALUES (?, NULL, ?, NULL, ?, ?, ?, ?, CURRENT_DATE, 'DEPOSIT', ?, ?)
|
||||
""",
|
||||
txnId, accountNo, denomId,
|
||||
value, count, BigDecimal.valueOf((long) value * count),
|
||||
cashierId, cashierUsername
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the denomination breakdown the cashier entered at submit time into
|
||||
* m_savings_txn_denomination once the deposit is approved, so the cashier's
|
||||
@@ -797,56 +739,6 @@ public class CashierDepositRequestApiResource {
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDepositDenomination(final String denominationData,
|
||||
final Map<?, ?> record, final Long txnId) {
|
||||
final JsonObject denomMap;
|
||||
try {
|
||||
denomMap = gson.fromJson(denominationData, JsonObject.class);
|
||||
} catch (Exception e) {
|
||||
return; // malformed JSON — skip silently
|
||||
}
|
||||
if (denomMap == null || denomMap.size() == 0) return;
|
||||
|
||||
final Object saObj = record.get("savings_account_id");
|
||||
final Long savingsAccountId = saObj != null ? Long.valueOf(saObj.toString()) : null;
|
||||
final String accountNo = (String) record.get("account_no");
|
||||
final Object clientIdObj = record.get("client_id");
|
||||
final Long clientId = clientIdObj != null ? Long.valueOf(clientIdObj.toString()) : null;
|
||||
final Object cashierIdObj = record.get("cashier_id");
|
||||
final Long cashierId = cashierIdObj != null ? Long.valueOf(cashierIdObj.toString()) : null;
|
||||
final String cashierUsername = (String) record.get("cashier_username");
|
||||
|
||||
for (final Map.Entry<String, JsonElement> entry : denomMap.entrySet()) {
|
||||
final int count;
|
||||
try { count = entry.getValue().getAsInt(); } catch (Exception e) { continue; }
|
||||
if (count <= 0) continue;
|
||||
|
||||
final String[] parts = entry.getKey().split(":", 2); // "note:500" or "coin:1"
|
||||
if (parts.length != 2) continue;
|
||||
final String kind = parts[0];
|
||||
final int value;
|
||||
try { value = Integer.parseInt(parts[1]); } catch (Exception e) { continue; }
|
||||
|
||||
final List<Long> ids = jdbcTemplate.queryForList(
|
||||
"SELECT id FROM m_denomination_master WHERE denomination = ? AND kind = ?"
|
||||
+ " AND (deleted IS NULL OR deleted = false) LIMIT 1",
|
||||
Long.class, value, kind);
|
||||
if (ids.isEmpty()) continue;
|
||||
final Long denomId = ids.get(0);
|
||||
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO m_savings_txn_denomination
|
||||
(txn_id, savings_account_id, account_no, client_id, denomination_id,
|
||||
denomination_value, count, total_amount, txn_date, txn_type, officer_id, officer_username)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_DATE, 'DEPOSIT', ?, ?)
|
||||
""",
|
||||
txnId, savingsAccountId, accountNo, clientId, denomId,
|
||||
value, count, BigDecimal.valueOf((long) value * count),
|
||||
cashierId, cashierUsername
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a cash denomination breakdown is provided, its total must exactly equal
|
||||
* the deposit amount — prevents the recorded vault movement from drifting
|
||||
@@ -946,7 +838,7 @@ public class CashierDepositRequestApiResource {
|
||||
final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
} catch (Exception e) {
|
||||
return LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
return LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-6
@@ -31,6 +31,7 @@ import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierRep
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashintercashier")
|
||||
@Component
|
||||
@@ -69,7 +70,7 @@ public class CashInterCashierApiResource {
|
||||
}
|
||||
|
||||
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
|
||||
? LocalDate.parse(json.get("transferDate").getAsString()) : LocalDate.now();
|
||||
? LocalDate.parse(json.get("transferDate").getAsString()) : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull()
|
||||
? json.get("fromParty").getAsString() : null;
|
||||
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull()
|
||||
@@ -95,13 +96,15 @@ public class CashInterCashierApiResource {
|
||||
header.setToParty(toParty);
|
||||
header.setTransferAmount(total);
|
||||
header.setNote(note);
|
||||
header.setCreatedAt(LocalDateTime.now());
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
header.setIsReversed(false);
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
header.setPerformedBy(user.getId());
|
||||
header.setPerformedByUsername(user.getUsername());
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
CashInterCashier saved = repository.saveAndFlush(header);
|
||||
saveDetails(saved.getId(), denominations);
|
||||
|
||||
@@ -128,13 +131,17 @@ public class CashInterCashierApiResource {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
reason = json.has("reason") ? json.get("reason").getAsString() : null;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
AppUser user = null;
|
||||
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {}
|
||||
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
record.setIsReversed(true);
|
||||
record.setReversedAt(LocalDateTime.now());
|
||||
record.setReversedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
record.setReverseReason(reason);
|
||||
if (user != null) record.setReversedBy(user.getId());
|
||||
repository.save(record);
|
||||
|
||||
+9
-6
@@ -32,6 +32,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashreconciliation")
|
||||
@Component
|
||||
@@ -62,7 +63,7 @@ public class CashReconciliationApiResource {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
|
||||
"cashierId query param is required.");
|
||||
}
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
|
||||
// In: vault → cashier allocations — GROUP BY denomination_id+kind to keep coin and note separate
|
||||
final String sqlIn1 = "SELECT ctd.denomination_id, ctd.denomination_value, COALESCE(dm.kind, 'note') AS kind, SUM(ctd.count) AS total_count "
|
||||
@@ -187,7 +188,7 @@ public class CashReconciliationApiResource {
|
||||
@Path("expected/me")
|
||||
public String getExpectedForMe(@QueryParam("asOfDate") String asOfDate) {
|
||||
context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION");
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
Long appUserId;
|
||||
try {
|
||||
appUserId = context.authenticatedUser().getId();
|
||||
@@ -223,7 +224,7 @@ public class CashReconciliationApiResource {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
|
||||
"cashierId query param is required.");
|
||||
}
|
||||
final String reconDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||
final String reconDate = (date != null && !date.isBlank()) ? date : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)",
|
||||
Integer.class, cashierId, reconDate);
|
||||
@@ -247,7 +248,7 @@ public class CashReconciliationApiResource {
|
||||
"cashierId is required.");
|
||||
}
|
||||
final LocalDate reconDate = json.has("reconDate") && !json.get("reconDate").isJsonNull()
|
||||
? LocalDate.parse(json.get("reconDate").getAsString()) : LocalDate.now();
|
||||
? LocalDate.parse(json.get("reconDate").getAsString()) : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String cashierName = json.has("cashierName") && !json.get("cashierName").isJsonNull()
|
||||
? json.get("cashierName").getAsString() : null;
|
||||
final Long tellerId = json.has("tellerId") && !json.get("tellerId").isJsonNull()
|
||||
@@ -299,12 +300,14 @@ public class CashReconciliationApiResource {
|
||||
header.setVariance(variance);
|
||||
header.setStatus(status);
|
||||
header.setNote(note);
|
||||
header.setCreatedAt(LocalDateTime.now());
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
header.setPerformedBy(user.getId());
|
||||
header.setPerformedByUsername(user.getUsername());
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
CashReconciliation saved = repository.saveAndFlush(header);
|
||||
|
||||
for (CashReconciliationDetail d : details) {
|
||||
|
||||
+19
-10
@@ -18,6 +18,7 @@ import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper;
|
||||
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashreports")
|
||||
@Component
|
||||
@@ -34,7 +35,7 @@ public class CashReportApiResource {
|
||||
@GET
|
||||
@Path("closing")
|
||||
public String closing(@QueryParam("date") String date) {
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
|
||||
// Single query — conditional aggregation for deposit, withdrawal, net per denomination
|
||||
final String sql = "SELECT std.denomination_id, std.denomination_value, COALESCE(dm.kind, 'note') AS kind, "
|
||||
@@ -238,7 +239,7 @@ public class CashReportApiResource {
|
||||
return gson.toJson(result);
|
||||
}
|
||||
|
||||
final String today = LocalDate.now().toString();
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
|
||||
// Find the earliest transfer date before today for which no settlement exists on or after that date
|
||||
final String sql = "SELECT MIN(ct.transfer_date) AS earliest_unsettled "
|
||||
@@ -256,7 +257,9 @@ public class CashReportApiResource {
|
||||
java.sql.Date earliestDate = null;
|
||||
try {
|
||||
earliestDate = jdbcTemplate.queryForObject(sql, java.sql.Date.class, cashierId, today, cashierId);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
final boolean unsettled = earliestDate != null;
|
||||
final String unsettledSince = earliestDate != null ? earliestDate.toString() : null;
|
||||
@@ -265,7 +268,9 @@ public class CashReportApiResource {
|
||||
if (unsettled) {
|
||||
try {
|
||||
pendingAmount = denominationBreakdownWriter.getCashierTotalCash(cashierId);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether today's morning cash transfer has been received
|
||||
@@ -276,7 +281,9 @@ public class CashReportApiResource {
|
||||
Integer todayTransferCount = 0;
|
||||
try {
|
||||
todayTransferCount = jdbcTemplate.queryForObject(morningTransferSql, Integer.class, cashierId, today);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
final boolean morningTransferReceived = todayTransferCount != null && todayTransferCount > 0;
|
||||
|
||||
result.put("cashierId", cashierId);
|
||||
@@ -295,7 +302,7 @@ public class CashReportApiResource {
|
||||
@Path("cashier-position")
|
||||
public String cashierPosition(@QueryParam("cashierId") Long cashierId,
|
||||
@QueryParam("asOfDate") String asOfDate) {
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
|
||||
// CASHIER role: always scope to their own position only
|
||||
final Long scopedId = cashierRoleHelper.getCashierIdForFiltering();
|
||||
@@ -326,7 +333,7 @@ public class CashReportApiResource {
|
||||
@GET
|
||||
@Path("cashier-position/me")
|
||||
public String cashierPositionMe(@QueryParam("asOfDate") String asOfDate) {
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now().toString();
|
||||
final String dateFilter = (asOfDate != null && !asOfDate.isBlank()) ? asOfDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
Long appUserId;
|
||||
try {
|
||||
appUserId = context.authenticatedUser().getId();
|
||||
@@ -362,7 +369,7 @@ public class CashReportApiResource {
|
||||
@Path("cashier-morning")
|
||||
public String cashierMorning(@QueryParam("cashierId") Long cashierId,
|
||||
@QueryParam("date") String date) {
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
if (cashierId == null) {
|
||||
Map<String, Object> empty = new LinkedHashMap<>();
|
||||
empty.put("cashierId", null);
|
||||
@@ -515,7 +522,9 @@ public class CashReportApiResource {
|
||||
cashierName = jdbcTemplate.queryForObject(
|
||||
"SELECT ms.display_name FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id WHERE mc.id = ?",
|
||||
String.class, cashierId);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("cashierId", cashierId);
|
||||
@@ -562,7 +571,7 @@ public class CashReportApiResource {
|
||||
@QueryParam("offset") Integer offset,
|
||||
@QueryParam("limit") Integer limit) {
|
||||
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||
final String txnDate = (date != null && !date.isBlank()) ? date : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
final int resolvedLimit = (limit != null && limit > 0) ? limit : 20;
|
||||
final int resolvedOffset = (offset != null && offset >= 0) ? offset : 0;
|
||||
|
||||
|
||||
+24
-11
@@ -36,6 +36,7 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashsettlement")
|
||||
@Component
|
||||
@@ -70,7 +71,7 @@ public class CashSettlementApiResource {
|
||||
}
|
||||
|
||||
final LocalDate settlementDate = json.has("settlementDate") && !json.get("settlementDate").isJsonNull()
|
||||
? LocalDate.parse(json.get("settlementDate").getAsString()) : LocalDate.now();
|
||||
? LocalDate.parse(json.get("settlementDate").getAsString()) : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull()
|
||||
? json.get("fromParty").getAsString() : null;
|
||||
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull()
|
||||
@@ -116,13 +117,15 @@ public class CashSettlementApiResource {
|
||||
header.setToParty(toParty);
|
||||
header.setSettlementAmount(total);
|
||||
header.setNote(note);
|
||||
header.setCreatedAt(LocalDateTime.now());
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
header.setIsReversed(false);
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
header.setPerformedBy(user.getId());
|
||||
header.setPerformedByUsername(user.getUsername());
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
// Commit header + details first, then GL dispatch separately to avoid
|
||||
// EclipseLink flushing pending detail rows inside Fineract's GL transaction.
|
||||
@@ -142,7 +145,9 @@ public class CashSettlementApiResource {
|
||||
Long resolved = jdbcTemplate.queryForObject(
|
||||
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, fromCashierId);
|
||||
if (resolved != null) glTellerId = resolved;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
glDispatchService.settleCashFromCashier(glTellerId, fromCashierId, total,
|
||||
currencyCode, settlementDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
note != null ? note : "");
|
||||
@@ -174,13 +179,17 @@ public class CashSettlementApiResource {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
reason = json.has("reason") ? json.get("reason").getAsString() : null;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
AppUser user = null;
|
||||
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {}
|
||||
try { user = this.context.authenticatedUser(); } catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
settlement.setIsReversed(true);
|
||||
settlement.setReversedAt(LocalDateTime.now());
|
||||
settlement.setReversedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
settlement.setReverseReason(reason);
|
||||
if (user != null) settlement.setReversedBy(user.getId());
|
||||
repository.save(settlement);
|
||||
@@ -192,12 +201,16 @@ public class CashSettlementApiResource {
|
||||
Long resolved = jdbcTemplate.queryForObject(
|
||||
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, settlement.getFromCashierId());
|
||||
if (resolved != null) glTellerId = resolved;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
glDispatchService.allocateCashToCashier(glTellerId,
|
||||
settlement.getFromCashierId(), settlement.getSettlementAmount(), "INR",
|
||||
LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
"Reversal of cash settlement #" + id + (reason != null ? ": " + reason : ""));
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject result = new JsonObject();
|
||||
@@ -214,7 +227,7 @@ public class CashSettlementApiResource {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashsettlement.cashier.required",
|
||||
"cashierId query param is required.");
|
||||
}
|
||||
final String settlementDate = (date != null && !date.isBlank()) ? date : LocalDate.now().toString();
|
||||
final String settlementDate = (date != null && !date.isBlank()) ? date : LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM m_cash_settlement WHERE from_cashier_id = ? AND settlement_date = CAST(? AS DATE) AND (is_reversed IS NULL OR is_reversed = false)",
|
||||
Integer.class, cashierId, settlementDate);
|
||||
|
||||
+20
-9
@@ -36,6 +36,7 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/cashtransfer")
|
||||
@Component
|
||||
@@ -105,7 +106,7 @@ public class CashTransferApiResource {
|
||||
header.setToCashierId(toCashierId);
|
||||
header.setTransferAmount(total);
|
||||
header.setNote(note);
|
||||
header.setCreatedAt(LocalDateTime.now());
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
if (user != null) {
|
||||
@@ -136,8 +137,10 @@ public class CashTransferApiResource {
|
||||
Long resolved = jdbcTemplate.queryForObject(
|
||||
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, toCashierId);
|
||||
if (resolved != null) glTellerId = resolved;
|
||||
} catch (Exception ignored) {}
|
||||
final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now();
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
glDispatchService.allocateCashToCashier(glTellerId, toCashierId, total,
|
||||
currencyCode, txnDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
note != null ? note : "");
|
||||
@@ -169,15 +172,19 @@ public class CashTransferApiResource {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
reason = json.has("reason") ? json.get("reason").getAsString() : null;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
transfer.setReversedBy(user.getId());
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
transfer.setIsReversed(true);
|
||||
transfer.setReversedAt(java.time.LocalDateTime.now());
|
||||
transfer.setReversedAt(java.time.LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
transfer.setReverseReason(reason);
|
||||
repository.save(transfer);
|
||||
|
||||
@@ -188,12 +195,16 @@ public class CashTransferApiResource {
|
||||
Long resolved = jdbcTemplate.queryForObject(
|
||||
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, transfer.getToCashierId());
|
||||
if (resolved != null) glTellerId = resolved;
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
glDispatchService.settleCashFromCashier(glTellerId,
|
||||
transfer.getToCashierId(), transfer.getTransferAmount(), "INR",
|
||||
java.time.LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
java.time.LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
"Reversal of cash transfer #" + id + (reason != null ? ": " + reason : ""));
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject result = new JsonObject();
|
||||
|
||||
+6
-3
@@ -38,6 +38,7 @@ import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomin
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Records cashier denomination breakdown for GL account transactions (journal entries).
|
||||
@@ -90,7 +91,7 @@ public class CashierGlDenominationApiResource {
|
||||
final String txnDateStr = fromJsonHelper.extractStringNamed("txnDate", root);
|
||||
final LocalDate txnDate = (txnDateStr != null && !txnDateStr.isBlank())
|
||||
? LocalDate.parse(txnDateStr)
|
||||
: LocalDate.now();
|
||||
: LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
if (!fromJsonHelper.parameterExists("denominationBreakdown", root)) {
|
||||
return "{\"result\":\"skipped\"}";
|
||||
@@ -109,7 +110,9 @@ public class CashierGlDenominationApiResource {
|
||||
officerId = user.getId();
|
||||
officerUsername = user.getUsername();
|
||||
}
|
||||
} catch (final Exception ignored) {}
|
||||
} catch (final Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
|
||||
final String resolvedTxnType = (txnType != null && !txnType.isBlank())
|
||||
? txnType.toUpperCase()
|
||||
@@ -144,7 +147,7 @@ public class CashierGlDenominationApiResource {
|
||||
.count(count)
|
||||
.totalAmount(lineTotal)
|
||||
.txnDate(txnDate)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()))
|
||||
.txnType(resolvedTxnType)
|
||||
.officerId(officerId)
|
||||
.officerUsername(officerUsername)
|
||||
|
||||
+3
-2
@@ -29,6 +29,7 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Cashier Share Collection API — POST /v1/cashier/sharecollection
|
||||
@@ -131,7 +132,7 @@ public class ShareCollectionApiResource {
|
||||
final LocalDate entryDate;
|
||||
try {
|
||||
entryDate = transactionDateStr != null
|
||||
? LocalDate.parse(transactionDateStr) : LocalDate.now();
|
||||
? LocalDate.parse(transactionDateStr) : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new PlatformDataIntegrityException(
|
||||
"error.msg.sharecollection.date.invalid",
|
||||
@@ -193,7 +194,7 @@ public class ShareCollectionApiResource {
|
||||
final String txnId = "SC-" + shareAccountId + "-" + System.currentTimeMillis();
|
||||
final long userId = currentUser.getId();
|
||||
final String collectorName = currentUser.getUsername();
|
||||
final LocalDateTime now = LocalDateTime.now();
|
||||
final LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
// ── Post GL journal entries ────────────────────────────────────────
|
||||
// Dr Cash at Vault (SHARES_REFERENCE) = totalAmount
|
||||
|
||||
-1
@@ -41,7 +41,6 @@ import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSer
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintStatsData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintTicketData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.TimelineEventData;
|
||||
import org.apache.fineract.portfolio.cfcfrms.service.CfcfrmsComplaintService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ import org.springframework.stereotype.Component;
|
||||
* POST /v1/simulator/cfcfrms/verify-account – Verify account at bank
|
||||
* POST /v1/simulator/cfcfrms/verify-transaction – Verify transaction ref
|
||||
*
|
||||
* --- STATUS & REPORTING ---
|
||||
* --- STATUS & REPORTING ---
|
||||
* GET /v1/simulator/cfcfrms/complaint-status/{ref} – Complaint status from CFCFRMS side
|
||||
* GET /v1/simulator/cfcfrms/bank-performance – Bank performance metrics
|
||||
* </pre>
|
||||
|
||||
+2
-1
@@ -36,6 +36,7 @@ import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Seeds CFCFRMS complaint tickets, audit logs, and simulator logs on startup.
|
||||
@@ -59,7 +60,7 @@ public class CfcfrmsDataSeeder {
|
||||
}
|
||||
|
||||
LOG.info("Seeding CFCFRMS complaint data...");
|
||||
final OffsetDateTime now = OffsetDateTime.now();
|
||||
final OffsetDateTime now = OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
// ─── 3 RECEIVED ──────────────────────────────────────────────
|
||||
seedTicket("TKT-20250318-001", "Rajesh Kumar Sharma", "9876543210",
|
||||
|
||||
+3
-2
@@ -46,6 +46,7 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -206,7 +207,7 @@ public class CfcfrmsComplaintServiceImpl implements CfcfrmsComplaintService {
|
||||
final Map<String, Object> result = new HashMap<>();
|
||||
result.put("escalatedTo", nextIfsc);
|
||||
result.put("escalationRef", escalationRef);
|
||||
result.put("timestamp", OffsetDateTime.now().toString());
|
||||
result.put("timestamp", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -218,7 +219,7 @@ public class CfcfrmsComplaintServiceImpl implements CfcfrmsComplaintService {
|
||||
}
|
||||
|
||||
private String generateTicketId() {
|
||||
final String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
final String date = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
final int seq = (int) (ticketRepository.count() + 1);
|
||||
return String.format("TKT-%s-%03d", date, seq);
|
||||
}
|
||||
|
||||
+8
-9
@@ -25,12 +25,10 @@ import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.cfcfrms.data.CfcfrmsConstants;
|
||||
@@ -44,6 +42,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -76,7 +75,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("cfcfrmsRef", cfcfrmsRef);
|
||||
response.put("ticketId", ticketData.getTicketId());
|
||||
response.put("pushedToBankAt", OffsetDateTime.now().toString());
|
||||
response.put("pushedToBankAt", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
response.put("bankAcknowledged", true);
|
||||
response.put("message", "Complaint pushed to " + CfcfrmsConstants.BANK_IFSC + " successfully");
|
||||
return response;
|
||||
@@ -85,7 +84,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
@Override
|
||||
public Map<String, Object> pushHoldDirective(final JsonObject json) {
|
||||
final String cfcfrmsRef = getStringOrThrow(json, "cfcfrmsRef");
|
||||
final String directiveRef = "DIR-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
|
||||
final String directiveRef = "DIR-" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyyMMdd"))
|
||||
+ "-" + String.format("%04d", RANDOM.nextInt(10000));
|
||||
|
||||
final String directiveType = getStringOrThrow(json, "directiveType");
|
||||
@@ -112,7 +111,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
response.put("directiveRef", directiveRef);
|
||||
response.put("status", "DIRECTIVE_SENT");
|
||||
response.put("bankResponse", bankResponse);
|
||||
response.put("timestamp", OffsetDateTime.now().toString());
|
||||
response.put("timestamp", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -208,7 +207,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
public Map<String, Object> receiveEscalation(final JsonObject json) {
|
||||
final String cfcfrmsRef = getStringOrThrow(json, "cfcfrmsRef");
|
||||
final String ticketId = getStringOrThrow(json, "ticketId");
|
||||
final String escalationRef = "ESC-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MMdd"))
|
||||
final String escalationRef = "ESC-" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyy-MMdd"))
|
||||
+ "-" + String.format("%04d", RANDOM.nextInt(10000));
|
||||
|
||||
// Extract nextBank from fundsMovedTo
|
||||
@@ -268,7 +267,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
final Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("transactionFound", true);
|
||||
response.put("transactionStatus", "COMPLETED");
|
||||
response.put("transactionTimestamp", OffsetDateTime.now().minusHours(2).toString());
|
||||
response.put("transactionTimestamp", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).minusHours(2).toString());
|
||||
response.put("senderAccount", json.has("accountNumber") ? json.get("accountNumber").getAsString() : "UNKNOWN");
|
||||
response.put("receiverAccount", json.has("accountNumber") ? json.get("accountNumber").getAsString() : "UNKNOWN");
|
||||
|
||||
@@ -369,14 +368,14 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
|
||||
.setScale(1, java.math.RoundingMode.HALF_UP) + "%"
|
||||
: "0.0%";
|
||||
response.put("tatComplianceRate", tatCompliance);
|
||||
response.put("lastActivityAt", OffsetDateTime.now().toString());
|
||||
response.put("lastActivityAt", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
private String generateCfcfrmsRef() {
|
||||
final String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MMdd"));
|
||||
final String date = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyy-MMdd"));
|
||||
return "CFCFRMS-" + date + "-" + String.format("%04d", RANDOM.nextInt(10000));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ public class NpciInwardPushService {
|
||||
final String presentingBranchCode = getString(instrument, "depositBranchCode");
|
||||
item.recordInwardMeta(getString(instrument, "serialNumber"), presentingBranchCode, batch.getReturnTatDeadline());
|
||||
|
||||
final Boolean instrumentStopPaymentFlag = instrument.has("stopPaymentFlag") && !instrument.get("stopPaymentFlag").isJsonNull()
|
||||
final Boolean instrumentStopPaymentFlag = (instrument.has("stopPaymentFlag") && !instrument.get("stopPaymentFlag").isJsonNull())
|
||||
? instrument.get("stopPaymentFlag").getAsBoolean()
|
||||
: null;
|
||||
item.recordSourceDetails(getString(instrument, "ctsReferenceNumber"), instrumentStopPaymentFlag,
|
||||
|
||||
+11
-54
@@ -70,6 +70,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Path("/v1/clerk/disbursement-requests")
|
||||
@Component
|
||||
@@ -383,7 +384,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
final JsonArray creditEntries = gson.fromJson(creditEntriesJson, JsonArray.class);
|
||||
final Object ptId = record.get("payment_type_id");
|
||||
final String description = (String) record.get("description");
|
||||
final String txnDateStr = LocalDate.now().toString();
|
||||
final String txnDateStr = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
|
||||
|
||||
final Long sourceGlId = record.get("gl_account_id") != null
|
||||
? ((Number) record.get("gl_account_id")).longValue() : null;
|
||||
@@ -556,7 +557,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
final String entryType = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS";
|
||||
final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : "?";
|
||||
final Long entryPtId = entry.has("paymentTypeId") && !entry.get("paymentTypeId").isJsonNull()
|
||||
? entry.get("paymentTypeId").getAsLong()
|
||||
? Long.valueOf(entry.get("paymentTypeId").getAsLong())
|
||||
: (ptId != null ? ((Number) ptId).longValue() : null);
|
||||
|
||||
if ("GL".equals(entryType)) {
|
||||
@@ -810,7 +811,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
? req.get("currencyCode").getAsString() : "INR";
|
||||
final String txnDateStr = req.has("transactionDate") && !req.get("transactionDate").isJsonNull()
|
||||
? req.get("transactionDate").getAsString()
|
||||
: LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
: LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
final String description = req.has("description") && !req.get("description").isJsonNull()
|
||||
? req.get("description").getAsString() : null;
|
||||
|
||||
@@ -830,7 +831,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
|
||||
// Validate all entries: savings/current only (no GL), savingsAccountId present, amount > 0
|
||||
final Set<Long> drAccountIds = new LinkedHashSet<>();
|
||||
final Set<Long> crAccountIds = new LinkedHashSet<>();
|
||||
|
||||
for (final JsonObject e : drEntries) {
|
||||
final String acTyp = e.has("accountType") ? e.get("accountType").getAsString() : "SAVINGS";
|
||||
@@ -858,7 +858,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
if (drAccountIds.contains(crId)) throw apiError(400,
|
||||
"Account " + (e.has("accountNo") ? e.get("accountNo").getAsString() : crId)
|
||||
+ " cannot appear on both DR and CR sides of the same transfer.");
|
||||
crAccountIds.add(crId);
|
||||
}
|
||||
|
||||
// Validate balanced entry: total DR == total CR
|
||||
@@ -999,7 +998,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
* under concurrent inserts on the same date.
|
||||
*/
|
||||
private String generateBtVoucherNo(final long logId) {
|
||||
return "BT" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd"))
|
||||
return "BT" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyMMdd"))
|
||||
+ String.format("%05d", logId);
|
||||
}
|
||||
|
||||
@@ -1053,7 +1052,9 @@ public class ClerkDisbursementRequestApiResource {
|
||||
try {
|
||||
final Long txnIdLong = Long.parseLong(txnId);
|
||||
undoOps.add(() -> undoAccountTransaction(accountId, txnIdLong, isCurrentAccount));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
|
||||
private void undoAccountTransaction(final Long accountId, final Long transactionId,
|
||||
@@ -1072,50 +1073,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
commandsSourceWritePlatformService.logCommandSource(cmd);
|
||||
}
|
||||
|
||||
private String executeSavingsDeposit(
|
||||
final Long savingsAccountId, final BigDecimal amount,
|
||||
final Long paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String date = formatForFineract(txnDateStr);
|
||||
final JsonObject payload = new JsonObject();
|
||||
payload.addProperty("locale", "en");
|
||||
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||
payload.addProperty("transactionDate", date);
|
||||
payload.addProperty("transactionAmount", amount);
|
||||
if (paymentTypeId != null) payload.addProperty("paymentTypeId", paymentTypeId);
|
||||
if (note != null && !note.isBlank()) payload.addProperty("note", note);
|
||||
|
||||
final CommandWrapper cmd = new CommandWrapperBuilder()
|
||||
.savingsAccountDeposit(savingsAccountId)
|
||||
.withJson(payload.toString())
|
||||
.build();
|
||||
fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, null);
|
||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(cmd);
|
||||
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
||||
}
|
||||
|
||||
private String executeCurrentAccountDeposit(
|
||||
final Long currentAccountId, final BigDecimal amount,
|
||||
final Long paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String date = formatForFineract(txnDateStr);
|
||||
final JsonObject payload = new JsonObject();
|
||||
payload.addProperty("locale", "en");
|
||||
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||
payload.addProperty("transactionDate", date);
|
||||
payload.addProperty("transactionAmount", amount);
|
||||
if (paymentTypeId != null) payload.addProperty("paymentTypeId", paymentTypeId);
|
||||
if (note != null && !note.isBlank()) payload.addProperty("note", note);
|
||||
|
||||
final CommandWrapper cmd = new CommandWrapperBuilder()
|
||||
.currentAccountDeposit(currentAccountId)
|
||||
.withJson(payload.toString())
|
||||
.build();
|
||||
fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, null);
|
||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(cmd);
|
||||
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
||||
}
|
||||
|
||||
private boolean isCurrentAccountById(final Long accountId) {
|
||||
try {
|
||||
final Integer count = jdbcTemplate.queryForObject(
|
||||
@@ -1131,7 +1088,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
} catch (Exception e) {
|
||||
return LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
return LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1380,7 +1337,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
? (BigDecimal) accts.get(0).get("account_balance_derived") : BigDecimal.ZERO;
|
||||
|
||||
final java.sql.Date sqlDate = java.sql.Date.valueOf(txnDate);
|
||||
final OffsetDateTime now = OffsetDateTime.now();
|
||||
final OffsetDateTime now = OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String refNo = UUID.randomUUID().toString().replace("-", "").substring(0, 20);
|
||||
final Long userId = currentUser.getId();
|
||||
|
||||
@@ -1534,7 +1491,7 @@ public class ClerkDisbursementRequestApiResource {
|
||||
+ ". Available: " + currentBalance + ", Requested: " + amount);
|
||||
|
||||
final java.sql.Date sqlDate = java.sql.Date.valueOf(txnDate);
|
||||
final OffsetDateTime now = OffsetDateTime.now();
|
||||
final OffsetDateTime now = OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String refNo = UUID.randomUUID().toString().replace("-", "").substring(0, 20);
|
||||
final Long userId = currentUser.getId();
|
||||
|
||||
|
||||
+7
-4
@@ -59,6 +59,9 @@ import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.base.Splitter;
|
||||
|
||||
@Path("/v1/clerk/withdrawal-requests")
|
||||
@Component
|
||||
@@ -437,7 +440,7 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final BigDecimal amount = new BigDecimal(record.get("amount").toString());
|
||||
final Object ptId = record.get("payment_type_id");
|
||||
final Object txnDateObj = record.get("txn_date");
|
||||
final String txnDateStr = txnDateObj != null ? txnDateObj.toString().split(" ")[0] : null;
|
||||
final String txnDateStr = txnDateObj != null ? Iterables.get(Splitter.on(' ').split(txnDateObj.toString()), 0) : null;
|
||||
final String description = (String) record.get("description");
|
||||
|
||||
String txnId = null;
|
||||
@@ -713,7 +716,7 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final Long savingsAccountId, final BigDecimal amount,
|
||||
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
final JsonObject payload = new JsonObject();
|
||||
@@ -741,7 +744,7 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final Long currentAccountId, final BigDecimal amount,
|
||||
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||
|
||||
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
final JsonObject payload = new JsonObject();
|
||||
@@ -780,7 +783,7 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
} catch (Exception e) {
|
||||
return LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
return LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.data.ClientPendingEditData;
|
||||
import org.apache.fineract.portfolio.client.service.ClientEditWritePlatformService;
|
||||
import org.apache.fineract.portfolio.client.service.ClientPendingEditReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/clients")
|
||||
@Component
|
||||
@Tag(name = "Client Edit Approval",
|
||||
description = "Maker-checker workflow for editing an existing Client's core profile details. Edits are held in "
|
||||
+ "m_client_pending_edit (a pending row there is itself the lock) and only applied to the real client once a "
|
||||
+ "different user (the checker) approves — see /clients/{clientId}/edit/approve and /reject below.")
|
||||
@RequiredArgsConstructor
|
||||
public class ClientEditApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClientEditWritePlatformService clientEditWritePlatformService;
|
||||
private final ClientPendingEditReadPlatformService clientPendingEditReadPlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||
private final DefaultToApiJsonSerializer<CommandProcessingResult> toApiJsonSerializer;
|
||||
private final DefaultToApiJsonSerializer<ClientPendingEditData> pendingEditApiJsonSerializer;
|
||||
|
||||
@POST
|
||||
@Path("{clientId}/edit")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Submit a details edit for a Client",
|
||||
description = "Stages the proposed changes in m_client_pending_edit instead of applying them immediately — see "
|
||||
+ "/clients/{clientId}/edit/approve and /reject below.")
|
||||
public String submitEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
|
||||
this.context.authenticatedUser().validateHasPermissionTo("SUBMITEDIT_CLIENT");
|
||||
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(clientId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
|
||||
this.fromApiJsonHelper);
|
||||
|
||||
final CommandProcessingResult result = this.clientEditWritePlatformService.submitEditForApproval(clientId, command);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("edit/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Client details edits pending approval",
|
||||
description = "Supports status/clientName/fromDate/toDate filtering (fromDate/toDate as yyyy-MM-dd, inclusive, filtered "
|
||||
+ "on submitted date; clientName is a case-insensitive substring match) plus offset/limit pagination — mirrors "
|
||||
+ "/chequebook/pending-requests.")
|
||||
public Page<ClientPendingEditData> retrieveAllPendingEdits(@QueryParam("status") final String status,
|
||||
@QueryParam("clientName") final String clientName, @QueryParam("fromDate") final String fromDate,
|
||||
@QueryParam("toDate") final String toDate, @QueryParam("offset") @DefaultValue("0") final Integer offset,
|
||||
@QueryParam("limit") @DefaultValue("20") final Integer limit) {
|
||||
|
||||
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||
|
||||
final int safeLimit = Math.min(limit, 200);
|
||||
return this.clientPendingEditReadPlatformService.retrieveAllPendingEdits(status, clientName, fromDate, toDate, offset, safeLimit);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{clientId}/edit/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve the pending details edit for a Client, if any")
|
||||
public String retrievePendingEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||
@Context final UriInfo uriInfo) {
|
||||
|
||||
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||
|
||||
final ClientPendingEditData pendingEdit = this.clientPendingEditReadPlatformService.retrievePendingEdit(clientId);
|
||||
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.pendingEditApiJsonSerializer.serialize(settings, pendingEdit);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{clientId}/edit/approve")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Approve a pending Client details edit",
|
||||
description = "Applies the proposed changes to the client. The maker of the edit cannot also approve it — no exceptions.")
|
||||
public String approvePendingEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId) {
|
||||
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVEEDIT_CLIENT")) {
|
||||
throw new NoAuthorizationException("User has no authority to approve client edits");
|
||||
}
|
||||
|
||||
final CommandProcessingResult result = this.clientEditWritePlatformService.approveEdit(clientId);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{clientId}/edit/reject")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Reject a pending Client details edit",
|
||||
description = "Discards the proposed changes without applying them. The maker of the edit cannot also reject it — no exceptions.")
|
||||
public String rejectPendingEdit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECTEDIT_CLIENT")) {
|
||||
throw new NoAuthorizationException("User has no authority to reject client edits");
|
||||
}
|
||||
|
||||
String rejectReason = null;
|
||||
if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) {
|
||||
final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson);
|
||||
if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) {
|
||||
rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString();
|
||||
}
|
||||
}
|
||||
|
||||
final CommandProcessingResult result = this.clientEditWritePlatformService.rejectEdit(clientId, rejectReason);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
}
|
||||
+20
-2
@@ -39,6 +39,8 @@ import jakarta.ws.rs.core.MediaType;
|
||||
import java.io.InputStream;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -158,7 +160,13 @@ public class ClientKycApiResource {
|
||||
template.put("clientTypeName", clientTypeName);
|
||||
template.put("legalFormId", legalFormId);
|
||||
|
||||
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId));
|
||||
// Minor detection mirrors the frontend's own rule (age < 18) so the document list
|
||||
// served here stays consistent with what the UI computes from the same date of birth.
|
||||
final LocalDate dateOfBirth = client.dateOfBirth();
|
||||
final boolean isMinor = dateOfBirth != null && Period.between(dateOfBirth, LocalDate.now()).getYears() < 18;
|
||||
template.put("isMinor", isMinor);
|
||||
|
||||
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId, isMinor));
|
||||
|
||||
return toApiJsonSerializer.serialize(template);
|
||||
}
|
||||
@@ -166,11 +174,21 @@ public class ClientKycApiResource {
|
||||
/**
|
||||
* Returns required KYC documents based on client type (RBI Master Direction on KYC).
|
||||
*/
|
||||
private List<Map<String, Object>> getRequiredDocuments(final String clientTypeName, final Integer legalFormId) {
|
||||
private List<Map<String, Object>> getRequiredDocuments(final String clientTypeName, final Integer legalFormId,
|
||||
final boolean isMinor) {
|
||||
final List<Map<String, Object>> docs = new ArrayList<>();
|
||||
|
||||
// Individual types (legalFormId=1)
|
||||
if (legalFormId != null && legalFormId == 1) {
|
||||
if (isMinor) {
|
||||
// Minors cannot hold PAN, Voter ID, Driving Licence or an NREGA job card — RBI
|
||||
// accepts Aadhaar, Passport, Birth Certificate or School ID Card instead.
|
||||
docs.add(doc("Aadhaar Card", "AADHAAR", true));
|
||||
docs.add(doc("Birth Certificate", "BIRTH_CERTIFICATE", false));
|
||||
docs.add(doc("School ID Card", "SCHOOL_ID_CARD", false));
|
||||
docs.add(doc("Passport", "PASSPORT", false));
|
||||
return docs;
|
||||
}
|
||||
docs.add(doc("Aadhaar Card", "AADHAAR", true));
|
||||
docs.add(doc("PAN Card", "PAN", true));
|
||||
docs.add(doc("Voter ID", "VOTER_ID", false));
|
||||
|
||||
+13
-5
@@ -44,6 +44,7 @@ import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.commands.data.AuditData;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.AuditReadPlatformService;
|
||||
@@ -142,7 +143,8 @@ public class ClientsApiResource {
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Clients", description = "The list capability of clients can support pagination and sorting.\n\n"
|
||||
+ "Example Requests:\n" + "\n" + "clients\n" + "\n" + "clients?fields=displayName,officeName,timeline\n" + "\n"
|
||||
+ "clients?offset=10&limit=50\n" + "\n" + "clients?orderBy=displayName&sortOrder=DESC")
|
||||
+ "clients?offset=10&limit=50\n" + "\n" + "clients?orderBy=displayName&sortOrder=DESC\n\n"
|
||||
+ "clients?fromDate=2026-01-01&toDate=2026-01-31 (submittedon_date range, yyyy-MM-dd, inclusive)")
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ClientsApiResourceSwagger.GetClientsResponse.class)))
|
||||
public String retrieveAll(@Context final UriInfo uriInfo,
|
||||
@QueryParam("officeId") @Parameter(description = "officeId") final Long officeId,
|
||||
@@ -157,10 +159,15 @@ public class ClientsApiResource {
|
||||
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
|
||||
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
|
||||
@QueryParam("orphansOnly") @Parameter(description = "orphansOnly") final Boolean orphansOnly,
|
||||
@QueryParam("legalForm") final Integer legalForm) {
|
||||
@QueryParam("legalForm") final Integer legalForm,
|
||||
@QueryParam("fromDate") @Parameter(description = "fromDate") final String fromDate,
|
||||
@QueryParam("toDate") @Parameter(description = "toDate") final String toDate) {
|
||||
|
||||
final LocalDate fromDateParsed = StringUtils.isNotBlank(fromDate) ? LocalDate.parse(fromDate) : null;
|
||||
final LocalDate toDateParsed = StringUtils.isNotBlank(toDate) ? LocalDate.parse(toDate) : null;
|
||||
|
||||
return retrieveAll(uriInfo, officeId, externalId, displayName, firstname, lastname, status, legalForm, hierarchy, offset, limit,
|
||||
orderBy, sortOrder, orphansOnly, false);
|
||||
orderBy, sortOrder, orphansOnly, false, fromDateParsed, toDateParsed);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -437,7 +444,7 @@ public class ClientsApiResource {
|
||||
public String retrieveAll(final UriInfo uriInfo, final Long officeId, final String externalId, final String displayName,
|
||||
final String firstname, final String lastname, final String status, final Integer legalForm, final String hierarchy,
|
||||
final Integer offset, final Integer limit, final String orderBy, final String sortOrder, final Boolean orphansOnly,
|
||||
final boolean isSelfUser) {
|
||||
final boolean isSelfUser, final LocalDate fromDate, final LocalDate toDate) {
|
||||
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||
sqlValidator.validate(orderBy);
|
||||
sqlValidator.validate(sortOrder);
|
||||
@@ -445,7 +452,8 @@ public class ClientsApiResource {
|
||||
sqlValidator.validate(hierarchy);
|
||||
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).officeId(officeId).externalId(externalId)
|
||||
.name(displayName).hierarchy(hierarchy).firstname(firstname).lastname(lastname).status(status).orphansOnly(orphansOnly)
|
||||
.isSelfUser(isSelfUser).offset(offset).orderBy(orderBy).sortOrder(sortOrder).legalForm(legalForm).build();
|
||||
.isSelfUser(isSelfUser).offset(offset).orderBy(orderBy).sortOrder(sortOrder).legalForm(legalForm).fromDate(fromDate)
|
||||
.toDate(toDate).build();
|
||||
final Page<ClientData> clientData = clientReadPlatformService.retrieveAll(searchParameters);
|
||||
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return toApiJsonSerializer.serialize(settings, clientData, ClientApiConstants.CLIENT_RESPONSE_DATA_PARAMETERS);
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.data.KycRiskRuleData;
|
||||
import org.apache.fineract.portfolio.client.service.KycRiskRulesReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/kyc/risk-rules")
|
||||
@Component
|
||||
@Tag(name = "KYC Risk Rules", description = "Retrieve database-driven master rules for client AML/CFT risk rating calculation.")
|
||||
@RequiredArgsConstructor
|
||||
public class KycRiskRulesApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final KycRiskRulesReadPlatformService readPlatformService;
|
||||
private final DefaultToApiJsonSerializer<KycRiskRuleData> toApiJsonSerializer;
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve KYC risk rules", description = "Returns list of database-defined risk rules ordered by precedence.")
|
||||
public String retrieveRiskRules() {
|
||||
this.context.authenticatedUser();
|
||||
final List<KycRiskRuleData> rules = this.readPlatformService.retrieveAllRules();
|
||||
return this.toApiJsonSerializer.serialize(rules);
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientKyc;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Dummy / stub implementation of {@link CkycSftpUploadService}.
|
||||
@@ -60,7 +61,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
|
||||
|
||||
final String ckycNumber = generateCkycNumber(kyc.getClient().getId());
|
||||
final String batchId = generateBatchId();
|
||||
final LocalDate today = LocalDate.now();
|
||||
final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
return CkycUploadResult.builder()
|
||||
.ckycNumber(ckycNumber)
|
||||
@@ -82,7 +83,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
|
||||
return CkycUploadResult.builder()
|
||||
.status("FAILED")
|
||||
.remarks("Uploaded file is not a valid ZIP archive (bad magic bytes).")
|
||||
.uploadedOn(LocalDate.now())
|
||||
.uploadedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
|
||||
return CkycUploadResult.builder()
|
||||
.ckycNumber(ckycNumber)
|
||||
.batchId(batchId)
|
||||
.uploadedOn(LocalDate.now())
|
||||
.uploadedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))
|
||||
.status("SUCCESS")
|
||||
.remarks("Dummy CKYC ZIP upload accepted (" + zipBytes.length + " bytes). "
|
||||
+ "Replace with real CERSAI SFTP integration before go-live.")
|
||||
@@ -112,7 +113,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
|
||||
}
|
||||
|
||||
private String generateBatchId() {
|
||||
return "DUMMYCKYC-" + BATCH_ID_FORMAT.format(LocalDate.now()) + "-"
|
||||
return "DUMMYCKYC-" + BATCH_ID_FORMAT.format(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())) + "-"
|
||||
+ String.format("%06d", BATCH_COUNTER.getAndIncrement());
|
||||
}
|
||||
}
|
||||
|
||||
+59
-37
@@ -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;
|
||||
@@ -50,6 +49,7 @@ import org.apache.fineract.portfolio.client.ckyc.domain.CkycBatchRepository;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -61,16 +61,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<String> 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;
|
||||
@@ -86,11 +92,11 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
||||
throw new IllegalArgumentException("clientIds must not be empty");
|
||||
}
|
||||
|
||||
final LocalDate today = LocalDate.now();
|
||||
final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final int seq = nextBatchSequenceForToday(today);
|
||||
final String batchId = "BATCH-" + BATCH_ID_FMT.format(today) + "-" + String.format("%03d", seq);
|
||||
// ZIP filename: "CERSAI-ZIP-<yyyy-MM-dd-HH-mm-ss>.zip" — hyphen-separated per spec.
|
||||
final String filename = "CERSAI-ZIP-" + ZIP_NAME_TS_FMT.format(java.time.LocalDateTime.now()) + ".zip";
|
||||
final String filename = "CERSAI-ZIP-" + ZIP_NAME_TS_FMT.format(java.time.LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant())) + ".zip";
|
||||
|
||||
final List<CkycBatchManifestEntry> manifest = new ArrayList<>();
|
||||
final ByteArrayOutputStream zipBytesOut = new ByteArrayOutputStream();
|
||||
@@ -111,13 +117,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 +149,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 +196,40 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
||||
entry.setCifNumber(cifNumber);
|
||||
entry.setDisplayName(displayName);
|
||||
|
||||
// Photo — packaged as "<CIF>-PI-PHOTO.<ext>" using m_client.image_id → m_image
|
||||
// Photo — packaged as "PHOTO/<CIF>-PI-PHOTO.<ext>" 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<String, Object> 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 "<CIF>-PI-SIGNATURE.<ext>"
|
||||
// Signature — packaged as "SIGNATURE/<CIF>-PI-SIGNATURE.<ext>"
|
||||
final Long signatureDocId = findSignatureDocId(clientId);
|
||||
final Map<String, Object> 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<Map<String, Object>> 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 +241,9 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
|
||||
for (final Map<String, Object> 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 +263,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-7
@@ -26,7 +26,6 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -36,6 +35,7 @@ import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecific
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -219,6 +219,7 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
// ===== Customer List =====
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("StringSplitter")
|
||||
public Map<String, Object> listCustomers(String name, String branch, String city, String gender, String state, String dateFrom,
|
||||
String dateTo, Integer page, Integer pageSize, String sort) {
|
||||
|
||||
@@ -375,13 +376,13 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
switch (format != null ? format.toLowerCase() : "csv") {
|
||||
case "txt":
|
||||
contentType = "text/plain";
|
||||
filename = "ckyc_export_" + LocalDate.now() + ".txt";
|
||||
filename = "ckyc_export_" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()) + ".txt";
|
||||
content = buildPipeDelimitedExport(rows);
|
||||
break;
|
||||
case "csv":
|
||||
default:
|
||||
contentType = "text/csv";
|
||||
filename = "ckyc_export_" + LocalDate.now() + ".csv";
|
||||
filename = "ckyc_export_" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()) + ".csv";
|
||||
content = buildCsvExport(rows);
|
||||
break;
|
||||
}
|
||||
@@ -431,7 +432,7 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
errors.add("Parse error: " + e.getMessage());
|
||||
}
|
||||
|
||||
String fileId = "UPLOAD-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-" + System.currentTimeMillis();
|
||||
String fileId = "UPLOAD-" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-" + System.currentTimeMillis();
|
||||
result.put("fileId", fileId);
|
||||
result.put("totalRecords", totalRecords);
|
||||
result.put("customers", customers);
|
||||
@@ -453,9 +454,9 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
@Override
|
||||
public Response generateCkycFile(Map<String, Object> request) {
|
||||
// Placeholder — delegates to CkycBatchBuilderService for actual ZIP generation
|
||||
String content = "10|BATCH|" + LocalDate.now() + "|0\n";
|
||||
String content = "10|BATCH|" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()) + "|0\n";
|
||||
return Response.ok(content.getBytes(StandardCharsets.UTF_8), "text/plain")
|
||||
.header("Content-Disposition", "attachment; filename=\"ckyc_submission_" + LocalDate.now() + ".txt\"")
|
||||
.header("Content-Disposition", "attachment; filename=\"ckyc_submission_" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()) + ".txt\"")
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -572,7 +573,7 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
|
||||
|
||||
private String buildPipeDelimitedExport(List<Map<String, Object>> rows) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("10|CKYC_EXPORT|").append(LocalDate.now()).append("|").append(rows.size()).append("\n");
|
||||
sb.append("10|CKYC_EXPORT|").append(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())).append("|").append(rows.size()).append("\n");
|
||||
for (Map<String, Object> row : rows) {
|
||||
sb.append("20|");
|
||||
sb.append(val(row.get("cif"))).append("|");
|
||||
|
||||
+3
-2
@@ -47,6 +47,7 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Dummy / stub implementation of {@link CkycBatchSftpUploadService}.
|
||||
@@ -149,10 +150,10 @@ public class DummyCkycBatchSftpUploadServiceImpl implements CkycBatchSftpUploadS
|
||||
}
|
||||
|
||||
// 3) Build the ACK reference and timestamp
|
||||
final LocalDateTime now = LocalDateTime.now();
|
||||
final LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final String ackReference = "CERSAI-ACK-" + ACK_TS_FMT.format(now) + "-"
|
||||
+ String.format("%04d", ACK_COUNTER.getAndIncrement());
|
||||
final LocalDate today = LocalDate.now();
|
||||
final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
// 4) Parse manifest → generate a CKYC number per included client → persist on m_client_kyc
|
||||
final List<Map<String, Object>> perClient = new ArrayList<>();
|
||||
|
||||
+19
-7
@@ -71,6 +71,15 @@ public class ClientKycCommand {
|
||||
}
|
||||
|
||||
public void validateForInitiate() {
|
||||
validateForInitiate(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param skipIdentityDocumentCheck true for minors, who identify via the Birth Certificate /
|
||||
* School ID document-type picker (a separate upload path)
|
||||
* rather than PAN/Aadhaar/GSTIN.
|
||||
*/
|
||||
public void validateForInitiate(final boolean skipIdentityDocumentCheck) {
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
|
||||
|
||||
@@ -101,13 +110,16 @@ public class ClientKycCommand {
|
||||
"GSTIN must be a valid 15-character GST Identification Number");
|
||||
}
|
||||
|
||||
// At least one ID document must be provided
|
||||
final boolean hasPan = panNumber != null && !panNumber.isBlank();
|
||||
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
|
||||
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
|
||||
if (!hasPan && !hasAadhaar && !hasGstin) {
|
||||
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
|
||||
.notBlank();
|
||||
// At least one ID document must be provided — except for minors, who prove identity via
|
||||
// the Birth Certificate / School ID document-type picker instead.
|
||||
if (!skipIdentityDocumentCheck) {
|
||||
final boolean hasPan = panNumber != null && !panNumber.isBlank();
|
||||
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
|
||||
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
|
||||
if (!hasPan && !hasAadhaar && !hasGstin) {
|
||||
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
|
||||
.notBlank();
|
||||
}
|
||||
}
|
||||
|
||||
throwExceptionIfValidationWarningsExist(errors);
|
||||
|
||||
+3
-1
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.client.contactverification.api;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -152,7 +154,7 @@ public class ClientContactVerificationApiResource {
|
||||
}
|
||||
final String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) {
|
||||
return fwd.split(",")[0].trim();
|
||||
return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
}
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
+3
-1
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.client.contactverification.api;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -147,7 +149,7 @@ public class ClientCreationContactVerificationApiResource {
|
||||
}
|
||||
final String fwd = req.getHeader("X-Forwarded-For");
|
||||
if (fwd != null && !fwd.isBlank()) {
|
||||
return fwd.split(",")[0].trim();
|
||||
return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
|
||||
}
|
||||
return req.getRemoteAddr();
|
||||
}
|
||||
|
||||
+2
-1
@@ -53,6 +53,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.HttpServerErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* See {@link ClientContactOtpService}. Deliberately does NOT touch {@code LoginOtpServiceImpl} /
|
||||
@@ -209,7 +210,7 @@ public class ClientContactOtpServiceImpl implements ClientContactOtpService {
|
||||
log.setExpiresAt(OffsetDateTime.now(ZoneOffset.UTC).plusSeconds(ttlSeconds));
|
||||
|
||||
final String html = emailTemplateService.renderOtp(null, otp, validityMinutes);
|
||||
final String subject = "Your OTP for Contact Verification - " + appName + " (" + LocalDateTime.now().format(SUBJECT_TIMESTAMP)
|
||||
final String subject = "Your OTP for Contact Verification - " + appName + " (" + LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(SUBJECT_TIMESTAMP)
|
||||
+ ")";
|
||||
|
||||
final boolean sent = emailSenderService.sendHtml(emailAddress, subject, html);
|
||||
|
||||
+6
-4
@@ -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,
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public class ClientPendingEditData {
|
||||
|
||||
private final Long id;
|
||||
private final Long clientId;
|
||||
private final String clientDisplayName;
|
||||
private final String payloadJson;
|
||||
private final String originalJson;
|
||||
private final String status;
|
||||
private final Long makerId;
|
||||
private final String makerName;
|
||||
private final OffsetDateTime submittedDate;
|
||||
private final Long checkerId;
|
||||
private final String checkerName;
|
||||
private final OffsetDateTime checkedDate;
|
||||
private final String rejectReason;
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public final class KycRiskRuleData implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long id;
|
||||
private final String ruleName;
|
||||
private final Integer legalFormId;
|
||||
private final Long occupationCategoryCvId;
|
||||
private final String occupationCategoryName;
|
||||
private final Long occupationCvId;
|
||||
private final String occupationName;
|
||||
private final BigDecimal minMonthlyIncome;
|
||||
private final BigDecimal maxMonthlyIncome;
|
||||
private final BigDecimal minAnnualIncome;
|
||||
private final BigDecimal maxAnnualIncome;
|
||||
private final BigDecimal minAnnualTurnover;
|
||||
private final BigDecimal maxAnnualTurnover;
|
||||
private final Integer riskCategory;
|
||||
private final String reason;
|
||||
private final Integer orderPosition;
|
||||
|
||||
public KycRiskRuleData(final Long id, final String ruleName, final Integer legalFormId,
|
||||
final Long occupationCategoryCvId, final String occupationCategoryName,
|
||||
final Long occupationCvId, final String occupationName,
|
||||
final BigDecimal minMonthlyIncome, final BigDecimal maxMonthlyIncome,
|
||||
final BigDecimal minAnnualIncome, final BigDecimal maxAnnualIncome,
|
||||
final BigDecimal minAnnualTurnover, final BigDecimal maxAnnualTurnover,
|
||||
final Integer riskCategory, final String reason, final Integer orderPosition) {
|
||||
this.id = id;
|
||||
this.ruleName = ruleName;
|
||||
this.legalFormId = legalFormId;
|
||||
this.occupationCategoryCvId = occupationCategoryCvId;
|
||||
this.occupationCategoryName = occupationCategoryName;
|
||||
this.occupationCvId = occupationCvId;
|
||||
this.occupationName = occupationName;
|
||||
this.minMonthlyIncome = minMonthlyIncome;
|
||||
this.maxMonthlyIncome = maxMonthlyIncome;
|
||||
this.minAnnualIncome = minAnnualIncome;
|
||||
this.maxAnnualIncome = maxAnnualIncome;
|
||||
this.minAnnualTurnover = minAnnualTurnover;
|
||||
this.maxAnnualTurnover = maxAnnualTurnover;
|
||||
this.riskCategory = riskCategory;
|
||||
this.reason = reason;
|
||||
this.orderPosition = orderPosition;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getRuleName() {
|
||||
return ruleName;
|
||||
}
|
||||
|
||||
public Integer getLegalFormId() {
|
||||
return legalFormId;
|
||||
}
|
||||
|
||||
public Long getOccupationCategoryCvId() {
|
||||
return occupationCategoryCvId;
|
||||
}
|
||||
|
||||
public String getOccupationCategoryName() {
|
||||
return occupationCategoryName;
|
||||
}
|
||||
|
||||
public Long getOccupationCvId() {
|
||||
return occupationCvId;
|
||||
}
|
||||
|
||||
public String getOccupationName() {
|
||||
return occupationName;
|
||||
}
|
||||
|
||||
public BigDecimal getMinMonthlyIncome() {
|
||||
return minMonthlyIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxMonthlyIncome() {
|
||||
return maxMonthlyIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMinAnnualIncome() {
|
||||
return minAnnualIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxAnnualIncome() {
|
||||
return maxAnnualIncome;
|
||||
}
|
||||
|
||||
public BigDecimal getMinAnnualTurnover() {
|
||||
return minAnnualTurnover;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxAnnualTurnover() {
|
||||
return maxAnnualTurnover;
|
||||
}
|
||||
|
||||
public Integer getRiskCategory() {
|
||||
return riskCategory;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public Integer getOrderPosition() {
|
||||
return orderPosition;
|
||||
}
|
||||
}
|
||||
+8
@@ -19,6 +19,7 @@
|
||||
|
||||
package org.apache.fineract.portfolio.client.domain;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
@@ -52,4 +53,11 @@ public interface ClientFamilyMembersRepository
|
||||
@Param("relationshipId") Long relationshipId,
|
||||
@Param("excludeFamilyMemberId") Long excludeFamilyMemberId);
|
||||
|
||||
/**
|
||||
* All non-deleted family members for a client, with their relationship CodeValue. Backs the
|
||||
* "Mother required, plus Father or Caretaker" activation gate.
|
||||
*/
|
||||
@Query("SELECT fm FROM ClientFamilyMembers fm WHERE fm.client.id = :clientId AND fm.isDeleted = false")
|
||||
List<ClientFamilyMembers> findAllActiveByClient(@Param("clientId") Long clientId);
|
||||
|
||||
}
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.OffsetDateTime;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
/**
|
||||
* Bespoke maker-checker for Client edits — modelled on {@link ClientPendingEditStatus} /
|
||||
* {@code m_share_account_pending_edit} elsewhere in this codebase rather than Fineract's native
|
||||
* command-approval engine, because that engine's hold is bypassed by any role carrying
|
||||
* ALL_FUNCTIONS (Super user, Clerk in this tenant), making it a no-op for this CBS's actual admin
|
||||
* accounts.
|
||||
*
|
||||
* <p>Holds the maker's raw PUT /clients/{id}/edit payload as-is (rather than one typed column per
|
||||
* editable Client field, which — given how many fields Client actually has across general,
|
||||
* financial-classification and KYC-adjacent data — would mean a huge, fragile shadow-column table
|
||||
* that has to be kept in lockstep with the entity forever). Approval reconstructs a
|
||||
* {@code JsonCommand} from the stored JSON and replays it through the existing
|
||||
* {@code ClientWritePlatformService.updateClient(...)}, so all of Client's existing validation and
|
||||
* change-detection logic runs exactly as it would for a direct edit — just deferred until a
|
||||
* different user approves it. The maker can never approve/reject their own submission — enforced
|
||||
* in the write service, not just by role, with no bypass for CHECKER_SUPER_USER.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_client_pending_edit")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class ClientPendingEdit extends AbstractPersistableCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "client_id", nullable = false)
|
||||
private Client client;
|
||||
|
||||
@Column(name = "payload_json", nullable = false)
|
||||
private String payloadJson;
|
||||
|
||||
/** Point-in-time "before" snapshot the maker's client had when the edit was submitted — see
|
||||
* client-edit-diff.util.ts's buildOriginalValuesSnapshot(). Null for edits submitted before
|
||||
* this column existed. Lets the checker's compare view show a true historical diff regardless
|
||||
* of whether the live client has since changed (e.g. once approved, current == proposed). */
|
||||
@Column(name = "original_json")
|
||||
private String originalJson;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status;
|
||||
|
||||
@Column(name = "maker_id", nullable = false)
|
||||
private Long makerId;
|
||||
|
||||
@Column(name = "submitted_date", nullable = false)
|
||||
private OffsetDateTime submittedDate;
|
||||
|
||||
@Column(name = "checker_id")
|
||||
private Long checkerId;
|
||||
|
||||
@Column(name = "checked_date")
|
||||
private OffsetDateTime checkedDate;
|
||||
|
||||
@Column(name = "reject_reason", length = 500)
|
||||
private String rejectReason;
|
||||
|
||||
@Column(name = "pending_lock_key", length = 60, unique = true)
|
||||
private String pendingLockKey;
|
||||
|
||||
public static ClientPendingEdit submit(final Client client, final String payloadJson, final String originalJson,
|
||||
final AppUser maker) {
|
||||
final ClientPendingEdit edit = new ClientPendingEdit();
|
||||
edit.client = client;
|
||||
edit.payloadJson = payloadJson;
|
||||
edit.originalJson = originalJson;
|
||||
edit.status = ClientPendingEditStatus.PENDING.name();
|
||||
edit.makerId = maker.getId();
|
||||
edit.submittedDate = DateUtils.getOffsetDateTimeOfTenant();
|
||||
edit.pendingLockKey = lockKeyFor(client.getId());
|
||||
return edit;
|
||||
}
|
||||
|
||||
public static String lockKeyFor(final Long clientId) {
|
||||
return "CLIENT_EDIT:" + clientId;
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return ClientPendingEditStatus.PENDING.name().equals(this.status);
|
||||
}
|
||||
|
||||
/** The maker can never be their own checker — a distinct human must approve/reject an edit. */
|
||||
public boolean isMadeBy(final Long userId) {
|
||||
return this.makerId != null && this.makerId.equals(userId);
|
||||
}
|
||||
|
||||
public void approve(final AppUser checker) {
|
||||
this.status = ClientPendingEditStatus.APPROVED.name();
|
||||
this.checkerId = checker.getId();
|
||||
this.checkedDate = DateUtils.getOffsetDateTimeOfTenant();
|
||||
this.pendingLockKey = null;
|
||||
}
|
||||
|
||||
public void reject(final AppUser checker, final String reason) {
|
||||
this.status = ClientPendingEditStatus.REJECTED.name();
|
||||
this.checkerId = checker.getId();
|
||||
this.checkedDate = DateUtils.getOffsetDateTimeOfTenant();
|
||||
this.rejectReason = reason;
|
||||
this.pendingLockKey = null;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ClientPendingEditRepository extends JpaRepository<ClientPendingEdit, Long> {
|
||||
|
||||
Optional<ClientPendingEdit> findByClientIdAndStatus(Long clientId, String status);
|
||||
|
||||
List<ClientPendingEdit> findByStatusOrderBySubmittedDateDesc(String status);
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientNoPendingEditException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ClientPendingEditRepositoryWrapper {
|
||||
|
||||
private final ClientPendingEditRepository repository;
|
||||
|
||||
public ClientPendingEdit findPendingByClientIdWithNotFoundDetection(final Long clientId) {
|
||||
return this.repository.findByClientIdAndStatus(clientId, ClientPendingEditStatus.PENDING.name())
|
||||
.orElseThrow(() -> new ClientNoPendingEditException(clientId));
|
||||
}
|
||||
|
||||
public Optional<ClientPendingEdit> findPendingByClientId(final Long clientId) {
|
||||
return this.repository.findByClientIdAndStatus(clientId, ClientPendingEditStatus.PENDING.name());
|
||||
}
|
||||
|
||||
public List<ClientPendingEdit> findAllPending() {
|
||||
return this.repository.findByStatusOrderBySubmittedDateDesc(ClientPendingEditStatus.PENDING.name());
|
||||
}
|
||||
|
||||
public ClientPendingEdit saveAndFlush(final ClientPendingEdit edit) {
|
||||
return this.repository.saveAndFlush(edit);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
/** Lifecycle of a {@link ClientPendingEdit}. Terminal once APPROVED or REJECTED. */
|
||||
public enum ClientPendingEditStatus {
|
||||
|
||||
PENDING, //
|
||||
APPROVED, //
|
||||
REJECTED; //
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown when the user attempting to activate a client is the same user who created it — a
|
||||
* four-eyes (maker-checker) requirement with no bypass, including for users holding
|
||||
* CHECKER_SUPER_USER, since a bypass would recreate the exact "ALL_FUNCTIONS/super user ignores
|
||||
* the hold" loophole this check exists to close.
|
||||
*/
|
||||
public class ClientActivationSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientActivationSelfApprovalNotAllowedException() {
|
||||
super("error.msg.client.activation.self.approval.not.allowed",
|
||||
"You cannot activate a client you created yourself — a different user must activate it.");
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class ClientEditPendingApprovalException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientEditPendingApprovalException(final Long clientId) {
|
||||
super("error.msg.client.edit.pending.approval", "Client with identifier " + clientId + " already has an edit pending approval",
|
||||
clientId);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown when the user attempting to approve/reject a Client's pending edit is the same user who
|
||||
* submitted it — no bypass, including for users holding CHECKER_SUPER_USER, since a bypass would
|
||||
* recreate the exact "role bypasses the hold" loophole this check exists to close.
|
||||
*/
|
||||
public class ClientEditSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientEditSelfApprovalNotAllowedException(final Long clientId) {
|
||||
super("error.msg.client.edit.self.approval.not.allowed",
|
||||
"You cannot approve or reject an edit you submitted yourself for client " + clientId
|
||||
+ " — a different user must review it.",
|
||||
clientId);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown on client activation when neither an active "Father" nor "Caretaker" family member has
|
||||
* been recorded. Kept separate from {@link ClientFamilyMemberMotherRequiredException} so the two
|
||||
* failure reasons surface as distinct errors instead of one merged message.
|
||||
*/
|
||||
public class ClientFamilyMemberFatherOrCaretakerRequiredException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientFamilyMemberFatherOrCaretakerRequiredException() {
|
||||
super("error.msg.client.family.member.father.or.caretaker.required",
|
||||
"Either Father or a Caretaker is required as a family member before this client can be activated.");
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
/**
|
||||
* Thrown on client activation when no active "Mother" family member has been recorded. Kept
|
||||
* separate from {@link ClientFamilyMemberFatherOrCaretakerRequiredException} so the two failure
|
||||
* reasons surface as distinct errors instead of one merged message.
|
||||
*/
|
||||
public class ClientFamilyMemberMotherRequiredException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientFamilyMemberMotherRequiredException() {
|
||||
super("error.msg.client.family.member.mother.required",
|
||||
"Mother is required as a family member before this client can be activated.");
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class ClientNoPendingEditException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ClientNoPendingEditException(final Long clientId) {
|
||||
super("error.msg.client.no.pending.edit", "Client with identifier " + clientId + " has no edit pending approval", clientId);
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -34,6 +34,7 @@ import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.Named;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Mapper(config = MapstructMapperConfig.class)
|
||||
public interface ClientMapper {
|
||||
@@ -65,12 +66,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 +199,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();
|
||||
@@ -271,7 +283,7 @@ public interface ClientMapper {
|
||||
default void computeAge(Client source, @MappingTarget ClientData target) {
|
||||
final LocalDate dob = source.getDateOfBirth();
|
||||
if (dob != null) {
|
||||
final Period age = Period.between(dob, LocalDate.now());
|
||||
final Period age = Period.between(dob, LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
target.setAgeYears(age.getYears());
|
||||
target.setAgeMonths(age.getMonths());
|
||||
target.setAgeDays(age.getDays());
|
||||
|
||||
+2
-1
@@ -25,6 +25,7 @@ import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepository;
|
||||
import org.apache.fineract.portfolio.client.domain.LegalForm;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* Generates CIF numbers in the format <code>TTYYYYNNNNNN</code>:
|
||||
@@ -90,6 +91,6 @@ public class CifNumberGenerator {
|
||||
|
||||
private int resolveYear(final Client client) {
|
||||
final LocalDate submittedOn = client.getSubmittedOnDate();
|
||||
return (submittedOn != null ? submittedOn : LocalDate.now()).getYear();
|
||||
return (submittedOn != null ? submittedOn : LocalDate.now(DateUtils.getDateTimeZoneOfTenant())).getYear();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* See {@link ClientChangeNotificationService}.
|
||||
@@ -88,7 +89,7 @@ public class ClientChangeNotificationServiceImpl implements ClientChangeNotifica
|
||||
final String alertTitle = fieldLabel + " Updated";
|
||||
final String alertMessage = "Your " + fieldLabel.toLowerCase() + " on file was changed from "
|
||||
+ describeForDisplay(fieldLabel, oldValue) + " to " + describeForDisplay(fieldLabel, newValue) + " on "
|
||||
+ LocalDateTime.now().format(TIMESTAMP) + ". If you did not authorize this change, please contact us immediately.";
|
||||
+ LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(TIMESTAMP) + ". If you did not authorize this change, please contact us immediately.";
|
||||
final String html = emailTemplateService.renderAlert(recipientName, alertTitle, alertMessage);
|
||||
final String subject = alertTitle + " - " + appName;
|
||||
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
/** Maker-checker for Client edits — see {@link org.apache.fineract.portfolio.client.domain.ClientPendingEdit}. */
|
||||
public interface ClientEditWritePlatformService {
|
||||
|
||||
CommandProcessingResult submitEditForApproval(Long clientId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult approveEdit(Long clientId);
|
||||
|
||||
CommandProcessingResult rejectEdit(Long clientId, String rejectReason);
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientPendingEdit;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientPendingEditRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientEditPendingApprovalException;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientEditSelfApprovalNotAllowedException;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ClientEditWritePlatformServiceImpl implements ClientEditWritePlatformService {
|
||||
|
||||
/** Matches ORIGINAL_VALUES_PAYLOAD_KEY in client-edit-diff.util.ts. */
|
||||
private static final String ORIGINAL_VALUES_PAYLOAD_KEY = "_originalValues";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final ClientRepositoryWrapper clientRepository;
|
||||
private final ClientPendingEditRepositoryWrapper pendingEditRepository;
|
||||
private final ClientWritePlatformService clientWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult submitEditForApproval(final Long clientId, final JsonCommand command) {
|
||||
final AppUser maker = this.context.authenticatedUser();
|
||||
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
|
||||
|
||||
// Four-eyes lock: refuse a second maker while one edit is already awaiting approval for
|
||||
// this client, rather than silently overwriting or queueing it — matches
|
||||
// m_share_account_pending_edit / m_savings_product_temp elsewhere in this codebase.
|
||||
if (this.pendingEditRepository.findPendingByClientId(clientId).isPresent()) {
|
||||
throw new ClientEditPendingApprovalException(clientId);
|
||||
}
|
||||
|
||||
// The frontend rides its point-in-time "before" snapshot along under _originalValues so
|
||||
// the checker's compare view can show a true historical diff later, even after the edit
|
||||
// has been applied (by then the live client equals the proposed values, not the original
|
||||
// ones — see client-edit-diff.util.ts). Split it out here: the rest replays through
|
||||
// updateClient() at approval time and must not carry an unrecognised parameter.
|
||||
final JsonElement parsedPayload = this.fromApiJsonHelper.parse(command.json());
|
||||
String originalJson = null;
|
||||
String cleanPayloadJson = command.json();
|
||||
if (parsedPayload != null && parsedPayload.isJsonObject()) {
|
||||
final JsonObject payloadObject = parsedPayload.getAsJsonObject();
|
||||
final JsonElement originalValues = payloadObject.remove(ORIGINAL_VALUES_PAYLOAD_KEY);
|
||||
if (originalValues != null) {
|
||||
originalJson = originalValues.toString();
|
||||
}
|
||||
cleanPayloadJson = payloadObject.toString();
|
||||
}
|
||||
|
||||
final ClientPendingEdit pendingEdit = ClientPendingEdit.submit(client, cleanPayloadJson, originalJson, maker);
|
||||
this.pendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
.withOfficeId(client.officeId()) //
|
||||
.withClientId(clientId) //
|
||||
.withEntityId(pendingEdit.getId()) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult approveEdit(final Long clientId) {
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ClientPendingEdit pendingEdit = this.pendingEditRepository.findPendingByClientIdWithNotFoundDetection(clientId);
|
||||
|
||||
// No bypass — including CHECKER_SUPER_USER — a bypass here would recreate the exact
|
||||
// "role bypasses the hold" loophole this whole feature exists to close.
|
||||
if (pendingEdit.isMadeBy(checker.getId())) {
|
||||
throw new ClientEditSelfApprovalNotAllowedException(clientId);
|
||||
}
|
||||
|
||||
// Replay the staged JSON through the SAME write-service method a direct PUT would have
|
||||
// used, so every bit of Client's existing validation/change-detection logic runs exactly
|
||||
// as it would for an immediate edit — just deferred until a different user approves it.
|
||||
final JsonElement parsedJson = this.fromApiJsonHelper.parse(pendingEdit.getPayloadJson());
|
||||
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(clientId, parsedJson, this.fromApiJsonHelper);
|
||||
final CommandProcessingResult result = this.clientWritePlatformService.updateClient(clientId, replayedCommand);
|
||||
|
||||
pendingEdit.approve(checker);
|
||||
this.pendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult rejectEdit(final Long clientId, final String rejectReason) {
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ClientPendingEdit pendingEdit = this.pendingEditRepository.findPendingByClientIdWithNotFoundDetection(clientId);
|
||||
|
||||
if (pendingEdit.isMadeBy(checker.getId())) {
|
||||
throw new ClientEditSelfApprovalNotAllowedException(clientId);
|
||||
}
|
||||
|
||||
pendingEdit.reject(checker, rejectReason);
|
||||
this.pendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withClientId(clientId) //
|
||||
.withEntityId(pendingEdit.getId()) //
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+8
@@ -33,4 +33,12 @@ public interface ClientFamilyMembersWritePlatformService {
|
||||
|
||||
CommandProcessingResult deleteFamilyMember(Long familyMemberId, JsonCommand command);
|
||||
|
||||
/**
|
||||
* Enforces that an individual client has "Mother" plus either "Father" or "Caretaker" recorded
|
||||
* as active family members. No-op for non-individual (entity) clients. Throws
|
||||
* {@code ClientFamilyMemberMotherRequiredException} or
|
||||
* {@code ClientFamilyMemberFatherOrCaretakerRequiredException} on failure.
|
||||
*/
|
||||
void validateFamilyRequirementsForActivation(Client client);
|
||||
|
||||
}
|
||||
|
||||
+44
@@ -37,10 +37,13 @@ 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.security.service.PlatformSecurityContext;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientFamilyMembers;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientFamilyMembersRepository;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientFamilyMemberFatherOrCaretakerRequiredException;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientFamilyMemberMotherRequiredException;
|
||||
import org.apache.fineract.portfolio.client.serialization.ClientFamilyMemberCommandFromApiJsonDeserializer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -131,6 +134,47 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateFamilyRequirementsForActivation(final Client client) {
|
||||
if (client == null || client.getId() == null || client.getLegalForm() == null) {
|
||||
return;
|
||||
}
|
||||
// Family (Mother/Father/Caretaker) is an individual-client concept — see
|
||||
// assertCanHaveFamilyMembers above; non-individual entities are exempt from this gate.
|
||||
if (org.apache.fineract.portfolio.client.domain.LegalForm.fromInt(client.getLegalForm()).isEntity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<ClientFamilyMembers> members = clientFamilyRepository.findAllActiveByClient(client.getId());
|
||||
boolean hasMother = false;
|
||||
boolean hasFather = false;
|
||||
boolean hasCaretaker = false;
|
||||
for (final ClientFamilyMembers member : members) {
|
||||
final CodeValue relationship = member.getRelationship();
|
||||
if (relationship == null || relationship.getLabel() == null) {
|
||||
continue;
|
||||
}
|
||||
final String label = relationship.getLabel().trim().toLowerCase(Locale.ROOT);
|
||||
switch (label) {
|
||||
case "mother" -> hasMother = true;
|
||||
case "father" -> hasFather = true;
|
||||
case "caretaker" -> hasCaretaker = true;
|
||||
default -> {
|
||||
// other relationships don't affect this rule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reported as two independent failures rather than one merged message, so the caller can
|
||||
// tell which specific requirement is unmet.
|
||||
if (!hasMother) {
|
||||
throw new ClientFamilyMemberMotherRequiredException();
|
||||
}
|
||||
if (!hasFather && !hasCaretaker) {
|
||||
throw new ClientFamilyMemberFatherOrCaretakerRequiredException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult addFamilyMember(final long clientId, final JsonCommand command) {
|
||||
|
||||
|
||||
+7
-3
@@ -84,9 +84,13 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
||||
this.context.authenticatedUser();
|
||||
LOG.info("initiateKyc: clientId={}, requestJson={}", clientId, command.json());
|
||||
|
||||
final Client clientForValidation = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||
|
||||
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||
command.json(), ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
|
||||
kycCommand.validateForInitiate();
|
||||
// Minors identify via the Birth Certificate / School ID document-type picker (a separate
|
||||
// upload path), not PAN/Aadhaar/GSTIN — so the "at least one ID" check doesn't apply to them.
|
||||
kycCommand.validateForInitiate(Boolean.TRUE.equals(clientForValidation.getIsMinor()));
|
||||
|
||||
// CIN / GSTIN are only applicable for NON_INDIVIDUAL clients
|
||||
final Integer kycTypeOnInitiate = command.integerValueOfParameterNamed("kycType");
|
||||
@@ -104,7 +108,7 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
||||
}
|
||||
|
||||
try {
|
||||
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
|
||||
final Client client = clientForValidation;
|
||||
final ClientKyc kyc = ClientKyc.initiateKyc(client, command);
|
||||
kycRepositoryWrapper.saveAndFlush(kyc);
|
||||
|
||||
@@ -957,7 +961,7 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
|
||||
public void initiateKycInline(final Client client, final String kycJson) {
|
||||
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
|
||||
kycJson, ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
|
||||
kycCommand.validateForInitiate();
|
||||
kycCommand.validateForInitiate(Boolean.TRUE.equals(client.getIsMinor()));
|
||||
|
||||
if (kycRepositoryWrapper.existsByClientId(client.getId())) {
|
||||
LOG.info("KYC record already exists for client {}. Skipping inline initiation.", client.getId());
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.portfolio.client.data.ClientPendingEditData;
|
||||
|
||||
public interface ClientPendingEditReadPlatformService {
|
||||
|
||||
ClientPendingEditData retrievePendingEdit(Long clientId);
|
||||
|
||||
/**
|
||||
* @param status defaults to PENDING when blank
|
||||
* @param clientName case-insensitive substring match against the client's display name
|
||||
* @param fromDate ISO (yyyy-MM-dd), inclusive, filters on submitted_date
|
||||
* @param toDate ISO (yyyy-MM-dd), inclusive, filters on submitted_date
|
||||
*/
|
||||
Page<ClientPendingEditData> retrieveAllPendingEdits(String status, String clientName, String fromDate, String toDate, Integer offset,
|
||||
Integer limit);
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.portfolio.client.data.ClientPendingEditData;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientNoPendingEditException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ClientPendingEditReadPlatformServiceImpl implements ClientPendingEditReadPlatformService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final String SELECT_SQL = """
|
||||
SELECT pe.id AS id, pe.client_id AS clientId, c.display_name AS clientDisplayName,
|
||||
pe.payload_json AS payloadJson, pe.original_json AS originalJson, pe.status AS status,
|
||||
pe.maker_id AS makerId, (maker.firstname || ' ' || maker.lastname) AS makerName,
|
||||
pe.submitted_date AS submittedDate,
|
||||
pe.checker_id AS checkerId, (checker.firstname || ' ' || checker.lastname) AS checkerName,
|
||||
pe.checked_date AS checkedDate, pe.reject_reason AS rejectReason
|
||||
FROM m_client_pending_edit pe
|
||||
JOIN m_client c ON c.id = pe.client_id
|
||||
JOIN m_appuser maker ON maker.id = pe.maker_id
|
||||
LEFT JOIN m_appuser checker ON checker.id = pe.checker_id
|
||||
""";
|
||||
|
||||
private static final RowMapper<ClientPendingEditData> MAPPER = (ResultSet rs, int rowNum) -> {
|
||||
final OffsetDateTime submittedDate = rs.getObject("submittedDate", OffsetDateTime.class);
|
||||
final OffsetDateTime checkedDate = rs.getObject("checkedDate", OffsetDateTime.class);
|
||||
return ClientPendingEditData.builder() //
|
||||
.id(rs.getLong("id")) //
|
||||
.clientId(rs.getLong("clientId")) //
|
||||
.clientDisplayName(rs.getString("clientDisplayName")) //
|
||||
.payloadJson(rs.getString("payloadJson")) //
|
||||
.originalJson(rs.getString("originalJson")) //
|
||||
.status(rs.getString("status")) //
|
||||
.makerId(rs.getLong("makerId")) //
|
||||
.makerName(rs.getString("makerName")) //
|
||||
.submittedDate(submittedDate) //
|
||||
.checkerId(JdbcSupport.getLong(rs, "checkerId")) //
|
||||
.checkerName(rs.getString("checkerName")) //
|
||||
.checkedDate(checkedDate) //
|
||||
.rejectReason(rs.getString("rejectReason")) //
|
||||
.build();
|
||||
};
|
||||
|
||||
@Override
|
||||
public ClientPendingEditData retrievePendingEdit(final Long clientId) {
|
||||
try {
|
||||
return this.jdbcTemplate.queryForObject(SELECT_SQL + " WHERE pe.client_id = ? AND pe.status = 'PENDING'", MAPPER, clientId);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new ClientNoPendingEditException(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ClientPendingEditData> retrieveAllPendingEdits(final String status, final String clientName, final String fromDate,
|
||||
final String toDate, final Integer offset, final Integer limit) {
|
||||
final StringBuilder where = new StringBuilder(" WHERE 1=1");
|
||||
final List<Object> args = new ArrayList<>();
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
where.append(" AND pe.status = ?");
|
||||
args.add(status.trim().toUpperCase());
|
||||
} else {
|
||||
where.append(" AND pe.status = 'PENDING'");
|
||||
}
|
||||
if (StringUtils.isNotBlank(clientName)) {
|
||||
where.append(" AND c.display_name ILIKE ?");
|
||||
args.add("%" + clientName.trim() + "%");
|
||||
}
|
||||
if (StringUtils.isNotBlank(fromDate)) {
|
||||
where.append(" AND pe.submitted_date::date >= ?::date");
|
||||
args.add(fromDate);
|
||||
}
|
||||
if (StringUtils.isNotBlank(toDate)) {
|
||||
where.append(" AND pe.submitted_date::date <= ?::date");
|
||||
args.add(toDate);
|
||||
}
|
||||
|
||||
final String countSql = "SELECT COUNT(*) FROM m_client_pending_edit pe JOIN m_client c ON c.id = pe.client_id" + where;
|
||||
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
|
||||
|
||||
final int pageOffset = offset == null ? 0 : offset;
|
||||
final int pageLimit = limit == null ? 10 : limit;
|
||||
final String pageSql = SELECT_SQL + where + " ORDER BY pe.submitted_date DESC LIMIT ? OFFSET ?";
|
||||
final List<Object> pageArgs = new ArrayList<>(args);
|
||||
pageArgs.add(pageLimit);
|
||||
pageArgs.add(pageOffset);
|
||||
final List<ClientPendingEditData> items = this.jdbcTemplate.query(pageSql, MAPPER, pageArgs.toArray());
|
||||
|
||||
return new Page<>(items, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
|
||||
}
|
||||
}
|
||||
+51
-2
@@ -63,6 +63,7 @@ import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -177,6 +178,16 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
|
||||
extraCriteria += " and c.status_enum = " + clientStatus.getValue().toString() + " ";
|
||||
}
|
||||
|
||||
if (searchParameters.hasFromDate()) {
|
||||
paramList.add(searchParameters.getFromDate().toString());
|
||||
extraCriteria += " and c.submittedon_date >= ?::date ";
|
||||
}
|
||||
|
||||
if (searchParameters.hasToDate()) {
|
||||
paramList.add(searchParameters.getToDate().toString());
|
||||
extraCriteria += " and c.submittedon_date <= ?::date ";
|
||||
}
|
||||
|
||||
if (firstname != null) {
|
||||
paramList.add(firstname);
|
||||
extraCriteria += " and c.firstname like ? ";
|
||||
@@ -321,7 +332,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 +400,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,13 +500,20 @@ 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;
|
||||
Integer ageDays1 = null;
|
||||
if (dateOfBirth != null) {
|
||||
final Period age = Period.between(dateOfBirth, LocalDate.now());
|
||||
final Period age = Period.between(dateOfBirth, LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
ageYears1 = age.getYears();
|
||||
ageMonths1 = age.getMonths();
|
||||
ageDays1 = age.getDays();
|
||||
@@ -520,7 +545,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 +744,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 +811,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,13 +910,20 @@ 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;
|
||||
Integer ageDays = null;
|
||||
if (dateOfBirth != null) {
|
||||
final Period age = Period.between(dateOfBirth, LocalDate.now());
|
||||
final Period age = Period.between(dateOfBirth, LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
ageYears = age.getYears();
|
||||
ageMonths = age.getMonths();
|
||||
ageDays = age.getDays();
|
||||
@@ -911,7 +955,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);
|
||||
|
||||
+4
@@ -126,6 +126,9 @@ public class ClientTemplateReadPlatformServiceImpl implements ClientTemplateRead
|
||||
final List<CodeValueData> socialCategoryOptions = new ArrayList<>(
|
||||
this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.SOCIAL_CATEGORY));
|
||||
|
||||
final List<CodeValueData> pwdCategoryOptions = new ArrayList<>(
|
||||
this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.PWD_CATEGORY));
|
||||
|
||||
final List<CodeValueData> 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;
|
||||
}
|
||||
|
||||
|
||||
+71
@@ -74,6 +74,7 @@ import org.apache.fineract.portfolio.client.domain.ClientRepository;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientStatus;
|
||||
import org.apache.fineract.portfolio.client.domain.LegalForm;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientActivationSelfApprovalNotAllowedException;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientActiveForUpdateException;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientHasNoStaffException;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientMustBePendingToBeDeletedException;
|
||||
@@ -395,6 +396,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 +489,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 +992,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);
|
||||
@@ -1387,6 +1450,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
|
||||
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId, true);
|
||||
validateParentGroupRulesBeforeClientActivation(client);
|
||||
this.clientFamilyMembersWritePlatformService.validateFamilyRequirementsForActivation(client);
|
||||
final Locale locale = command.extractLocale();
|
||||
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
|
||||
final LocalDate activationDate = command.localDateValueOfParameterNamed("activationDate");
|
||||
@@ -1394,6 +1458,12 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
runEntityDatatableCheck(clientId, client.getLegalForm());
|
||||
|
||||
final AppUser currentUser = this.context.authenticatedUser();
|
||||
// Four-eyes principle: whoever created this client (the maker) cannot also be the one
|
||||
// who activates it (the checker) — no bypass, including for CHECKER_SUPER_USER, since
|
||||
// that would recreate the exact "role bypasses the hold" loophole this closes.
|
||||
if (client.getCreatedBy().isPresent() && client.getCreatedBy().get().equals(currentUser.getId())) {
|
||||
throw new ClientActivationSelfApprovalNotAllowedException();
|
||||
}
|
||||
client.activate(currentUser, fmt, activationDate);
|
||||
CommandProcessingResult result = openSavingsAccount(client, fmt);
|
||||
clientRepository.saveAndFlush(client);
|
||||
@@ -1786,6 +1856,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
|
||||
* verifying KYC names against ID documents). Null / blank → null so we don't accidentally
|
||||
* persist an empty string in place of an unset field.
|
||||
*/
|
||||
@SuppressWarnings("StringSplitter")
|
||||
private static String toTitleCase(final String raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.client.data.KycRiskRuleData;
|
||||
|
||||
public interface KycRiskRulesReadPlatformService {
|
||||
List<KycRiskRuleData> retrieveAllRules();
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.client.data.KycRiskRuleData;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRiskRule;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientRiskRuleRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlatformService {
|
||||
|
||||
private final ClientRiskRuleRepository repository;
|
||||
|
||||
@Override
|
||||
public List<KycRiskRuleData> retrieveAllRules() {
|
||||
final List<ClientRiskRule> rules = this.repository.findAllOrderedByOrderPosition();
|
||||
return rules.stream().map(this::mapToData).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private KycRiskRuleData mapToData(final ClientRiskRule rule) {
|
||||
final Long occCatId = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getId() : null;
|
||||
final String occCatName = rule.getOccupationCategory() != null ? rule.getOccupationCategory().getLabel() : null;
|
||||
final Long occId = rule.getOccupation() != null ? rule.getOccupation().getId() : null;
|
||||
final String occName = rule.getOccupation() != null ? rule.getOccupation().getLabel() : null;
|
||||
|
||||
return new KycRiskRuleData(
|
||||
rule.getId(),
|
||||
rule.getRuleName(),
|
||||
rule.getLegalFormId(),
|
||||
occCatId,
|
||||
occCatName,
|
||||
occId,
|
||||
occName,
|
||||
rule.getMinMonthlyIncome(),
|
||||
rule.getMaxMonthlyIncome(),
|
||||
rule.getMinAnnualIncome(),
|
||||
rule.getMaxAnnualIncome(),
|
||||
rule.getMinAnnualTurnover(),
|
||||
rule.getMaxAnnualTurnover(),
|
||||
rule.getRiskCategory(),
|
||||
rule.getReason(),
|
||||
rule.getOrderPosition()
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user