add basic fund concept to platform.

This commit is contained in:
Keith Woodlock
2012-07-11 09:37:34 +01:00
parent 40f7627eb2
commit 21d5a4cc1a
14 changed files with 603 additions and 5 deletions
@@ -7,6 +7,7 @@ import javax.persistence.criteria.Root;
import org.mifosng.platform.client.domain.Client;
import org.mifosng.platform.client.domain.Note;
import org.mifosng.platform.fund.domain.Fund;
import org.mifosng.platform.loan.domain.Loan;
import org.mifosng.platform.loan.domain.LoanProduct;
import org.mifosng.platform.loan.domain.LoanTransaction;
@@ -94,6 +95,15 @@ public final class Specifications {
};
}
public static Specification<Fund> fundsThatMatch(final Organisation organisation, final Long id) {
return new Specification<Fund>() {
@Override
public Predicate toPredicate(final Root<Fund> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.and(cb.equal(root.get("organisation"), organisation), cb.equal(root.get("id"), id));
}
};
}
public static Specification<Client> clientsThatMatch(final Organisation organisation, final Long id) {
return new Specification<Client>() {
@@ -0,0 +1,111 @@
package org.mifosng.platform.api;
import java.util.Collection;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.mifosng.platform.api.commands.FundCommand;
import org.mifosng.platform.api.data.EntityIdentifier;
import org.mifosng.platform.api.data.FundData;
import org.mifosng.platform.api.infrastructure.ApiDataConversionService;
import org.mifosng.platform.api.infrastructure.ApiJSONFormattingService;
import org.mifosng.platform.fund.service.FundReadPlatformService;
import org.mifosng.platform.fund.service.FundWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Path("/funds")
@Component
@Scope("singleton")
public class FundsApiResource {
private String defaultFieldList = "openingDate";
private String allowedFieldList = "allowedParents";
private String filterName = "myFilter";
@Autowired
private FundReadPlatformService readPlatformService;
@Autowired
private FundWritePlatformService writePlatformService;
@Autowired
private ApiDataConversionService apiDataConversionService;
@Autowired
private ApiJSONFormattingService jsonFormattingService;
@GET
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON })
public String retrieveFunds(@Context UriInfo uriInfo) {
Collection<FundData> funds = this.readPlatformService.retrieveAllFunds();
String selectedFields = "";
return this.jsonFormattingService.convertRequest(funds, filterName,
allowedFieldList, selectedFields, uriInfo.getQueryParameters());
}
@POST
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON })
public Response createFund(final FundCommand command) {
// LocalDate openingLocalDate = apiDataConversionService.convertFrom(
// command.getOpeningDate(), "openingDate",
// command.getDateFormat());
// command.setOpeningLocalDate(openingLocalDate);
Long fundId = this.writePlatformService.createFund(command);
return Response.ok().entity(new EntityIdentifier(fundId)).build();
}
@GET
@Path("{fundId}")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON })
public String retreiveOffice(@PathParam("fundId") final Long fundId,
@QueryParam("template") String template, @Context UriInfo uriInfo) {
FundData fund = this.readPlatformService.retrieveFund(fundId);
// if (template != null && template.equalsIgnoreCase("true")) {
// office.setAllowedParents(this.readPlatformService
// .retrieveAllowedParents(officeId));
// }
String selectedFields = "";
return this.jsonFormattingService.convertRequest(fund, filterName,
allowedFieldList, selectedFields, uriInfo.getQueryParameters());
}
@PUT
@Path("{fundId}")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
@Produces({ MediaType.APPLICATION_JSON })
public Response updateOffice(@PathParam("fundId") final Long fundId,
final FundCommand command) {
// LocalDate openingLocalDate = apiDataConversionService.convertFrom(
// command.getOpeningDate(), "openingDate",
// command.getDateFormat());
// command.setOpeningLocalDate(openingLocalDate);
command.setId(fundId);
Long entityId = this.writePlatformService.updateFund(command);
return Response.ok().entity(new EntityIdentifier(entityId)).build();
}
}
@@ -0,0 +1,96 @@
package org.mifosng.platform.api.commands;
import javax.xml.bind.annotation.XmlRootElement;
import org.joda.time.LocalDate;
/**
* Command used for create and update fund operations.
*/
@XmlRootElement
public class FundCommand {
private Long id;
private String name;
private String externalId;
private String dateFormat;
private String activatedOnDate;
private LocalDate activatedOnLocalDate;
private String deactivatedOnDate;
private LocalDate deactivatedOnLocalDate;
protected FundCommand() {
//
}
public FundCommand(final String fundName, final String externalId) {
this.name = fundName;
this.externalId = externalId;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getExternalId() {
return externalId;
}
public void setExternalId(String externalId) {
this.externalId = externalId;
}
public String getDateFormat() {
return dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
public String getActivatedOnDate() {
return activatedOnDate;
}
public void setActivatedOnDate(String activatedOnDate) {
this.activatedOnDate = activatedOnDate;
}
public LocalDate getActivatedOnLocalDate() {
return activatedOnLocalDate;
}
public void setActivatedOnLocalDate(LocalDate activatedOnLocalDate) {
this.activatedOnLocalDate = activatedOnLocalDate;
}
public String getDeactivatedOnDate() {
return deactivatedOnDate;
}
public void setDeactivatedOnDate(String deactivatedOnDate) {
this.deactivatedOnDate = deactivatedOnDate;
}
public LocalDate getDeactivatedOnLocalDate() {
return deactivatedOnLocalDate;
}
public void setDeactivatedOnLocalDate(LocalDate deactivatedOnLocalDate) {
this.deactivatedOnLocalDate = deactivatedOnLocalDate;
}
}
@@ -0,0 +1,47 @@
package org.mifosng.platform.api.data;
import java.io.Serializable;
import org.codehaus.jackson.map.annotate.JsonFilter;
@JsonFilter("myFilter")
public class FundData implements Serializable {
private Long id;
private String name;
private String externalId;
public FundData() {
//
}
public FundData(final Long id, final String name, final String externalId) {
this.id = id;
this.name = name;
this.externalId = externalId;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getExternalId() {
return externalId;
}
public void setExternalId(String externalId) {
this.externalId = externalId;
}
}
@@ -0,0 +1,11 @@
package org.mifosng.platform.exceptions;
/**
* A {@link RuntimeException} thrown when fund resources are not found.
*/
public class FundNotFoundException extends AbstractPlatformResourceNotFoundException {
public FundNotFoundException(final Long id) {
super("error.msg.fund.id.invalid", "Fund with identifier " + id + " does not exist", id);
}
}
@@ -5,7 +5,7 @@ package org.mifosng.platform.exceptions;
*/
public class OfficeNotFoundException extends AbstractPlatformResourceNotFoundException {
public OfficeNotFoundException(Long id) {
public OfficeNotFoundException(final Long id) {
super("error.msg.office.id.invalid", "Office with identifier " + id + " does not exist", id);
}
}
@@ -0,0 +1,63 @@
package org.mifosng.platform.fund.domain;
import java.math.BigDecimal;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.mifosng.platform.api.commands.FundCommand;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
import org.mifosng.platform.organisation.domain.Organisation;
import org.mifosng.platform.user.domain.AppUser;
@Entity
@Table(name = "org_fund")
public class Fund extends AbstractAuditableCustom<AppUser, Long> {
@ManyToOne
@JoinColumn(name = "org_id", nullable = false)
private final Organisation organisation;
@Column(name = "name")
private String name;
@Column(name = "external_id", length=100)
private String externalId;
@Temporal(TemporalType.DATE)
@Column(name = "activatedon_date")
private Date activatedOnDate;
@Temporal(TemporalType.DATE)
@Column(name = "deactivatedon_date")
private Date deactivatedOnDate;
@Column(name = "initial_balance", scale = 6, precision = 19)
private BigDecimal initialBalance;
public static Fund createNew(final Organisation organisation, final String fundName) {
return new Fund(organisation, fundName);
}
protected Fund() {
this.organisation = null;
}
private Fund(final Organisation organisation, final String fundName) {
this.organisation = organisation;
this.name = fundName;
this.initialBalance = BigDecimal.ZERO;
}
public void update(FundCommand command) {
if (command.getName() != null) {
this.name = command.getName();
}
}
}
@@ -0,0 +1,8 @@
package org.mifosng.platform.fund.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface FundRepository extends JpaRepository<Fund, Long>, JpaSpecificationExecutor<Fund> {
// no added behaviour
}
@@ -0,0 +1,52 @@
package org.mifosng.platform.fund.service;
import java.util.ArrayList;
import java.util.List;
import org.mifosng.platform.DataValidatorBuilder;
import org.mifosng.platform.api.commands.FundCommand;
import org.mifosng.platform.api.data.ApiParameterError;
import org.mifosng.platform.exceptions.PlatformApiDataValidationException;
public class FundCommandValidator {
private final FundCommand command;
public FundCommandValidator(FundCommand command) {
this.command = command;
}
public void validateForCreate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("fund");
// baseDataValidator.reset().parameter("name").value(command.getName()).notBlank();
// baseDataValidator.reset().parameter("openingDate").value(command.getOpeningDate()).notBlank();
// baseDataValidator.reset().parameter("externalId").value(command.getExternalId()).notExceedingLengthOf(100);
// baseDataValidator.reset().parameter("parentId").value(command.getParentId()).notNull().greaterThanZero();
//
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors);
}
}
public void validateForUpdate() {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("fund");
//
// baseDataValidator.reset().parameter("id").value(command.getId()).notNull();
// baseDataValidator.reset().parameter("name").value(command.getName()).ignoreIfNull().notBlank();
// baseDataValidator.reset().parameter("openingDate").value(command.getOpeningDate()).ignoreIfNull().notBlank();
// baseDataValidator.reset().parameter("externalId").value(command.getExternalId()).ignoreIfNull().notExceedingLengthOf(100);
// baseDataValidator.reset().parameter("parentId").value(command.getParentId()).ignoreIfNull().notNull().greaterThanZero();
//
// baseDataValidator.reset().anyOfNotNull(command.getName(), command.getOpeningDate(), command.getExternalId(), command.getParentId());
//
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors);
}
}
}
@@ -0,0 +1,12 @@
package org.mifosng.platform.fund.service;
import java.util.Collection;
import org.mifosng.platform.api.data.FundData;
public interface FundReadPlatformService {
Collection<FundData> retrieveAllFunds();
FundData retrieveFund(Long fundId);
}
@@ -0,0 +1,82 @@
package org.mifosng.platform.fund.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import javax.sql.DataSource;
import org.mifosng.platform.api.data.FundData;
import org.mifosng.platform.exceptions.FundNotFoundException;
import org.mifosng.platform.security.PlatformSecurityContext;
import org.mifosng.platform.user.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class FundReadPlatformServiceImpl implements FundReadPlatformService {
private final SimpleJdbcTemplate jdbcTemplate;
private final PlatformSecurityContext context;
@Autowired
public FundReadPlatformServiceImpl(final PlatformSecurityContext context,
final DataSource dataSource) {
this.context = context;
this.jdbcTemplate = new SimpleJdbcTemplate(dataSource);
}
private static final class FundMapper implements RowMapper<FundData> {
public String officeSchema() {
return " f.id as id, f.name as name, f.external_id as externalId from org_fund f ";
}
@Override
public FundData mapRow(final ResultSet rs, final int rowNum)
throws SQLException {
Long id = rs.getLong("id");
String name = rs.getString("name");
String externalId = rs.getString("externalId");
// LocalDate openingDate = JdbcSupport.getLocalDate(rs, "openingDate");
return new FundData(id, name, externalId);
}
}
@Override
public Collection<FundData> retrieveAllFunds() {
AppUser currentUser = context.authenticatedUser();
FundMapper rm = new FundMapper();
String sql = "select "
+ rm.officeSchema()
+ " where f.org_id = ? order by f.name";
return this.jdbcTemplate.query(sql, rm, new Object[] {currentUser.getOrganisation().getId()});
}
@Override
public FundData retrieveFund(final Long fundId) {
try {
AppUser currentUser = context.authenticatedUser();
FundMapper rm = new FundMapper();
String sql = "select " + rm.officeSchema()
+ " where f.org_id = ? and f.id = ?";
FundData selectedFund = this.jdbcTemplate.queryForObject(sql,
rm, new Object[] { currentUser.getOrganisation().getId(), fundId});
return selectedFund;
} catch (EmptyResultDataAccessException e) {
throw new FundNotFoundException(fundId);
}
}
}
@@ -0,0 +1,13 @@
package org.mifosng.platform.fund.service;
import org.mifosng.platform.api.commands.FundCommand;
import org.springframework.security.access.prepost.PreAuthorize;
public interface FundWritePlatformService {
@PreAuthorize(value = "hasRole('ORGANISATION_ADMINISTRATION_SUPER_USER_ROLE')")
Long createFund(final FundCommand command);
@PreAuthorize(value = "hasRole('ORGANISATION_ADMINISTRATION_SUPER_USER_ROLE')")
Long updateFund(final FundCommand command);
}
@@ -0,0 +1,95 @@
package org.mifosng.platform.fund.service;
import static org.mifosng.platform.Specifications.fundsThatMatch;
import org.mifosng.platform.api.commands.FundCommand;
import org.mifosng.platform.exceptions.FundNotFoundException;
import org.mifosng.platform.exceptions.PlatformDataIntegrityException;
import org.mifosng.platform.fund.domain.Fund;
import org.mifosng.platform.fund.domain.FundRepository;
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;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class FundWritePlatformServiceJpaRepositoryImpl implements FundWritePlatformService {
private final static Logger logger = LoggerFactory.getLogger(FundWritePlatformServiceJpaRepositoryImpl.class);
private final PlatformSecurityContext context;
private final FundRepository fundRepository;
@Autowired
public FundWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final FundRepository fundRepository) {
this.context = context;
this.fundRepository = fundRepository;
}
@Transactional
@Override
public Long createFund(final FundCommand command) {
try {
AppUser currentUser = context.authenticatedUser();
FundCommandValidator validator = new FundCommandValidator(command);
validator.validateForCreate();
Fund fund = Fund.createNew(currentUser.getOrganisation(), command.getName());
this.fundRepository.saveAndFlush(fund);
return fund.getId();
} catch (DataIntegrityViolationException dve) {
handleFundDataIntegrityIssues(command, dve);
return Long.valueOf(-1);
}
}
@Transactional
@Override
public Long updateFund(final FundCommand command) {
try {
AppUser currentUser = context.authenticatedUser();
FundCommandValidator validator = new FundCommandValidator(command);
validator.validateForCreate();
final Long fundId = command.getId();
Fund fund = this.fundRepository.findOne(fundsThatMatch(currentUser.getOrganisation(), fundId));
if (fund == null) {
throw new FundNotFoundException(fundId);
}
fund.update(command);
this.fundRepository.save(fund);
return fund.getId();
} catch (DataIntegrityViolationException dve) {
handleFundDataIntegrityIssues(command, dve);
return Long.valueOf(-1);
}
}
/*
* Guaranteed to throw an exception no matter what the data integrity issue is.
*/
private void handleFundDataIntegrityIssues(final FundCommand command, DataIntegrityViolationException dve) {
Throwable realCause = dve.getMostSpecificCause();
if (realCause.getMessage().contains("externalid_org")) {
throw new PlatformDataIntegrityException("error.msg.office.duplicate.externalId", "Office with externalId {0} already exists", "externalId", command.getExternalId());
} else if (realCause.getMessage().contains("name_org")) {
throw new PlatformDataIntegrityException("error.msg.office.duplicate.name", "Office with name {0} already exists", "name", command.getName());
}
logger.error(dve.getMessage(), dve);
throw new PlatformDataIntegrityException("error.msg.office.unknown.data.integrity.issue", "Unknown data integrity issue with resource.");
}
}
@@ -3,10 +3,8 @@
<persistence-unit name="jpa-pu" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
<!--
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
-->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.jdbc.batch_size" value="200"/>
<property name="hibernate.order_inserts" value="true"/>
</properties>