minor change on client
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s

This commit is contained in:
2026-07-28 10:43:06 +05:30
parent bc30241950
commit 026d902199
164 changed files with 813 additions and 607 deletions
@@ -18,7 +18,6 @@
*/ */
package org.apache.fineract.accounting.journalentry; package org.apache.fineract.accounting.journalentry;
import java.time.OffsetDateTime;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType; import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.journalentry.data.JournalEntryData; import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry; import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
@@ -277,14 +277,14 @@ public class ProductToGLAccountMappingHelper {
} }
final JsonArray chargeToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayFragmentName, element); 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 * Variable stores a map representation of charges (key) and their associated income Id's (value) extracted from
* the passed in Jsoncommand * the passed in Jsoncommand
**/ */
final Map<Long, Long> inputChargeToIncomeAccountMap = new HashMap<>(); final Map<Long, Long> inputChargeToIncomeAccountMap = new HashMap<>();
/*** /*
* Variable stores all charges which have already been mapped to Income Accounts in the system * Variable stores all charges which have already been mapped to Income Accounts in the system
**/ */
final Set<Long> existingCharges = new HashSet<>(); final Set<Long> existingCharges = new HashSet<>();
if (chargeToIncomeAccountMappingArray != null) { if (chargeToIncomeAccountMappingArray != null) {
if (changes != null) { if (changes != null) {
@@ -301,13 +301,13 @@ public class ProductToGLAccountMappingHelper {
// If input map is empty, delete all existing mappings // If input map is empty, delete all existing mappings
if (inputChargeToIncomeAccountMap.size() == 0) { if (inputChargeToIncomeAccountMap.size() == 0) {
this.accountMappingRepository.deleteAll(existingChargeToIncomeAccountMappings); this.accountMappingRepository.deleteAll(existingChargeToIncomeAccountMappings);
} /** } /*
* Else, <br/> * Else, <br/>
* update existing mappings OR <br/> * update existing mappings OR <br/>
* delete old mappings (which are already present, but not passed in as a part of Jsoncommand)<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 * Create new mappings for charges that are passed in as a part of the Jsoncommand but not already present
* *
**/ */
else { else {
for (final ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) { for (final ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) {
final Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId(); final Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId();
@@ -356,14 +356,14 @@ public class ProductToGLAccountMappingHelper {
.findAllPaymentTypeToFundSourceMappings(productId, portfolioProductType.getValue()); .findAllPaymentTypeToFundSourceMappings(productId, portfolioProductType.getValue());
final JsonArray paymentChannelMappingArray = this.fromApiJsonHelper final JsonArray paymentChannelMappingArray = this.fromApiJsonHelper
.extractJsonArrayNamed(LoanProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), element); .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 * Variable stores a map representation of Payment channels (key) and their fund sources (value) extracted from
* the passed in Jsoncommand * the passed in Jsoncommand
**/ */
final Map<Long, Long> inputPaymentChannelFundSourceMap = new HashMap<>(); final Map<Long, Long> inputPaymentChannelFundSourceMap = new HashMap<>();
/*** /*
* Variable stores all payment types which have already been mapped to Fund Sources in the system * Variable stores all payment types which have already been mapped to Fund Sources in the system
**/ */
final Set<Long> existingPaymentTypes = new HashSet<>(); final Set<Long> existingPaymentTypes = new HashSet<>();
if (paymentChannelMappingArray != null) { if (paymentChannelMappingArray != null) {
if (changes != null) { if (changes != null) {
@@ -381,14 +381,14 @@ public class ProductToGLAccountMappingHelper {
// If input map is empty, delete all existing mappings // If input map is empty, delete all existing mappings
if (inputPaymentChannelFundSourceMap.isEmpty()) { if (inputPaymentChannelFundSourceMap.isEmpty()) {
this.accountMappingRepository.deleteAll(existingPaymentChannelToFundSourceMappings); this.accountMappingRepository.deleteAll(existingPaymentChannelToFundSourceMappings);
} /** } /*
* Else, <br/> * Else, <br/>
* update existing mappings OR <br/> * update existing mappings OR <br/>
* delete old mappings (which re already present, but not passed in as a part of Jsoncommand)<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 * Create new mappings for payment types that are passed in as a part of the Jsoncommand but not already
* present * present
* *
**/ */
else { else {
for (final ProductToGLAccountMapping existingPaymentChannelToFundSourceMapping : existingPaymentChannelToFundSourceMappings) { for (final ProductToGLAccountMapping existingPaymentChannelToFundSourceMapping : existingPaymentChannelToFundSourceMappings) {
final Long currentPaymentChannelId = existingPaymentChannelToFundSourceMapping.getPaymentType().getId(); final Long currentPaymentChannelId = existingPaymentChannelToFundSourceMapping.getPaymentType().getId();
@@ -584,9 +584,9 @@ public class ProductToGLAccountMappingHelper {
// based on input condition // based on input condition
GLAccount glAccount; GLAccount glAccount;
/** /*
* Both CASH and Accrual placeholders have the same value for income from Interest and penalties * Both CASH and Accrual placeholders have the same value for income from Interest and penalties
**/ */
CashAccountsForLoan placeHolderAccountType; CashAccountsForLoan placeHolderAccountType;
if (isPenalty) { if (isPenalty) {
glAccount = getAccountByIdAndType(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), GLAccountType.INCOME, glAccount = getAccountByIdAndType(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), GLAccountType.INCOME,
@@ -181,12 +181,12 @@ public class TellerApiResource {
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Create Cashiers", description = """ @Operation(summary = "Create Cashiers", description = """
Mandatory Fields:\s Mandatory Fields:
Cashier/staff, Fromm Date, To Date, Full Day or From time and To time Cashier/staff, Fromm Date, To Date, Full Day or From time and To time
Optional Fields:\s Optional Fields:
Description/Notes""") Description/Notes""")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersRequest.class))) @RequestBody(required = true, content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersResponse.class))) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = TellerApiResourceSwagger.PostTellersTellerIdCashiersResponse.class)))
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; 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. * Simulator-side audit log for CFCFRMS (I4C/MHA) events.
@@ -69,7 +70,7 @@ public class CfcfrmsSimulatorLog extends AbstractAuditableWithUTCDateTimeCustom<
this.eventType = eventType; this.eventType = eventType;
this.payload = payload; this.payload = payload;
this.responsePayload = responsePayload; this.responsePayload = responsePayload;
this.eventTimestamp = OffsetDateTime.now(); this.eventTimestamp = DateUtils.getOffsetDateTimeOfTenant();
} }
public CfcfrmsSimulatorLog(final String cfcfrmsRef, final String ticketId, final String bankIfsc, public CfcfrmsSimulatorLog(final String cfcfrmsRef, final String ticketId, final String bankIfsc,
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; 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. * 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.event = event;
this.description = description; this.description = description;
this.actor = actor; this.actor = actor;
this.eventTimestamp = OffsetDateTime.now(); this.eventTimestamp = DateUtils.getOffsetDateTimeOfTenant();
} }
public ComplaintAuditLog(final String ticketId, final String event, final String description, public ComplaintAuditLog(final String ticketId, final String event, final String description,
@@ -26,6 +26,7 @@ import jakarta.persistence.Table;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; 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. * 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.status = ComplaintStatus.RECEIVED;
ticket.assignedOfficer = assignedOfficer; ticket.assignedOfficer = assignedOfficer;
ticket.statePoliceRef = statePoliceRef; ticket.statePoliceRef = statePoliceRef;
ticket.receivedAt = OffsetDateTime.now(); ticket.receivedAt = DateUtils.getOffsetDateTimeOfTenant();
return ticket; return ticket;
} }
@@ -132,7 +133,7 @@ public class ComplaintTicket extends AbstractAuditableWithUTCDateTimeCustom<Long
this.status = newStatus; this.status = newStatus;
this.actionTaken = actionNote; this.actionTaken = actionNote;
if (newStatus == ComplaintStatus.RESOLVED || newStatus == ComplaintStatus.REJECTED) { if (newStatus == ComplaintStatus.RESOLVED || newStatus == ComplaintStatus.REJECTED) {
this.resolvedAt = OffsetDateTime.now(); this.resolvedAt = DateUtils.getOffsetDateTimeOfTenant();
} }
} }
@@ -29,6 +29,7 @@ import lombok.Getter;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; 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. * 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.estimatedTotalIncome = command.bigDecimalValueOfParameterNamed("estimatedTotalIncome");
form.reasonForNoPan = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("reasonForNoPan"), null); form.reasonForNoPan = StringUtils.defaultIfEmpty(command.stringValueOfParameterNamed("reasonForNoPan"), null);
form.declaration = command.booleanPrimitiveValueOfParameterNamed("declaration"); form.declaration = command.booleanPrimitiveValueOfParameterNamed("declaration");
form.submittedOn = LocalDate.now(); form.submittedOn = DateUtils.getLocalDateOfTenant();
return form; return form;
} }
} }
@@ -32,6 +32,7 @@ import java.util.Map;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; 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 * 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.gstinVerified = false;
this.cinVerified = false; this.cinVerified = false;
this.dinVerified = false; this.dinVerified = false;
this.initiatedOn = LocalDate.now(); this.initiatedOn = DateUtils.getLocalDateOfTenant();
} }
public static ClientKyc initiateKyc(final Client client, final JsonCommand command) { 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) { public void markCkycUploaded(final String ckycNumber, final String batchId) {
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null); this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
this.ckycBatchId = StringUtils.defaultIfEmpty(batchId, 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. */ /** Called after successful 3rd-party verification of all required documents. */
public void markVerified(final String ckycNumber) { public void markVerified(final String ckycNumber) {
this.kycStatus = ClientKycStatus.VERIFIED.getValue(); this.kycStatus = ClientKycStatus.VERIFIED.getValue();
this.verifiedOn = LocalDate.now(); this.verifiedOn = DateUtils.getLocalDateOfTenant();
this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null); this.ckycNumber = StringUtils.defaultIfEmpty(ckycNumber, null);
final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory); final KycRiskCategory risk = KycRiskCategory.fromInt(this.riskCategory);
@@ -474,14 +475,14 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
/** Called when verification fails. */ /** Called when verification fails. */
public void markRejected(final String remarks) { public void markRejected(final String remarks) {
this.kycStatus = ClientKycStatus.REJECTED.getValue(); this.kycStatus = ClientKycStatus.REJECTED.getValue();
this.rejectedOn = LocalDate.now(); this.rejectedOn = DateUtils.getLocalDateOfTenant();
this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks); this.remarks = StringUtils.defaultIfEmpty(remarks, this.remarks);
} }
public void markPanVerified(final String fullName, final String firstName, final String lastName, public void markPanVerified(final String fullName, final String firstName, final String lastName,
final String aadhaarSeedingStatus, final String category, final String status) { final String aadhaarSeedingStatus, final String category, final String status) {
this.panVerified = true; this.panVerified = true;
this.panVerifiedOn = LocalDate.now(); this.panVerifiedOn = DateUtils.getLocalDateOfTenant();
this.panFullName = StringUtils.defaultIfEmpty(fullName, null); this.panFullName = StringUtils.defaultIfEmpty(fullName, null);
this.panFirstName = StringUtils.defaultIfEmpty(firstName, null); this.panFirstName = StringUtils.defaultIfEmpty(firstName, null);
this.panLastName = StringUtils.defaultIfEmpty(lastName, null); this.panLastName = StringUtils.defaultIfEmpty(lastName, null);
@@ -507,28 +508,28 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public void markAadhaarVerified() { public void markAadhaarVerified() {
this.aadhaarVerified = true; this.aadhaarVerified = true;
this.aadhaarVerifiedOn = LocalDate.now(); this.aadhaarVerifiedOn = DateUtils.getLocalDateOfTenant();
} }
public void markGstinVerified() { public void markGstinVerified() {
this.gstinVerified = true; this.gstinVerified = true;
this.gstinVerifiedOn = LocalDate.now(); this.gstinVerifiedOn = DateUtils.getLocalDateOfTenant();
} }
public void markCinVerified() { public void markCinVerified() {
this.cinVerified = true; this.cinVerified = true;
this.cinVerifiedOn = LocalDate.now(); this.cinVerifiedOn = DateUtils.getLocalDateOfTenant();
} }
public void markDinVerified() { public void markDinVerified() {
this.dinVerified = true; this.dinVerified = true;
this.dinVerifiedOn = LocalDate.now(); this.dinVerifiedOn = DateUtils.getLocalDateOfTenant();
} }
public void markDinVerified(final String dinNumber, final String mcaResponseJson) { public void markDinVerified(final String dinNumber, final String mcaResponseJson) {
this.dinNumber = dinNumber; this.dinNumber = dinNumber;
this.dinVerified = true; this.dinVerified = true;
this.dinVerifiedOn = LocalDate.now(); this.dinVerifiedOn = DateUtils.getLocalDateOfTenant();
this.isDirector = true; this.isDirector = true;
this.dinMcaResponse = mcaResponseJson; this.dinMcaResponse = mcaResponseJson;
} }
@@ -24,6 +24,7 @@ import jakarta.persistence.Table;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import lombok.Getter; import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; 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. * Activity log for each DIP / MNRL mobile number lookup request.
@@ -78,6 +79,6 @@ public class MobileLookupLog extends AbstractAuditableWithUTCDateTimeCustom<Long
this.rawResponse = rawResponse; this.rawResponse = rawResponse;
this.lookupSource = lookupSource; this.lookupSource = lookupSource;
this.clientId = clientId; this.clientId = clientId;
this.lookedUpOn = OffsetDateTime.now(); this.lookedUpOn = DateUtils.getOffsetDateTimeOfTenant();
} }
} }
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Activity log for each sanctions screening request. * Activity log for each sanctions screening request.
@@ -73,7 +74,7 @@ public class SanctionsScreeningLog extends AbstractAuditableWithUTCDateTimeCusto
this.screeningSource = screeningSource; this.screeningSource = screeningSource;
this.clientId = clientId; this.clientId = clientId;
this.ipAddress = ipAddress; this.ipAddress = ipAddress;
this.screenedOn = OffsetDateTime.now(); this.screenedOn = DateUtils.getOffsetDateTimeOfTenant();
} }
// ── Accessors ───────────────────────────────────────────────────────────── // ── Accessors ─────────────────────────────────────────────────────────────
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.search.data; package org.apache.fineract.portfolio.search.data;
import java.util.Locale;
import org.apache.fineract.portfolio.search.SearchConstants.SearchSupportedResources; import org.apache.fineract.portfolio.search.SearchConstants.SearchSupportedResources;
public class SearchConditions { public class SearchConditions {
@@ -37,17 +38,17 @@ public class SearchConditions {
this.searchResource = searchResource; this.searchResource = searchResource;
this.exactMatch = exactMatch; this.exactMatch = exactMatch;
this.clientSearch = null == searchResource 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 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 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 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 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 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, public SearchConditions(final String searchQueryParam, final String searchResource, final Boolean clientSearch,
@@ -321,6 +321,6 @@ public class SearchUtil {
public String camelToSnake(final String camelStr) { public String camelToSnake(final String camelStr) {
return camelStr == null ? null 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);
} }
} }
@@ -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 * 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 &gt;= 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 * 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 * 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 * accept (the verify side filters too, but the gate is what kicks the user into the DSC flow
@@ -29,6 +29,7 @@ import java.time.LocalDateTime;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.useradministration.domain.AppUser; 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) { public CurrentAccountPendingEdit(final CurrentAccount currentAccount, final AppUser maker) {
this.currentAccount = currentAccount; this.currentAccount = currentAccount;
this.maker = maker; this.maker = maker;
this.createdOn = LocalDateTime.now(); this.createdOn = DateUtils.getLocalDateTimeOfTenant();
this.status = STATUS_PENDING; this.status = STATUS_PENDING;
this.pendingLockKey = lockKeyFor(currentAccount.getId()); this.pendingLockKey = lockKeyFor(currentAccount.getId());
} }
@@ -582,14 +583,14 @@ public class CurrentAccountPendingEdit extends AbstractPersistableCustom<Long> {
public void markApproved(final AppUser checker) { public void markApproved(final AppUser checker) {
this.status = STATUS_APPROVED; this.status = STATUS_APPROVED;
this.checker = checker; this.checker = checker;
this.checkedOn = LocalDateTime.now(); this.checkedOn = DateUtils.getLocalDateTimeOfTenant();
this.pendingLockKey = null; this.pendingLockKey = null;
} }
public void markRejected(final AppUser checker, final String reason) { public void markRejected(final AppUser checker, final String reason) {
this.status = STATUS_REJECTED; this.status = STATUS_REJECTED;
this.checker = checker; this.checker = checker;
this.checkedOn = LocalDateTime.now(); this.checkedOn = DateUtils.getLocalDateTimeOfTenant();
this.rejectReason = reason; this.rejectReason = reason;
this.pendingLockKey = null; this.pendingLockKey = null;
} }
@@ -51,7 +51,9 @@ public class CurrentAccountNumberGenerator {
if (val > maxVal) { if (val > maxVal) {
maxVal = val; maxVal = val;
} }
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {
// non-numeric suffix - skip when computing the max existing sequence
}
} }
} }
@@ -54,7 +54,6 @@ import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.MonthDay;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; 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.SavingsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType; import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType;
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType; 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.savings.SavingsPostingInterestPeriodType;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
import org.apache.fineract.portfolio.savings.data.SavingsProductAccountingDataValidator; import org.apache.fineract.portfolio.savings.data.SavingsProductAccountingDataValidator;
@@ -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.customerSegmentParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.productCodeParamName; 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.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.introducerRequiredParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareLinkingEnabledParamName; 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.nominationFacilityParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dormancyAlertParamName; 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.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.enableLockinPeriodParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.balanceSwapOptionParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.balanceSwapOptionParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.msmeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.msmeParamName;
@@ -26,6 +26,7 @@ import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_current_product_deposit_limit_kyc") @Table(name = "m_current_product_deposit_limit_kyc")
@@ -57,12 +58,12 @@ public class CurrentProductDepositLimitKyc extends AbstractPersistableCustom<Lon
private LocalDateTime createdDate; private LocalDateTime createdDate;
protected CurrentProductDepositLimitKyc() { protected CurrentProductDepositLimitKyc() {
this.createdDate = LocalDateTime.now();
} }
public CurrentProductDepositLimitKyc(CurrentProduct currentProduct) { public CurrentProductDepositLimitKyc(CurrentProduct currentProduct) {
this(); this();
this.currentProduct = currentProduct; this.currentProduct = currentProduct;
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
} }
public CurrentProduct getCurrentProduct() { return currentProduct; } public CurrentProduct getCurrentProduct() { return currentProduct; }
@@ -26,6 +26,7 @@ import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_current_product_facility_config") @Table(name = "m_current_product_facility_config")
@@ -87,12 +88,12 @@ public class CurrentProductFacilityConfig extends AbstractPersistableCustom<Long
private LocalDateTime createdDate; private LocalDateTime createdDate;
protected CurrentProductFacilityConfig() { protected CurrentProductFacilityConfig() {
this.createdDate = LocalDateTime.now();
} }
public CurrentProductFacilityConfig(CurrentProduct currentProduct) { public CurrentProductFacilityConfig(CurrentProduct currentProduct) {
this(); this();
this.currentProduct = currentProduct; this.currentProduct = currentProduct;
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
} }
public CurrentProduct getCurrentProduct() { return currentProduct; } public CurrentProduct getCurrentProduct() { return currentProduct; }
@@ -26,6 +26,7 @@ import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_current_product_facility_limits") @Table(name = "m_current_product_facility_limits")
@@ -51,7 +52,6 @@ public class CurrentProductFacilityLimit extends AbstractPersistableCustom<Long>
private LocalDateTime createdDate; private LocalDateTime createdDate;
protected CurrentProductFacilityLimit() { protected CurrentProductFacilityLimit() {
this.createdDate = LocalDateTime.now();
} }
private CurrentProductFacilityLimit(CurrentProduct currentProduct, String limitType, private CurrentProductFacilityLimit(CurrentProduct currentProduct, String limitType,
@@ -62,6 +62,7 @@ public class CurrentProductFacilityLimit extends AbstractPersistableCustom<Long>
this.generalLimit = generalLimit; this.generalLimit = generalLimit;
this.minorAccountLimit = minorAccountLimit; this.minorAccountLimit = minorAccountLimit;
this.smallSavingLimit = smallSavingLimit; this.smallSavingLimit = smallSavingLimit;
this.createdDate = DateUtils.getLocalDateTimeOfTenant();
} }
public static CurrentProductFacilityLimit create(CurrentProduct currentProduct, String limitType, public static CurrentProductFacilityLimit create(CurrentProduct currentProduct, String limitType,
@@ -19,13 +19,11 @@
package org.apache.fineract.portfolio.currentproduct.service; package org.apache.fineract.portfolio.currentproduct.service;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -75,7 +75,7 @@ import org.springframework.stereotype.Component;
@Path("/v1/journalentries") @Path("/v1/journalentries")
@Component @Component
@Tag(name = "Journal Entries", description = """ @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""") A journal entry directly changes the account balances on the general ledger""")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.email.service;
import java.time.Year; import java.time.Year;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Service @Service
public class EmailTemplateServiceImpl implements EmailTemplateService { public class EmailTemplateServiceImpl implements EmailTemplateService {
@@ -129,7 +130,7 @@ public class EmailTemplateServiceImpl implements EmailTemplateService {
+ escape(supportEmail) + "</a>" + escape(supportEmail) + "</a>"
+ " </p>" + " </p>"
+ " <p style=\"margin:0;font-size:12px;color:#94a3b8;\">" + " <p style=\"margin:0;font-size:12px;color:#94a3b8;\">"
+ " &copy; " + Year.now().getValue() + " " + escape(brandName) + ". All rights reserved." + " &copy; " + Year.now(DateUtils.getDateTimeZoneOfTenant()).getValue() + " " + escape(brandName) + ". All rights reserved."
+ " </p>" + " </p>"
+ " </td></tr>" + " </td></tr>"
+ " </table>" + " </table>"
@@ -109,6 +109,7 @@ public class ImpsCryptoUtil {
} }
/** Decrypt inbound payload using the CBS private key. */ /** Decrypt inbound payload using the CBS private key. */
@SuppressWarnings("StringSplitter")
public String decryptJson(String encryptedPayload) { public String decryptJson(String encryptedPayload) {
if (cbsPrivateKey == null) { if (cbsPrivateKey == null) {
throw new IllegalStateException("CBS private key not loaded — check fineract.imps.cbs.private-key-path"); throw new IllegalStateException("CBS private key not loaded — check fineract.imps.cbs.private-key-path");
@@ -34,6 +34,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* STUB implementation — returns hard-coded SUCCESS for every request and * STUB implementation — returns hard-coded SUCCESS for every request and
@@ -108,7 +109,7 @@ public class ImpsCbsServiceImpl implements ImpsCbsService {
resp.setRefId(request.getRefId()); resp.setRefId(request.getRefId());
resp.setUtr(request.getUtr()); resp.setUtr(request.getUtr());
resp.setAmount(request.getAmount()); resp.setAmount(request.getAmount());
resp.setTimeStamp(OffsetDateTime.now().format(ISO_OFFSET)); resp.setTimeStamp(OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(ISO_OFFSET));
resp.setStatus("SUCCESS"); resp.setStatus("SUCCESS");
resp.setResponseCode("00"); resp.setResponseCode("00");
AccountData payeeAcc = Optional.ofNullable(request.getPayee()).map(PartyData::getAccount).orElse(null); AccountData payeeAcc = Optional.ofNullable(request.getPayee()).map(PartyData::getAccount).orElse(null);
@@ -28,7 +28,6 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import okhttp3.Credentials; import okhttp3.Credentials;
import okhttp3.MediaType; 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.OpenProjectStatusSummaryData;
import org.apache.fineract.infrastructure.openproject.data.OpenProjectWorkPackageData; import org.apache.fineract.infrastructure.openproject.data.OpenProjectWorkPackageData;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Service @Service
@Slf4j @Slf4j
@@ -164,7 +164,7 @@ public class OpenProjectService {
} }
// Set required custom field: "Last Follow-up Date" (customField8) // 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()) { if (typeName != null && !typeName.isBlank()) {
JsonObject links = new JsonObject(); JsonObject links = new JsonObject();
@@ -47,6 +47,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -127,7 +128,7 @@ public class RbiFeedFetchService {
NodeList items = doc.getElementsByTagName("item"); NodeList items = doc.getElementsByTagName("item");
log.debug("RBI FeedFetchService - Parsed {} items from RSS for category: {}", items.getLength(), category); log.debug("RBI FeedFetchService - Parsed {} items from RSS for category: {}", items.getLength(), category);
List<RbiFeedItem> newItems = new ArrayList<>(); List<RbiFeedItem> newItems = new ArrayList<>();
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
for (int i = 0; i < items.getLength(); i++) { for (int i = 0; i < items.getLength(); i++) {
Element item = (Element) items.item(i); Element item = (Element) items.item(i);
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.infrastructure.security.otp.api; 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.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -107,7 +109,7 @@ public class EmailLoginOtpApiResource {
} }
final String fwd = req.getHeader("X-Forwarded-For"); final String fwd = req.getHeader("X-Forwarded-For");
if (fwd != null && !fwd.isBlank()) { if (fwd != null && !fwd.isBlank()) {
return fwd.split(",")[0].trim(); return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
} }
return req.getRemoteAddr(); return req.getRemoteAddr();
} }
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.infrastructure.security.otp.api; 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.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -107,7 +109,7 @@ public class LoginOtpApiResource {
} }
final String fwd = req.getHeader("X-Forwarded-For"); final String fwd = req.getHeader("X-Forwarded-For");
if (fwd != null && !fwd.isBlank()) { if (fwd != null && !fwd.isBlank()) {
return fwd.split(",")[0].trim(); return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
} }
return req.getRemoteAddr(); return req.getRemoteAddr();
} }
@@ -45,6 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Service @Service
public class EmailLoginOtpServiceImpl implements EmailLoginOtpService { 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. // Render the branded template — same look and feel for OTP, welcome, password-reset, alerts.
final String html = emailTemplateService.renderOtp(recipientName, otp, validityMinutes); 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); final boolean sent = emailSenderService.sendHtml(email, subject, html);
if (sent) { if (sent) {
@@ -20,7 +20,6 @@ package org.apache.fineract.organisation.teller.service;
import jakarta.persistence.PersistenceException; import jakarta.persistence.PersistenceException;
import java.util.Map; import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; 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.CashierTxnType;
import org.apache.fineract.organisation.teller.domain.Teller; import org.apache.fineract.organisation.teller.domain.Teller;
import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper; 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.CashierNotFoundException;
import org.apache.fineract.organisation.teller.exception.TellerAlreadyExistsException; import org.apache.fineract.organisation.teller.exception.TellerAlreadyExistsException;
import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer; import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer;
@@ -33,6 +33,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Path("/v1/cashexchange") @Path("/v1/cashexchange")
@Component @Component
@@ -94,7 +95,7 @@ public class CashExchangeApiResource {
header.setNote(note); header.setNote(note);
header.setCustomerIdProof(customerIdProof); header.setCustomerIdProof(customerIdProof);
header.setIsReversed(false); header.setIsReversed(false);
header.setCreatedAt(LocalDateTime.now()); header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
try { try {
AppUser user = this.context.authenticatedUser(); AppUser user = this.context.authenticatedUser();
if (user != null) { if (user != null) {
@@ -107,7 +108,9 @@ public class CashExchangeApiResource {
+ "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?",
Long.class, user.getId()); Long.class, user.getId());
header.setCashierId(cashierId); header.setCashierId(cashierId);
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
} }
} catch (Exception e) { } catch (Exception e) {
// resolving the current user must never block recording the exchange // resolving the current user must never block recording the exchange
@@ -160,14 +163,18 @@ public class CashExchangeApiResource {
try { try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject(); JsonObject json = JsonParser.parseString(body).getAsJsonObject();
reason = json.has("reason") && !json.get("reason").isJsonNull() ? json.get("reason").getAsString() : null; 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.setIsReversed(true);
exchange.setReversedAt(LocalDateTime.now()); exchange.setReversedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
exchange.setReverseReason(reason); exchange.setReverseReason(reason);
try { try {
AppUser user = this.context.authenticatedUser(); AppUser user = this.context.authenticatedUser();
if (user != null) exchange.setReversedBy(user.getId()); if (user != null) exchange.setReversedBy(user.getId());
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
repository.save(exchange); repository.save(exchange);
JsonObject result = new JsonObject(); JsonObject result = new JsonObject();
result.addProperty("resourceId", id); result.addProperty("resourceId", id);
@@ -59,6 +59,9 @@ import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; 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") @Path("/v1/cashier/deposit-requests")
@Component @Component
@@ -364,7 +367,7 @@ public class CashierDepositRequestApiResource {
final BigDecimal amount = new BigDecimal(record.get("amount").toString()); final BigDecimal amount = new BigDecimal(record.get("amount").toString());
final Object ptId = record.get("payment_type_id"); final Object ptId = record.get("payment_type_id");
final Object txnDateObj = record.get("txn_date"); 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 description = (String) record.get("description");
final String storedAccountType = (String) record.get("account_type"); 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 BigDecimal amount = new BigDecimal(record.get("amount").toString());
final Object ptId = record.get("payment_type_id"); final Object ptId = record.get("payment_type_id");
final Object txnDateObj = record.get("txn_date"); 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 description = (String) record.get("description");
if (savingsAccountId == null || !isCurrentAccountById(savingsAccountId)) { if (savingsAccountId == null || !isCurrentAccountById(savingsAccountId)) {
@@ -559,7 +562,7 @@ public class CashierDepositRequestApiResource {
final Long savingsAccountId, final BigDecimal amount, final Long savingsAccountId, final BigDecimal amount,
final Object paymentTypeId, final String txnDateStr, final String note) { 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 String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
final JsonObject payload = new JsonObject(); final JsonObject payload = new JsonObject();
@@ -587,7 +590,7 @@ public class CashierDepositRequestApiResource {
final Long currentAccountId, final BigDecimal amount, final Long currentAccountId, final BigDecimal amount,
final Object paymentTypeId, final String txnDateStr, final String note) { 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 String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
final JsonObject payload = new JsonObject(); final JsonObject payload = new JsonObject();
@@ -644,7 +647,7 @@ public class CashierDepositRequestApiResource {
final Object paymentTypeId, final String txnDateStr, final String note, final Object paymentTypeId, final String txnDateStr, final String note,
final boolean isDeposit) { 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; final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
// Deposit (CR): cashier receives cash → credit selected GL, debit cash vault (contra) // 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; 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 * Saves the denomination breakdown the cashier entered at submit time into
* m_savings_txn_denomination once the deposit is approved, so the cashier's * 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 * If a cash denomination breakdown is provided, its total must exactly equal
* the deposit amount — prevents the recorded vault movement from drifting * 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); final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE);
return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy")); return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
} catch (Exception e) { } catch (Exception e) {
return LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy")); return LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
} }
} }
@@ -31,6 +31,7 @@ import org.apache.fineract.portfolio.cashintercashier.domain.CashInterCashierRep
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Path("/v1/cashintercashier") @Path("/v1/cashintercashier")
@Component @Component
@@ -69,7 +70,7 @@ public class CashInterCashierApiResource {
} }
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull() 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() final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull()
? json.get("fromParty").getAsString() : null; ? json.get("fromParty").getAsString() : null;
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull() final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull()
@@ -95,13 +96,15 @@ public class CashInterCashierApiResource {
header.setToParty(toParty); header.setToParty(toParty);
header.setTransferAmount(total); header.setTransferAmount(total);
header.setNote(note); header.setNote(note);
header.setCreatedAt(LocalDateTime.now()); header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
header.setIsReversed(false); header.setIsReversed(false);
try { try {
AppUser user = this.context.authenticatedUser(); AppUser user = this.context.authenticatedUser();
header.setPerformedBy(user.getId()); header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername()); header.setPerformedByUsername(user.getUsername());
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
CashInterCashier saved = repository.saveAndFlush(header); CashInterCashier saved = repository.saveAndFlush(header);
saveDetails(saved.getId(), denominations); saveDetails(saved.getId(), denominations);
@@ -128,13 +131,17 @@ public class CashInterCashierApiResource {
try { try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject(); JsonObject json = JsonParser.parseString(body).getAsJsonObject();
reason = json.has("reason") ? json.get("reason").getAsString() : null; reason = json.has("reason") ? json.get("reason").getAsString() : null;
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
AppUser user = null; 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.setIsReversed(true);
record.setReversedAt(LocalDateTime.now()); record.setReversedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
record.setReverseReason(reason); record.setReverseReason(reason);
if (user != null) record.setReversedBy(user.getId()); if (user != null) record.setReversedBy(user.getId());
repository.save(record); repository.save(record);
@@ -32,6 +32,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Path("/v1/cashreconciliation") @Path("/v1/cashreconciliation")
@Component @Component
@@ -62,7 +63,7 @@ public class CashReconciliationApiResource {
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required", throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
"cashierId query param is 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 // 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 " 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") @Path("expected/me")
public String getExpectedForMe(@QueryParam("asOfDate") String asOfDate) { public String getExpectedForMe(@QueryParam("asOfDate") String asOfDate) {
context.authenticatedUser().validateHasPermissionTo("READ_CASHRECONCILIATION"); 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; Long appUserId;
try { try {
appUserId = context.authenticatedUser().getId(); appUserId = context.authenticatedUser().getId();
@@ -223,7 +224,7 @@ public class CashReconciliationApiResource {
throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required", throw new PlatformDataIntegrityException("error.msg.cashreconciliation.cashier.required",
"cashierId query param is 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( Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)", "SELECT COUNT(*) FROM m_cash_reconciliation WHERE cashier_id = ? AND recon_date = CAST(? AS DATE)",
Integer.class, cashierId, reconDate); Integer.class, cashierId, reconDate);
@@ -247,7 +248,7 @@ public class CashReconciliationApiResource {
"cashierId is required."); "cashierId is required.");
} }
final LocalDate reconDate = json.has("reconDate") && !json.get("reconDate").isJsonNull() 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() final String cashierName = json.has("cashierName") && !json.get("cashierName").isJsonNull()
? json.get("cashierName").getAsString() : null; ? json.get("cashierName").getAsString() : null;
final Long tellerId = json.has("tellerId") && !json.get("tellerId").isJsonNull() final Long tellerId = json.has("tellerId") && !json.get("tellerId").isJsonNull()
@@ -299,12 +300,14 @@ public class CashReconciliationApiResource {
header.setVariance(variance); header.setVariance(variance);
header.setStatus(status); header.setStatus(status);
header.setNote(note); header.setNote(note);
header.setCreatedAt(LocalDateTime.now()); header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
try { try {
AppUser user = this.context.authenticatedUser(); AppUser user = this.context.authenticatedUser();
header.setPerformedBy(user.getId()); header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername()); header.setPerformedByUsername(user.getUsername());
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
CashReconciliation saved = repository.saveAndFlush(header); CashReconciliation saved = repository.saveAndFlush(header);
for (CashReconciliationDetail d : details) { for (CashReconciliationDetail d : details) {
@@ -18,6 +18,7 @@ import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Path("/v1/cashreports") @Path("/v1/cashreports")
@Component @Component
@@ -34,7 +35,7 @@ public class CashReportApiResource {
@GET @GET
@Path("closing") @Path("closing")
public String closing(@QueryParam("date") String date) { 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 // 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, " 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); 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 // 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 " final String sql = "SELECT MIN(ct.transfer_date) AS earliest_unsettled "
@@ -256,7 +257,9 @@ public class CashReportApiResource {
java.sql.Date earliestDate = null; java.sql.Date earliestDate = null;
try { try {
earliestDate = jdbcTemplate.queryForObject(sql, java.sql.Date.class, cashierId, today, cashierId); 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 boolean unsettled = earliestDate != null;
final String unsettledSince = earliestDate != null ? earliestDate.toString() : null; final String unsettledSince = earliestDate != null ? earliestDate.toString() : null;
@@ -265,7 +268,9 @@ public class CashReportApiResource {
if (unsettled) { if (unsettled) {
try { try {
pendingAmount = denominationBreakdownWriter.getCashierTotalCash(cashierId); pendingAmount = denominationBreakdownWriter.getCashierTotalCash(cashierId);
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
} }
// Check whether today's morning cash transfer has been received // Check whether today's morning cash transfer has been received
@@ -276,7 +281,9 @@ public class CashReportApiResource {
Integer todayTransferCount = 0; Integer todayTransferCount = 0;
try { try {
todayTransferCount = jdbcTemplate.queryForObject(morningTransferSql, Integer.class, cashierId, today); todayTransferCount = jdbcTemplate.queryForObject(morningTransferSql, Integer.class, cashierId, today);
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
final boolean morningTransferReceived = todayTransferCount != null && todayTransferCount > 0; final boolean morningTransferReceived = todayTransferCount != null && todayTransferCount > 0;
result.put("cashierId", cashierId); result.put("cashierId", cashierId);
@@ -295,7 +302,7 @@ public class CashReportApiResource {
@Path("cashier-position") @Path("cashier-position")
public String cashierPosition(@QueryParam("cashierId") Long cashierId, public String cashierPosition(@QueryParam("cashierId") Long cashierId,
@QueryParam("asOfDate") String asOfDate) { @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 // CASHIER role: always scope to their own position only
final Long scopedId = cashierRoleHelper.getCashierIdForFiltering(); final Long scopedId = cashierRoleHelper.getCashierIdForFiltering();
@@ -326,7 +333,7 @@ public class CashReportApiResource {
@GET @GET
@Path("cashier-position/me") @Path("cashier-position/me")
public String cashierPositionMe(@QueryParam("asOfDate") String asOfDate) { 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; Long appUserId;
try { try {
appUserId = context.authenticatedUser().getId(); appUserId = context.authenticatedUser().getId();
@@ -362,7 +369,7 @@ public class CashReportApiResource {
@Path("cashier-morning") @Path("cashier-morning")
public String cashierMorning(@QueryParam("cashierId") Long cashierId, public String cashierMorning(@QueryParam("cashierId") Long cashierId,
@QueryParam("date") String date) { @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) { if (cashierId == null) {
Map<String, Object> empty = new LinkedHashMap<>(); Map<String, Object> empty = new LinkedHashMap<>();
empty.put("cashierId", null); empty.put("cashierId", null);
@@ -515,7 +522,9 @@ public class CashReportApiResource {
cashierName = jdbcTemplate.queryForObject( cashierName = jdbcTemplate.queryForObject(
"SELECT ms.display_name FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id WHERE mc.id = ?", "SELECT ms.display_name FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id WHERE mc.id = ?",
String.class, cashierId); String.class, cashierId);
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
Map<String, Object> result = new LinkedHashMap<>(); Map<String, Object> result = new LinkedHashMap<>();
result.put("cashierId", cashierId); result.put("cashierId", cashierId);
@@ -562,7 +571,7 @@ public class CashReportApiResource {
@QueryParam("offset") Integer offset, @QueryParam("offset") Integer offset,
@QueryParam("limit") Integer limit) { @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 resolvedLimit = (limit != null && limit > 0) ? limit : 20;
final int resolvedOffset = (offset != null && offset >= 0) ? offset : 0; final int resolvedOffset = (offset != null && offset >= 0) ? offset : 0;
@@ -36,6 +36,7 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Path("/v1/cashsettlement") @Path("/v1/cashsettlement")
@Component @Component
@@ -70,7 +71,7 @@ public class CashSettlementApiResource {
} }
final LocalDate settlementDate = json.has("settlementDate") && !json.get("settlementDate").isJsonNull() 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() final String fromParty = json.has("fromParty") && !json.get("fromParty").isJsonNull()
? json.get("fromParty").getAsString() : null; ? json.get("fromParty").getAsString() : null;
final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull() final String toParty = json.has("toParty") && !json.get("toParty").isJsonNull()
@@ -116,13 +117,15 @@ public class CashSettlementApiResource {
header.setToParty(toParty); header.setToParty(toParty);
header.setSettlementAmount(total); header.setSettlementAmount(total);
header.setNote(note); header.setNote(note);
header.setCreatedAt(LocalDateTime.now()); header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
header.setIsReversed(false); header.setIsReversed(false);
try { try {
AppUser user = this.context.authenticatedUser(); AppUser user = this.context.authenticatedUser();
header.setPerformedBy(user.getId()); header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername()); header.setPerformedByUsername(user.getUsername());
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
// Commit header + details first, then GL dispatch separately to avoid // Commit header + details first, then GL dispatch separately to avoid
// EclipseLink flushing pending detail rows inside Fineract's GL transaction. // EclipseLink flushing pending detail rows inside Fineract's GL transaction.
@@ -142,7 +145,9 @@ public class CashSettlementApiResource {
Long resolved = jdbcTemplate.queryForObject( Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, fromCashierId); "SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, fromCashierId);
if (resolved != null) glTellerId = resolved; if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
glDispatchService.settleCashFromCashier(glTellerId, fromCashierId, total, glDispatchService.settleCashFromCashier(glTellerId, fromCashierId, total,
currencyCode, settlementDate.format(DateTimeFormatter.ISO_LOCAL_DATE), currencyCode, settlementDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
note != null ? note : ""); note != null ? note : "");
@@ -174,13 +179,17 @@ public class CashSettlementApiResource {
try { try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject(); JsonObject json = JsonParser.parseString(body).getAsJsonObject();
reason = json.has("reason") ? json.get("reason").getAsString() : null; reason = json.has("reason") ? json.get("reason").getAsString() : null;
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
AppUser user = null; 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.setIsReversed(true);
settlement.setReversedAt(LocalDateTime.now()); settlement.setReversedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
settlement.setReverseReason(reason); settlement.setReverseReason(reason);
if (user != null) settlement.setReversedBy(user.getId()); if (user != null) settlement.setReversedBy(user.getId());
repository.save(settlement); repository.save(settlement);
@@ -192,12 +201,16 @@ public class CashSettlementApiResource {
Long resolved = jdbcTemplate.queryForObject( Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, settlement.getFromCashierId()); "SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, settlement.getFromCashierId());
if (resolved != null) glTellerId = resolved; if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
glDispatchService.allocateCashToCashier(glTellerId, glDispatchService.allocateCashToCashier(glTellerId,
settlement.getFromCashierId(), settlement.getSettlementAmount(), "INR", 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 : "")); "Reversal of cash settlement #" + id + (reason != null ? ": " + reason : ""));
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
} }
JsonObject result = new JsonObject(); JsonObject result = new JsonObject();
@@ -214,7 +227,7 @@ public class CashSettlementApiResource {
throw new PlatformDataIntegrityException("error.msg.cashsettlement.cashier.required", throw new PlatformDataIntegrityException("error.msg.cashsettlement.cashier.required",
"cashierId query param is 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( 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)", "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); Integer.class, cashierId, settlementDate);
@@ -36,6 +36,7 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Path("/v1/cashtransfer") @Path("/v1/cashtransfer")
@Component @Component
@@ -105,7 +106,7 @@ public class CashTransferApiResource {
header.setToCashierId(toCashierId); header.setToCashierId(toCashierId);
header.setTransferAmount(total); header.setTransferAmount(total);
header.setNote(note); header.setNote(note);
header.setCreatedAt(LocalDateTime.now()); header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
try { try {
AppUser user = this.context.authenticatedUser(); AppUser user = this.context.authenticatedUser();
if (user != null) { if (user != null) {
@@ -136,8 +137,10 @@ public class CashTransferApiResource {
Long resolved = jdbcTemplate.queryForObject( Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, toCashierId); "SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, toCashierId);
if (resolved != null) glTellerId = resolved; if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {} } catch (Exception ignored) {
final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now(); // intentionally ignored
}
final LocalDate txnDate = transferDate != null ? transferDate : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
glDispatchService.allocateCashToCashier(glTellerId, toCashierId, total, glDispatchService.allocateCashToCashier(glTellerId, toCashierId, total,
currencyCode, txnDate.format(DateTimeFormatter.ISO_LOCAL_DATE), currencyCode, txnDate.format(DateTimeFormatter.ISO_LOCAL_DATE),
note != null ? note : ""); note != null ? note : "");
@@ -169,15 +172,19 @@ public class CashTransferApiResource {
try { try {
JsonObject json = JsonParser.parseString(body).getAsJsonObject(); JsonObject json = JsonParser.parseString(body).getAsJsonObject();
reason = json.has("reason") ? json.get("reason").getAsString() : null; reason = json.has("reason") ? json.get("reason").getAsString() : null;
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
try { try {
AppUser user = this.context.authenticatedUser(); AppUser user = this.context.authenticatedUser();
transfer.setReversedBy(user.getId()); transfer.setReversedBy(user.getId());
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
transfer.setIsReversed(true); transfer.setIsReversed(true);
transfer.setReversedAt(java.time.LocalDateTime.now()); transfer.setReversedAt(java.time.LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
transfer.setReverseReason(reason); transfer.setReverseReason(reason);
repository.save(transfer); repository.save(transfer);
@@ -188,12 +195,16 @@ public class CashTransferApiResource {
Long resolved = jdbcTemplate.queryForObject( Long resolved = jdbcTemplate.queryForObject(
"SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, transfer.getToCashierId()); "SELECT teller_id FROM m_cashiers WHERE id = ?", Long.class, transfer.getToCashierId());
if (resolved != null) glTellerId = resolved; if (resolved != null) glTellerId = resolved;
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
glDispatchService.settleCashFromCashier(glTellerId, glDispatchService.settleCashFromCashier(glTellerId,
transfer.getToCashierId(), transfer.getTransferAmount(), "INR", 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 : "")); "Reversal of cash transfer #" + id + (reason != null ? ": " + reason : ""));
} catch (Exception ignored) {} } catch (Exception ignored) {
// intentionally ignored
}
} }
JsonObject result = new JsonObject(); JsonObject result = new JsonObject();
@@ -38,6 +38,7 @@ import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomin
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Records cashier denomination breakdown for GL account transactions (journal entries). * 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 String txnDateStr = fromJsonHelper.extractStringNamed("txnDate", root);
final LocalDate txnDate = (txnDateStr != null && !txnDateStr.isBlank()) final LocalDate txnDate = (txnDateStr != null && !txnDateStr.isBlank())
? LocalDate.parse(txnDateStr) ? LocalDate.parse(txnDateStr)
: LocalDate.now(); : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
if (!fromJsonHelper.parameterExists("denominationBreakdown", root)) { if (!fromJsonHelper.parameterExists("denominationBreakdown", root)) {
return "{\"result\":\"skipped\"}"; return "{\"result\":\"skipped\"}";
@@ -109,7 +110,9 @@ public class CashierGlDenominationApiResource {
officerId = user.getId(); officerId = user.getId();
officerUsername = user.getUsername(); officerUsername = user.getUsername();
} }
} catch (final Exception ignored) {} } catch (final Exception ignored) {
// intentionally ignored
}
final String resolvedTxnType = (txnType != null && !txnType.isBlank()) final String resolvedTxnType = (txnType != null && !txnType.isBlank())
? txnType.toUpperCase() ? txnType.toUpperCase()
@@ -144,7 +147,7 @@ public class CashierGlDenominationApiResource {
.count(count) .count(count)
.totalAmount(lineTotal) .totalAmount(lineTotal)
.txnDate(txnDate) .txnDate(txnDate)
.createdAt(LocalDateTime.now()) .createdAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()))
.txnType(resolvedTxnType) .txnType(resolvedTxnType)
.officerId(officerId) .officerId(officerId)
.officerUsername(officerUsername) .officerUsername(officerUsername)
@@ -29,6 +29,7 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Cashier Share Collection API — POST /v1/cashier/sharecollection * Cashier Share Collection API — POST /v1/cashier/sharecollection
@@ -131,7 +132,7 @@ public class ShareCollectionApiResource {
final LocalDate entryDate; final LocalDate entryDate;
try { try {
entryDate = transactionDateStr != null entryDate = transactionDateStr != null
? LocalDate.parse(transactionDateStr) : LocalDate.now(); ? LocalDate.parse(transactionDateStr) : LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
} catch (DateTimeParseException e) { } catch (DateTimeParseException e) {
throw new PlatformDataIntegrityException( throw new PlatformDataIntegrityException(
"error.msg.sharecollection.date.invalid", "error.msg.sharecollection.date.invalid",
@@ -193,7 +194,7 @@ public class ShareCollectionApiResource {
final String txnId = "SC-" + shareAccountId + "-" + System.currentTimeMillis(); final String txnId = "SC-" + shareAccountId + "-" + System.currentTimeMillis();
final long userId = currentUser.getId(); final long userId = currentUser.getId();
final String collectorName = currentUser.getUsername(); final String collectorName = currentUser.getUsername();
final LocalDateTime now = LocalDateTime.now(); final LocalDateTime now = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
// ── Post GL journal entries ──────────────────────────────────────── // ── Post GL journal entries ────────────────────────────────────────
// Dr Cash at Vault (SHARES_REFERENCE) = totalAmount // Dr Cash at Vault (SHARES_REFERENCE) = totalAmount
@@ -41,7 +41,6 @@ import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSer
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintStatsData; import org.apache.fineract.portfolio.cfcfrms.data.ComplaintStatsData;
import org.apache.fineract.portfolio.cfcfrms.data.ComplaintTicketData; 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.apache.fineract.portfolio.cfcfrms.service.CfcfrmsComplaintService;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
@@ -56,7 +56,7 @@ import org.springframework.stereotype.Component;
* POST /v1/simulator/cfcfrms/verify-account – Verify account at bank * POST /v1/simulator/cfcfrms/verify-account – Verify account at bank
* POST /v1/simulator/cfcfrms/verify-transaction – Verify transaction ref * POST /v1/simulator/cfcfrms/verify-transaction – Verify transaction ref
* *
* --- STATUS & REPORTING --- * --- STATUS &amp; REPORTING ---
* GET /v1/simulator/cfcfrms/complaint-status/{ref} – Complaint status from CFCFRMS side * GET /v1/simulator/cfcfrms/complaint-status/{ref} – Complaint status from CFCFRMS side
* GET /v1/simulator/cfcfrms/bank-performance – Bank performance metrics * GET /v1/simulator/cfcfrms/bank-performance – Bank performance metrics
* </pre> * </pre>
@@ -36,6 +36,7 @@ import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; 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. * Seeds CFCFRMS complaint tickets, audit logs, and simulator logs on startup.
@@ -59,7 +60,7 @@ public class CfcfrmsDataSeeder {
} }
LOG.info("Seeding CFCFRMS complaint data..."); LOG.info("Seeding CFCFRMS complaint data...");
final OffsetDateTime now = OffsetDateTime.now(); final OffsetDateTime now = OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant());
// ─── 3 RECEIVED ────────────────────────────────────────────── // ─── 3 RECEIVED ──────────────────────────────────────────────
seedTicket("TKT-20250318-001", "Rajesh Kumar Sharma", "9876543210", seedTicket("TKT-20250318-001", "Rajesh Kumar Sharma", "9876543210",
@@ -46,6 +46,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -206,7 +207,7 @@ public class CfcfrmsComplaintServiceImpl implements CfcfrmsComplaintService {
final Map<String, Object> result = new HashMap<>(); final Map<String, Object> result = new HashMap<>();
result.put("escalatedTo", nextIfsc); result.put("escalatedTo", nextIfsc);
result.put("escalationRef", escalationRef); result.put("escalationRef", escalationRef);
result.put("timestamp", OffsetDateTime.now().toString()); result.put("timestamp", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
return result; return result;
} }
@@ -218,7 +219,7 @@ public class CfcfrmsComplaintServiceImpl implements CfcfrmsComplaintService {
} }
private String generateTicketId() { 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); final int seq = (int) (ticketRepository.count() + 1);
return String.format("TKT-%s-%03d", date, seq); return String.format("TKT-%s-%03d", date, seq);
} }
@@ -25,12 +25,10 @@ import java.time.LocalDate;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.cfcfrms.data.CfcfrmsConstants; import org.apache.fineract.portfolio.cfcfrms.data.CfcfrmsConstants;
@@ -44,6 +42,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -76,7 +75,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
final Map<String, Object> response = new LinkedHashMap<>(); final Map<String, Object> response = new LinkedHashMap<>();
response.put("cfcfrmsRef", cfcfrmsRef); response.put("cfcfrmsRef", cfcfrmsRef);
response.put("ticketId", ticketData.getTicketId()); 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("bankAcknowledged", true);
response.put("message", "Complaint pushed to " + CfcfrmsConstants.BANK_IFSC + " successfully"); response.put("message", "Complaint pushed to " + CfcfrmsConstants.BANK_IFSC + " successfully");
return response; return response;
@@ -85,7 +84,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
@Override @Override
public Map<String, Object> pushHoldDirective(final JsonObject json) { public Map<String, Object> pushHoldDirective(final JsonObject json) {
final String cfcfrmsRef = getStringOrThrow(json, "cfcfrmsRef"); 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)); + "-" + String.format("%04d", RANDOM.nextInt(10000));
final String directiveType = getStringOrThrow(json, "directiveType"); final String directiveType = getStringOrThrow(json, "directiveType");
@@ -112,7 +111,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
response.put("directiveRef", directiveRef); response.put("directiveRef", directiveRef);
response.put("status", "DIRECTIVE_SENT"); response.put("status", "DIRECTIVE_SENT");
response.put("bankResponse", bankResponse); response.put("bankResponse", bankResponse);
response.put("timestamp", OffsetDateTime.now().toString()); response.put("timestamp", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
return response; return response;
} }
@@ -208,7 +207,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
public Map<String, Object> receiveEscalation(final JsonObject json) { public Map<String, Object> receiveEscalation(final JsonObject json) {
final String cfcfrmsRef = getStringOrThrow(json, "cfcfrmsRef"); final String cfcfrmsRef = getStringOrThrow(json, "cfcfrmsRef");
final String ticketId = getStringOrThrow(json, "ticketId"); 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)); + "-" + String.format("%04d", RANDOM.nextInt(10000));
// Extract nextBank from fundsMovedTo // Extract nextBank from fundsMovedTo
@@ -268,7 +267,7 @@ public class CfcfrmsSimulatorServiceImpl implements CfcfrmsSimulatorService {
final Map<String, Object> response = new LinkedHashMap<>(); final Map<String, Object> response = new LinkedHashMap<>();
response.put("transactionFound", true); response.put("transactionFound", true);
response.put("transactionStatus", "COMPLETED"); 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("senderAccount", json.has("accountNumber") ? json.get("accountNumber").getAsString() : "UNKNOWN");
response.put("receiverAccount", 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) + "%" .setScale(1, java.math.RoundingMode.HALF_UP) + "%"
: "0.0%"; : "0.0%";
response.put("tatComplianceRate", tatCompliance); response.put("tatComplianceRate", tatCompliance);
response.put("lastActivityAt", OffsetDateTime.now().toString()); response.put("lastActivityAt", OffsetDateTime.now(DateUtils.getDateTimeZoneOfTenant()).toString());
return response; return response;
} }
// ── Helpers ── // ── Helpers ──
private String generateCfcfrmsRef() { 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)); return "CFCFRMS-" + date + "-" + String.format("%04d", RANDOM.nextInt(10000));
} }
@@ -201,7 +201,7 @@ public class NpciInwardPushService {
final String presentingBranchCode = getString(instrument, "depositBranchCode"); final String presentingBranchCode = getString(instrument, "depositBranchCode");
item.recordInwardMeta(getString(instrument, "serialNumber"), presentingBranchCode, batch.getReturnTatDeadline()); 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() ? instrument.get("stopPaymentFlag").getAsBoolean()
: null; : null;
item.recordSourceDetails(getString(instrument, "ctsReferenceNumber"), instrumentStopPaymentFlag, item.recordSourceDetails(getString(instrument, "ctsReferenceNumber"), instrumentStopPaymentFlag,
@@ -70,6 +70,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Path("/v1/clerk/disbursement-requests") @Path("/v1/clerk/disbursement-requests")
@Component @Component
@@ -383,7 +384,7 @@ public class ClerkDisbursementRequestApiResource {
final JsonArray creditEntries = gson.fromJson(creditEntriesJson, JsonArray.class); final JsonArray creditEntries = gson.fromJson(creditEntriesJson, JsonArray.class);
final Object ptId = record.get("payment_type_id"); final Object ptId = record.get("payment_type_id");
final String description = (String) record.get("description"); 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 final Long sourceGlId = record.get("gl_account_id") != null
? ((Number) record.get("gl_account_id")).longValue() : 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 entryType = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS";
final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : "?"; final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : "?";
final Long entryPtId = entry.has("paymentTypeId") && !entry.get("paymentTypeId").isJsonNull() 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); : (ptId != null ? ((Number) ptId).longValue() : null);
if ("GL".equals(entryType)) { if ("GL".equals(entryType)) {
@@ -810,7 +811,7 @@ public class ClerkDisbursementRequestApiResource {
? req.get("currencyCode").getAsString() : "INR"; ? req.get("currencyCode").getAsString() : "INR";
final String txnDateStr = req.has("transactionDate") && !req.get("transactionDate").isJsonNull() final String txnDateStr = req.has("transactionDate") && !req.get("transactionDate").isJsonNull()
? req.get("transactionDate").getAsString() ? 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() final String description = req.has("description") && !req.get("description").isJsonNull()
? req.get("description").getAsString() : null; ? req.get("description").getAsString() : null;
@@ -830,7 +831,6 @@ public class ClerkDisbursementRequestApiResource {
// Validate all entries: savings/current only (no GL), savingsAccountId present, amount > 0 // Validate all entries: savings/current only (no GL), savingsAccountId present, amount > 0
final Set<Long> drAccountIds = new LinkedHashSet<>(); final Set<Long> drAccountIds = new LinkedHashSet<>();
final Set<Long> crAccountIds = new LinkedHashSet<>();
for (final JsonObject e : drEntries) { for (final JsonObject e : drEntries) {
final String acTyp = e.has("accountType") ? e.get("accountType").getAsString() : "SAVINGS"; 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, if (drAccountIds.contains(crId)) throw apiError(400,
"Account " + (e.has("accountNo") ? e.get("accountNo").getAsString() : crId) "Account " + (e.has("accountNo") ? e.get("accountNo").getAsString() : crId)
+ " cannot appear on both DR and CR sides of the same transfer."); + " cannot appear on both DR and CR sides of the same transfer.");
crAccountIds.add(crId);
} }
// Validate balanced entry: total DR == total CR // Validate balanced entry: total DR == total CR
@@ -999,7 +998,7 @@ public class ClerkDisbursementRequestApiResource {
* under concurrent inserts on the same date. * under concurrent inserts on the same date.
*/ */
private String generateBtVoucherNo(final long logId) { 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); + String.format("%05d", logId);
} }
@@ -1053,7 +1052,9 @@ public class ClerkDisbursementRequestApiResource {
try { try {
final Long txnIdLong = Long.parseLong(txnId); final Long txnIdLong = Long.parseLong(txnId);
undoOps.add(() -> undoAccountTransaction(accountId, txnIdLong, isCurrentAccount)); undoOps.add(() -> undoAccountTransaction(accountId, txnIdLong, isCurrentAccount));
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {
// intentionally ignored
}
} }
private void undoAccountTransaction(final Long accountId, final Long transactionId, private void undoAccountTransaction(final Long accountId, final Long transactionId,
@@ -1072,50 +1073,6 @@ public class ClerkDisbursementRequestApiResource {
commandsSourceWritePlatformService.logCommandSource(cmd); 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) { private boolean isCurrentAccountById(final Long accountId) {
try { try {
final Integer count = jdbcTemplate.queryForObject( final Integer count = jdbcTemplate.queryForObject(
@@ -1131,7 +1088,7 @@ public class ClerkDisbursementRequestApiResource {
final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE); final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE);
return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy")); return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
} catch (Exception e) { } 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; ? (BigDecimal) accts.get(0).get("account_balance_derived") : BigDecimal.ZERO;
final java.sql.Date sqlDate = java.sql.Date.valueOf(txnDate); 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 String refNo = UUID.randomUUID().toString().replace("-", "").substring(0, 20);
final Long userId = currentUser.getId(); final Long userId = currentUser.getId();
@@ -1534,7 +1491,7 @@ public class ClerkDisbursementRequestApiResource {
+ ". Available: " + currentBalance + ", Requested: " + amount); + ". Available: " + currentBalance + ", Requested: " + amount);
final java.sql.Date sqlDate = java.sql.Date.valueOf(txnDate); 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 String refNo = UUID.randomUUID().toString().replace("-", "").substring(0, 20);
final Long userId = currentUser.getId(); final Long userId = currentUser.getId();
@@ -59,6 +59,9 @@ import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; 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") @Path("/v1/clerk/withdrawal-requests")
@Component @Component
@@ -437,7 +440,7 @@ public class ClerkWithdrawalRequestApiResource {
final BigDecimal amount = new BigDecimal(record.get("amount").toString()); final BigDecimal amount = new BigDecimal(record.get("amount").toString());
final Object ptId = record.get("payment_type_id"); final Object ptId = record.get("payment_type_id");
final Object txnDateObj = record.get("txn_date"); 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 description = (String) record.get("description");
String txnId = null; String txnId = null;
@@ -713,7 +716,7 @@ public class ClerkWithdrawalRequestApiResource {
final Long savingsAccountId, final BigDecimal amount, final Long savingsAccountId, final BigDecimal amount,
final Object paymentTypeId, final String txnDateStr, final String note) { 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 String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
final JsonObject payload = new JsonObject(); final JsonObject payload = new JsonObject();
@@ -741,7 +744,7 @@ public class ClerkWithdrawalRequestApiResource {
final Long currentAccountId, final BigDecimal amount, final Long currentAccountId, final BigDecimal amount,
final Object paymentTypeId, final String txnDateStr, final String note) { 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 String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
final JsonObject payload = new JsonObject(); final JsonObject payload = new JsonObject();
@@ -780,7 +783,7 @@ public class ClerkWithdrawalRequestApiResource {
final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE); final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE);
return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy")); return ld.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
} catch (Exception e) { } catch (Exception e) {
return LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy")); return LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
} }
} }
@@ -39,6 +39,8 @@ import jakarta.ws.rs.core.MediaType;
import java.io.InputStream; import java.io.InputStream;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam; import org.glassfish.jersey.media.multipart.FormDataParam;
import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -158,7 +160,13 @@ public class ClientKycApiResource {
template.put("clientTypeName", clientTypeName); template.put("clientTypeName", clientTypeName);
template.put("legalFormId", legalFormId); template.put("legalFormId", legalFormId);
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId)); // Minor detection mirrors the frontend's own rule (age < 10) 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() < 10;
template.put("isMinor", isMinor);
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId, isMinor));
return toApiJsonSerializer.serialize(template); return toApiJsonSerializer.serialize(template);
} }
@@ -166,11 +174,21 @@ public class ClientKycApiResource {
/** /**
* Returns required KYC documents based on client type (RBI Master Direction on KYC). * 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<>(); final List<Map<String, Object>> docs = new ArrayList<>();
// Individual types (legalFormId=1) // Individual types (legalFormId=1)
if (legalFormId != null && 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("Aadhaar Card", "AADHAAR", true));
docs.add(doc("PAN Card", "PAN", true)); docs.add(doc("PAN Card", "PAN", true));
docs.add(doc("Voter ID", "VOTER_ID", false)); docs.add(doc("Voter ID", "VOTER_ID", false));
@@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicLong;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.portfolio.client.domain.ClientKyc; import org.apache.fineract.portfolio.client.domain.ClientKyc;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Dummy / stub implementation of {@link CkycSftpUploadService}. * Dummy / stub implementation of {@link CkycSftpUploadService}.
@@ -60,7 +61,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
final String ckycNumber = generateCkycNumber(kyc.getClient().getId()); final String ckycNumber = generateCkycNumber(kyc.getClient().getId());
final String batchId = generateBatchId(); final String batchId = generateBatchId();
final LocalDate today = LocalDate.now(); final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
return CkycUploadResult.builder() return CkycUploadResult.builder()
.ckycNumber(ckycNumber) .ckycNumber(ckycNumber)
@@ -82,7 +83,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
return CkycUploadResult.builder() return CkycUploadResult.builder()
.status("FAILED") .status("FAILED")
.remarks("Uploaded file is not a valid ZIP archive (bad magic bytes).") .remarks("Uploaded file is not a valid ZIP archive (bad magic bytes).")
.uploadedOn(LocalDate.now()) .uploadedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))
.build(); .build();
} }
@@ -92,7 +93,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
return CkycUploadResult.builder() return CkycUploadResult.builder()
.ckycNumber(ckycNumber) .ckycNumber(ckycNumber)
.batchId(batchId) .batchId(batchId)
.uploadedOn(LocalDate.now()) .uploadedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))
.status("SUCCESS") .status("SUCCESS")
.remarks("Dummy CKYC ZIP upload accepted (" + zipBytes.length + " bytes). " .remarks("Dummy CKYC ZIP upload accepted (" + zipBytes.length + " bytes). "
+ "Replace with real CERSAI SFTP integration before go-live.") + "Replace with real CERSAI SFTP integration before go-live.")
@@ -112,7 +113,7 @@ public class DummyCkycSftpUploadServiceImpl implements CkycSftpUploadService {
} }
private String generateBatchId() { 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()); + String.format("%06d", BATCH_COUNTER.getAndIncrement());
} }
} }
@@ -50,6 +50,7 @@ import org.apache.fineract.portfolio.client.ckyc.domain.CkycBatchRepository;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Slf4j @Slf4j
@Service @Service
@@ -86,11 +87,11 @@ public class CkycBatchBuilderServiceImpl implements CkycBatchBuilderService {
throw new IllegalArgumentException("clientIds must not be empty"); 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 int seq = nextBatchSequenceForToday(today);
final String batchId = "BATCH-" + BATCH_ID_FMT.format(today) + "-" + String.format("%03d", seq); 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. // 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 List<CkycBatchManifestEntry> manifest = new ArrayList<>();
final ByteArrayOutputStream zipBytesOut = new ByteArrayOutputStream(); final ByteArrayOutputStream zipBytesOut = new ByteArrayOutputStream();
@@ -26,7 +26,6 @@ import java.nio.charset.StandardCharsets;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Slf4j @Slf4j
@Service @Service
@@ -219,6 +219,7 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
// ===== Customer List ===== // ===== Customer List =====
@Override @Override
@SuppressWarnings("StringSplitter")
public Map<String, Object> listCustomers(String name, String branch, String city, String gender, String state, String dateFrom, 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) { String dateTo, Integer page, Integer pageSize, String sort) {
@@ -375,13 +376,13 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
switch (format != null ? format.toLowerCase() : "csv") { switch (format != null ? format.toLowerCase() : "csv") {
case "txt": case "txt":
contentType = "text/plain"; contentType = "text/plain";
filename = "ckyc_export_" + LocalDate.now() + ".txt"; filename = "ckyc_export_" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()) + ".txt";
content = buildPipeDelimitedExport(rows); content = buildPipeDelimitedExport(rows);
break; break;
case "csv": case "csv":
default: default:
contentType = "text/csv"; contentType = "text/csv";
filename = "ckyc_export_" + LocalDate.now() + ".csv"; filename = "ckyc_export_" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()) + ".csv";
content = buildCsvExport(rows); content = buildCsvExport(rows);
break; break;
} }
@@ -431,7 +432,7 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
errors.add("Parse error: " + e.getMessage()); 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("fileId", fileId);
result.put("totalRecords", totalRecords); result.put("totalRecords", totalRecords);
result.put("customers", customers); result.put("customers", customers);
@@ -453,9 +454,9 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
@Override @Override
public Response generateCkycFile(Map<String, Object> request) { public Response generateCkycFile(Map<String, Object> request) {
// Placeholder — delegates to CkycBatchBuilderService for actual ZIP generation // 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") 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(); .build();
} }
@@ -572,7 +573,7 @@ public class CkycReportsReadServiceImpl implements CkycReportsReadService {
private String buildPipeDelimitedExport(List<Map<String, Object>> rows) { private String buildPipeDelimitedExport(List<Map<String, Object>> rows) {
StringBuilder sb = new StringBuilder(); 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) { for (Map<String, Object> row : rows) {
sb.append("20|"); sb.append("20|");
sb.append(val(row.get("cif"))).append("|"); sb.append(val(row.get("cif"))).append("|");
@@ -47,6 +47,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Dummy / stub implementation of {@link CkycBatchSftpUploadService}. * Dummy / stub implementation of {@link CkycBatchSftpUploadService}.
@@ -149,10 +150,10 @@ public class DummyCkycBatchSftpUploadServiceImpl implements CkycBatchSftpUploadS
} }
// 3) Build the ACK reference and timestamp // 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) + "-" final String ackReference = "CERSAI-ACK-" + ACK_TS_FMT.format(now) + "-"
+ String.format("%04d", ACK_COUNTER.getAndIncrement()); + 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 // 4) Parse manifest → generate a CKYC number per included client → persist on m_client_kyc
final List<Map<String, Object>> perClient = new ArrayList<>(); final List<Map<String, Object>> perClient = new ArrayList<>();
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.portfolio.client.contactverification.api; 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.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -152,7 +154,7 @@ public class ClientContactVerificationApiResource {
} }
final String fwd = req.getHeader("X-Forwarded-For"); final String fwd = req.getHeader("X-Forwarded-For");
if (fwd != null && !fwd.isBlank()) { if (fwd != null && !fwd.isBlank()) {
return fwd.split(",")[0].trim(); return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
} }
return req.getRemoteAddr(); return req.getRemoteAddr();
} }
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.portfolio.client.contactverification.api; 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.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -147,7 +149,7 @@ public class ClientCreationContactVerificationApiResource {
} }
final String fwd = req.getHeader("X-Forwarded-For"); final String fwd = req.getHeader("X-Forwarded-For");
if (fwd != null && !fwd.isBlank()) { if (fwd != null && !fwd.isBlank()) {
return fwd.split(",")[0].trim(); return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
} }
return req.getRemoteAddr(); return req.getRemoteAddr();
} }
@@ -53,6 +53,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* See {@link ClientContactOtpService}. Deliberately does NOT touch {@code LoginOtpServiceImpl} / * 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)); log.setExpiresAt(OffsetDateTime.now(ZoneOffset.UTC).plusSeconds(ttlSeconds));
final String html = emailTemplateService.renderOtp(null, otp, validityMinutes); 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); final boolean sent = emailSenderService.sendHtml(emailAddress, subject, html);
@@ -34,6 +34,7 @@ import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget; import org.mapstruct.MappingTarget;
import org.mapstruct.Named; import org.mapstruct.Named;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Mapper(config = MapstructMapperConfig.class) @Mapper(config = MapstructMapperConfig.class)
public interface ClientMapper { public interface ClientMapper {
@@ -271,7 +272,7 @@ public interface ClientMapper {
default void computeAge(Client source, @MappingTarget ClientData target) { default void computeAge(Client source, @MappingTarget ClientData target) {
final LocalDate dob = source.getDateOfBirth(); final LocalDate dob = source.getDateOfBirth();
if (dob != null) { 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.setAgeYears(age.getYears());
target.setAgeMonths(age.getMonths()); target.setAgeMonths(age.getMonths());
target.setAgeDays(age.getDays()); target.setAgeDays(age.getDays());
@@ -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.ClientRepository;
import org.apache.fineract.portfolio.client.domain.LegalForm; import org.apache.fineract.portfolio.client.domain.LegalForm;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Generates CIF numbers in the format <code>TTYYYYNNNNNN</code>: * Generates CIF numbers in the format <code>TTYYYYNNNNNN</code>:
@@ -90,6 +91,6 @@ public class CifNumberGenerator {
private int resolveYear(final Client client) { private int resolveYear(final Client client) {
final LocalDate submittedOn = client.getSubmittedOnDate(); final LocalDate submittedOn = client.getSubmittedOnDate();
return (submittedOn != null ? submittedOn : LocalDate.now()).getYear(); return (submittedOn != null ? submittedOn : LocalDate.now(DateUtils.getDateTimeZoneOfTenant())).getYear();
} }
} }
@@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* See {@link ClientChangeNotificationService}. * See {@link ClientChangeNotificationService}.
@@ -88,7 +89,7 @@ public class ClientChangeNotificationServiceImpl implements ClientChangeNotifica
final String alertTitle = fieldLabel + " Updated"; final String alertTitle = fieldLabel + " Updated";
final String alertMessage = "Your " + fieldLabel.toLowerCase() + " on file was changed from " final String alertMessage = "Your " + fieldLabel.toLowerCase() + " on file was changed from "
+ describeForDisplay(fieldLabel, oldValue) + " to " + describeForDisplay(fieldLabel, newValue) + " on " + 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 html = emailTemplateService.renderAlert(recipientName, alertTitle, alertMessage);
final String subject = alertTitle + " - " + appName; final String subject = alertTitle + " - " + appName;
@@ -63,6 +63,7 @@ import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -488,7 +489,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
Integer ageMonths1 = null; Integer ageMonths1 = null;
Integer ageDays1 = null; Integer ageDays1 = null;
if (dateOfBirth != 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(); ageYears1 = age.getYears();
ageMonths1 = age.getMonths(); ageMonths1 = age.getMonths();
ageDays1 = age.getDays(); ageDays1 = age.getDays();
@@ -879,7 +880,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
Integer ageMonths = null; Integer ageMonths = null;
Integer ageDays = null; Integer ageDays = null;
if (dateOfBirth != 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(); ageYears = age.getYears();
ageMonths = age.getMonths(); ageMonths = age.getMonths();
ageDays = age.getDays(); ageDays = age.getDays();
@@ -1786,6 +1786,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
* verifying KYC names against ID documents). Null / blank → null so we don't accidentally * verifying KYC names against ID documents). Null / blank → null so we don't accidentally
* persist an empty string in place of an unset field. * persist an empty string in place of an unset field.
*/ */
@SuppressWarnings("StringSplitter")
private static String toTitleCase(final String raw) { private static String toTitleCase(final String raw) {
if (raw == null) { if (raw == null) {
return null; return null;
@@ -582,21 +582,6 @@ public class CtsClearingWritePlatformServiceImpl implements CtsClearingWritePlat
} }
} }
/** SHA-256 hex digest of the given content, used as a visible integrity fingerprint in the CTS header. */
private String sha256Hex(final String content) {
try {
final java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
final byte[] hash = digest.digest(content.getBytes(java.nio.charset.StandardCharsets.UTF_8));
final StringBuilder hex = new StringBuilder();
for (final byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (final java.security.NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 is not available", e);
}
}
/** Masks all but the last 4 characters of an account number (matches the CTS file convention of not exposing full numbers). */ /** Masks all but the last 4 characters of an account number (matches the CTS file convention of not exposing full numbers). */
private String maskAccountNumber(final String accountNumber) { private String maskAccountNumber(final String accountNumber) {
if (accountNumber == null || accountNumber.isBlank()) { if (accountNumber == null || accountNumber.isBlank()) {
@@ -26,6 +26,7 @@ import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMaste
import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterRepository; import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterRepository;
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Path("/v1/denominationmaster") @Path("/v1/denominationmaster")
@Component @Component
@@ -105,7 +106,7 @@ public class DenominationMasterApiResource {
writeAudit("ACTIVATE", dm, null); writeAudit("ACTIVATE", dm, null);
} else if ("deactivate".equals(command)) { } else if ("deactivate".equals(command)) {
dm.setIsActive(false); dm.setIsActive(false);
dm.setDiscontinuedOn(LocalDate.now()); dm.setDiscontinuedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
repository.save(dm); repository.save(dm);
writeAudit("DEACTIVATE", dm, null); writeAudit("DEACTIVATE", dm, null);
} else if ("restore".equals(command)) { } else if ("restore".equals(command)) {
@@ -15,6 +15,7 @@ import java.time.LocalDateTime;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_denomination_master_audit") @Table(name = "m_denomination_master_audit")
@@ -77,7 +78,7 @@ public class DenominationMasterAudit {
a.changes = changes; a.changes = changes;
a.performedBy = performedBy; a.performedBy = performedBy;
a.performedByUsername = performedByUsername; a.performedByUsername = performedByUsername;
a.performedOn = LocalDateTime.now(); a.performedOn = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
return a; return a;
} }
} }
@@ -26,6 +26,7 @@ import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomin
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** /**
* Validates and persists the per-transaction cash denomination breakdown sent by the UI * Validates and persists the per-transaction cash denomination breakdown sent by the UI
@@ -163,7 +164,7 @@ public class DenominationBreakdownWriter {
} }
if (cashierId == null) return; if (cashierId == null) return;
final String today = java.time.LocalDate.now().toString(); final String today = java.time.LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
final String sql = "SELECT COUNT(*) FROM m_cash_transfer ct " final String sql = "SELECT COUNT(*) FROM m_cash_transfer ct "
+ "WHERE ct.to_cashier_id = ? " + "WHERE ct.to_cashier_id = ? "
+ " AND ct.transfer_date < CAST(? AS DATE) " + " AND ct.transfer_date < CAST(? AS DATE) "
@@ -203,7 +204,7 @@ public class DenominationBreakdownWriter {
} }
if (cashierId == null) return; if (cashierId == null) return;
final String today = java.time.LocalDate.now().toString(); final String today = java.time.LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).toString();
final String sql = "SELECT COUNT(*) FROM m_cash_transfer ct " final String sql = "SELECT COUNT(*) FROM m_cash_transfer ct "
+ "WHERE ct.to_cashier_id = ? " + "WHERE ct.to_cashier_id = ? "
+ " AND ct.transfer_date = CAST(? AS DATE) " + " AND ct.transfer_date = CAST(? AS DATE) "
@@ -511,7 +512,7 @@ public class DenominationBreakdownWriter {
.count(count) .count(count)
.totalAmount(lineTotal) .totalAmount(lineTotal)
.txnDate(txnDate) .txnDate(txnDate)
.createdAt(LocalDateTime.now()) .createdAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()))
.txnType(resolvedTxnType) .txnType(resolvedTxnType)
.officerId(officerId) .officerId(officerId)
.officerUsername(officerUsername) .officerUsername(officerUsername)
@@ -1,6 +1,13 @@
package org.apache.fineract.portfolio.election.api; package org.apache.fineract.portfolio.election.api;
import jakarta.ws.rs.*; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -3,7 +3,17 @@ package org.apache.fineract.portfolio.election.api;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.*; import jakarta.ws.rs.Consumes;
import com.google.common.collect.Iterables;
import com.google.common.base.Splitter;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PATCH;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
@@ -138,6 +148,6 @@ public class CommitteeMeetingApiResource {
private String clientIp(HttpServletRequest req) { private String clientIp(HttpServletRequest req) {
String ip = req.getHeader("X-Forwarded-For"); String ip = req.getHeader("X-Forwarded-For");
return (ip != null && !ip.isBlank()) ? ip.split(",")[0].trim() : req.getRemoteAddr(); return (ip != null && !ip.isBlank()) ? Iterables.get(Splitter.on(',').split(ip), 0).trim() : req.getRemoteAddr();
} }
} }
@@ -4,7 +4,18 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.ws.rs.*; import jakarta.ws.rs.Consumes;
import com.google.common.collect.Iterables;
import com.google.common.base.Splitter;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
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.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
@@ -125,6 +136,6 @@ public class DirectorAppointmentApiResource {
private String clientIp(HttpServletRequest req) { private String clientIp(HttpServletRequest req) {
String ip = req.getHeader("X-Forwarded-For"); String ip = req.getHeader("X-Forwarded-For");
return (ip != null && !ip.isBlank()) ? ip.split(",")[0].trim() : req.getRemoteAddr(); return (ip != null && !ip.isBlank()) ? Iterables.get(Splitter.on(',').split(ip), 0).trim() : req.getRemoteAddr();
} }
} }
@@ -15,6 +15,8 @@ import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService; import org.apache.fineract.infrastructure.contentstore.service.ContentStoreService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
@@ -123,7 +125,9 @@ public class ElectionCandidateApiResource {
if (existing != null && !existing.startsWith("data:") && existing.contains("|")) { if (existing != null && !existing.startsWith("data:") && existing.contains("|")) {
String[] ep = existing.split("\\|", 2); String[] ep = existing.split("\\|", 2);
if (ep.length == 2) { if (ep.length == 2) {
try { contentStoreService.delete(ep[1]); } catch (Exception ignored) {} try { contentStoreService.delete(ep[1]); } catch (Exception ignored) {
// intentionally ignored
}
} }
} }
@@ -218,7 +222,7 @@ public class ElectionCandidateApiResource {
private static String clientIp(HttpServletRequest req) { private static String clientIp(HttpServletRequest req) {
if (req == null) return null; if (req == null) return null;
String fwd = req.getHeader("X-Forwarded-For"); String fwd = req.getHeader("X-Forwarded-For");
if (fwd != null && !fwd.isBlank()) return fwd.split(",")[0].trim(); if (fwd != null && !fwd.isBlank()) return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
return req.getRemoteAddr(); return req.getRemoteAddr();
} }
@@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST; import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT; import jakarta.ws.rs.PUT;
@@ -17,6 +16,8 @@ import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.election.data.ElectionConfigData; import org.apache.fineract.portfolio.election.data.ElectionConfigData;
import org.apache.fineract.portfolio.election.data.ElectionConfigRequest; import org.apache.fineract.portfolio.election.data.ElectionConfigRequest;
@@ -89,7 +90,9 @@ public class ElectionConfigApiResource {
throw e; throw e;
} }
String updateDetails = null; String updateDetails = null;
try { updateDetails = objectMapper.writeValueAsString(request); } catch (Exception ignored) {} try { updateDetails = objectMapper.writeValueAsString(request); } catch (Exception ignored) {
// intentionally ignored
}
electionAuditService.log(ElectionAuditAction.UPDATE_ELECTION, "ELECTION", electionId, electionId, electionAuditService.log(ElectionAuditAction.UPDATE_ELECTION, "ELECTION", electionId, electionId,
"Election updated: " + request.getTitle(), updateDetails, clientIp(httpRequest)); "Election updated: " + request.getTitle(), updateDetails, clientIp(httpRequest));
return Response.ok(Map.of("resourceId", electionId)).build(); return Response.ok(Map.of("resourceId", electionId)).build();
@@ -214,7 +217,7 @@ public class ElectionConfigApiResource {
private static String clientIp(HttpServletRequest req) { private static String clientIp(HttpServletRequest req) {
if (req == null) return null; if (req == null) return null;
String fwd = req.getHeader("X-Forwarded-For"); String fwd = req.getHeader("X-Forwarded-For");
if (fwd != null && !fwd.isBlank()) return fwd.split(",")[0].trim(); if (fwd != null && !fwd.isBlank()) return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
return req.getRemoteAddr(); return req.getRemoteAddr();
} }
@@ -15,6 +15,8 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.election.data.ElectionApprovalActionRequest; import org.apache.fineract.portfolio.election.data.ElectionApprovalActionRequest;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import org.apache.fineract.portfolio.election.data.ElectionApprovalStepData; import org.apache.fineract.portfolio.election.data.ElectionApprovalStepData;
import org.apache.fineract.portfolio.election.data.ElectionNominationData; import org.apache.fineract.portfolio.election.data.ElectionNominationData;
import org.apache.fineract.portfolio.election.data.ElectionNominationRequest; import org.apache.fineract.portfolio.election.data.ElectionNominationRequest;
@@ -295,7 +297,7 @@ public class ElectionNominationApiResource {
private static String clientIp(HttpServletRequest req) { private static String clientIp(HttpServletRequest req) {
if (req == null) return null; if (req == null) return null;
String fwd = req.getHeader("X-Forwarded-For"); String fwd = req.getHeader("X-Forwarded-For");
if (fwd != null && !fwd.isBlank()) return fwd.split(",")[0].trim(); if (fwd != null && !fwd.isBlank()) return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
return req.getRemoteAddr(); return req.getRemoteAddr();
} }
@@ -15,6 +15,8 @@ import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST; import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam; import jakarta.ws.rs.PathParam;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
@@ -104,10 +106,10 @@ public class ElectionVotingApiResource {
} }
winDet.append("]"); winDet.append("]");
String declareDetails = wFirst ? null : winDet.toString(); String declareDetails = wFirst ? null : winDet.toString();
String declareDesc = "Results declared" + (declareDetails != null ? " — " + (results.stream() String declareDesc = "Results declared" + (declareDetails != null ? " — " + results.stream()
.filter(r -> Integer.valueOf(1).equals(r.getRankPosition()) || "WON".equalsIgnoreCase(r.getResult())) .filter(r -> Integer.valueOf(1).equals(r.getRankPosition()) || "WON".equalsIgnoreCase(r.getResult()))
.map(r -> r.getCandidateName() + " won " + r.getPostName()) .map(r -> r.getCandidateName() + " won " + r.getPostName())
.reduce((a, b) -> a + "; " + b).orElse("")) : ""); .reduce((a, b) -> a + "; " + b).orElse("") : "");
electionAuditService.log(ElectionAuditAction.DECLARE_RESULTS, "RESULT", electionId, electionId, electionAuditService.log(ElectionAuditAction.DECLARE_RESULTS, "RESULT", electionId, electionId,
declareDesc, declareDetails, clientIp(httpRequest)); declareDesc, declareDetails, clientIp(httpRequest));
return Response.ok(results).build(); return Response.ok(results).build();
@@ -127,7 +129,7 @@ public class ElectionVotingApiResource {
private static String clientIp(HttpServletRequest req) { private static String clientIp(HttpServletRequest req) {
if (req == null) return null; if (req == null) return null;
String fwd = req.getHeader("X-Forwarded-For"); String fwd = req.getHeader("X-Forwarded-For");
if (fwd != null && !fwd.isBlank()) return fwd.split(",")[0].trim(); if (fwd != null && !fwd.isBlank()) return Iterables.get(Splitter.on(',').split(fwd), 0).trim();
return req.getRemoteAddr(); return req.getRemoteAddr();
} }
@@ -2,7 +2,12 @@ package org.apache.fineract.portfolio.election.api;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.*; import jakarta.ws.rs.Consumes;
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.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -2,7 +2,13 @@ package org.apache.fineract.portfolio.election.api;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.*; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -1,6 +1,13 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -1,6 +1,13 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -1,6 +1,14 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
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 lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -26,7 +26,7 @@ public class CommitteeMember {
private Long directorAppointmentId; private Long directorAppointmentId;
@Column(name = "added_date", nullable = false) @Column(name = "added_date", nullable = false)
private LocalDate addedDate = LocalDate.now(); private LocalDate addedDate;
public static CommitteeMember create(Committee committee, Long directorAppointmentId) { public static CommitteeMember create(Committee committee, Long directorAppointmentId) {
CommitteeMember m = new CommitteeMember(); CommitteeMember m = new CommitteeMember();
@@ -1,6 +1,12 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -8,6 +14,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_director_appointment") @Table(name = "m_election_director_appointment")
@@ -58,12 +65,12 @@ public class DirectorAppointment extends AbstractPersistableCustom<Long> {
@PrePersist @PrePersist
void onPersist() { void onPersist() {
this.createdDate = LocalDateTime.now(); this.createdDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
} }
@PreUpdate @PreUpdate
void onUpdate() { void onUpdate() {
this.lastModifiedDate = LocalDateTime.now(); this.lastModifiedDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
} }
public static DirectorAppointment create(Long electionId, Long candidateId, public static DirectorAppointment create(Long electionId, Long candidateId,
@@ -14,6 +14,7 @@ import java.util.List;
@Repository @Repository
public interface DirectorAppointmentRepository extends JpaRepository<DirectorAppointment, Long> { public interface DirectorAppointmentRepository extends JpaRepository<DirectorAppointment, Long> {
@Override
Page<DirectorAppointment> findAll(Pageable pageable); Page<DirectorAppointment> findAll(Pageable pageable);
Page<DirectorAppointment> findByStatus(DirectorAppointmentStatus status, Pageable pageable); Page<DirectorAppointment> findByStatus(DirectorAppointmentStatus status, Pageable pageable);
@@ -1,12 +1,20 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_approval_step") @Table(name = "m_election_approval_step")
@@ -63,7 +71,7 @@ public class ElectionApprovalStep extends AbstractPersistableCustom<Long> {
this.approverUserId = approverUserId; this.approverUserId = approverUserId;
this.status = ApprovalStepStatus.APPROVED; this.status = ApprovalStepStatus.APPROVED;
this.remarks = remarks; this.remarks = remarks;
this.actionDate = LocalDateTime.now(); this.actionDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
this.familyDetailsVerified = familyVerified; this.familyDetailsVerified = familyVerified;
this.familyDetailsJson = familyJson; this.familyDetailsJson = familyJson;
} }
@@ -72,6 +80,6 @@ public class ElectionApprovalStep extends AbstractPersistableCustom<Long> {
this.approverUserId = approverUserId; this.approverUserId = approverUserId;
this.status = ApprovalStepStatus.REJECTED; this.status = ApprovalStepStatus.REJECTED;
this.remarks = remarks; this.remarks = remarks;
this.actionDate = LocalDateTime.now(); this.actionDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
} }
} }
@@ -14,6 +14,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_audit_log") @Table(name = "m_election_audit_log")
@@ -63,6 +64,6 @@ public class ElectionAuditLog {
@PrePersist @PrePersist
void prePersist() { void prePersist() {
if (createdAt == null) createdAt = LocalDateTime.now(); if (createdAt == null) createdAt = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
} }
} }
@@ -1,12 +1,19 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_candidate") @Table(name = "m_election_candidate")
@@ -67,7 +74,7 @@ public class ElectionCandidate extends AbstractPersistableCustom<Long> {
candidate.candidateName = candidateName; candidate.candidateName = candidateName;
candidate.serialNumber = serialNumber; candidate.serialNumber = serialNumber;
candidate.approvedBy = approvedBy; candidate.approvedBy = approvedBy;
candidate.approvedDate = LocalDateTime.now(); candidate.approvedDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
candidate.isActive = true; candidate.isActive = true;
return candidate; return candidate;
} }
@@ -1,6 +1,13 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -8,6 +15,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_ceo_precheck") @Table(name = "m_election_ceo_precheck")
@@ -152,13 +160,13 @@ public class ElectionCeoPrecheck extends AbstractPersistableCustom<Long> {
public void generateNoDueCertificate(String certificateNumber) { public void generateNoDueCertificate(String certificateNumber) {
this.noDueCertificateNumber = certificateNumber; this.noDueCertificateNumber = certificateNumber;
this.noDueGeneratedDate = LocalDateTime.now(); this.noDueGeneratedDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
this.noDueCheckResult = CheckResult.PASS; this.noDueCheckResult = CheckResult.PASS;
} }
public void finalise(Long checkedBy, String remarks) { public void finalise(Long checkedBy, String remarks) {
this.checkedBy = checkedBy; this.checkedBy = checkedBy;
this.checkedDate = LocalDateTime.now(); this.checkedDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
this.remarks = remarks; this.remarks = remarks;
// NDC is generated manually after pre-check; only automated checks determine overall result // NDC is generated manually after pre-check; only automated checks determine overall result
boolean autoChecksPass = directorLoanCheckResult == CheckResult.PASS boolean autoChecksPass = directorLoanCheckResult == CheckResult.PASS
@@ -1,6 +1,13 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -9,6 +16,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_config") @Table(name = "m_election_config")
@@ -78,7 +86,7 @@ public class ElectionConfig extends AbstractAuditableWithUTCDateTimeCustom<Long>
} }
public boolean isNominationOpen() { public boolean isNominationOpen() {
LocalDate today = LocalDate.now(); LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
return status == ElectionStatus.ACTIVE return status == ElectionStatus.ACTIVE
&& !today.isBefore(nominationStartDate) && !today.isBefore(nominationStartDate)
&& !today.isAfter(nominationEndDate); && !today.isAfter(nominationEndDate);
@@ -1,12 +1,20 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_eligibility_check") @Table(name = "m_election_eligibility_check")
@@ -71,7 +79,7 @@ public class ElectionEligibilityCheck extends AbstractPersistableCustom<Long> {
public void complete(Long checkedBy, String remarks, boolean passed) { public void complete(Long checkedBy, String remarks, boolean passed) {
this.checkedBy = checkedBy; this.checkedBy = checkedBy;
this.checkedDate = LocalDateTime.now(); this.checkedDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
this.remarks = remarks; this.remarks = remarks;
this.overallResult = passed ? CheckResult.PASS : CheckResult.FAIL; this.overallResult = passed ? CheckResult.PASS : CheckResult.FAIL;
this.ageEligible = passed; this.ageEligible = passed;
@@ -1,6 +1,16 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -10,6 +20,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_nomination", @Table(name = "m_election_nomination",
@@ -121,7 +132,7 @@ public class ElectionNomination extends AbstractAuditableWithUTCDateTimeCustom<L
public void submit(String applicationNumber) { public void submit(String applicationNumber) {
this.applicationNumber = applicationNumber; this.applicationNumber = applicationNumber;
this.status = NominationStatus.SUBMITTED; this.status = NominationStatus.SUBMITTED;
this.submittedDate = LocalDateTime.now(); this.submittedDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
} }
public void markUnderReview() { public void markUnderReview() {
@@ -1,6 +1,11 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -1,12 +1,18 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_result") @Table(name = "m_election_result")
@@ -53,7 +59,7 @@ public class ElectionResult extends AbstractPersistableCustom<Long> {
r.result = result; r.result = result;
r.rankPosition = rankPosition; r.rankPosition = rankPosition;
r.declaredBy = declaredBy; r.declaredBy = declaredBy;
r.declaredDate = LocalDateTime.now(); r.declaredDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
return r; return r;
} }
} }
@@ -1,12 +1,19 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_vote", @Table(name = "m_election_vote",
@@ -50,7 +57,7 @@ public class ElectionVote extends AbstractPersistableCustom<Long> {
vote.electionPost = post; vote.electionPost = post;
vote.voterMemberId = voterMemberId; vote.voterMemberId = voterMemberId;
vote.candidate = candidate; vote.candidate = candidate;
vote.voteTimestamp = LocalDateTime.now(); vote.voteTimestamp = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
vote.boothId = boothId; vote.boothId = boothId;
vote.ipAddress = ipAddress; vote.ipAddress = ipAddress;
return vote; return vote;
@@ -1,11 +1,19 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_meeting_agenda_resolution") @Table(name = "m_election_meeting_agenda_resolution")
@@ -41,7 +49,7 @@ public class MeetingAgendaResolution {
r.agendaId = agendaId; r.agendaId = agendaId;
r.resolution = resolution; r.resolution = resolution;
r.discussionNotes = notes; r.discussionNotes = notes;
r.resolvedAt = LocalDateTime.now(); r.resolvedAt = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
return r; return r;
} }
} }
@@ -1,11 +1,20 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_meeting_member_vote", @Table(name = "m_election_meeting_member_vote",
@@ -46,7 +55,7 @@ public class MeetingMemberVote {
v.agendaId = agendaId; v.agendaId = agendaId;
v.memberId = memberId; v.memberId = memberId;
v.vote = vote; v.vote = vote;
v.votedAt = LocalDateTime.now(); v.votedAt = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
v.remark = (remark != null && !remark.isBlank()) ? remark.trim() : null; v.remark = (remark != null && !remark.isBlank()) ? remark.trim() : null;
return v; return v;
} }
@@ -1,12 +1,16 @@
package org.apache.fineract.portfolio.election.domain; package org.apache.fineract.portfolio.election.domain;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.UUID; import java.util.UUID;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Entity @Entity
@Table(name = "m_election_meeting_vote_token") @Table(name = "m_election_meeting_vote_token")
@@ -49,12 +53,12 @@ public class MeetingVoteToken {
t.memberName = memberName; t.memberName = memberName;
t.designation = designation; t.designation = designation;
t.used = false; t.used = false;
t.createdAt = LocalDateTime.now(); t.createdAt = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
t.expiresAt = t.createdAt.plusDays(validDays); t.expiresAt = t.createdAt.plusDays(validDays);
return t; return t;
} }
public boolean isExpired() { public boolean isExpired() {
return LocalDateTime.now().isAfter(expiresAt); return LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()).isAfter(expiresAt);
} }
} }
@@ -32,6 +32,7 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Slf4j @Slf4j
@Service @Service
@@ -215,7 +216,7 @@ public class CommitteeMeetingServiceImpl implements CommitteeMeetingService {
private String buildMeetingInviteHtml(String recipientName, String committeeName, CommitteeMeeting meeting, boolean isUpdate) { private String buildMeetingInviteHtml(String recipientName, String committeeName, CommitteeMeeting meeting, boolean isUpdate) {
DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("dd MMMM yyyy"); DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("dd MMMM yyyy");
String sentAt = java.time.LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd MMM yyyy, hh:mm a")); String sentAt = java.time.LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMM yyyy, hh:mm a"));
String dateStr = meeting.getMeetingDate() != null ? meeting.getMeetingDate().format(dateFmt) : "—"; String dateStr = meeting.getMeetingDate() != null ? meeting.getMeetingDate().format(dateFmt) : "—";
String timeStr = meeting.getMeetingTime() != null ? meeting.getMeetingTime() : "—"; String timeStr = meeting.getMeetingTime() != null ? meeting.getMeetingTime() : "—";
boolean isOnline = meeting.getMode() == CommitteeMeetingMode.ONLINE; boolean isOnline = meeting.getMode() == CommitteeMeetingMode.ONLINE;
@@ -381,7 +382,7 @@ public class CommitteeMeetingServiceImpl implements CommitteeMeetingService {
if (date == null) { if (date == null) {
throw new IllegalArgumentException("Meeting date is required"); throw new IllegalArgumentException("Meeting date is required");
} }
if (date.isBefore(LocalDate.now())) { if (date.isBefore(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))) {
throw new IllegalArgumentException("Meeting date cannot be in the past"); throw new IllegalArgumentException("Meeting date cannot be in the past");
} }
} }
@@ -26,6 +26,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -132,7 +133,7 @@ public class CommitteeServiceImpl implements CommitteeService {
@Override @Override
public List<DirectorAppointmentData> getCurrentElectionDirectors() { public List<DirectorAppointmentData> getCurrentElectionDirectors() {
var currentElection = electionConfigRepository.findFirstWithActiveDuration(LocalDate.now()); var currentElection = electionConfigRepository.findFirstWithActiveDuration(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
// fallback: most recent RESULT_DECLARED election if none has active duration // fallback: most recent RESULT_DECLARED election if none has active duration
if (currentElection.isEmpty()) { if (currentElection.isEmpty()) {
@@ -28,6 +28,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -109,12 +110,12 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic
@Transactional @Transactional
@Scheduled(cron = "0 0 0 * * *") @Scheduled(cron = "0 0 0 * * *")
public void autoExpireByTenure() { public void autoExpireByTenure() {
int updated = repository.expireByTenureDate(LocalDate.now()); int updated = repository.expireByTenureDate(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
if (updated > 0) { if (updated > 0) {
log.info("Auto-expired {} director appointment(s) whose tenure ended.", updated); log.info("Auto-expired {} director appointment(s) whose tenure ended.", updated);
auditService.log(ElectionAuditAction.UPDATE_DIRECTOR_APPOINTMENT, "DIRECTOR_APPOINTMENT", null, null, auditService.log(ElectionAuditAction.UPDATE_DIRECTOR_APPOINTMENT, "DIRECTOR_APPOINTMENT", null, null,
"Scheduler auto-expired " + updated + " director appointment(s)", "Scheduler auto-expired " + updated + " director appointment(s)",
"expiredCount=" + updated + ", date=" + LocalDate.now(), null); "expiredCount=" + updated + ", date=" + LocalDate.now(DateUtils.getDateTimeZoneOfTenant()), null);
} }
} }
@@ -126,7 +127,9 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic
DirectorAppointmentStatus statusEnum = null; DirectorAppointmentStatus statusEnum = null;
if (status != null && !status.isBlank()) { if (status != null && !status.isBlank()) {
try { statusEnum = DirectorAppointmentStatus.valueOf(status.toUpperCase()); } catch (Exception ignored) {} try { statusEnum = DirectorAppointmentStatus.valueOf(status.toUpperCase()); } catch (Exception ignored) {
// intentionally ignored
}
} }
if (electionId != null && statusEnum != null) { if (electionId != null && statusEnum != null) {
@@ -177,7 +180,9 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic
if (req.getAppointmentDate() != null) d.setAppointmentDate(req.getAppointmentDate()); if (req.getAppointmentDate() != null) d.setAppointmentDate(req.getAppointmentDate());
if (req.getRemarks() != null) d.setRemarks(req.getRemarks()); if (req.getRemarks() != null) d.setRemarks(req.getRemarks());
if (req.getStatus() != null) { if (req.getStatus() != null) {
try { d.setStatus(DirectorAppointmentStatus.valueOf(req.getStatus().toUpperCase())); } catch (Exception ignored) {} try { d.setStatus(DirectorAppointmentStatus.valueOf(req.getStatus().toUpperCase())); } catch (Exception ignored) {
// intentionally ignored
}
} }
if (req.getCandidateName() != null) d.setCandidateName(req.getCandidateName()); if (req.getCandidateName() != null) d.setCandidateName(req.getCandidateName());
if (req.getPostName() != null) d.setPostName(req.getPostName()); if (req.getPostName() != null) d.setPostName(req.getPostName());
@@ -234,10 +239,6 @@ public class DirectorAppointmentServiceImpl implements DirectorAppointmentServic
return toData(d, null, null); return toData(d, null, null);
} }
private DirectorAppointmentData toData(DirectorAppointment d, String electionTitle) {
return toData(d, electionTitle, null);
}
private DirectorAppointmentData toData(DirectorAppointment d, String electionTitle, Long memberId) { private DirectorAppointmentData toData(DirectorAppointment d, String electionTitle, Long memberId) {
return DirectorAppointmentData.builder() return DirectorAppointmentData.builder()
.id(d.getId()) .id(d.getId())
@@ -15,6 +15,7 @@ import java.time.Period;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Slf4j @Slf4j
@Service @Service
@@ -318,7 +319,7 @@ public class ElectionAnalyticsServiceImpl implements ElectionAnalyticsService {
Object dob = info.get("date_of_birth"); Object dob = info.get("date_of_birth");
if (dob instanceof java.sql.Date) { if (dob instanceof java.sql.Date) {
LocalDate dobDate = ((java.sql.Date) dob).toLocalDate(); LocalDate dobDate = ((java.sql.Date) dob).toLocalDate();
candidateAge = Period.between(dobDate, LocalDate.now()).getYears(); candidateAge = Period.between(dobDate, LocalDate.now(DateUtils.getDateTimeZoneOfTenant())).getYears();
} }
// ── Voter pool ───────────────────────────────────────────────────────── // ── Voter pool ─────────────────────────────────────────────────────────
@@ -16,6 +16,7 @@ import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Slf4j @Slf4j
@Service @Service
@@ -104,7 +105,7 @@ public class ElectionCeoPreCheckServiceImpl implements ElectionCeoPreCheckServic
if (status == null || status != 300) { if (status == null || status != 300) {
String label = statusLabel(status); String label = statusLabel(status);
precheck.completeKycCheck(CheckResult.FAIL, "KYC not verified (status: " + label + ")"); precheck.completeKycCheck(CheckResult.FAIL, "KYC not verified (status: " + label + ")");
} else if (expiry != null && expiry.isBefore(LocalDate.now())) { } else if (expiry != null && expiry.isBefore(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))) {
precheck.completeKycCheck(CheckResult.FAIL, "KYC expired on " + expiry); precheck.completeKycCheck(CheckResult.FAIL, "KYC expired on " + expiry);
} else { } else {
String expiryNote = expiry != null ? ", expires " + expiry : ""; String expiryNote = expiry != null ? ", expires " + expiry : "";
@@ -155,7 +156,7 @@ public class ElectionCeoPreCheckServiceImpl implements ElectionCeoPreCheckServic
issues.add(name + ": No KYC record"); issues.add(name + ": No KYC record");
} else if (status != 300) { } else if (status != 300) {
issues.add(name + ": KYC not verified (" + statusLabel(status) + ")"); issues.add(name + ": KYC not verified (" + statusLabel(status) + ")");
} else if (expiry != null && expiry.isBefore(LocalDate.now())) { } else if (expiry != null && expiry.isBefore(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))) {
issues.add(name + ": KYC expired on " + expiry); issues.add(name + ": KYC expired on " + expiry);
} }
} }
@@ -8,6 +8,7 @@ import org.springframework.stereotype.Service;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Slf4j @Slf4j
@Service @Service
@@ -18,7 +19,7 @@ public class ElectionNoDueCertificateServiceImpl implements ElectionNoDueCertifi
@Override @Override
public String generateCertificateNumber(ElectionNomination nomination) { public String generateCertificateNumber(ElectionNomination nomination) {
String datePart = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); String datePart = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
return "NDC-" + datePart + "-" + String.format("%06d", nomination.getId()); return "NDC-" + datePart + "-" + String.format("%06d", nomination.getId());
} }
@@ -36,6 +36,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -315,7 +316,7 @@ public class ElectionReadPlatformServiceImpl implements ElectionReadPlatformServ
private ElectionNominationData toNominationData(ElectionNomination n) { private ElectionNominationData toNominationData(ElectionNomination n) {
Integer age = null; Integer age = null;
if (n.getDateOfBirth() != null) { if (n.getDateOfBirth() != null) {
age = Period.between(n.getDateOfBirth(), LocalDate.now()).getYears(); age = Period.between(n.getDateOfBirth(), LocalDate.now(DateUtils.getDateTimeZoneOfTenant())).getYears();
} }
return ElectionNominationData.builder() return ElectionNominationData.builder()
.id(n.getId()) .id(n.getId())

Some files were not shown because too many files have changed in this diff Show More