feat: implement full current account transaction write service

- New CurrentAccountTransactionWritePlatformService interface with deposit,
  withdrawal, holdAmount, releaseAmount, undoTransaction, reverseTransaction,
  and adjustTransaction operations
- New CurrentAccountTransactionWritePlatformServiceImpl using m_current_account
  tables directly (not m_savings_account), with GL journal entry posting
- Add holdAmount, releaseAmount, reversal factory methods to CurrentAccountTransaction
- Wire all 7 current account transaction command handlers to new write service
- Fix CurrentAccountTransactionsApiResource template SQL column: currency_multiplesof
- Fix account number search in SearchParameters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-05-21 11:20:30 +05:30
co-authored by Claude Sonnet 4.6
parent b25d5a30d2
commit ce0fb18eeb
21 changed files with 6961 additions and 6252 deletions
@@ -1,144 +1,145 @@
/**
* 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.service;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
@Builder
@Getter
@AllArgsConstructor
public class SearchParameters {
public static final int DEFAULT_MAX_LIMIT = 200;
private Long officeId;
private String externalId;
private String name;
private String hierarchy;
private String firstname;
private String lastname;
private String status;
private Integer offset;
@Getter(AccessLevel.NONE)
private Integer limit;
private String orderBy;
private String sortOrder;
private String accountNo;
private String currencyCode;
private Long staffId;
private Long loanId;
private Long clientId;
private Long savingsId;
@Getter(AccessLevel.NONE)
private Boolean orphansOnly;
private Long provisioningEntryId;
private Long productId;
private Long categoryId;
@Getter(AccessLevel.NONE)
private Boolean isSelfUser;
private Integer legalForm;
public Integer getLimit() {
if (limit == null) {
return DEFAULT_MAX_LIMIT;
}
if (limit > 0) {
return limit;
}
return null; // unlimited (0 or less)
}
public Boolean getOrphansOnly() {
return Boolean.TRUE.equals(orphansOnly);
}
public Boolean getIsSelfUser() {
return Boolean.TRUE.equals(isSelfUser);
}
public boolean hasOrderBy() {
return StringUtils.isNotBlank(this.orderBy);
}
public boolean hasSortOrder() {
return StringUtils.isNotBlank(this.sortOrder);
}
public boolean hasOfficeId() {
return this.officeId != null && this.officeId != 0;
}
public boolean hasCurrencyCode() {
return StringUtils.isNotBlank(this.currencyCode);
}
public boolean hasLimit() {
return this.limit != null && this.limit > 0;
}
public boolean hasOffset() {
return this.offset != null;
}
public boolean hasHierarchy() {
return StringUtils.isNotBlank(this.hierarchy);
}
public boolean hasStaffId() {
return this.staffId != null && this.staffId != 0;
}
public boolean hasLoanId() {
return this.loanId != null && this.loanId != 0;
}
public boolean hasSavingsId() {
return this.savingsId != null && this.savingsId != 0;
}
public boolean hasProvisioningEntryId() {
return this.provisioningEntryId != null && this.provisioningEntryId != 0;
}
public boolean hasProductId() {
return this.productId != null && this.productId != 0;
}
public boolean hasCategoryId() {
return this.categoryId != null && this.categoryId != 0;
}
public boolean isPerson() {
return this.legalForm != null && this.legalForm == 1;
}
public boolean isEntity() {
return this.legalForm != null && this.legalForm == 2;
}
public boolean hasLegalForm() {
return this.legalForm != null;
}
}
/**
* 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.service;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
@Builder
@Getter
@AllArgsConstructor
public class SearchParameters {
public static final int DEFAULT_MAX_LIMIT = 200;
private Long officeId;
private String externalId;
private String name;
private String hierarchy;
private String firstname;
private String lastname;
private String status;
private Integer offset;
@Getter(AccessLevel.NONE)
private Integer limit;
private String orderBy;
private String sortOrder;
private String accountNo;
private Integer depositType;
private String currencyCode;
private Long staffId;
private Long loanId;
private Long clientId;
private Long savingsId;
@Getter(AccessLevel.NONE)
private Boolean orphansOnly;
private Long provisioningEntryId;
private Long productId;
private Long categoryId;
@Getter(AccessLevel.NONE)
private Boolean isSelfUser;
private Integer legalForm;
public Integer getLimit() {
if (limit == null) {
return DEFAULT_MAX_LIMIT;
}
if (limit > 0) {
return limit;
}
return null; // unlimited (0 or less)
}
public Boolean getOrphansOnly() {
return Boolean.TRUE.equals(orphansOnly);
}
public Boolean getIsSelfUser() {
return Boolean.TRUE.equals(isSelfUser);
}
public boolean hasOrderBy() {
return StringUtils.isNotBlank(this.orderBy);
}
public boolean hasSortOrder() {
return StringUtils.isNotBlank(this.sortOrder);
}
public boolean hasOfficeId() {
return this.officeId != null && this.officeId != 0;
}
public boolean hasCurrencyCode() {
return StringUtils.isNotBlank(this.currencyCode);
}
public boolean hasLimit() {
return this.limit != null && this.limit > 0;
}
public boolean hasOffset() {
return this.offset != null;
}
public boolean hasHierarchy() {
return StringUtils.isNotBlank(this.hierarchy);
}
public boolean hasStaffId() {
return this.staffId != null && this.staffId != 0;
}
public boolean hasLoanId() {
return this.loanId != null && this.loanId != 0;
}
public boolean hasSavingsId() {
return this.savingsId != null && this.savingsId != 0;
}
public boolean hasProvisioningEntryId() {
return this.provisioningEntryId != null && this.provisioningEntryId != 0;
}
public boolean hasProductId() {
return this.productId != null && this.productId != 0;
}
public boolean hasCategoryId() {
return this.categoryId != null && this.categoryId != 0;
}
public boolean isPerson() {
return this.legalForm != null && this.legalForm == 1;
}
public boolean isEntity() {
return this.legalForm != null && this.legalForm == 2;
}
public boolean hasLegalForm() {
return this.legalForm != null;
}
}
@@ -1,149 +1,193 @@
/**
* 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.currentaccount.domain;
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 java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.office.domain.Office;
@Entity
@Table(name = "m_current_account_transaction")
@Getter
@Setter
public class CurrentAccountTransaction extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "current_account_id", nullable = false)
private CurrentAccount currentAccount;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "office_id", nullable = false)
private Office office;
// 1=Deposit, 2=Withdrawal, 3=InterestPosting, 4=Charge, 5=ChargeWaiver, 6=Transfer, 7=Reversal
@Column(name = "transaction_type_enum", nullable = false)
private Integer transactionTypeEnum;
@Column(name = "transaction_date", nullable = false)
private LocalDate transactionDate;
@Column(name = "amount", nullable = false, precision = 19, scale = 6)
private BigDecimal amount;
@Column(name = "ref_no", length = 100)
private String refNo;
@Column(name = "remarks", length = 500)
private String remarks;
@Column(name = "is_reversed", nullable = false)
private boolean reversed = false;
@Column(name = "is_reversal", nullable = false)
private boolean reversal = false;
@Column(name = "original_transaction_id")
private Long originalTransactionId;
@Column(name = "is_manual", nullable = false)
private boolean manual = false;
@Column(name = "running_balance_derived", precision = 19, scale = 6)
private BigDecimal runningBalanceDerived;
@Column(name = "cumulative_balance_derived", precision = 19, scale = 6)
private BigDecimal cumulativeBalanceDerived;
@Column(name = "balance_end_date_derived")
private LocalDate balanceEndDateDerived;
@Column(name = "balance_number_of_days_derived")
private Integer balanceNumberOfDaysDerived;
@Column(name = "overdraft_amount_derived", precision = 19, scale = 6)
private BigDecimal overdraftAmountDerived;
@Column(name = "is_lien_transaction", nullable = false)
private boolean lienTransaction = false;
@Column(name = "release_id_of_hold_amount_transaction")
private Long releaseIdOfHoldAmountTransaction;
@Column(name = "reason_for_block", length = 200)
private String reasonForBlock;
@Column(name = "submitted_on_date")
private LocalDate submittedOnDate;
@Column(name = "created_date")
private LocalDateTime createdDate;
@Column(name = "created_by")
private Long createdBy;
protected CurrentAccountTransaction() {}
public static CurrentAccountTransaction deposit(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, String refNo) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 1;
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.refNo = refNo;
txn.submittedOnDate = transactionDate;
return txn;
}
public static CurrentAccountTransaction chargeDeduction(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 7; // PAY_CHARGE
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.submittedOnDate = transactionDate;
return txn;
}
public static CurrentAccountTransaction withdrawal(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, String refNo) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 2;
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.refNo = refNo;
txn.submittedOnDate = transactionDate;
return txn;
}
}
/**
* 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.currentaccount.domain;
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 java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.office.domain.Office;
@Entity
@Table(name = "m_current_account_transaction")
@Getter
@Setter
public class CurrentAccountTransaction extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "current_account_id", nullable = false)
private CurrentAccount currentAccount;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "office_id", nullable = false)
private Office office;
// 1=Deposit, 2=Withdrawal, 3=InterestPosting, 4=Charge, 5=ChargeWaiver, 6=Transfer, 7=Reversal
@Column(name = "transaction_type_enum", nullable = false)
private Integer transactionTypeEnum;
@Column(name = "transaction_date", nullable = false)
private LocalDate transactionDate;
@Column(name = "amount", nullable = false, precision = 19, scale = 6)
private BigDecimal amount;
@Column(name = "ref_no", length = 100)
private String refNo;
@Column(name = "remarks", length = 500)
private String remarks;
@Column(name = "is_reversed", nullable = false)
private boolean reversed = false;
@Column(name = "is_reversal", nullable = false)
private boolean reversal = false;
@Column(name = "original_transaction_id")
private Long originalTransactionId;
@Column(name = "is_manual", nullable = false)
private boolean manual = false;
@Column(name = "running_balance_derived", precision = 19, scale = 6)
private BigDecimal runningBalanceDerived;
@Column(name = "cumulative_balance_derived", precision = 19, scale = 6)
private BigDecimal cumulativeBalanceDerived;
@Column(name = "balance_end_date_derived")
private LocalDate balanceEndDateDerived;
@Column(name = "balance_number_of_days_derived")
private Integer balanceNumberOfDaysDerived;
@Column(name = "overdraft_amount_derived", precision = 19, scale = 6)
private BigDecimal overdraftAmountDerived;
@Column(name = "is_lien_transaction", nullable = false)
private boolean lienTransaction = false;
@Column(name = "release_id_of_hold_amount_transaction")
private Long releaseIdOfHoldAmountTransaction;
@Column(name = "reason_for_block", length = 200)
private String reasonForBlock;
@Column(name = "submitted_on_date")
private LocalDate submittedOnDate;
@Column(name = "created_date")
private LocalDateTime createdDate;
@Column(name = "created_by")
private Long createdBy;
protected CurrentAccountTransaction() {}
public static CurrentAccountTransaction deposit(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, String refNo) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 1;
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.refNo = refNo;
txn.submittedOnDate = transactionDate;
return txn;
}
public static CurrentAccountTransaction chargeDeduction(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 7; // PAY_CHARGE
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.submittedOnDate = transactionDate;
return txn;
}
public static CurrentAccountTransaction withdrawal(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, String refNo) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 2;
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.refNo = refNo;
txn.submittedOnDate = transactionDate;
return txn;
}
// transactionTypeEnum = 8 (Hold Amount)
public static CurrentAccountTransaction holdAmount(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, String reasonForBlock) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 8;
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.lienTransaction = true;
txn.reasonForBlock = reasonForBlock;
txn.submittedOnDate = transactionDate;
return txn;
}
// transactionTypeEnum = 9 (Release Amount)
public static CurrentAccountTransaction releaseAmount(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, Long holdTransactionId) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 9;
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.releaseIdOfHoldAmountTransaction = holdTransactionId;
txn.submittedOnDate = transactionDate;
return txn;
}
// transactionTypeEnum = 7 (Reversal / Undo)
public static CurrentAccountTransaction reversal(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, Long originalTransactionId) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 7;
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.reversal = true;
txn.originalTransactionId = originalTransactionId;
txn.submittedOnDate = transactionDate;
return txn;
}
}
@@ -1,29 +1,31 @@
/**
* 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.currentaccount.service;
import java.util.List;
import java.util.Map;
public interface CurrentAccountReadPlatformService {
Map<String, Object> retrieveOne(Long accountId);
List<Map<String, Object>> retrieveAll();
}
/**
* 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.currentaccount.service;
import java.util.List;
import java.util.Map;
public interface CurrentAccountReadPlatformService {
Map<String, Object> retrieveOne(Long accountId);
List<Map<String, Object>> retrieveAll();
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName);
}
@@ -0,0 +1,39 @@
/**
* 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.currentaccount.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface CurrentAccountTransactionWritePlatformService {
CommandProcessingResult deposit(Long accountId, JsonCommand command);
CommandProcessingResult withdrawal(Long accountId, JsonCommand command);
CommandProcessingResult holdAmount(Long accountId, JsonCommand command);
CommandProcessingResult releaseAmount(Long accountId, Long transactionId);
CommandProcessingResult undoTransaction(Long accountId, Long transactionId, boolean isReversal);
CommandProcessingResult reverseTransaction(Long accountId, Long transactionId, boolean isReversal, JsonCommand command);
CommandProcessingResult adjustTransaction(Long accountId, Long transactionId, JsonCommand command);
}
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.portfolio.savings.api;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -32,6 +34,10 @@ import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import java.time.LocalDate;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
@@ -43,10 +49,14 @@ import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamE
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@Path("/v1/currentaccounts/{accountId}/transactions")
@@ -55,11 +65,58 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
public class CurrentAccountTransactionsApiResource {
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<SavingsAccountTransactionData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final SavingsAccountReadPlatformService savingsAccountReadPlatformService;
private final PaymentTypeReadService paymentTypeReadService;
private final JdbcTemplate jdbcTemplate;
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve current account transaction template")
@ApiResponse(responseCode = "200", description = "OK")
public String retrieveTemplate(@PathParam("accountId") final Long accountId, @Context final UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
final String sql = "SELECT ca.currency_code, ca.currency_digits, ca.currency_multiplesof, "
+ "mc.display_symbol, mc.name AS currency_name, mc.internationalized_name_code "
+ "FROM m_current_account ca "
+ "LEFT JOIN m_currency mc ON mc.code = ca.currency_code "
+ "WHERE ca.id = ?";
final Map<String, Object> template;
try {
template = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
final Map<String, Object> map = new LinkedHashMap<>();
final LocalDate today = LocalDate.now();
map.put("date", new int[] { today.getYear(), today.getMonthValue(), today.getDayOfMonth() });
final Map<String, Object> currency = new LinkedHashMap<>();
currency.put("code", rs.getString("currency_code"));
currency.put("decimalPlaces", rs.getInt("currency_digits"));
currency.put("inMultiplesOf", rs.getInt("currency_multiplesof"));
currency.put("displaySymbol", rs.getString("display_symbol"));
currency.put("name", rs.getString("currency_name"));
currency.put("nameCode", rs.getString("internationalized_name_code"));
map.put("currency", currency);
return map;
}, accountId);
} catch (org.springframework.dao.EmptyResultDataAccessException e) {
throw new CurrentAccountNotFoundException(accountId);
}
final Collection<PaymentTypeData> paymentTypeOptions = paymentTypeReadService.retrieveAllPaymentTypes();
template.put("paymentTypeOptions", paymentTypeOptions);
return GSON.toJson(template);
}
@GET
@Path("{transactionId}")
@@ -1,214 +1,216 @@
/**
* 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.savings.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
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.MediaType;
import jakarta.ws.rs.core.UriInfo;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParser;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.apache.fineract.portfolio.savings.service.CurrentAccountTemplateReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/currentaccounts")
@Component
@Tag(name = "Current Account", description = "Current accounts are instances of a particular current product created for an individual or group.")
@RequiredArgsConstructor
public class CurrentAccountsApiResource {
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private final CurrentAccountTemplateReadPlatformService currentAccountTemplateReadPlatformService;
private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<SavingsAccountData> toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final SqlValidator sqlValidator;
private final CurrentAccountWritePlatformService currentAccountWritePlatformService;
private final CurrentAccountReadPlatformService currentAccountReadPlatformService;
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Current Account Template")
@ApiResponse(responseCode = "200", description = "OK")
public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId,
@QueryParam("groupId") @Parameter(description = "groupId") final Long groupId,
@QueryParam("productId") @Parameter(description = "productId") final Long productId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@Context final UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
final SavingsAccountData savingsAccount = currentAccountTemplateReadPlatformService.retrieveTemplate(clientId, groupId, productId,
staffInSelectedOfficeOnly);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, savingsAccount, SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List current accounts")
@ApiResponse(responseCode = "200", description = "OK")
public String retrieveAll(@Context final UriInfo uriInfo,
@QueryParam("externalId") final String externalId,
@QueryParam("offset") final Integer offset,
@QueryParam("limit") final Integer limit,
@QueryParam("orderBy") final String orderBy,
@QueryParam("sortOrder") final String sortOrder) {
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
final List<Map<String, Object>> accounts = currentAccountReadPlatformService.retrieveAll();
return GSON.toJson(accounts);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Submit new current account application")
@ApiResponse(responseCode = "200", description = "OK")
public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final org.apache.fineract.infrastructure.core.api.JsonCommand command =
org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null,
JsonParser.parseString(apiRequestBodyAsJson != null ? apiRequestBodyAsJson : "{}"));
final CommandProcessingResult result = currentAccountWritePlatformService.submitApplication(command);
return toApiJsonSerializer.serialize(result);
}
@GET
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a current account")
@ApiResponse(responseCode = "200", description = "OK")
public String retrieveOne(@PathParam("accountId") final Long accountId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus,
@Context final UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
final Map<String, Object> account = currentAccountReadPlatformService.retrieveOne(accountId);
return GSON.toJson(account);
}
@PUT
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Modify a current account application")
@ApiResponse(responseCode = "200", description = "OK")
public String update(@PathParam("accountId") final Long accountId,
@Parameter(hidden = true) final String apiRequestBodyAsJson,
@QueryParam("command") final String commandParam) {
final org.apache.fineract.infrastructure.core.api.JsonCommand command =
org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null,
JsonParser.parseString(apiRequestBodyAsJson != null ? apiRequestBodyAsJson : "{}"));
final CommandProcessingResult result = currentAccountWritePlatformService.modifyApplication(accountId, command);
return toApiJsonSerializer.serialize(result);
}
@POST
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Approve/Reject/Activate/Close/Deposit/Withdrawal and other current account commands")
@ApiResponse(responseCode = "200", description = "OK")
public String handleCommands(@PathParam("accountId") final Long accountId,
@QueryParam("command") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
String jsonApiRequest = apiRequestBodyAsJson;
if (StringUtils.isBlank(jsonApiRequest)) {
jsonApiRequest = "{}";
}
final org.apache.fineract.infrastructure.core.api.JsonCommand command =
org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null,
JsonParser.parseString(jsonApiRequest));
CommandProcessingResult result = null;
if (is(commandParam, "reject")) {
result = currentAccountWritePlatformService.rejectApplication(accountId, command);
} else if (is(commandParam, "withdrawnByApplicant")) {
result = currentAccountWritePlatformService.withdrawApplication(accountId, command);
} else if (is(commandParam, "approve")) {
result = currentAccountWritePlatformService.approveApplication(accountId, command);
} else if (is(commandParam, "undoapproval")) {
result = currentAccountWritePlatformService.undoApproval(accountId, command);
} else if (is(commandParam, "activate")) {
result = currentAccountWritePlatformService.activate(accountId, command);
} else if (is(commandParam, "close")) {
result = currentAccountWritePlatformService.closeAccount(accountId, command);
}
if (result == null) {
throw new UnrecognizedQueryParamException("command", commandParam,
new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "close" });
}
return toApiJsonSerializer.serialize(result);
}
@DELETE
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a current account application")
@ApiResponse(responseCode = "200", description = "OK")
public String delete(@PathParam("accountId") final Long accountId) {
final CommandProcessingResult result = currentAccountWritePlatformService.deleteApplication(accountId);
return toApiJsonSerializer.serialize(result);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
}
/**
* 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.savings.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
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.MediaType;
import jakarta.ws.rs.core.UriInfo;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParser;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.apache.fineract.portfolio.savings.service.CurrentAccountTemplateReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/currentaccounts")
@Component
@Tag(name = "Current Account", description = "Current accounts are instances of a particular current product created for an individual or group.")
@RequiredArgsConstructor
public class CurrentAccountsApiResource {
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private final CurrentAccountTemplateReadPlatformService currentAccountTemplateReadPlatformService;
private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<SavingsAccountData> toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final SqlValidator sqlValidator;
private final CurrentAccountWritePlatformService currentAccountWritePlatformService;
private final CurrentAccountReadPlatformService currentAccountReadPlatformService;
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Current Account Template")
@ApiResponse(responseCode = "200", description = "OK")
public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId,
@QueryParam("groupId") @Parameter(description = "groupId") final Long groupId,
@QueryParam("productId") @Parameter(description = "productId") final Long productId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@Context final UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
final SavingsAccountData savingsAccount = currentAccountTemplateReadPlatformService.retrieveTemplate(clientId, groupId, productId,
staffInSelectedOfficeOnly);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, savingsAccount, SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List current accounts")
@ApiResponse(responseCode = "200", description = "OK")
public String retrieveAll(@Context final UriInfo uriInfo,
@QueryParam("externalId") final String externalId,
@QueryParam("offset") final Integer offset,
@QueryParam("limit") final Integer limit,
@QueryParam("orderBy") final String orderBy,
@QueryParam("sortOrder") final String sortOrder,
@QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName) {
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
final List<Map<String, Object>> accounts = currentAccountReadPlatformService.retrieveAll(offset, limit, accountNo, clientName);
return GSON.toJson(accounts);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Submit new current account application")
@ApiResponse(responseCode = "200", description = "OK")
public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final org.apache.fineract.infrastructure.core.api.JsonCommand command =
org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null,
JsonParser.parseString(apiRequestBodyAsJson != null ? apiRequestBodyAsJson : "{}"));
final CommandProcessingResult result = currentAccountWritePlatformService.submitApplication(command);
return toApiJsonSerializer.serialize(result);
}
@GET
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a current account")
@ApiResponse(responseCode = "200", description = "OK")
public String retrieveOne(@PathParam("accountId") final Long accountId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus,
@Context final UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
final Map<String, Object> account = currentAccountReadPlatformService.retrieveOne(accountId);
return GSON.toJson(account);
}
@PUT
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Modify a current account application")
@ApiResponse(responseCode = "200", description = "OK")
public String update(@PathParam("accountId") final Long accountId,
@Parameter(hidden = true) final String apiRequestBodyAsJson,
@QueryParam("command") final String commandParam) {
final org.apache.fineract.infrastructure.core.api.JsonCommand command =
org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null,
JsonParser.parseString(apiRequestBodyAsJson != null ? apiRequestBodyAsJson : "{}"));
final CommandProcessingResult result = currentAccountWritePlatformService.modifyApplication(accountId, command);
return toApiJsonSerializer.serialize(result);
}
@POST
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Approve/Reject/Activate/Close/Deposit/Withdrawal and other current account commands")
@ApiResponse(responseCode = "200", description = "OK")
public String handleCommands(@PathParam("accountId") final Long accountId,
@QueryParam("command") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
String jsonApiRequest = apiRequestBodyAsJson;
if (StringUtils.isBlank(jsonApiRequest)) {
jsonApiRequest = "{}";
}
final org.apache.fineract.infrastructure.core.api.JsonCommand command =
org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null,
JsonParser.parseString(jsonApiRequest));
CommandProcessingResult result = null;
if (is(commandParam, "reject")) {
result = currentAccountWritePlatformService.rejectApplication(accountId, command);
} else if (is(commandParam, "withdrawnByApplicant")) {
result = currentAccountWritePlatformService.withdrawApplication(accountId, command);
} else if (is(commandParam, "approve")) {
result = currentAccountWritePlatformService.approveApplication(accountId, command);
} else if (is(commandParam, "undoapproval")) {
result = currentAccountWritePlatformService.undoApproval(accountId, command);
} else if (is(commandParam, "activate")) {
result = currentAccountWritePlatformService.activate(accountId, command);
} else if (is(commandParam, "close")) {
result = currentAccountWritePlatformService.closeAccount(accountId, command);
}
if (result == null) {
throw new UnrecognizedQueryParamException("command", commandParam,
new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "close" });
}
return toApiJsonSerializer.serialize(result);
}
@DELETE
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a current account application")
@ApiResponse(responseCode = "200", description = "OK")
public String delete(@PathParam("accountId") final Long accountId) {
final CommandProcessingResult result = currentAccountWritePlatformService.deleteApplication(accountId);
return toApiJsonSerializer.serialize(result);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
}
@@ -1,470 +1,472 @@
/**
* 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.savings.api;
import com.google.gson.JsonElement;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
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.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType;
import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookPopulatorService;
import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookService;
import org.apache.fineract.infrastructure.core.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.PaginationParameters;
import org.apache.fineract.infrastructure.core.data.UploadRequest;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.apache.fineract.portfolio.savings.data.DepositAccountData;
import org.apache.fineract.portfolio.savings.data.RecurringDepositAccountData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
import org.apache.fineract.portfolio.savings.service.RecurringDepositAccountInterestCalculationService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@Path("/v1/recurringdepositaccounts")
@Component
@Tag(name = "Recurring Deposit Account", description = "Recurring Deposit accounts are instances of a praticular recurring deposit product created. An application process around the creation of accounts is also supported.")
@RequiredArgsConstructor
public class RecurringDepositAccountsApiResource {
private final DepositAccountReadPlatformService depositAccountReadPlatformService;
private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<DepositAccountData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService;
private final FromJsonHelper fromJsonHelper;
private final DepositAccountPreMatureCalculationPlatformService accountPreMatureCalculationPlatformService;
private final BulkImportWorkbookService bulkImportWorkbookService;
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
private final RecurringDepositAccountInterestCalculationService recurringDepositAccountInterestCalculationService;
private final SqlValidator sqlValidator;
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve recurring Deposit Account Template", description = "This is a convenience resource. It can be useful when building maintenance user interface screens for recurring deposit applications. The template data returned consists of any or all of:\n"
+ "\n" + "Field Defaults\n" + "Allowed Value Lists\n\n" + "Example Requests:\n" + "\n"
+ "recurringdepositaccounts/template?clientId=1\n" + "\n" + "\n" + "recurringdepositaccounts/template?clientId=1&productId=1")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsTemplateResponse.class))) })
public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId,
@QueryParam("groupId") @Parameter(description = "groupId") final Long groupId,
@QueryParam("productId") @Parameter(description = "productId") final Long productId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
final DepositAccountData account = this.depositAccountReadPlatformService.retrieveTemplate(DepositAccountType.RECURRING_DEPOSIT,
clientId, groupId, productId, staffInSelectedOfficeOnly);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Recurring deposit applications/accounts", description = "Lists Recurring deposit applications/accounts\n\n"
+ "Example Requests:\n" + "\n" + "recurringdepositaccounts\n" + "\n" + "\n" + "recurringdepositaccounts?fields=name")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsResponse.class)))) })
public String retrieveAll(@Context final UriInfo uriInfo, @QueryParam("paged") @Parameter(description = "paged") final Boolean paged,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
final PaginationParameters paginationParameters = PaginationParameters.builder().paged(Boolean.TRUE.equals(paged)).limit(limit)
.offset(offset).orderBy(orderBy).sortOrder(sortOrder).build();
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
if (paginationParameters.isPaged()) {
final Page<DepositAccountData> account = this.depositAccountReadPlatformService
.retrieveAllPaged(DepositAccountType.RECURRING_DEPOSIT, paginationParameters);
return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
final Collection<DepositAccountData> account = this.depositAccountReadPlatformService
.retrieveAll(DepositAccountType.RECURRING_DEPOSIT, paginationParameters);
return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Submit new recurring deposit application", description = "Submits new recurring deposit application\n\n"
+ "Mandatory Fields: clientId or groupId, productId, submittedOnDate, depositAmount, depositPeriod, depositPeriodFrequencyId\n\n"
+ "Optional Fields: accountNo, externalId, fieldOfficerId,linkAccountId(if provided initial deposit amount will be collected from this account),transferInterestToSavings(By enabling this flag all interest postings will be transferred to linked saving account )\n\n"
+ "Inherited from Product (if not provided): interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, lockinPeriodFrequency, lockinPeriodFrequencyType, preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnTypeId, charts, withHoldTax")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsResponse.class))) })
public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createRecurringDepositAccount().withJson(apiRequestBodyAsJson)
.build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@GET
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a recurring deposit application/account", description = "Retrieves a recurring deposit application/account\n\n"
+ "Example Requests :\n" + "\n" + "recurringdepositaccounts/1\n" + "\n" + "\n" + "recurringdepositaccounts/1?associations=all")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsAccountIdResponse.class))) })
public String retrieveOne(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus,
@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) {
throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" });
}
final RecurringDepositAccountData account = (RecurringDepositAccountData) this.depositAccountReadPlatformService
.retrieveOneWithChartSlabs(DepositAccountType.RECURRING_DEPOSIT, accountId);
final Set<String> mandatoryResponseParameters = new HashSet<>();
final RecurringDepositAccountData accountTemplate = populateTemplateAndAssociations(accountId, account, staffInSelectedOfficeOnly,
chargeStatus, uriInfo, mandatoryResponseParameters);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(),
mandatoryResponseParameters);
return this.toApiJsonSerializer.serialize(settings, accountTemplate,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
private RecurringDepositAccountData populateTemplateAndAssociations(final Long accountId,
final RecurringDepositAccountData savingsAccount, final boolean staffInSelectedOfficeOnly, final String chargeStatus,
final UriInfo uriInfo, final Set<String> mandatoryResponseParameters) {
Collection<SavingsAccountTransactionData> transactions = null;
Collection<SavingsAccountChargeData> charges = null;
final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters());
if (!associationParameters.isEmpty()) {
if (associationParameters.contains("all")) {
associationParameters.addAll(Arrays.asList(SavingsApiConstants.transactions, SavingsApiConstants.charges));
}
if (associationParameters.contains(SavingsApiConstants.transactions)) {
mandatoryResponseParameters.add(SavingsApiConstants.transactions);
final Collection<SavingsAccountTransactionData> currentTransactions = this.depositAccountReadPlatformService
.retrieveAllTransactions(DepositAccountType.RECURRING_DEPOSIT, accountId);
if (!CollectionUtils.isEmpty(currentTransactions)) {
transactions = currentTransactions;
}
}
if (associationParameters.contains(SavingsApiConstants.charges)) {
mandatoryResponseParameters.add(SavingsApiConstants.charges);
final Collection<SavingsAccountChargeData> currentCharges = this.savingsAccountChargeReadPlatformService
.retrieveSavingsAccountCharges(accountId, chargeStatus);
if (!CollectionUtils.isEmpty(currentCharges)) {
charges = currentCharges;
}
}
}
RecurringDepositAccountData templateData = null;
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
if (settings.isTemplate()) {
templateData = (RecurringDepositAccountData) this.depositAccountReadPlatformService.retrieveTemplate(
DepositAccountType.RECURRING_DEPOSIT, savingsAccount.getClientId(), savingsAccount.getGroupId(),
savingsAccount.getDepositProductId(), staffInSelectedOfficeOnly);
}
return RecurringDepositAccountData.withTemplateOptions(savingsAccount, templateData, transactions, charges);
}
@PUT
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Modify a recurring deposit application", description = "Recurring deposit application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PutRecurringDepositAccountsAccountIdRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PutRecurringDepositAccountsAccountIdResponse.class))) })
public String update(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateRecurringDepositAccount(accountId)
.withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Approve recurring deposit application | Undo approval recurring deposit application | Reject recurring deposit application | Withdraw recurring deposit application | Activate a recurring deposit account | Update the recommended deposit amount for a recurring deposit account | Close a recurring deposit account | Premature Close a recurring deposit account | Calculate Premature amount on Recurring deposit account | Calculate Interest on recurring Deposit Account | Post Interest on recurring Deposit Account", description = "Approve recurring deposit application:\n\n"
+ "Approves recurring deposit application so long as its in 'Submitted and pending approval' state.\n\n"
+ "Undo approval recurring deposit application:\n\n"
+ "Will move 'approved' recurring deposit application back to 'Submitted and pending approval' state.\n\n"
+ "Reject recurring deposit application\n\n"
+ "Rejects recurring deposit application so long as its in 'Submitted and pending approval' state.\n\n"
+ "Withdraw recurring deposit application:\n\n"
+ "Used when an applicant withdraws from the recurring deposit application. It must be in 'Submitted and pending approval' state.\n\n"
+ "Activate a recurring deposit account:\n\n"
+ "Results in an approved recurring deposit application being converted into an 'active' recurring deposit account.\n\n"
+ "Update the recommended deposit amount for a recurring deposit account:\n\n"
+ "Updates the recommended deposit amount for a RD account as on the effective date.\n\n"
+ "Close a recurring deposit account\n\n"
+ "Results in a Matured recurring deposit account being converted into a 'closed' recurring deposit account.\n" + "\n"
+ "On account close allowed actions are." + "Premature Close a recurring deposit account:\n\n"
+ "Results in an Active recurring deposit account being converted into a 'Premature Closed' recurring deposit account with options to withdraw prematured amount. (premature amount is calculated using interest rate chart applicable along with penal interest if any.)\n"
+ "\n" + "On account premature closure allowed actions are.\n\n"
+ "Calculate Premature amount on Recurring deposit account:\n\n"
+ "Calculate premature amount on recurring deposit till premature close date. Premature amount is calculated based on interest chart and penal interest applicable if any.\n\n"
+ "Calculate Interest on recurring Deposit Account:\n\n"
+ "Calculates interest earned on a recurring deposit account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n"
+ "Post Interest on recurring Deposit Account:\n\n"
+ "Calculates and Posts interest earned on a recurring deposit account based on todays date and whether an interest posting or crediting event is due.\n\n"
+ "Showing request/response for 'Post Interest on recurring Deposit Account'")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsAccountIdRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsAccountIdResponse.class))) })
public String handleCommands(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
String jsonApiRequest = apiRequestBodyAsJson;
if (StringUtils.isBlank(jsonApiRequest)) {
jsonApiRequest = "{}";
}
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest);
CommandProcessingResult result = null;
if (is(commandParam, "reject")) {
final CommandWrapper commandRequest = builder.rejectRecurringDepositAccountApplication(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "withdrawnByApplicant")) {
final CommandWrapper commandRequest = builder.withdrawRecurringDepositAccountApplication(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "approve")) {
final CommandWrapper commandRequest = builder.approveRecurringDepositAccountApplication(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "undoapproval")) {
final CommandWrapper commandRequest = builder.undoRecurringDepositAccountApplication(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "activate")) {
final CommandWrapper commandRequest = builder.recurringDepositAccountActivation(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "calculateInterest")) {
final CommandWrapper commandRequest = builder.withNoJsonBody().recurringDepositAccountInterestCalculation(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, DepositsApiConstants.UPDATE_DEPOSIT_AMOUNT)) {
final CommandWrapper commandRequest = builder.updateDepositAmountForRecurringDepositAccount(accountId)
.withJson(apiRequestBodyAsJson).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "postInterest")) {
final CommandWrapper commandRequest = builder.recurringDepositAccountInterestPosting(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "close")) {
final CommandWrapper commandRequest = builder.closeRecurringDepositAccount(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "prematureClose")) {
final CommandWrapper commandRequest = builder.prematureCloseRecurringDepositAccount(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "calculatePrematureAmount")) {
final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper);
final DepositAccountData account = this.accountPreMatureCalculationPlatformService.calculatePreMatureAmount(accountId, query,
DepositAccountType.RECURRING_DEPOSIT);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
if (result == null) {
throw new UnrecognizedQueryParamException("command", commandParam,
new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest",
"postInterest", "close", "prematureClose", "calculatePrematureAmount" });
}
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("calculate-rd-interest")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String calculateRecurringDepositInterest(@Context final UriInfo uriInfo, final String apiRequestBodyAsJson) {
JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson);
HashMap<String, Object> result = recurringDepositAccountInterestCalculationService
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper));
return toApiJsonSerializer.serializeResult(result);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
@DELETE
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a recurring deposit application", description = "At present we support hard delete of recurring deposit application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the recurring deposit account.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.DeleteRecurringDepositAccountsResponse.class))) })
public String delete(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteRecurringDepositAccount(accountId).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@GET
@Path("{accountId}/template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String accountClosureTemplate(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
DepositAccountData account = null;
if (is(commandParam, "close")) {
account = this.depositAccountReadPlatformService.retrieveOneWithClosureTemplate(DepositAccountType.RECURRING_DEPOSIT,
accountId);
}
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
@GET
@Path("downloadtemplate")
@Produces("application/vnd.ms-excel")
public Response getRecurringDepositTemplate(@QueryParam("officeId") final Long officeId, @QueryParam("staffId") final Long staffId,
@QueryParam("dateFormat") final String dateFormat) {
return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS.toString(), officeId, staffId,
dateFormat);
}
@POST
@Path("uploadtemplate")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@RequestBody(description = "Upload recurring deposit template", content = {
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) })
public String postRecurringDepositTemplate(@FormDataParam("file") InputStream uploadedInputStream,
@FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale,
@FormDataParam("dateFormat") final String dateFormat) {
final Long importDocumentId = this.bulkImportWorkbookService.importWorkbook(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS.toString(),
uploadedInputStream, fileDetail, locale, dateFormat);
return this.toApiJsonSerializer.serialize(importDocumentId);
}
@GET
@Path("transactions/downloadtemplate")
@Produces("application/vnd.ms-excel")
public Response getRecurringDepositTransactionTemplate(@QueryParam("officeId") final Long officeId,
@QueryParam("dateFormat") final String dateFormat) {
return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS_TRANSACTIONS.toString(), officeId,
null, dateFormat);
}
@POST
@Path("transactions/uploadtemplate")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@RequestBody(description = "Upload recurring deposit transactions template", content = {
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) })
public String postRecurringDepositTransactionsTemplate(@FormDataParam("file") InputStream uploadedInputStream,
@FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale,
@FormDataParam("dateFormat") final String dateFormat) {
final Long importDocumentId = this.bulkImportWorkbookService.importWorkbook(
GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS_TRANSACTIONS.toString(), uploadedInputStream, fileDetail, locale, dateFormat);
return this.toApiJsonSerializer.serialize(importDocumentId);
}
}
/**
* 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.savings.api;
import com.google.gson.JsonElement;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
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.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType;
import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookPopulatorService;
import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookService;
import org.apache.fineract.infrastructure.core.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.PaginationParameters;
import org.apache.fineract.infrastructure.core.data.UploadRequest;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.apache.fineract.portfolio.savings.data.DepositAccountData;
import org.apache.fineract.portfolio.savings.data.RecurringDepositAccountData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
import org.apache.fineract.portfolio.savings.service.RecurringDepositAccountInterestCalculationService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@Path("/v1/recurringdepositaccounts")
@Component
@Tag(name = "Recurring Deposit Account", description = "Recurring Deposit accounts are instances of a praticular recurring deposit product created. An application process around the creation of accounts is also supported.")
@RequiredArgsConstructor
public class RecurringDepositAccountsApiResource {
private final DepositAccountReadPlatformService depositAccountReadPlatformService;
private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<DepositAccountData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService;
private final FromJsonHelper fromJsonHelper;
private final DepositAccountPreMatureCalculationPlatformService accountPreMatureCalculationPlatformService;
private final BulkImportWorkbookService bulkImportWorkbookService;
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
private final RecurringDepositAccountInterestCalculationService recurringDepositAccountInterestCalculationService;
private final SqlValidator sqlValidator;
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve recurring Deposit Account Template", description = "This is a convenience resource. It can be useful when building maintenance user interface screens for recurring deposit applications. The template data returned consists of any or all of:\n"
+ "\n" + "Field Defaults\n" + "Allowed Value Lists\n\n" + "Example Requests:\n" + "\n"
+ "recurringdepositaccounts/template?clientId=1\n" + "\n" + "\n" + "recurringdepositaccounts/template?clientId=1&productId=1")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsTemplateResponse.class))) })
public String template(@QueryParam("clientId") @Parameter(description = "clientId") final Long clientId,
@QueryParam("groupId") @Parameter(description = "groupId") final Long groupId,
@QueryParam("productId") @Parameter(description = "productId") final Long productId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
final DepositAccountData account = this.depositAccountReadPlatformService.retrieveTemplate(DepositAccountType.RECURRING_DEPOSIT,
clientId, groupId, productId, staffInSelectedOfficeOnly);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Recurring deposit applications/accounts", description = "Lists Recurring deposit applications/accounts\n\n"
+ "Example Requests:\n" + "\n" + "recurringdepositaccounts\n" + "\n" + "\n" + "recurringdepositaccounts?fields=name")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsResponse.class)))) })
public String retrieveAll(@Context final UriInfo uriInfo, @QueryParam("paged") @Parameter(description = "paged") final Boolean paged,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
final PaginationParameters paginationParameters = PaginationParameters.builder().paged(Boolean.TRUE.equals(paged)).limit(limit)
.offset(offset).orderBy(orderBy).sortOrder(sortOrder).build();
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
if (paginationParameters.isPaged()) {
final Page<DepositAccountData> account = this.depositAccountReadPlatformService
.retrieveAllPaged(DepositAccountType.RECURRING_DEPOSIT, paginationParameters, accountNo, clientName);
return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
final Collection<DepositAccountData> account = this.depositAccountReadPlatformService
.retrieveAll(DepositAccountType.RECURRING_DEPOSIT, paginationParameters);
return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Submit new recurring deposit application", description = "Submits new recurring deposit application\n\n"
+ "Mandatory Fields: clientId or groupId, productId, submittedOnDate, depositAmount, depositPeriod, depositPeriodFrequencyId\n\n"
+ "Optional Fields: accountNo, externalId, fieldOfficerId,linkAccountId(if provided initial deposit amount will be collected from this account),transferInterestToSavings(By enabling this flag all interest postings will be transferred to linked saving account )\n\n"
+ "Inherited from Product (if not provided): interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, lockinPeriodFrequency, lockinPeriodFrequencyType, preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnTypeId, charts, withHoldTax")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsResponse.class))) })
public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createRecurringDepositAccount().withJson(apiRequestBodyAsJson)
.build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@GET
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a recurring deposit application/account", description = "Retrieves a recurring deposit application/account\n\n"
+ "Example Requests :\n" + "\n" + "recurringdepositaccounts/1\n" + "\n" + "\n" + "recurringdepositaccounts/1?associations=all")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.GetRecurringDepositAccountsAccountIdResponse.class))) })
public String retrieveOne(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus,
@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) {
throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" });
}
final RecurringDepositAccountData account = (RecurringDepositAccountData) this.depositAccountReadPlatformService
.retrieveOneWithChartSlabs(DepositAccountType.RECURRING_DEPOSIT, accountId);
final Set<String> mandatoryResponseParameters = new HashSet<>();
final RecurringDepositAccountData accountTemplate = populateTemplateAndAssociations(accountId, account, staffInSelectedOfficeOnly,
chargeStatus, uriInfo, mandatoryResponseParameters);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(),
mandatoryResponseParameters);
return this.toApiJsonSerializer.serialize(settings, accountTemplate,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
private RecurringDepositAccountData populateTemplateAndAssociations(final Long accountId,
final RecurringDepositAccountData savingsAccount, final boolean staffInSelectedOfficeOnly, final String chargeStatus,
final UriInfo uriInfo, final Set<String> mandatoryResponseParameters) {
Collection<SavingsAccountTransactionData> transactions = null;
Collection<SavingsAccountChargeData> charges = null;
final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters());
if (!associationParameters.isEmpty()) {
if (associationParameters.contains("all")) {
associationParameters.addAll(Arrays.asList(SavingsApiConstants.transactions, SavingsApiConstants.charges));
}
if (associationParameters.contains(SavingsApiConstants.transactions)) {
mandatoryResponseParameters.add(SavingsApiConstants.transactions);
final Collection<SavingsAccountTransactionData> currentTransactions = this.depositAccountReadPlatformService
.retrieveAllTransactions(DepositAccountType.RECURRING_DEPOSIT, accountId);
if (!CollectionUtils.isEmpty(currentTransactions)) {
transactions = currentTransactions;
}
}
if (associationParameters.contains(SavingsApiConstants.charges)) {
mandatoryResponseParameters.add(SavingsApiConstants.charges);
final Collection<SavingsAccountChargeData> currentCharges = this.savingsAccountChargeReadPlatformService
.retrieveSavingsAccountCharges(accountId, chargeStatus);
if (!CollectionUtils.isEmpty(currentCharges)) {
charges = currentCharges;
}
}
}
RecurringDepositAccountData templateData = null;
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
if (settings.isTemplate()) {
templateData = (RecurringDepositAccountData) this.depositAccountReadPlatformService.retrieveTemplate(
DepositAccountType.RECURRING_DEPOSIT, savingsAccount.getClientId(), savingsAccount.getGroupId(),
savingsAccount.getDepositProductId(), staffInSelectedOfficeOnly);
}
return RecurringDepositAccountData.withTemplateOptions(savingsAccount, templateData, transactions, charges);
}
@PUT
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Modify a recurring deposit application", description = "Recurring deposit application can only be modified when in 'Submitted and pending approval' state. Once the application is approved, the details cannot be changed using this method. Specific api endpoints will be created to allow change of interest detail such as rate, compounding period, posting period etc")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PutRecurringDepositAccountsAccountIdRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PutRecurringDepositAccountsAccountIdResponse.class))) })
public String update(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateRecurringDepositAccount(accountId)
.withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Approve recurring deposit application | Undo approval recurring deposit application | Reject recurring deposit application | Withdraw recurring deposit application | Activate a recurring deposit account | Update the recommended deposit amount for a recurring deposit account | Close a recurring deposit account | Premature Close a recurring deposit account | Calculate Premature amount on Recurring deposit account | Calculate Interest on recurring Deposit Account | Post Interest on recurring Deposit Account", description = "Approve recurring deposit application:\n\n"
+ "Approves recurring deposit application so long as its in 'Submitted and pending approval' state.\n\n"
+ "Undo approval recurring deposit application:\n\n"
+ "Will move 'approved' recurring deposit application back to 'Submitted and pending approval' state.\n\n"
+ "Reject recurring deposit application\n\n"
+ "Rejects recurring deposit application so long as its in 'Submitted and pending approval' state.\n\n"
+ "Withdraw recurring deposit application:\n\n"
+ "Used when an applicant withdraws from the recurring deposit application. It must be in 'Submitted and pending approval' state.\n\n"
+ "Activate a recurring deposit account:\n\n"
+ "Results in an approved recurring deposit application being converted into an 'active' recurring deposit account.\n\n"
+ "Update the recommended deposit amount for a recurring deposit account:\n\n"
+ "Updates the recommended deposit amount for a RD account as on the effective date.\n\n"
+ "Close a recurring deposit account\n\n"
+ "Results in a Matured recurring deposit account being converted into a 'closed' recurring deposit account.\n" + "\n"
+ "On account close allowed actions are." + "Premature Close a recurring deposit account:\n\n"
+ "Results in an Active recurring deposit account being converted into a 'Premature Closed' recurring deposit account with options to withdraw prematured amount. (premature amount is calculated using interest rate chart applicable along with penal interest if any.)\n"
+ "\n" + "On account premature closure allowed actions are.\n\n"
+ "Calculate Premature amount on Recurring deposit account:\n\n"
+ "Calculate premature amount on recurring deposit till premature close date. Premature amount is calculated based on interest chart and penal interest applicable if any.\n\n"
+ "Calculate Interest on recurring Deposit Account:\n\n"
+ "Calculates interest earned on a recurring deposit account based on todays date. It does not attempt to post or credit the interest on the account. That is responsibility of the Post Interest API that will likely be called by overnight process.\n\n"
+ "Post Interest on recurring Deposit Account:\n\n"
+ "Calculates and Posts interest earned on a recurring deposit account based on todays date and whether an interest posting or crediting event is due.\n\n"
+ "Showing request/response for 'Post Interest on recurring Deposit Account'")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsAccountIdRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.PostRecurringDepositAccountsAccountIdResponse.class))) })
public String handleCommands(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
String jsonApiRequest = apiRequestBodyAsJson;
if (StringUtils.isBlank(jsonApiRequest)) {
jsonApiRequest = "{}";
}
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest);
CommandProcessingResult result = null;
if (is(commandParam, "reject")) {
final CommandWrapper commandRequest = builder.rejectRecurringDepositAccountApplication(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "withdrawnByApplicant")) {
final CommandWrapper commandRequest = builder.withdrawRecurringDepositAccountApplication(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "approve")) {
final CommandWrapper commandRequest = builder.approveRecurringDepositAccountApplication(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "undoapproval")) {
final CommandWrapper commandRequest = builder.undoRecurringDepositAccountApplication(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "activate")) {
final CommandWrapper commandRequest = builder.recurringDepositAccountActivation(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "calculateInterest")) {
final CommandWrapper commandRequest = builder.withNoJsonBody().recurringDepositAccountInterestCalculation(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, DepositsApiConstants.UPDATE_DEPOSIT_AMOUNT)) {
final CommandWrapper commandRequest = builder.updateDepositAmountForRecurringDepositAccount(accountId)
.withJson(apiRequestBodyAsJson).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "postInterest")) {
final CommandWrapper commandRequest = builder.recurringDepositAccountInterestPosting(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "close")) {
final CommandWrapper commandRequest = builder.closeRecurringDepositAccount(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "prematureClose")) {
final CommandWrapper commandRequest = builder.prematureCloseRecurringDepositAccount(accountId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "calculatePrematureAmount")) {
final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper);
final DepositAccountData account = this.accountPreMatureCalculationPlatformService.calculatePreMatureAmount(accountId, query,
DepositAccountType.RECURRING_DEPOSIT);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
if (result == null) {
throw new UnrecognizedQueryParamException("command", commandParam,
new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest",
"postInterest", "close", "prematureClose", "calculatePrematureAmount" });
}
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("calculate-rd-interest")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String calculateRecurringDepositInterest(@Context final UriInfo uriInfo, final String apiRequestBodyAsJson) {
JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson);
HashMap<String, Object> result = recurringDepositAccountInterestCalculationService
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper));
return toApiJsonSerializer.serializeResult(result);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
@DELETE
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a recurring deposit application", description = "At present we support hard delete of recurring deposit application so long as its in 'Submitted and pending approval' state. One the application is moves past this state, it is not possible to do a 'hard' delete of the application or the account. An API endpoint will be added to close/de-activate the recurring deposit account.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositAccountsApiResourceSwagger.DeleteRecurringDepositAccountsResponse.class))) })
public String delete(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteRecurringDepositAccount(accountId).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@GET
@Path("{accountId}/template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String accountClosureTemplate(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@QueryParam("command") @Parameter(description = "command") final String commandParam, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
DepositAccountData account = null;
if (is(commandParam, "close")) {
account = this.depositAccountReadPlatformService.retrieveOneWithClosureTemplate(DepositAccountType.RECURRING_DEPOSIT,
accountId);
}
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
@GET
@Path("downloadtemplate")
@Produces("application/vnd.ms-excel")
public Response getRecurringDepositTemplate(@QueryParam("officeId") final Long officeId, @QueryParam("staffId") final Long staffId,
@QueryParam("dateFormat") final String dateFormat) {
return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS.toString(), officeId, staffId,
dateFormat);
}
@POST
@Path("uploadtemplate")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@RequestBody(description = "Upload recurring deposit template", content = {
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) })
public String postRecurringDepositTemplate(@FormDataParam("file") InputStream uploadedInputStream,
@FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale,
@FormDataParam("dateFormat") final String dateFormat) {
final Long importDocumentId = this.bulkImportWorkbookService.importWorkbook(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS.toString(),
uploadedInputStream, fileDetail, locale, dateFormat);
return this.toApiJsonSerializer.serialize(importDocumentId);
}
@GET
@Path("transactions/downloadtemplate")
@Produces("application/vnd.ms-excel")
public Response getRecurringDepositTransactionTemplate(@QueryParam("officeId") final Long officeId,
@QueryParam("dateFormat") final String dateFormat) {
return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS_TRANSACTIONS.toString(), officeId,
null, dateFormat);
}
@POST
@Path("transactions/uploadtemplate")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@RequestBody(description = "Upload recurring deposit transactions template", content = {
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) })
public String postRecurringDepositTransactionsTemplate(@FormDataParam("file") InputStream uploadedInputStream,
@FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale,
@FormDataParam("dateFormat") final String dateFormat) {
final Long importDocumentId = this.bulkImportWorkbookService.importWorkbook(
GlobalEntityType.RECURRING_DEPOSIT_ACCOUNTS_TRANSACTIONS.toString(), uploadedInputStream, fileDetail, locale, dateFormat);
return this.toApiJsonSerializer.serialize(importDocumentId);
}
}
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "ADJUSTTRANSACTION")
public class CurrentAccountTransactionAdjustmentCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired
public CurrentAccountTransactionAdjustmentCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
public CurrentAccountTransactionAdjustmentCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@@ -42,6 +42,6 @@ public class CurrentAccountTransactionAdjustmentCommandHandler implements NewCom
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final Long transactionId = Long.valueOf(command.getTransactionId());
return this.writePlatformService.adjustSavingsTransaction(command.getSavingsId(), transactionId, command);
return this.writePlatformService.adjustTransaction(command.getSavingsId(), transactionId, command);
}
}
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "DEPOSIT")
public class DepositCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired
public DepositCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
public DepositCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "HOLDAMOUNT")
public class HoldAmountCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired
public HoldAmountCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
public HoldAmountCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "RELEASEAMOUNT")
public class ReleaseAmountCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired
public ReleaseAmountCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
public ReleaseAmountCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "REVERSETRANSACTION")
public class ReverseTransactionCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired
public ReverseTransactionCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
public ReverseTransactionCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "UNDOTRANSACTION")
public class UndoTransactionCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired
public UndoTransactionCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
public UndoTransactionCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "WITHDRAWAL")
public class WithdrawCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired
public WithdrawCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
public WithdrawCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@@ -0,0 +1,468 @@
/**
* 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.savings.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.PortfolioProductType;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class CurrentAccountTransactionWritePlatformServiceImpl implements CurrentAccountTransactionWritePlatformService {
// Savings entity type used for GL journal entries (EntityType.SAVINGS = 2)
private static final Integer ENTITY_TYPE_SAVINGS = 2;
private final PlatformSecurityContext context;
private final CurrentAccountRepository currentAccountRepository;
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
private final CurrentProductRepository currentProductRepository;
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
private final JournalEntryRepository journalEntryRepository;
@Transactional
@Override
public CommandProcessingResult deposit(final Long accountId, final JsonCommand command) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final Office office = account.getOffice();
LocalDate txnDate = command.localDateValueOfParameterNamed("transactionDate");
if (txnDate == null) txnDate = LocalDate.now();
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(amount, accountId, "deposit");
final String refNo = command.stringValueOfParameterNamed("accountNumber");
final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, office, amount, txnDate, refNo);
final BigDecimal oldBalance = safeBalance(account.getAccountBalanceDerived());
final BigDecimal newBalance = oldBalance.add(amount);
txn.setRunningBalanceDerived(newBalance);
currentAccountTransactionRepository.saveAndFlush(txn);
account.setAccountBalanceDerived(newBalance);
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
currentAccountRepository.save(account);
postTransactionGl(account, txn, JournalEntryType.DEBIT, JournalEntryType.CREDIT, "Current Account Deposit");
return new CommandProcessingResultBuilder()
.withEntityId(txn.getId())
.withSavingsId(accountId)
.withClientId(account.getClient() != null ? account.getClient().getId() : null)
.withOfficeId(office != null ? office.getId() : null)
.build();
}
@Transactional
@Override
public CommandProcessingResult withdrawal(final Long accountId, final JsonCommand command) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final Office office = account.getOffice();
LocalDate txnDate = command.localDateValueOfParameterNamed("transactionDate");
if (txnDate == null) txnDate = LocalDate.now();
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(amount, accountId, "withdrawal");
final BigDecimal currentBalance = safeBalance(account.getAccountBalanceDerived());
final BigDecimal onHold = safeBalance(account.getOnHoldFundsDerived());
final BigDecimal available = currentBalance.subtract(onHold);
if (!account.isAllowOverdraft() && amount.compareTo(available) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance",
"Insufficient available balance. Available: " + available + ", Requested: " + amount, accountId);
}
final String refNo = command.stringValueOfParameterNamed("accountNumber");
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, office, amount, txnDate, refNo);
final BigDecimal newBalance = currentBalance.subtract(amount);
txn.setRunningBalanceDerived(newBalance);
currentAccountTransactionRepository.saveAndFlush(txn);
account.setAccountBalanceDerived(newBalance);
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
currentAccountRepository.save(account);
// Withdrawal GL: DR SAVINGS_CONTROL (liability down), CR SAVINGS_REFERENCE (cash out)
postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal");
return new CommandProcessingResultBuilder()
.withEntityId(txn.getId())
.withSavingsId(accountId)
.withClientId(account.getClient() != null ? account.getClient().getId() : null)
.withOfficeId(office != null ? office.getId() : null)
.build();
}
@Transactional
@Override
public CommandProcessingResult holdAmount(final Long accountId, final JsonCommand command) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final Office office = account.getOffice();
LocalDate txnDate = command.localDateValueOfParameterNamed("transactionDate");
if (txnDate == null) txnDate = LocalDate.now();
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(amount, accountId, "hold amount");
final BigDecimal currentBalance = safeBalance(account.getAccountBalanceDerived());
final BigDecimal onHold = safeBalance(account.getOnHoldFundsDerived());
final BigDecimal available = currentBalance.subtract(onHold);
if (amount.compareTo(available) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance.for.hold",
"Insufficient available balance to place hold. Available: " + available + ", Requested: " + amount, accountId);
}
final String reason = command.stringValueOfParameterNamed("reasonForBlock");
final CurrentAccountTransaction txn = CurrentAccountTransaction.holdAmount(account, office, amount, txnDate, reason);
txn.setRunningBalanceDerived(currentBalance);
currentAccountTransactionRepository.saveAndFlush(txn);
account.setOnHoldFundsDerived(onHold.add(amount));
currentAccountRepository.save(account);
return new CommandProcessingResultBuilder()
.withEntityId(txn.getId())
.withSavingsId(accountId)
.withClientId(account.getClient() != null ? account.getClient().getId() : null)
.withOfficeId(office != null ? office.getId() : null)
.build();
}
@Transactional
@Override
public CommandProcessingResult releaseAmount(final Long accountId, final Long transactionId) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final Office office = account.getOffice();
final CurrentAccountTransaction holdTxn = currentAccountTransactionRepository.findById(transactionId)
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.current.account.transaction.not.found",
"Hold transaction not found: " + transactionId, transactionId));
if (!holdTxn.getCurrentAccount().getId().equals(accountId)) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found",
"Transaction does not belong to this account.", transactionId);
}
if (holdTxn.isReversed()) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed",
"Hold transaction is already released/reversed.", transactionId);
}
final BigDecimal holdAmount = holdTxn.getAmount();
final BigDecimal currentBalance = safeBalance(account.getAccountBalanceDerived());
// Mark hold transaction as reversed
holdTxn.setReversed(true);
currentAccountTransactionRepository.save(holdTxn);
// Create release transaction
final CurrentAccountTransaction releaseTxn = CurrentAccountTransaction.releaseAmount(
account, office, holdAmount, LocalDate.now(), transactionId);
releaseTxn.setRunningBalanceDerived(currentBalance);
currentAccountTransactionRepository.saveAndFlush(releaseTxn);
// Reduce onHold
final BigDecimal onHold = safeBalance(account.getOnHoldFundsDerived());
account.setOnHoldFundsDerived(onHold.subtract(holdAmount).max(BigDecimal.ZERO));
currentAccountRepository.save(account);
return new CommandProcessingResultBuilder()
.withEntityId(releaseTxn.getId())
.withSavingsId(accountId)
.build();
}
@Transactional
@Override
public CommandProcessingResult undoTransaction(final Long accountId, final Long transactionId, final boolean isReversal) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final CurrentAccountTransaction txn = currentAccountTransactionRepository.findById(transactionId)
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.current.account.transaction.not.found",
"Transaction not found: " + transactionId, transactionId));
if (!txn.getCurrentAccount().getId().equals(accountId)) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found",
"Transaction does not belong to this account.", transactionId);
}
if (txn.isReversed()) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed",
"Transaction is already reversed.", transactionId);
}
reverseTransactionBalances(account, txn);
txn.setReversed(true);
currentAccountTransactionRepository.save(txn);
currentAccountRepository.save(account);
return new CommandProcessingResultBuilder()
.withEntityId(transactionId)
.withSavingsId(accountId)
.build();
}
@Transactional
@Override
public CommandProcessingResult reverseTransaction(final Long accountId, final Long transactionId,
final boolean isReversal, final JsonCommand command) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final CurrentAccountTransaction original = currentAccountTransactionRepository.findById(transactionId)
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.current.account.transaction.not.found",
"Transaction not found: " + transactionId, transactionId));
if (!original.getCurrentAccount().getId().equals(accountId)) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found",
"Transaction does not belong to this account.", transactionId);
}
if (original.isReversed()) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed",
"Transaction is already reversed.", transactionId);
}
reverseTransactionBalances(account, original);
original.setReversed(true);
currentAccountTransactionRepository.save(original);
// Create a reversal entry
final CurrentAccountTransaction reversalTxn = CurrentAccountTransaction.reversal(
account, account.getOffice(), original.getAmount(), LocalDate.now(), transactionId);
reversalTxn.setRunningBalanceDerived(safeBalance(account.getAccountBalanceDerived()));
currentAccountTransactionRepository.saveAndFlush(reversalTxn);
currentAccountRepository.save(account);
return new CommandProcessingResultBuilder()
.withEntityId(reversalTxn.getId())
.withSavingsId(accountId)
.build();
}
@Transactional
@Override
public CommandProcessingResult adjustTransaction(final Long accountId, final Long transactionId,
final JsonCommand command) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final Office office = account.getOffice();
final CurrentAccountTransaction original = currentAccountTransactionRepository.findById(transactionId)
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.current.account.transaction.not.found",
"Transaction not found: " + transactionId, transactionId));
if (!original.getCurrentAccount().getId().equals(accountId)) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found",
"Transaction does not belong to this account.", transactionId);
}
if (original.isReversed()) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed",
"Transaction is already reversed.", transactionId);
}
final BigDecimal newAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(newAmount, accountId, "adjust");
LocalDate newDate = command.localDateValueOfParameterNamed("transactionDate");
if (newDate == null) newDate = original.getTransactionDate();
// Undo original
reverseTransactionBalances(account, original);
original.setReversed(true);
currentAccountTransactionRepository.save(original);
// Create adjusted transaction of the same type
final CurrentAccountTransaction adjusted;
if (original.getTransactionTypeEnum() == 1) {
adjusted = CurrentAccountTransaction.deposit(account, office, newAmount, newDate,
command.stringValueOfParameterNamed("accountNumber"));
} else {
adjusted = CurrentAccountTransaction.withdrawal(account, office, newAmount, newDate,
command.stringValueOfParameterNamed("accountNumber"));
}
adjusted.setOriginalTransactionId(transactionId);
adjusted.setRunningBalanceDerived(safeBalance(account.getAccountBalanceDerived()));
currentAccountTransactionRepository.saveAndFlush(adjusted);
// Apply new transaction to balance
applyTransactionToBalance(account, adjusted);
currentAccountRepository.save(account);
return new CommandProcessingResultBuilder()
.withEntityId(adjusted.getId())
.withSavingsId(accountId)
.build();
}
// ── Helpers ─────────────────────────────────────────────────────────────────
private CurrentAccount findActiveAccount(final Long accountId) {
final CurrentAccount account = currentAccountRepository.findById(accountId)
.orElseThrow(() -> new CurrentAccountNotFoundException(accountId));
if (!Integer.valueOf(300).equals(account.getStatusEnum())) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.not.active",
"Current account is not in Active state.", accountId);
}
return account;
}
private void validatePositiveAmount(final BigDecimal amount, final Long accountId, final String operation) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new GeneralPlatformDomainRuleException(
"error.msg.current.account." + operation + ".invalid.amount",
"Transaction amount must be greater than zero for " + operation + ".", accountId);
}
}
private BigDecimal safeBalance(final BigDecimal value) {
return value != null ? value : BigDecimal.ZERO;
}
private void reverseTransactionBalances(final CurrentAccount account, final CurrentAccountTransaction txn) {
final BigDecimal amount = txn.getAmount();
final Integer type = txn.getTransactionTypeEnum();
if (Integer.valueOf(1).equals(type)) {
// Was a deposit: reverse it
account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).subtract(amount));
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).subtract(amount).max(BigDecimal.ZERO));
} else if (Integer.valueOf(2).equals(type)) {
// Was a withdrawal: reverse it
account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).add(amount));
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).subtract(amount).max(BigDecimal.ZERO));
}
}
private void applyTransactionToBalance(final CurrentAccount account, final CurrentAccountTransaction txn) {
final BigDecimal amount = txn.getAmount();
final Integer type = txn.getTransactionTypeEnum();
if (Integer.valueOf(1).equals(type)) {
account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).add(amount));
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
} else if (Integer.valueOf(2).equals(type)) {
account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).subtract(amount));
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
}
}
// GL: cashEntryType=DEBIT/CREDIT for SAVINGS_REFERENCE; controlEntryType for SAVINGS_CONTROL
private void postTransactionGl(final CurrentAccount account, final CurrentAccountTransaction txn,
final JournalEntryType cashEntryType, final JournalEntryType controlEntryType, final String description) {
try {
final CurrentProduct product = currentProductRepository.findById(account.getProductId()).orElse(null);
if (product == null) return;
final Integer accountingType = product.getAccountingType();
if (accountingType == null || accountingType == 1) return; // NONE = 1
final GLAccount cashAccount = findGlAccount(account.getProductId(),
CashAccountsForSavings.SAVINGS_REFERENCE.getValue());
final GLAccount controlAccount = findGlAccount(account.getProductId(),
CashAccountsForSavings.SAVINGS_CONTROL.getValue());
if (cashAccount == null || controlAccount == null) {
log.warn("[GL] Missing GL account mapping for current account product id={}", account.getProductId());
return;
}
final String ref = "CAT" + txn.getId();
postGlEntry(account, cashAccount, cashEntryType, txn.getAmount(), txn.getTransactionDate(), ref, description);
postGlEntry(account, controlAccount, controlEntryType, txn.getAmount(), txn.getTransactionDate(), ref, description);
} catch (Exception e) {
log.error("[GL] Failed GL posting for CA transaction id={}: {}", txn.getId(), e.getMessage(), e);
}
}
private GLAccount findGlAccount(final Long productId, final int financialAccountType) {
final var mapping = productToGLAccountMappingRepository
.findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(
productId, PortfolioProductType.SAVING.getValue(), financialAccountType, null);
return mapping != null ? mapping.getGlAccount() : null;
}
private void postGlEntry(final CurrentAccount account, final GLAccount glAccount,
final JournalEntryType entryType, final BigDecimal amount,
final LocalDate date, final String transactionRef, final String description) {
final JournalEntry entry = JournalEntry.createNew(
account.getOffice(), null, glAccount,
account.getCurrencyCode(), transactionRef,
false, date, entryType, amount,
description,
ENTITY_TYPE_SAVINGS, account.getId(),
null, null, null, null, null);
journalEntryRepository.save(entry);
}
}
@@ -1,56 +1,59 @@
/**
* 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.savings.service;
import java.util.Collection;
import java.util.Map;
import org.apache.fineract.infrastructure.core.data.PaginationParameters;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.data.DepositAccountData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
public interface DepositAccountReadPlatformService {
Collection<DepositAccountData> retrieveAll(DepositAccountType depositAccountType, PaginationParameters paginationParameters);
Page<DepositAccountData> retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters);
Collection<DepositAccountData> retrieveAllForLookup(DepositAccountType depositAccountType);
DepositAccountData retrieveOne(DepositAccountType depositAccountType, Long accountId);
DepositAccountData retrieveOneWithClosureTemplate(DepositAccountType depositAccountType, Long accountId);
DepositAccountData retrieveOneWithChartSlabs(DepositAccountType depositAccountType, Long productId);
Collection<SavingsAccountTransactionData> retrieveAllTransactions(DepositAccountType depositAccountType, Long accountId);
DepositAccountData retrieveTemplate(DepositAccountType depositAccountType, Long clientId, Long groupId, Long productId,
boolean staffInSelectedOfficeOnly);
Collection<DepositAccountData> retrieveForMaturityUpdate();
SavingsAccountTransactionData retrieveRecurringAccountDepositTransactionTemplate(Long accountId);
Collection<AccountTransferDTO> retrieveDataForInterestTransfer();
Collection<Map<String, Object>> retriveDataForRDScheduleCreation();
}
/**
* 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.savings.service;
import java.util.Collection;
import java.util.Map;
import org.apache.fineract.infrastructure.core.data.PaginationParameters;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.data.DepositAccountData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
public interface DepositAccountReadPlatformService {
Collection<DepositAccountData> retrieveAll(DepositAccountType depositAccountType, PaginationParameters paginationParameters);
Page<DepositAccountData> retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters);
Page<DepositAccountData> retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters,
String accountNo, String clientName);
Collection<DepositAccountData> retrieveAllForLookup(DepositAccountType depositAccountType);
DepositAccountData retrieveOne(DepositAccountType depositAccountType, Long accountId);
DepositAccountData retrieveOneWithClosureTemplate(DepositAccountType depositAccountType, Long accountId);
DepositAccountData retrieveOneWithChartSlabs(DepositAccountType depositAccountType, Long productId);
Collection<SavingsAccountTransactionData> retrieveAllTransactions(DepositAccountType depositAccountType, Long accountId);
DepositAccountData retrieveTemplate(DepositAccountType depositAccountType, Long clientId, Long groupId, Long productId,
boolean staffInSelectedOfficeOnly);
Collection<DepositAccountData> retrieveForMaturityUpdate();
SavingsAccountTransactionData retrieveRecurringAccountDepositTransactionTemplate(Long accountId);
Collection<AccountTransferDTO> retrieveDataForInterestTransfer();
Collection<Map<String, Object>> retriveDataForRDScheduleCreation();
}