diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/DepositAccountsApiResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/DepositAccountsApiResource.java index d914ffca3..bbc3da5c6 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/DepositAccountsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/DepositAccountsApiResource.java @@ -20,7 +20,10 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import org.apache.commons.lang.StringUtils; import org.mifosng.platform.api.commands.DepositAccountCommand; +import org.mifosng.platform.api.commands.DepositStateTransitionCommand; +import org.mifosng.platform.api.commands.UndoStateTransitionCommand; import org.mifosng.platform.api.data.DepositAccountData; import org.mifosng.platform.api.data.DepositProductLookup; import org.mifosng.platform.api.data.EntityIdentifier; @@ -28,6 +31,7 @@ import org.mifosng.platform.api.data.EnumOptionData; import org.mifosng.platform.api.infrastructure.ApiDataConversionService; import org.mifosng.platform.api.infrastructure.ApiJsonSerializerService; import org.mifosng.platform.api.infrastructure.ApiParameterHelper; +import org.mifosng.platform.exceptions.UnrecognizedQueryParamException; import org.mifosng.platform.loan.domain.PeriodFrequencyType; import org.mifosng.platform.saving.service.DepositAccountReadPlatformService; import org.mifosng.platform.saving.service.DepositAccountWritePlatformService; @@ -191,4 +195,52 @@ public class DepositAccountsApiResource { return new DepositAccountData(account, interestCompoundedEveryPeriodTypeOptions, productOptions); } + + @POST + @Path("{accountId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public Response SubmitDepositApplication(@PathParam("accountId") final Long accountId, + @QueryParam("command") final String commandParam, final String jsonRequestBody) { + + DepositStateTransitionCommand command=apiDataConversionService.convertJsonToDepositStateTransitionCommand(accountId, jsonRequestBody); + + Response response=null; + + if (is(commandParam, "approve")) { + + EntityIdentifier identifier = this.depositAccountWritePlatformService.approveDepositApplication(command); + response = Response.ok().entity(identifier).build(); + } + + else if (is(commandParam, "reject")) { + + EntityIdentifier identifier = this.depositAccountWritePlatformService.rejectDepositApplication(command); + response = Response.ok().entity(identifier).build(); + + } else if (is(commandParam, "withdrewbyclient")) { + + EntityIdentifier identifier = this.depositAccountWritePlatformService.withdrawDepositApplication(command); + response = Response.ok().entity(identifier).build(); + + } + + UndoStateTransitionCommand undoCommand = new UndoStateTransitionCommand(accountId); + + if (is(commandParam, "undoapproval")) { + EntityIdentifier identifier = this.depositAccountWritePlatformService.undoDepositApproval(undoCommand); + response = Response.ok().entity(identifier).build(); + } + + if (response == null) { + throw new UnrecognizedQueryParamException("command", commandParam); + } + + return response; + } + + private boolean is(final String commandParam, final String commandValue) { + return StringUtils.isNotBlank(commandParam) + && commandParam.trim().equalsIgnoreCase(commandValue); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java index 07845664a..10aace440 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java @@ -187,9 +187,7 @@ public class LoansApiResource { @QueryParam("command") final String commandParam, final String jsonRequestBody) { - LoanStateTransitionCommand command = this.apiDataConversionService - .convertJsonToLoanStateTransitionCommand(loanId, - jsonRequestBody); + LoanStateTransitionCommand command = this.apiDataConversionService.convertJsonToLoanStateTransitionCommand(loanId, jsonRequestBody); Response response = null; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/DepositStateTransitionCommand.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/DepositStateTransitionCommand.java new file mode 100644 index 000000000..4aab88736 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/DepositStateTransitionCommand.java @@ -0,0 +1,23 @@ +package org.mifosng.platform.api.commands; + +import org.joda.time.LocalDate; + +public class DepositStateTransitionCommand { + + private final Long accountId; + private final LocalDate eventDate; + + public DepositStateTransitionCommand(final Long accountId, final LocalDate enentDate) { + this.accountId=accountId; + this.eventDate=enentDate; + } + + public Long getAccountId() { + return accountId; + } + + public LocalDate getEventDate() { + return eventDate; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/UndoStateTransitionCommand.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/UndoStateTransitionCommand.java index 01374b494..c1c7e6a85 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/UndoStateTransitionCommand.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/UndoStateTransitionCommand.java @@ -12,6 +12,11 @@ public class UndoStateTransitionCommand { this.loanId = loanId; this.note = note; } + + public UndoStateTransitionCommand(final Long loanId) { + this.loanId = loanId; + this.note=null; + } public Long getLoanId() { return this.loanId; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionService.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionService.java index 09ed8d31c..9fe66c670 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionService.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionService.java @@ -6,6 +6,7 @@ import org.mifosng.platform.api.commands.ChargeCommand; import org.mifosng.platform.api.commands.ClientCommand; import org.mifosng.platform.api.commands.DepositAccountCommand; import org.mifosng.platform.api.commands.DepositProductCommand; +import org.mifosng.platform.api.commands.DepositStateTransitionCommand; import org.mifosng.platform.api.commands.FundCommand; import org.mifosng.platform.api.commands.GroupCommand; import org.mifosng.platform.api.commands.LoanProductCommand; @@ -57,4 +58,6 @@ public interface ApiDataConversionService { NoteCommand convertJsonToNoteCommand(Long resourceIdentifier, Long clientId, String jsonRequestBody); DepositAccountCommand convertJsonToDepositAccountCommand(Long resourceIdentifier, String jsonRequestBody); + + DepositStateTransitionCommand convertJsonToDepositStateTransitionCommand(Long resourceIdentifier, String jsonRequestBody); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java index 7a439527b..01eefcc46 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java @@ -23,6 +23,7 @@ import org.mifosng.platform.api.commands.ChargeCommand; import org.mifosng.platform.api.commands.ClientCommand; import org.mifosng.platform.api.commands.DepositAccountCommand; import org.mifosng.platform.api.commands.DepositProductCommand; +import org.mifosng.platform.api.commands.DepositStateTransitionCommand; import org.mifosng.platform.api.commands.FundCommand; import org.mifosng.platform.api.commands.GroupCommand; import org.mifosng.platform.api.commands.LoanProductCommand; @@ -1004,4 +1005,26 @@ public class ApiDataConversionServiceImpl implements ApiDataConversionService { interestCompoundedEvery, interestCompoundedEveryPeriodType, canRenew, canPreClose, preClosureInterestRate); } + + @Override + public DepositStateTransitionCommand convertJsonToDepositStateTransitionCommand( + Long resourceIdentifier, String json) { + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + Type typeOfMap = new TypeToken>(){}.getType(); + Map requestMap = gsonConverter.fromJson(json, typeOfMap); + + Set supportedParams = new HashSet( Arrays.asList("eventDate", "dateFormat")); + + checkForUnsupportedParameters(requestMap, supportedParams); + + Set modifiedParameters = new HashSet(); + + LocalDate eventDate = extractLocalDateParameter("eventDate", requestMap, modifiedParameters); + + return new DepositStateTransitionCommand(resourceIdentifier, eventDate); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/InvalidDepositStateTransitionException.java b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/InvalidDepositStateTransitionException.java new file mode 100644 index 000000000..bb07baf0b --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/exceptions/InvalidDepositStateTransitionException.java @@ -0,0 +1,11 @@ +package org.mifosng.platform.exceptions; + +public class InvalidDepositStateTransitionException extends + AbstractPlatformDomainRuleException { + + public InvalidDepositStateTransitionException(final String action, String postFix, String defaultUserMessage, Object... defaultUserMessageArgs) { + super("error.msg.deposit." + action + "." + postFix, defaultUserMessage, defaultUserMessageArgs); + // TODO Auto-generated constructor stub + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java index db64a4005..6dde7dfa8 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java @@ -63,11 +63,9 @@ public class Loan extends AbstractAuditableCustom { @Embedded private final LoanProductRelatedDetail loanRepaymentScheduleDetail; - @SuppressWarnings("unused") @Column(name = "term_frequency", nullable = false) private Integer termFrequency; - @SuppressWarnings("unused") @Enumerated(EnumType.ORDINAL) @Column(name = "term_period_frequency_enum", nullable = false) private PeriodFrequencyType termPeriodFrequencyType; @@ -83,7 +81,6 @@ public class Loan extends AbstractAuditableCustom { @Column(name = "rejectedon_date") private Date rejectedOnDate; - @SuppressWarnings("unused") @Temporal(TemporalType.TIMESTAMP) @Column(name = "withdrawnon_date") private Date withdrawnOnDate; @@ -523,7 +520,7 @@ public class Loan extends AbstractAuditableCustom { } } - private void handleLoanOverpayment(@SuppressWarnings("unused") final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + private void handleLoanOverpayment(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) { LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, LoanStatus.fromInt(this.loanStatus)); this.loanStatus = statusEnum.getValue(); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositAccount.java b/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositAccount.java index 0f4f6eaf0..dc3c9f8b2 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositAccount.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositAccount.java @@ -20,20 +20,20 @@ import org.mifosng.platform.api.commands.DepositAccountCommand; import org.mifosng.platform.client.domain.Client; import org.mifosng.platform.currency.domain.MonetaryCurrency; import org.mifosng.platform.currency.domain.Money; +import org.mifosng.platform.exceptions.InvalidDepositStateTransitionException; +import org.mifosng.platform.infrastructure.AbstractAuditableCustom; import org.mifosng.platform.loan.domain.PeriodFrequencyType; import org.mifosng.platform.savingproduct.domain.DepositProduct; -import org.springframework.data.jpa.domain.AbstractPersistable; +import org.mifosng.platform.user.domain.AppUser; @Entity @Table(name = "m_deposit_account", uniqueConstraints = @UniqueConstraint(name="deposit_acc_external_id", columnNames = { "external_id" })) -public class DepositAccount extends AbstractPersistable { +public class DepositAccount extends AbstractAuditableCustom { - @SuppressWarnings("unused") @ManyToOne @JoinColumn(name = "client_id", nullable = false) private Client client; - @SuppressWarnings("unused") @ManyToOne @JoinColumn(name = "product_id") private final DepositProduct product; @@ -41,81 +41,81 @@ public class DepositAccount extends AbstractPersistable { @Column(name = "external_id") private String externalId; - @SuppressWarnings("unused") @Embedded private MonetaryCurrency currency; @Column(name = "deposit_amount", scale = 6, precision = 19, nullable = false) private BigDecimal depositAmount; - @SuppressWarnings("unused") @Column(name = "maturity_nominal_interest_rate", scale = 6, precision = 19, nullable = false) private BigDecimal interestRate; @Column(name = "tenure_months", nullable=false) private Integer tenureInMonths; - @SuppressWarnings("unused") @Column(name = "interest_compounded_every", nullable = false) private Integer interestCompoundedEvery; - @SuppressWarnings("unused") @Enumerated(EnumType.ORDINAL) @Column(name = "interest_compounded_every_period_enum", nullable = false) private PeriodFrequencyType interestCompoundedFrequencyType; - @SuppressWarnings("unused") @Temporal(TemporalType.DATE) @Column(name = "projected_commencement_date") private Date projectedCommencementDate; - @SuppressWarnings("unused") @Temporal(TemporalType.DATE) @Column(name = "actual_commencement_date") private Date actualCommencementDate; - @SuppressWarnings("unused") @Temporal(TemporalType.DATE) @Column(name = "projected_maturity_date") private Date projectedMaturityDate; - @SuppressWarnings("unused") @Temporal(TemporalType.DATE) @Column(name = "actual_maturity_date") private Date actualMaturityDate; - @SuppressWarnings("unused") @Column(name = "projected_interest_accrued_on_maturity", scale = 6, precision = 19, nullable = false) private BigDecimal projectedInterestAccruedOnMaturity; - @SuppressWarnings("unused") @Column(name = "actual_interest_accrued", scale = 6, precision = 19, nullable = false) private BigDecimal interestAccrued; - @SuppressWarnings("unused") @Column(name = "projected_total_maturity_amount", scale = 6, precision = 19, nullable = false) private BigDecimal projectedTotalOnMaturity; - @SuppressWarnings("unused") @Column(name = "actual_total_amount", scale = 6, precision = 19, nullable = false) private BigDecimal total; - @SuppressWarnings("unused") @Column(name = "pre_closure_interest_rate", scale = 6, precision = 19, nullable = false) private BigDecimal preClosureInterestRate; - @SuppressWarnings("unused") @Column(name = "is_renewal_allowed", nullable = false) private boolean renewalAllowed = false; - @SuppressWarnings("unused") @Column(name = "is_preclosure_allowed", nullable = false) private boolean preClosureAllowed = false; @Column(name = "is_deleted", nullable=false) private boolean deleted = false; - public static DepositAccount openNew( + @Column(name = "status_enum", nullable = false) + private Integer depositStatus; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "closedon_date") + private Date closedOnDate; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "rejectedon_date") + private Date rejectedOnDate; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "withdrawnon_date") + private Date withdrawnOnDate; + + public DepositAccount openNew( final Client client, final DepositProduct product, final String externalId, final Money deposit, final BigDecimal maturityInterestRate, final Integer tenureInMonths, final Integer interestCompoundedEvery, @@ -123,20 +123,30 @@ public class DepositAccount extends AbstractPersistable { final LocalDate commencementDate, final boolean renewalAllowed, final boolean preClosureAllowed, - final FixedTermDepositInterestCalculator fixedTermDepositInterestCalculator) { + final FixedTermDepositInterestCalculator fixedTermDepositInterestCalculator, + final DepositLifecycleStateMachine depositLifecycleStateMachine) { Money futureValueOnMaturity = fixedTermDepositInterestCalculator.calculateInterestOnMaturityFor(deposit, tenureInMonths, maturityInterestRate, interestCompoundedEvery, interestCompoundedFrequencyPeriodType); + + DepositStatus from = null; + if (depositStatus != null) { + from = DepositStatus.fromInt(depositStatus); + } + + DepositStatus statusEnum = depositLifecycleStateMachine.transition(DepositEvent.DEPOSIT_CREATED, from); + depositStatus = statusEnum.getValue(); + return new DepositAccount(client, product, externalId, deposit, maturityInterestRate, tenureInMonths, - interestCompoundedEvery, interestCompoundedFrequencyPeriodType, commencementDate, renewalAllowed, preClosureAllowed, futureValueOnMaturity); + interestCompoundedEvery, interestCompoundedFrequencyPeriodType, commencementDate, renewalAllowed, preClosureAllowed, futureValueOnMaturity,depositStatus); } - protected DepositAccount() { + public DepositAccount() { this.product = null; } - private DepositAccount( + public DepositAccount( final Client client, final DepositProduct product, final String externalId, final Money deposit, final BigDecimal interestRate, final Integer termInMonths, final Integer interestCompoundedEvery, @@ -144,7 +154,8 @@ public class DepositAccount extends AbstractPersistable { final LocalDate commencementDate, final boolean renewalAllowed, final boolean preClosureAllowed, - final Money futureValueOnMaturity) { + final Money futureValueOnMaturity, + final Integer depositStatus) { this.client = client; this.product = product; this.externalId = externalId; @@ -170,6 +181,7 @@ public class DepositAccount extends AbstractPersistable { // derived fields this.projectedInterestAccruedOnMaturity = futureValueOnMaturity.minus(deposit).getAmount(); this.projectedTotalOnMaturity = futureValueOnMaturity.getAmount(); + this.depositStatus=depositStatus; } public boolean isDeleted() { @@ -189,4 +201,103 @@ public class DepositAccount extends AbstractPersistable { public void update(final DepositAccountCommand command) { } + + public void approve(final LocalDate actualCommencementDate, DepositLifecycleStateMachine depositLifecycleStateMachine) { + + DepositStatus statusEnum = depositLifecycleStateMachine.transition(DepositEvent.DEPOSIT_APPROVED, DepositStatus.fromInt(this.depositStatus)); + this.depositStatus = statusEnum.getValue(); + + this.actualCommencementDate = actualCommencementDate + .toDateTimeAtCurrentTime().toDate(); + + if(this.actualCommencementDate != null) + this.actualMaturityDate = new LocalDate(this.actualCommencementDate).plusMonths(this.tenureInMonths).toDate(); + + LocalDate submittalDate = new LocalDate(this.projectedCommencementDate); + if (actualCommencementDate.isBefore(submittalDate)) { + final String errorMessage = "The date on which a deposit is approved cannot be before its submittal date: " + + submittalDate.toString(); + throw new InvalidDepositStateTransitionException("approval", + "cannot.be.before.submittal.date", errorMessage, + getActualCommencementDate(), submittalDate); + } + if (actualCommencementDate.isAfter(new LocalDate())) { + final String errorMessage = "The date on which a deposit is approved cannot be in the future."; + throw new InvalidDepositStateTransitionException("approval", + "cannot.be.a.future.date", errorMessage, + getActualCommencementDate()); + } + } + + public LocalDate getActualCommencementDate() { + LocalDate date=null; + if(this.actualCommencementDate!=null){ + date = new LocalDate(this.actualCommencementDate); + } + return date; + } + + public LocalDate getProjectedCommencementDate() { + LocalDate date=null; + if(this.projectedCommencementDate!=null){ + date = new LocalDate(this.projectedCommencementDate); + } + return date; + } + + public void reject(LocalDate rejectedOn, DepositLifecycleStateMachine depositLifecycleStateMachine) { + + DepositStatus statusEnum = depositLifecycleStateMachine.transition(DepositEvent.DEPOSIT_REJECTED, DepositStatus.fromInt(this.depositStatus)); + this.depositStatus = statusEnum.getValue(); + + this.rejectedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate(); + this.closedOnDate = rejectedOn.toDateTimeAtCurrentTime().toDate(); + + if (rejectedOn.isBefore(getProjectedCommencementDate())) { + + final String errorMessage = "The date on which a deposit is rejected cannot be before its submittal date: " + getProjectedCommencementDate().toString(); + throw new InvalidDepositStateTransitionException("reject", "cannot.be.before.submittal.date", errorMessage, rejectedOn, getProjectedCommencementDate()); + + } + if (rejectedOn.isAfter(new LocalDate())) { + + final String errorMessage = "The date on which a deposit is rejected cannot be in the future."; + throw new InvalidDepositStateTransitionException("reject", "cannot.be.a.future.date", errorMessage, rejectedOn); + + } + + } + + public void withdraw(LocalDate withdrawnOn, DepositLifecycleStateMachine depositLifecycleStateMachine) { + + DepositStatus statusEnum = depositLifecycleStateMachine.transition(DepositEvent.DEPOSIT_WITHDRAWN, DepositStatus.fromInt(this.depositStatus)); + this.depositStatus = statusEnum.getValue(); + + this.withdrawnOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate(); + this.closedOnDate = withdrawnOn.toDateTimeAtCurrentTime().toDate(); + + if (withdrawnOn.isBefore(getProjectedCommencementDate())) { + + final String errorMessage = "The date on which a deposit is rejected cannot be before its submittal date: " + getProjectedCommencementDate().toString(); + throw new InvalidDepositStateTransitionException("reject", "cannot.be.before.submittal.date", errorMessage, withdrawnOn, getProjectedCommencementDate()); + + } + if (withdrawnOn.isAfter(new LocalDate())) { + + final String errorMessage = "The date on which a deposit is rejected cannot be in the future."; + throw new InvalidDepositStateTransitionException("reject", "cannot.be.a.future.date", errorMessage, withdrawnOn); + + } + + } + + public void undoDepositApproval(DepositLifecycleStateMachine depositLifecycleStateMachine) { + + DepositStatus statusEnum = depositLifecycleStateMachine.transition(DepositEvent.DEPOSIT_APPROVAL_UNDO, DepositStatus.fromInt(this.depositStatus)); + this.depositStatus = statusEnum.getValue(); + + this.actualCommencementDate = null; + this.actualMaturityDate=null; + + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositEvent.java b/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositEvent.java new file mode 100644 index 000000000..2dac718f8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositEvent.java @@ -0,0 +1,7 @@ +package org.mifosng.platform.saving.domain; + +public enum DepositEvent { + + DEPOSIT_CREATED, DEPOSIT_REJECTED, DEPOSIT_APPROVED, DEPOSIT_APPROVAL_UNDO, DEPOSIT_WITHDRAWN, DEPOSIT_MATURED; + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositLifecycleStateMachine.java b/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositLifecycleStateMachine.java new file mode 100644 index 000000000..27e91d98f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositLifecycleStateMachine.java @@ -0,0 +1,8 @@ +package org.mifosng.platform.saving.domain; + + +public interface DepositLifecycleStateMachine { + + DepositStatus transition(DepositEvent loanEvent, DepositStatus from); + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositLifecycleStateMachineImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositLifecycleStateMachineImpl.java new file mode 100644 index 000000000..5b57fb6eb --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositLifecycleStateMachineImpl.java @@ -0,0 +1,81 @@ +package org.mifosng.platform.saving.domain; + +import java.util.List; + +public class DepositLifecycleStateMachineImpl implements + DepositLifecycleStateMachine { + + private final List allowedDepositStatuses; + + public DepositLifecycleStateMachineImpl(List allowedDepositStatuses) { + this.allowedDepositStatuses=allowedDepositStatuses; + } + + @Override + public DepositStatus transition(DepositEvent depositEvent, DepositStatus from) { + + DepositStatus newState = null; + + switch (depositEvent) { + case DEPOSIT_CREATED: + if (from == null) { + newState = stateOf(DepositStatus.SUBMITED_AND_PENDING_APPROVAL, allowedDepositStatuses); + } + break; + case DEPOSIT_REJECTED: + if (from.hasStateOf(DepositStatus.SUBMITED_AND_PENDING_APPROVAL)) { + newState = stateOf(DepositStatus.REJECTED, allowedDepositStatuses); + } + break; + case DEPOSIT_APPROVED: + if (from.hasStateOf(DepositStatus.SUBMITED_AND_PENDING_APPROVAL)) { + newState = stateOf(DepositStatus.APPROVED, allowedDepositStatuses); + } + break; + case DEPOSIT_WITHDRAWN: + if (this.anyOfAllowedWhenComingFrom(from, DepositStatus.SUBMITED_AND_PENDING_APPROVAL, DepositStatus.APPROVED)) { + newState = stateOf(DepositStatus.WITHDRAWN_BY_CLIENT, allowedDepositStatuses); + } + break; + case DEPOSIT_APPROVAL_UNDO: + if (from.hasStateOf(DepositStatus.APPROVED)) { + newState = stateOf(DepositStatus.SUBMITED_AND_PENDING_APPROVAL, allowedDepositStatuses); + } + break; + case DEPOSIT_MATURED: + if (from.hasStateOf(DepositStatus.APPROVED) ){ + newState = stateOf(DepositStatus.MATURED, allowedDepositStatuses); + } + break; + default: + break; + } + + return newState; + } + + private DepositStatus stateOf(DepositStatus state, List allowedLoanStatuses) { + DepositStatus match = null; + for (DepositStatus depositStatus : allowedLoanStatuses) { + if (depositStatus.hasStateOf(state)) { + match = depositStatus; + break; + } + } + return match; + } + + private boolean anyOfAllowedWhenComingFrom(final DepositStatus state, final DepositStatus... allowedStates) { + boolean allowed = false; + + for (DepositStatus allowedState : allowedStates) { + if (state.hasStateOf(allowedState)) { + allowed = true; + break; + } + } + + return allowed; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositStatus.java b/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositStatus.java new file mode 100644 index 000000000..31f24b69e --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/saving/domain/DepositStatus.java @@ -0,0 +1,89 @@ +package org.mifosng.platform.saving.domain; + + + +public enum DepositStatus { + + INVALID(0, "depositStatusType.invalid"), // + SUBMITED_AND_PENDING_APPROVAL(100, "depositStatusType.submitted.and.pending.approval"), // + APPROVED(200, "depositStatusType.approved"), // + ACTIVE(300, "depositStatusType.active"), // + WITHDRAWN_BY_CLIENT(400, "depositStatusType.withdrawn.by.client"), // + REJECTED(500, "depositStatusType.rejected"), // + MATURED(700,"depositStatusType.matured"), + CLOSED(600, "depositStatusType.closed"); + + private final Integer value; + private final String code; + + private DepositStatus(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public boolean hasStateOf(final DepositStatus state) { + return this.value.equals(state.getValue()); + } + + public Integer getValue() { + return value; + } + + public String getCode() { + return code; + } + + public boolean isSubmittedAndPendingApproval() { + return this.value.equals(DepositStatus.SUBMITED_AND_PENDING_APPROVAL.getValue()); + } + + public boolean isApproved() { + return this.value.equals(DepositStatus.APPROVED.getValue()); + } + + public boolean isActive() { + return this.value.equals(DepositStatus.ACTIVE.getValue()); + } + + public boolean isClosed() { + return this.value.equals(DepositStatus.CLOSED.getValue()); + } + + public boolean isWithdrawnByClient() { + return this.value.equals(DepositStatus.WITHDRAWN_BY_CLIENT.getValue()); + } + + public boolean isRejected() { + return this.value.equals(DepositStatus.REJECTED.getValue()); + } + + public static DepositStatus fromInt(final Integer statusValue) { + + DepositStatus enumeration = DepositStatus.INVALID; + switch (statusValue) { + case 100: + enumeration = DepositStatus.SUBMITED_AND_PENDING_APPROVAL; + break; + case 200: + enumeration = DepositStatus.APPROVED; + break; + case 300: + enumeration = DepositStatus.ACTIVE; + break; + case 400: + enumeration = DepositStatus.WITHDRAWN_BY_CLIENT; + break; + case 500: + enumeration = DepositStatus.REJECTED; + break; + case 600: + enumeration = DepositStatus.CLOSED; + break; + case 700: + enumeration = DepositStatus.MATURED; + break; + } + return enumeration; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountAssembler.java b/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountAssembler.java index 399d14b3b..0e29b830a 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountAssembler.java @@ -1,6 +1,8 @@ package org.mifosng.platform.saving.service; import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; import org.mifosng.platform.api.commands.DepositAccountCommand; import org.mifosng.platform.client.domain.Client; @@ -10,6 +12,9 @@ import org.mifosng.platform.exceptions.ClientNotFoundException; import org.mifosng.platform.exceptions.DepositProductNotFoundException; import org.mifosng.platform.loan.domain.PeriodFrequencyType; import org.mifosng.platform.saving.domain.DepositAccount; +import org.mifosng.platform.saving.domain.DepositLifecycleStateMachine; +import org.mifosng.platform.saving.domain.DepositLifecycleStateMachineImpl; +import org.mifosng.platform.saving.domain.DepositStatus; import org.mifosng.platform.saving.domain.FixedTermDepositInterestCalculator; import org.mifosng.platform.savingproduct.domain.DepositProduct; import org.mifosng.platform.savingproduct.domain.DepositProductRepository; @@ -80,13 +85,18 @@ public class DepositAccountAssembler { } // end of details allowed to be overriden from product - DepositAccount account = DepositAccount.openNew(client, product, command.getExternalId(), + DepositAccount account =new DepositAccount().openNew(client, product, command.getExternalId(), deposit, maturityInterestRate, tenureInMonths, compoundingInterestEvery, compoundingInterestFrequency, command.getCommencementDate(), - renewalAllowed, preClosureAllowed, this.fixedTermDepositInterestCalculator); + renewalAllowed, preClosureAllowed, this.fixedTermDepositInterestCalculator, defaultDepositLifecycleStateMachine()); return account; } + + private DepositLifecycleStateMachine defaultDepositLifecycleStateMachine() { + List allowedDepositStatuses = Arrays.asList(DepositStatus.values()); + return new DepositLifecycleStateMachineImpl(allowedDepositStatuses); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountWritePlatformService.java index 42de76df6..878d4ba13 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountWritePlatformService.java @@ -1,6 +1,8 @@ package org.mifosng.platform.saving.service; import org.mifosng.platform.api.commands.DepositAccountCommand; +import org.mifosng.platform.api.commands.DepositStateTransitionCommand; +import org.mifosng.platform.api.commands.UndoStateTransitionCommand; import org.mifosng.platform.api.data.EntityIdentifier; import org.springframework.security.access.prepost.PreAuthorize; @@ -14,4 +16,17 @@ public interface DepositAccountWritePlatformService { @PreAuthorize(value = "hasRole('ORGANISATION_ADMINISTRATION_SUPER_USER_ROLE')") EntityIdentifier deleteDepositAccount(Long productId); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_APPROVE_DEPOSIT_ROLE', 'CAN_APPROVE_DEPOSIT_IN_THE_PAST_ROLE')") + EntityIdentifier approveDepositApplication(DepositStateTransitionCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_APPROVE_DEPOSIT_ROLE', 'CAN_APPROVE_DEPOSIT_IN_THE_PAST_ROLE')") + EntityIdentifier rejectDepositApplication(DepositStateTransitionCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_APPROVE_DEPOSIT_ROLE', 'CAN_APPROVE_DEPOSIT_IN_THE_PAST_ROLE')") + EntityIdentifier withdrawDepositApplication(DepositStateTransitionCommand command); + + @PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE', 'CAN_APPROVE_DEPOSIT_ROLE', 'CAN_APPROVE_DEPOSIT_IN_THE_PAST_ROLE')") + EntityIdentifier undoDepositApproval(UndoStateTransitionCommand undoCommand); + } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index 37d503d98..1f18582fb 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -1,13 +1,24 @@ package org.mifosng.platform.saving.service; +import java.util.Arrays; +import java.util.List; + +import org.joda.time.LocalDate; import org.mifosng.platform.api.commands.DepositAccountCommand; +import org.mifosng.platform.api.commands.DepositStateTransitionCommand; +import org.mifosng.platform.api.commands.UndoStateTransitionCommand; import org.mifosng.platform.api.data.EntityIdentifier; import org.mifosng.platform.exceptions.DepositAccountNotFoundException; +import org.mifosng.platform.exceptions.NoAuthorizationException; import org.mifosng.platform.exceptions.PlatformDataIntegrityException; import org.mifosng.platform.exceptions.ProductNotFoundException; import org.mifosng.platform.saving.domain.DepositAccount; import org.mifosng.platform.saving.domain.DepositAccountRepository; +import org.mifosng.platform.saving.domain.DepositLifecycleStateMachine; +import org.mifosng.platform.saving.domain.DepositLifecycleStateMachineImpl; +import org.mifosng.platform.saving.domain.DepositStatus; import org.mifosng.platform.security.PlatformSecurityContext; +import org.mifosng.platform.user.domain.AppUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -112,4 +123,104 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo return new EntityIdentifier(accountId); } + + @Transactional + @Override + public EntityIdentifier approveDepositApplication(DepositStateTransitionCommand command) { + + AppUser currentUser = context.authenticatedUser(); + + DepositStateTransitionCommandValidator validator = new DepositStateTransitionCommandValidator(command); + validator.validate(); + + + DepositAccount account = this.depositAccountRepository.findOne(command.getAccountId()); + if (account == null || account.isDeleted()) { + throw new DepositAccountNotFoundException(command.getAccountId()); + } + + LocalDate eventDate = command.getEventDate(); + if (this.isBeforeToday(eventDate) && currentUser.canNotApproveLoanInPast()) { + throw new NoAuthorizationException("User has no authority to approve deposit with a date in the past."); + } + + account.approve(eventDate, defaultDepositLifecycleStateMachine()); + this.depositAccountRepository.save(account); + + return new EntityIdentifier(account.getId()); + + } + + private boolean isBeforeToday(final LocalDate date) { + return date.isBefore(new LocalDate()); + } + private DepositLifecycleStateMachine defaultDepositLifecycleStateMachine() { + List allowedDepositStatuses = Arrays.asList(DepositStatus.values()); + return new DepositLifecycleStateMachineImpl(allowedDepositStatuses); + } + + @Transactional + @Override + public EntityIdentifier rejectDepositApplication(DepositStateTransitionCommand command) { + + AppUser currentUser = context.authenticatedUser(); + + DepositStateTransitionCommandValidator validator = new DepositStateTransitionCommandValidator(command); + validator.validate(); + + DepositAccount account = this.depositAccountRepository.findOne(command.getAccountId()); + if (account == null || account.isDeleted()) { + throw new DepositAccountNotFoundException(command.getAccountId()); + } + + LocalDate eventDate = command.getEventDate(); + if (this.isBeforeToday(eventDate) && currentUser.canNotApproveLoanInPast()) { + throw new NoAuthorizationException("User has no authority to approve deposit with a date in the past."); + } + + account.reject(eventDate, defaultDepositLifecycleStateMachine()); + this.depositAccountRepository.save(account); + + return new EntityIdentifier(account.getId()); + } + + @Transactional + @Override + public EntityIdentifier withdrawDepositApplication(DepositStateTransitionCommand command) { + + AppUser currentUser = context.authenticatedUser(); + + DepositStateTransitionCommandValidator validator = new DepositStateTransitionCommandValidator(command); + validator.validate(); + + DepositAccount account = this.depositAccountRepository.findOne(command.getAccountId()); + if (account == null || account.isDeleted()) { + throw new DepositAccountNotFoundException(command.getAccountId()); + } + + LocalDate eventDate = command.getEventDate(); + if (this.isBeforeToday(eventDate) && currentUser.canNotApproveLoanInPast()) { + throw new NoAuthorizationException("User has no authority to approve deposit with a date in the past."); + } + + account.withdraw(eventDate, defaultDepositLifecycleStateMachine()); + return null; + } + + @Transactional + @Override + public EntityIdentifier undoDepositApproval(UndoStateTransitionCommand command) { + + context.authenticatedUser(); + + DepositAccount account = this.depositAccountRepository.findOne(command.getLoanId()); + if (account == null || account.isDeleted()) { + throw new DepositAccountNotFoundException(command.getLoanId()); + } + + account.undoDepositApproval(defaultDepositLifecycleStateMachine()); + this.depositAccountRepository.save(account); + + return null; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositStateTransitionCommandValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositStateTransitionCommandValidator.java new file mode 100644 index 000000000..7d5f96f59 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/saving/service/DepositStateTransitionCommandValidator.java @@ -0,0 +1,31 @@ +package org.mifosng.platform.saving.service; + +import java.util.ArrayList; +import java.util.List; + +import org.mifosng.platform.DataValidatorBuilder; +import org.mifosng.platform.api.commands.DepositStateTransitionCommand; +import org.mifosng.platform.api.data.ApiParameterError; +import org.mifosng.platform.exceptions.PlatformApiDataValidationException; + +public class DepositStateTransitionCommandValidator { + + private final DepositStateTransitionCommand command; + + public DepositStateTransitionCommandValidator(final DepositStateTransitionCommand command) { + this.command=command; + } + + public void validate(){ + + List dataValidationErrors = new ArrayList(); + DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("deposit.transition"); + baseDataValidator.reset().parameter("accountId").value(command.getAccountId()).notNull().integerGreaterThanZero(); + baseDataValidator.reset().parameter("eventDate").value(command.getEventDate()).notNull(); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); + } + } + +}