Refactor Client to support auditable fields

This commit is contained in:
Adam Saghy
2022-07-26 09:56:30 +02:00
committed by Arnold Gálovics
parent b8f8217148
commit d8f483ab45
9 changed files with 424 additions and 26 deletions
@@ -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;
}
}
@@ -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);
}
}
@@ -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();
}
@@ -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 ");
@@ -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>
@@ -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 &#13;&#10;mc.id AS &quot;id&quot;, &#13;&#10;mc.firstname AS &quot;firstName&quot;,&#13;&#10;mc.middlename AS &quot;middleName&quot;,&#13;&#10;mc.lastname AS &quot;lastName&quot;,&#13;&#10;mc.display_name AS &quot;fullName&quot;,&#13;&#10;mc.mobile_no AS &quot;mobileNo&quot;, &#13;&#10;ml.principal_amount AS &quot;loanAmount&quot;, &#13;&#10;(IFNULL(ml.principal_outstanding_derived, 0) + IFNULL(ml.interest_outstanding_derived, 0) + IFNULL(ml.fee_charges_outstanding_derived, 0) + IFNULL(ml.penalty_charges_outstanding_derived, 0)) AS &quot;loanOutstanding&quot;,&#13;&#10;ounder.id AS &quot;officeNumber&quot;, &#13;&#10;ml.account_no AS &quot;loanAccountNumber&quot;,&#13;&#10;SUM(lt.amount) AS &quot;repaymentAmount&quot;&#13;&#10;FROM m_office mo&#13;&#10;JOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, '%')&#13;&#10;INNER JOIN m_client mc ON mc.office_id=ounder.id&#13;&#10;INNER JOIN m_loan ml ON ml.client_id = mc.id&#13;&#10;INNER JOIN r_enum_value rev ON rev.enum_id=ml.loan_status_id AND rev.enum_name = 'loan_status_id'&#13;&#10;INNER JOIN m_loan_transaction lt ON lt.loan_id = ml.id&#13;&#10;INNER JOIN m_appuser au ON au.id = lt.createdby_id&#13;&#10;LEFT JOIN m_loan_arrears_aging laa ON laa.loan_id=ml.id&#13;&#10;LEFT JOIN m_payment_detail mpd ON mpd.id=lt.payment_detail_id&#13;&#10;LEFT JOIN m_currency cur ON cur.code = ml.currency_code&#13;&#10;LEFT JOIN m_group_client gc ON gc.client_id = mc.id&#13;&#10;LEFT JOIN m_group g ON g.id = gc.group_id&#13;&#10;LEFT JOIN m_staff lo ON lo.id = ml.loan_officer_id&#13;&#10;LEFT JOIN m_guarantor gua ON gua.loan_id = ml.id&#13;&#10;WHERE ml.loan_status_id=300 AND mo.id=${officeId} AND (IFNULL(ml.loan_officer_id, -10) = ${loanOfficerId} OR &quot;-1&quot; = ${loanOfficerId}) AND (DATEDIFF(CURDATE(), lt.transaction_date) BETWEEN ${fromX} AND ${toY}) AND lt.is_reversed=0 AND lt.transaction_type_enum=2 AND laa.loan_id IS NULL&#13;&#10;GROUP BY ml.id&#13;&#10;ORDER BY ounder.hierarchy, ml.currency_code, mc.account_no, ml.account_no"/>
<where>report_name = 'Loan payments received (Active Loans)'</where>
</update>
<update tableName="stretchy_report">
<column name="report_sql"
value="SELECT &#13;&#10;ml.id AS &quot;loanId&quot;, &#13;&#10;mc.id AS &quot;id&quot;, &#13;&#10;mc.firstname AS &quot;firstName&quot;,&#13;&#10;mc.middlename AS &quot;middleName&quot;,&#13;&#10;mc.lastname AS &quot;lastName&quot;,&#13;&#10;mc.display_name AS &quot;fullName&quot;,&#13;&#10;mc.mobile_no AS &quot;mobileNo&quot;, &#13;&#10;ml.principal_amount AS &quot;loanAmount&quot;, &#13;&#10;(IFNULL(ml.principal_outstanding_derived, 0) + IFNULL(ml.interest_outstanding_derived, 0) + IFNULL(ml.fee_charges_outstanding_derived, 0) + IFNULL(ml.penalty_charges_outstanding_derived, 0)) AS &quot;loanOutstanding&quot;,&#13;&#10;ounder.id AS &quot;officeNumber&quot;, &#13;&#10;ml.account_no AS &quot;loanAccountNumber&quot;,&#13;&#10;SUM(lt.amount) AS &quot;repaymentAmount&quot;&#13;&#10;FROM m_office mo&#13;&#10;JOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, '%')&#13;&#10;INNER JOIN m_client mc ON mc.office_id=ounder.id&#13;&#10;INNER JOIN m_loan ml ON ml.client_id = mc.id&#13;&#10;INNER JOIN r_enum_value rev ON rev.enum_id=ml.loan_status_id AND rev.enum_name = 'loan_status_id'&#13;&#10;INNER JOIN m_loan_arrears_aging laa ON laa.loan_id=ml.id&#13;&#10;INNER JOIN m_loan_transaction lt ON lt.loan_id = ml.id&#13;&#10;INNER JOIN m_appuser au ON au.id = lt.created_by&#13;&#10;LEFT JOIN m_payment_detail mpd ON mpd.id=lt.payment_detail_id&#13;&#10;LEFT JOIN m_currency cur ON cur.code = ml.currency_code&#13;&#10;LEFT JOIN m_group_client gc ON gc.client_id = mc.id&#13;&#10;LEFT JOIN m_group g ON g.id = gc.group_id&#13;&#10;LEFT JOIN m_staff lo ON lo.id = ml.loan_officer_id&#13;&#10;LEFT JOIN m_guarantor gua ON gua.loan_id = ml.id&#13;&#10;WHERE ml.loan_status_id=300 AND mo.id=${officeId} AND (IFNULL(ml.loan_officer_id, -10) = ${loanOfficerId} OR &quot;-1&quot; = ${loanOfficerId}) AND (DATEDIFF(CURDATE(), lt.transaction_date) BETWEEN ${fromX} AND ${toY}) AND (DATEDIFF(CURDATE(), laa.overdue_since_date_derived) BETWEEN ${overdueX} AND ${overdueY}) AND lt.is_reversed=0 AND lt.transaction_type_enum=2&#13;&#10;GROUP BY ml.id&#13;&#10;ORDER BY ounder.hierarchy, ml.currency_code, mc.account_no, ml.account_no"/>
<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 loanAccountNumber, SUM(lt.amount) AS repaymentAmount FROM m_office mo JOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, '%') INNER JOIN m_client mc ON mc.office_id = ounder.id INNER JOIN m_loan ml ON ml.client_id = mc.id INNER JOIN r_enum_value rev ON rev.enum_id = ml.loan_status_id AND rev.enum_name = 'loan_status_id' INNER JOIN m_loan_transaction lt ON lt.loan_id = ml.id INNER JOIN m_appuser au ON au.id = lt.created_by LEFT JOIN m_loan_arrears_aging laa ON laa.loan_id = ml.id LEFT JOIN m_payment_detail mpd ON mpd.id = lt.payment_detail_id LEFT JOIN m_currency cur ON cur.code = ml.currency_code LEFT JOIN m_group_client gc ON gc.client_id = mc.id LEFT JOIN m_group g ON g.id = gc.group_id LEFT JOIN m_staff lo ON lo.id = ml.loan_officer_id LEFT JOIN m_guarantor gua ON gua.loan_id = ml.id WHERE ml.loan_status_id = 300 AND mo.id = '${officeId}' AND (COALESCE(ml.loan_officer_id, -10) = ${loanOfficerId} OR '-1' = ${loanOfficerId}) AND (EXTRACT(DAY FROM (CURRENT_DATE - lt.transaction_date::TIMESTAMP)) BETWEEN 9${fromX} AND ${toY}) AND lt.is_reversed = false AND lt.transaction_type_enum = 2 AND laa.loan_id IS NULL GROUP BY ml.id, mc.id, ounder.id ORDER BY ounder.hierarchy, ml.currency_code, mc.account_no, ml.account_no"/>
<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_no AS loanAccountNumber, SUM(lt.amount) AS repaymentAmount FROM m_office mo JOIN m_office ounder ON ounder.hierarchy LIKE CONCAT(mo.hierarchy, '%') INNER JOIN m_client mc ON mc.office_id = ounder.id INNER JOIN m_loan ml ON ml.client_id = mc.id INNER JOIN r_enum_value rev ON rev.enum_id = ml.loan_status_id AND rev.enum_name = 'loan_status_id' INNER JOIN m_loan_arrears_aging laa ON laa.loan_id = ml.id INNER JOIN m_loan_transaction lt ON lt.loan_id = ml.id INNER JOIN m_appuser au ON au.id = lt.created_by LEFT JOIN m_payment_detail mpd ON mpd.id = lt.payment_detail_id LEFT JOIN m_currency cur ON cur.code = ml.currency_code LEFT JOIN m_group_client gc ON gc.client_id = mc.id LEFT JOIN m_group g ON g.id = gc.group_id LEFT JOIN m_staff lo ON lo.id = ml.loan_officer_id LEFT JOIN m_guarantor gua ON gua.loan_id = ml.id WHERE ml.loan_status_id = 300 AND mo.id = '${officeId}' AND (COALESCE(ml.loan_officer_id, -10) = ${loanOfficerId} OR '-1' = ${loanOfficerId}) AND (EXTRACT(DAY FROM(CURRENT_DATE - lt.transaction_date::TIMESTAMP)) BETWEEN ${fromX} AND ${toY}) AND (EXTRACT(DAY FROM(CURRENT_DATE - laa.overdue_since_date_derived::TIMESTAMP)) BETWEEN ${overdueX} AND ${overdueY}) AND lt.is_reversed = false AND lt.transaction_type_enum = 2 GROUP BY ml.id, mc.id, ounder.id ORDER BY ounder.hierarchy, ml.currency_code, mc.account_no, ml.account_no"/>
<where>report_name = 'Loan payments received (Overdue Loans)'</where>
</update>
</changeSet>
</databaseChangeLog>
@@ -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());
}
}
@@ -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);
@@ -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) {