You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2022/07/26 07:56:36 UTC
[fineract] 02/06: Refactor Client to support auditable fields
This is an automated email from the ASF dual-hosted git repository.
arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
commit d8f483ab45ba80c3bef01e9941a5b7ba2fd0cce9
Author: Adam Saghy <ad...@gmail.com>
AuthorDate: Sun Jul 24 23:14:45 2022 +0200
Refactor Client to support auditable fields
---
.../core/api/OffsetDateTimeAdapter.java | 43 +++++++
.../api/InternalClientInformationApiResource.java | 87 +++++++++++++
.../fineract/portfolio/client/domain/Client.java | 31 ++---
.../service/ClientReadPlatformServiceImpl.java | 4 +-
.../db/changelog/tenant/changelog-tenant.xml | 1 +
.../tenant/parts/0020_add_audit_entries.xml | 141 +++++++++++++++++++++
.../ClientAuditingIntegrationTest.java | 132 +++++++++++++++++++
.../integrationtests/common/ClientHelper.java | 9 +-
.../useradministration/users/UserHelper.java | 2 +-
9 files changed, 424 insertions(+), 26 deletions(-)
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/OffsetDateTimeAdapter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/OffsetDateTimeAdapter.java
new file mode 100644
index 000000000..c98f5610d
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/api/OffsetDateTimeAdapter.java
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.core.api;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import java.lang.reflect.Type;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Serializer for Java Offset Date Time {@link OffsetDateTime} that returns the date in ISO-8601 Offset date time format
+ */
+public class OffsetDateTimeAdapter implements JsonSerializer<OffsetDateTime> {
+
+ @Override
+ @SuppressWarnings("unused")
+ public JsonElement serialize(final OffsetDateTime dateTime, final Type typeOfSrc, final JsonSerializationContext context) {
+ JsonElement object = null;
+ if (dateTime != null) {
+ object = new JsonPrimitive(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(dateTime));
+ }
+ return object;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java
new file mode 100644
index 000000000..f302abefe
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.client.api;
+
+import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.CREATED_BY;
+import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.CREATED_DATE;
+import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.LAST_MODIFIED_BY;
+import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.LAST_MODIFIED_DATE;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
+import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
+import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
+import org.apache.fineract.portfolio.client.domain.Client;
+import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.stereotype.Component;
+
+//@Profile("test")
+@Component
+@Path("/internal/client")
+@RequiredArgsConstructor
+@Slf4j
+public class InternalClientInformationApiResource implements InitializingBean {
+
+ private final ClientRepositoryWrapper clientRepositoryWrapper;
+ private final ToApiJsonSerializer<Map> toApiJsonSerializer;
+ private final ApiRequestParameterHelper apiRequestParameterHelper;
+
+ @Override
+ public void afterPropertiesSet() {
+ log.warn("------------------------------------------------------------");
+ log.warn(" ");
+ log.warn("DO NOT USE THIS IN PRODUCTION!");
+ log.warn("Internal client services mode is enabled");
+ log.warn("DO NOT USE THIS IN PRODUCTION!");
+ log.warn(" ");
+ log.warn("------------------------------------------------------------");
+
+ }
+
+ @GET
+ @Path("{clientId}/audit")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ public String getClientAuditFields(@Context final UriInfo uriInfo, @PathParam("clientId") Long clientId) {
+ log.warn("------------------------------------------------------------");
+ log.warn(" ");
+ log.warn("Fetching client with {}", clientId);
+ log.warn(" ");
+ log.warn("------------------------------------------------------------");
+
+ final Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
+ Map<String, Object> auditFields = new HashMap<>(
+ Map.of(CREATED_BY, client.getCreatedBy().orElse(null), CREATED_DATE, client.getCreatedDate().orElse(null), LAST_MODIFIED_BY,
+ client.getLastModifiedBy().orElse(null), LAST_MODIFIED_DATE, client.getLastModifiedDate().orElse(null)));
+ final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
+ return this.toApiJsonSerializer.serialize(settings, auditFields);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java
index c092ef970..059c88f66 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/Client.java
@@ -44,7 +44,7 @@ import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
-import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.documentmanagement.domain.Image;
@@ -59,7 +59,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
@Entity
@Table(name = "m_client", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "account_no_UNIQUE"), //
@UniqueConstraint(columnNames = { "mobile_no" }, name = "mobile_no_UNIQUE") })
-public final class Client extends AbstractPersistableCustom {
+public class Client extends AbstractAuditableWithUTCDateTimeCustom {
@Column(name = "account_no", length = 20, unique = true, nullable = false)
private String accountNumber;
@@ -180,16 +180,14 @@ public final class Client extends AbstractPersistableCustom {
@Column(name = "submittedon_date", nullable = true)
private LocalDate submittedOnDate;
- @ManyToOne(optional = true, fetch = FetchType.LAZY)
- @JoinColumn(name = "submittedon_userid", nullable = true)
- private AppUser submittedBy;
+ // Deprecated since common Auditable fields were introduced. Columns and data left untouched to help migration.
- @Column(name = "updated_on", nullable = true)
- private LocalDate updatedOnDate;
+ // @Column(name = "updated_on", nullable = true)
+ // private LocalDate updatedOnDate;
- @ManyToOne(optional = true, fetch = FetchType.LAZY)
- @JoinColumn(name = "updated_by", nullable = true)
- private AppUser updatedBy;
+ // @ManyToOne(optional = true, fetch = FetchType.LAZY)
+ // @JoinColumn(name = "updated_by", nullable = true)
+ // private AppUser updatedBy;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "activatedon_userid", nullable = true)
@@ -270,9 +268,7 @@ public final class Client extends AbstractPersistableCustom {
savingsAccountId, dataOfBirth, gender, clientType, clientClassification, legalForm, isStaff);
}
- Client() {
- this.setLegalForm(null);
- }
+ protected Client() {}
private Client(final AppUser currentUser, final ClientStatus status, final Office office, final Group clientParentGroup,
final String accountNo, final String firstname, final String middlename, final String lastname, final String fullname,
@@ -289,7 +285,6 @@ public final class Client extends AbstractPersistableCustom {
}
this.submittedOnDate = submittedOnDate;
- this.submittedBy = currentUser;
this.status = status.getValue();
this.office = office;
@@ -969,8 +964,6 @@ public final class Client extends AbstractPersistableCustom {
this.rejectionReason = rejectionReason;
this.rejectionDate = rejectionDate;
this.rejectedBy = currentUser;
- this.updatedBy = currentUser;
- this.updatedOnDate = rejectionDate;
this.status = ClientStatus.REJECTED.getValue();
}
@@ -979,8 +972,6 @@ public final class Client extends AbstractPersistableCustom {
this.withdrawalReason = withdrawalReason;
this.withdrawalDate = withdrawalDate;
this.withdrawnBy = currentUser;
- this.updatedBy = currentUser;
- this.updatedOnDate = withdrawalDate;
this.status = ClientStatus.WITHDRAWN.getValue();
}
@@ -990,8 +981,6 @@ public final class Client extends AbstractPersistableCustom {
this.closureReason = null;
this.reactivateDate = reactivateDate;
this.reactivatedBy = currentUser;
- this.updatedBy = currentUser;
- this.updatedOnDate = reactivateDate;
this.status = ClientStatus.PENDING.getValue();
}
@@ -999,8 +988,6 @@ public final class Client extends AbstractPersistableCustom {
public void reOpened(AppUser currentUser, LocalDate reopenedDate) {
this.reopenedDate = reopenedDate;
this.reopenedBy = currentUser;
- this.updatedBy = currentUser;
- this.updatedOnDate = reopenedDate;
this.status = ClientStatus.PENDING.getValue();
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java
index 4c8525e5a..5804fb96f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java
@@ -430,7 +430,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
sqlBuilder.append("left join m_savings_product sp on sp.id = c.default_savings_product ");
sqlBuilder.append("left join m_office transferToOffice on transferToOffice.id = c.transfer_to_office_id ");
- sqlBuilder.append("left join m_appuser sbu on sbu.id = c.submittedon_userid ");
+ sqlBuilder.append("left join m_appuser sbu on sbu.id = c.created_by ");
sqlBuilder.append("left join m_appuser acu on acu.id = c.activatedon_userid ");
sqlBuilder.append("left join m_appuser clu on clu.id = c.closedon_userid ");
sqlBuilder.append("left join m_code_value cv on cv.id = c.gender_cv_id ");
@@ -618,7 +618,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
builder.append("left join m_staff s on s.id = c.staff_id ");
builder.append("left join m_savings_product sp on sp.id = c.default_savings_product ");
builder.append("left join m_office transferToOffice on transferToOffice.id = c.transfer_to_office_id ");
- builder.append("left join m_appuser sbu on sbu.id = c.submittedon_userid ");
+ builder.append("left join m_appuser sbu on sbu.id = c.created_by ");
builder.append("left join m_appuser acu on acu.id = c.activatedon_userid ");
builder.append("left join m_appuser clu on clu.id = c.closedon_userid ");
builder.append("left join m_code_value cv on cv.id = c.gender_cv_id ");
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index e4954526c..25e775ef4 100644
--- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -39,4 +39,5 @@
<include file="parts/0017_fix_stretchy_reports.xml" relativeToChangelogFile="true"/>
<include file="parts/0018_pentaho_reports_to_table.xml" relativeToChangelogFile="true"/>
<include file="parts/0019_refactor_loan_transaction.xml" relativeToChangelogFile="true"/>
+ <include file="parts/0020_add_audit_entries.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0020_add_audit_entries.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0020_add_audit_entries.xml
new file mode 100644
index 000000000..779bd1da2
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0020_add_audit_entries.xml
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+ <!--Client-->
+ <changeSet author="fineract" id="1" context="mysql">
+ <addColumn tableName="m_client">
+ <column name="created_on_utc" type="DATETIME"/>
+ <column name="created_by" type="BIGINT" valueComputed="submittedon_userid"/>
+ <column name="last_modified_by" type="BIGINT"/>
+ <column name="last_modified_on_utc" type="DATETIME"/>
+ </addColumn>
+ </changeSet>
+ <changeSet author="fineract" id="1" context="postgresql">
+ <addColumn tableName="m_client">
+ <column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE"/>
+ <column name="created_by" type="BIGINT" valueComputed="submittedon_userid"/>
+ <column name="last_modified_by" type="BIGINT"/>
+ <column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE"/>
+ </addColumn>
+ </changeSet>
+ <changeSet author="fineract" id="2">
+ <addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_client"
+ constraintName="FK_client_created_by" deferrable="false" initiallyDeferred="false"
+ onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
+ referencedTableName="m_appuser" validate="true"/>
+ <addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_client"
+ constraintName="FK_client_last_modified_by" deferrable="false" initiallyDeferred="false"
+ onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
+ referencedTableName="m_appuser" validate="true"/>
+ </changeSet>
+ <changeSet id="3" author="fineract">
+ <dropColumn tableName="m_client">
+ <column name="submittedon_userid"/>
+ </dropColumn>
+ </changeSet>
+ <!--Loan-->
+ <changeSet author="fineract" id="4" context="mysql">
+ <addColumn tableName="m_loan">
+ <column name="created_on_utc" type="DATETIME"/>
+ <column name="created_by" type="BIGINT" valueComputed="submittedon_userid"/>
+ <column name="last_modified_by" type="BIGINT"/>
+ <column name="last_modified_on_utc" type="DATETIME"/>
+ </addColumn>
+ </changeSet>
+ <changeSet author="fineract" id="4" context="postgresql">
+ <addColumn tableName="m_loan">
+ <column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE"/>
+ <column name="created_by" type="BIGINT" valueComputed="submittedon_userid"/>
+ <column name="last_modified_by" type="BIGINT"/>
+ <column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE"/>
+ </addColumn>
+ </changeSet>
+ <changeSet author="fineract" id="5">
+ <addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_loan"
+ constraintName="FK_loan_created_by" deferrable="false" initiallyDeferred="false"
+ onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
+ referencedTableName="m_appuser" validate="true"/>
+ <addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_loan"
+ constraintName="FK_loan_last_modified_by" deferrable="false" initiallyDeferred="false"
+ onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
+ referencedTableName="m_appuser" validate="true"/>
+ </changeSet>
+ <changeSet id="6" author="fineract">
+ <dropForeignKeyConstraint baseTableName="m_loan" constraintName="FK_submittedon_userid"/>
+ <dropColumn tableName="m_loan">
+ <column name="submittedon_userid"/>
+ </dropColumn>
+ </changeSet>
+ <!-- Loan transactions -->
+ <changeSet author="fineract" id="7" context="mysql">
+ <addColumn tableName="m_loan_transaction">
+ <column name="created_on_utc" type="DATETIME"/>
+ <column name="last_modified_on_utc" type="DATETIME"/>
+ </addColumn>
+ </changeSet>
+ <changeSet author="fineract" id="8" context="postgresql">
+ <addColumn tableName="m_loan_transaction">
+ <column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE"/>
+ <column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE"/>
+ </addColumn>
+ </changeSet>
+ <changeSet id="9" author="fineract">
+ <renameColumn tableName="m_loan_transaction" oldColumnName="createdby_id" newColumnName="created_by" columnDataType="BIGINT"/>
+ <renameColumn tableName="m_loan_transaction" oldColumnName="lastmodifiedby_id" newColumnName="last_modified_by" columnDataType="BIGINT"/>
+ </changeSet>
+ <changeSet author="fineract" id="10">
+ <addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_loan_transaction"
+ constraintName="FK_loan_transaction_created_by" deferrable="false" initiallyDeferred="false"
+ onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
+ referencedTableName="m_appuser" validate="true"/>
+ <addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_loan_transaction"
+ constraintName="FK_loan_transaction_last_modified_by" deferrable="false" initiallyDeferred="false"
+ onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
+ referencedTableName="m_appuser" validate="true"/>
+ </changeSet>
+ <changeSet id="11" author="fineract" context="mysql">
+ <update tableName="stretchy_report">
+ <column name="report_sql"
+ value="SELECT mc.id AS "id", mc.firstname AS "firstName", mc.middlename AS "middleName", mc.lastname AS "lastName", mc.display_name AS "fullName", mc.mobile_no AS "mobileNo", ml.principal_amount AS "loanAmount", (IFNULL(ml.principal_outstanding_derived, 0) + IFNULL(ml.interest_outstanding_derived, 0) + IFNULL(ml.fee_charges_ [...]
+ <where>report_name = 'Loan payments received (Active Loans)'</where>
+ </update>
+ <update tableName="stretchy_report">
+ <column name="report_sql"
+ value="SELECT ml.id AS "loanId", mc.id AS "id", mc.firstname AS "firstName", mc.middlename AS "middleName", mc.lastname AS "lastName", mc.display_name AS "fullName", mc.mobile_no AS "mobileNo", ml.principal_amount AS "loanAmount", (IFNULL(ml.principal_outstanding_derived, 0) + IFNULL(ml.interest_outstandi [...]
+ <where>report_name = 'Loan payments received (Overdue Loans)'</where>
+ </update>
+ </changeSet>
+ <changeSet id="12" author="fineract" context="postgresql">
+ <update tableName="stretchy_report">
+ <column name="report_sql"
+ value="SELECT mc.id AS id, mc.firstname AS firstName, mc.middlename AS middleName, mc.lastname AS lastName, mc.display_name AS fullName, mc.mobile_no AS mobileNo, ml.principal_amount AS loanAmount, (COALESCE(ml.principal_outstanding_derived, 0) + COALESCE(ml.interest_outstanding_derived, 0) + COALESCE(ml.fee_charges_outstanding_derived, 0) + COALESCE(ml.penalty_charges_outstanding_derived, 0)) AS loanOutstanding, ounder.id AS officeNumber, ml.account_no AS loanAccount [...]
+ <where>report_name = 'Loan payments received (Active Loans)'</where>
+ </update>
+ <update tableName="stretchy_report">
+ <column name="report_sql"
+ value="SELECT ml.id AS loanId, mc.id AS id, mc.firstname AS firstName, mc.middlename AS middleName, mc.lastname AS lastName, mc.display_name AS fullName, mc.mobile_no AS mobileNo, ml.principal_amount AS loanAmount, (COALESCE(ml.principal_outstanding_derived, 0) + COALESCE(ml.interest_outstanding_derived, 0) + COALESCE(ml.fee_charges_outstanding_derived, 0) + COALESCE(ml.penalty_charges_outstanding_derived, 0)) AS loanOutstanding, ounder.id AS officeNumber, ml.account_ [...]
+ <where>report_name = 'Loan payments received (Overdue Loans)'</where>
+ </update>
+ </changeSet>
+</databaseChangeLog>
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientAuditingIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientAuditingIntegrationTest.java
new file mode 100644
index 000000000..145c5b96b
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientAuditingIntegrationTest.java
@@ -0,0 +1,132 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.CREATED_BY;
+import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.CREATED_DATE;
+import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.LAST_MODIFIED_BY;
+import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.LAST_MODIFIED_DATE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.organisation.StaffHelper;
+import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ClientAuditingIntegrationTest {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ClientAuditingIntegrationTest.class);
+ private ResponseSpecification responseSpec;
+ private RequestSpecification requestSpec;
+ private ClientHelper clientHelper;
+
+ @BeforeEach
+ public void setup() {
+ Utils.initializeRESTAssured();
+ this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+ this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+
+ this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+ this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec);
+ }
+
+ @Test
+ public void checkAuditDates() throws InterruptedException {
+ final Integer staffId = StaffHelper.createStaff(this.requestSpec, this.responseSpec);
+ String username = Utils.randomNameGenerator("user", 8);
+ final Integer userId = (Integer) UserHelper.createUser(this.requestSpec, this.responseSpec, 1, staffId, username, "resourceId");
+ OffsetDateTime now = OffsetDateTime.now(ZoneId.of("Asia/Kolkata"));
+ // Testing in minutes precision, but still need to take care around the end of the actual minute
+ if (now.getSecond() > 56) {
+ Thread.sleep(5000);
+ now = OffsetDateTime.now(ZoneId.of("Asia/Kolkata"));
+ }
+ LOG.info("-------------------------Creating Client---------------------------");
+
+ final Integer clientID = ClientHelper.createClientPending(requestSpec, responseSpec);
+ ClientHelper.verifyClientCreatedOnServer(requestSpec, responseSpec, clientID);
+ Map<String, Object> auditFieldsResponse = ClientHelper.getClientAuditFields(requestSpec, responseSpec, clientID, "");
+
+ OffsetDateTime createdDate = OffsetDateTime.parse((String) auditFieldsResponse.get(CREATED_DATE),
+ DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+
+ OffsetDateTime lastModifiedDate = OffsetDateTime.parse((String) auditFieldsResponse.get(LAST_MODIFIED_DATE),
+ DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+
+ LOG.info("-------------------------Check Audit dates---------------------------");
+ assertEquals(1, auditFieldsResponse.get(CREATED_BY));
+ assertEquals(1, auditFieldsResponse.get(LAST_MODIFIED_BY));
+ assertEquals(now.getYear(), createdDate.getYear());
+ assertEquals(now.getMonth(), createdDate.getMonth());
+ assertEquals(now.getDayOfMonth(), createdDate.getDayOfMonth());
+ assertEquals(now.getHour(), createdDate.getHour());
+ assertEquals(now.getMinute(), createdDate.getMinute());
+
+ assertEquals(now.getYear(), lastModifiedDate.getYear());
+ assertEquals(now.getMonth(), lastModifiedDate.getMonth());
+ assertEquals(now.getDayOfMonth(), lastModifiedDate.getDayOfMonth());
+ assertEquals(now.getHour(), lastModifiedDate.getHour());
+ assertEquals(now.getMinute(), lastModifiedDate.getMinute());
+
+ LOG.info("-------------------------Modify Client with System user---------------------------");
+ this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+ this.requestSpec.header("Authorization",
+ "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(username, "password"));
+
+ this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec);
+ this.clientHelper.activateClient(clientID);
+ auditFieldsResponse = ClientHelper.getClientAuditFields(requestSpec, responseSpec, clientID, "");
+
+ createdDate = OffsetDateTime.parse((String) auditFieldsResponse.get(CREATED_DATE), DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+
+ lastModifiedDate = OffsetDateTime.parse((String) auditFieldsResponse.get(LAST_MODIFIED_DATE),
+ DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+
+ LOG.info("-------------------------Check Audit dates---------------------------");
+ assertEquals(1, auditFieldsResponse.get(CREATED_BY));
+ assertEquals(now.getYear(), createdDate.getYear());
+ assertEquals(now.getMonth(), createdDate.getMonth());
+ assertEquals(now.getDayOfMonth(), createdDate.getDayOfMonth());
+ assertEquals(now.getHour(), createdDate.getHour());
+ assertEquals(now.getMinute(), createdDate.getMinute());
+
+ now = OffsetDateTime.now(ZoneId.of("Asia/Kolkata"));
+
+ assertEquals(userId, auditFieldsResponse.get(LAST_MODIFIED_BY));
+ assertEquals(now.getYear(), lastModifiedDate.getYear());
+ assertEquals(now.getMonth(), lastModifiedDate.getMonth());
+ assertEquals(now.getDayOfMonth(), lastModifiedDate.getDayOfMonth());
+ assertEquals(now.getHour(), lastModifiedDate.getHour());
+ assertEquals(now.getMinute(), lastModifiedDate.getMinute());
+ }
+
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java
index ba6ec8cf6..63d2bf5d3 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java
@@ -346,6 +346,13 @@ public class ClientHelper {
return Utils.performServerGet(requestSpec, responseSpec, GET_CLIENT_URL, jsonReturn);
}
+ public static HashMap<String, Object> getClientAuditFields(final RequestSpecification requestSpec,
+ final ResponseSpecification responseSpec, final Integer clientId, final String jsonReturn) {
+ final String GET_CLIENT_URL = "/fineract-provider/api/v1/internal/client/" + clientId + "/audit?" + Utils.TENANT_IDENTIFIER;
+ log.info("---------------------------------GET A CLIENT ENTITY AUDIT FIELDS---------------------------------------------");
+ return Utils.performServerGet(requestSpec, responseSpec, GET_CLIENT_URL, jsonReturn);
+ }
+
/* Client status is a map.So adding SuppressWarnings */
@SuppressWarnings("unchecked")
public static HashMap<String, Object> getClientStatus(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
@@ -435,7 +442,7 @@ public class ClientHelper {
}
- private String getActivateClientAsJSON(String date) {
+ private static String getActivateClientAsJSON(String date) {
final HashMap<String, String> map = new HashMap<>();
map.put("locale", CommonConstants.LOCALE);
map.put("dateFormat", CommonConstants.DATE_FORMAT);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/useradministration/users/UserHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/useradministration/users/UserHelper.java
index 1a86dca53..e00851245 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/useradministration/users/UserHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/useradministration/users/UserHelper.java
@@ -86,7 +86,7 @@ public final class UserHelper {
private static String getTestCreateUserAsJSON(int roleId, int staffId, String username) {
return "{ \"username\": \"" + username + "\", \"firstname\": \"Test\", \"lastname\": \"User\", \"email\": \"whatever@mifos.org\","
+ " \"officeId\": \"1\", \"staffId\": " + "\"" + staffId + "\",\"roles\": [\"" + roleId
- + "\"], \"sendPasswordToEmail\": false}";
+ + "\"], \"sendPasswordToEmail\": false, \"password\": \"password\"," + " \"repeatPassword\": \"password\"}";
}
private static String getTestUpdateUserAsJSON(String username) {