FINERACT-1926: External owner accounting

This commit is contained in:
Adam Saghy
2023-06-12 11:24:31 +02:00
committed by Arnold Gálovics
parent 437b8e9cae
commit 582f36ad34
416 changed files with 1952 additions and 613 deletions
@@ -0,0 +1,57 @@
/**
* 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.accounting.closure.api;
import java.util.HashSet;
import java.util.Set;
/***
* Enum of all parameters passed in while creating/updating a loan product
***/
public enum GLClosureJsonInputParams {
ID("id"), OFFICE_ID("officeId"), CLOSING_DATE("closingDate"), COMMENTS("comments"), LOCALE("locale"), DATE_FORMAT("dateFormat");
private final String value;
GLClosureJsonInputParams(final String value) {
this.value = value;
}
private static final Set<String> values = new HashSet<>();
static {
for (final GLClosureJsonInputParams type : GLClosureJsonInputParams.values()) {
values.add(type.value);
}
}
public static Set<String> getAllValues() {
return values;
}
@Override
public String toString() {
return name().toString().replaceAll("_", " ");
}
public String getValue() {
return this.value;
}
}
@@ -0,0 +1,170 @@
/**
* 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.accounting.closure.api;
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.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 java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.data.GLClosureData;
import org.apache.fineract.accounting.closure.service.GLClosureReadPlatformService;
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.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
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.organisation.office.service.OfficeReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/glclosures")
@Component
@Tag(name = "Accounting Closure", description = "An accounting closure indicates that no more journal entries may be logged (or reversed) in the system, either manually or via the portfolio with an entry date prior to the defined closure date\n"
+ "\n" + "Field Descriptions\n" + "closingDate\n" + "The date for which the accounting closure is defined\n" + "officeId\n"
+ "The identifer of the branch for which accounting has been closed\n" + "comments\n"
+ "Description associated with an Accounting closure")
@RequiredArgsConstructor
public class GLClosuresApiResource {
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList("id", "officeId", "officeName", "closingDate", "deleted", "createdDate", "lastUpdatedDate", "createdByUserId",
"createdByUsername", "lastUpdatedByUserId", "lastUpdatedByUsername"));
private final String resourceNameForPermission = "GLCLOSURE";
private final PlatformSecurityContext context;
private final GLClosureReadPlatformService glClosureReadPlatformService;
private final DefaultToApiJsonSerializer<GLClosureData> apiJsonSerializerService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final OfficeReadPlatformService officeReadPlatformService;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Accounting closures", description = "Example Requests:\n" + "\n" + "glclosures")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = GLClosuresApiResourceSwagger.GetGlClosureResponse.class)))) })
public String retrieveAllClosures(@Context final UriInfo uriInfo,
@QueryParam("officeId") @Parameter(name = "officeId") final Long officeId) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission);
final List<GLClosureData> glClosureDatas = this.glClosureReadPlatformService.retrieveAllGLClosures(officeId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serialize(settings, glClosureDatas, RESPONSE_DATA_PARAMETERS);
}
@GET
@Path("{glClosureId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve an Accounting Closure", description = "Example Requests:\n" + "\n" + "glclosures/1\n" + "\n" + "\n"
+ "/glclosures/1?fields=officeName,closingDate")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.GetGlClosureResponse.class))) })
public String retreiveClosure(@PathParam("glClosureId") @Parameter(description = "glClosureId") final Long glClosureId,
@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
final GLClosureData glClosureData = this.glClosureReadPlatformService.retrieveGLClosureById(glClosureId);
if (settings.isTemplate()) {
glClosureData.setAllowedOffices(this.officeReadPlatformService.retrieveAllOfficesForDropdown());
}
return this.apiJsonSerializerService.serialize(settings, glClosureData, RESPONSE_DATA_PARAMETERS);
}
// NOTE: proposal slight changes @Aleks
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create an Accounting Closure", description = "Mandatory Fields\n" + "officeId,closingDate")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.PostGlClosuresRequest.class, description = "Request Body")))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.PostGlClosuresResponse.class))) })
public String createGLClosure(@Parameter(hidden = true) final String jsonRequestBody) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createGLClosure().withJson(jsonRequestBody).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.apiJsonSerializerService.serialize(result);
}
@PUT
@Path("{glClosureId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update an Accounting closure", description = "Once an accounting closure is created, only the comments associated with it may be edited")
@RequestBody(content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.PutGlClosuresRequest.class, required = true)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.PutGlClosuresResponse.class))) })
public String updateGLClosure(@PathParam("glClosureId") @Parameter(description = "glClosureId") final Long glClosureId,
@Parameter(hidden = true) final String jsonRequestBody) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateGLClosure(glClosureId).withJson(jsonRequestBody).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.apiJsonSerializerService.serialize(result);
}
@DELETE
@Path("{glClosureId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete an accounting closure", description = "Note: Only the latest accounting closure associated with a branch may be deleted.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLClosuresApiResourceSwagger.DeleteGlClosuresResponse.class))) })
public String deleteGLClosure(@PathParam("glClosureId") @Parameter(description = "glclosureId") final Long glClosureId) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteGLClosure(glClosureId).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.apiJsonSerializerService.serialize(result);
}
}
@@ -0,0 +1,139 @@
/**
* 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.accounting.closure.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
final class GLClosuresApiResourceSwagger {
private GLClosuresApiResourceSwagger() {
// don't allow to instantiate; use only for live API documentation
}
/**
* TODO: describe where this belongs: {@link GLClosuresApiResource } {@link GLClosuresApiResource}
*/
// Check !!
@Schema(description = "GetGLClosureResponse")
public static final class GetGlClosureResponse {
private GetGlClosureResponse() {
// dont allow to initiatiate
}
@Schema(example = "7")
public Long id;
@Schema(example = "1")
public Long officeId;
@Schema(example = "Head Office")
public String officeName;
@Schema(example = "2013,1,2")
public LocalDate closingDate;
@Schema(example = "false")
public boolean deleted;
@Schema(example = "2013,1,3")
public LocalDate createdDate;
@Schema(example = "2013,1,3")
public LocalDate lastUpdatedDate;
@Schema(example = "1")
public Long createdByUserId;
@Schema(example = "mifos")
public String createdByUsername;
@Schema(example = "1")
public Long lastUpdatedByUserId;
@Schema(example = "mifos")
public String lastUpdatedByUsername;
@Schema(example = "closed")
public String comments;
}
@Schema(description = "PostGLCLosuresRequest")
public static final class PostGlClosuresRequest {
private PostGlClosuresRequest() {
// don't allow to instantiate; use only for live API documentation
}
@Schema(example = "1")
public Long officeId;
@Schema(example = "06 December 2012")
public LocalDate closingDate;
@Schema(example = "The accountants are heading for a carribean vacation")
public String comments;
@Schema(example = "en")
public String locale;
@Schema(example = "dd MMMM yyyy")
public String dateFormat;
}
@Schema(description = "PostGlClosuresResponse")
public static final class PostGlClosuresResponse {
private PostGlClosuresResponse() {
// don't allow to instantiate; use only for live API documentation
}
@Schema(example = "1")
public Long officeId;
@Schema(example = "9")
public Long resourceId;
}
@Schema(description = "PutGlClosuresRequest")
public static final class PutGlClosuresRequest {
private PutGlClosuresRequest() {
// don't allow to instantiate; use only for live API documentation
}
@Schema(example = "All transactions verified by Johnny Cash")
public String comments;
}
@Schema(description = "PutGlClosuresResponse")
public static final class PutGlClosuresResponse {
private PutGlClosuresResponse() {
// don't allow to instantiate; use only for live API documentation
}
@Schema(example = "1")
public Long officeId;
@Schema(example = "9")
public Long resourceId;
@Schema(example = "All transactions verified by Johnny Cash")
public String comments;
}
@Schema(description = "DeleteGlClosuresResponse")
public static final class DeleteGlClosuresResponse {
private DeleteGlClosuresResponse() {
}
@Schema(example = "1")
public Long officeId;
@Schema(example = "9")
public Long resourceId;
}
}
@@ -0,0 +1,76 @@
/**
* 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.accounting.closure.command;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.api.GLClosureJsonInputParams;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
/**
* Immutable command for adding an accounting closure
*/
@RequiredArgsConstructor
@Getter
public class GLClosureCommand {
private final Long id;
private final Long officeId;
private final LocalDate closingDate;
private final String comments;
public void validateForCreate() {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("GLClosure");
baseDataValidator.reset().parameter(GLClosureJsonInputParams.CLOSING_DATE.getValue()).value(this.closingDate).notBlank();
baseDataValidator.reset().parameter(GLClosureJsonInputParams.OFFICE_ID.getValue()).value(this.officeId).notNull()
.integerGreaterThanZero();
baseDataValidator.reset().parameter(GLClosureJsonInputParams.COMMENTS.getValue()).value(this.comments).ignoreIfNull()
.notExceedingLengthOf(500);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
public void validateForUpdate() {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("GLClosure");
baseDataValidator.reset().parameter(GLClosureJsonInputParams.COMMENTS.getValue()).value(this.comments).ignoreIfNull()
.notExceedingLengthOf(500);
baseDataValidator.reset().anyOfNotNull(this.comments);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -0,0 +1,49 @@
/**
* 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.accounting.closure.data;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import lombok.Data;
import org.apache.fineract.organisation.office.data.OfficeData;
/**
* Immutable object representing a General Ledger Account
*
* Note: no getter/setters required as google-gson will produce json from fields of object.
*/
@Data
public class GLClosureData {
private final Long id;
private final Long officeId;
private final String officeName;
private final LocalDate closingDate;
private final boolean deleted;
private final LocalDate createdDate;
private final LocalDate lastUpdatedDate;
private final Long createdByUserId;
private final String createdByUsername;
private final Long lastUpdatedByUserId;
private final String lastUpdatedByUsername;
private final String comments;
private Collection<OfficeData> allowedOffices = new ArrayList<>();
}
@@ -0,0 +1,93 @@
/**
* 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.accounting.closure.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.closure.api.GLClosureJsonInputParams;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
import org.apache.fineract.organisation.office.domain.Office;
@Entity
@Table(name = "acc_gl_closure", uniqueConstraints = {
@UniqueConstraint(columnNames = { "office_id", "closing_date" }, name = "office_id_closing_date") })
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class GLClosure extends AbstractAuditableCustom {
@ManyToOne
@JoinColumn(name = "office_id", nullable = false)
private Office office;
@Column(name = "is_deleted", nullable = false)
private boolean deleted = true;
@Column(name = "closing_date")
private LocalDate closingDate;
@Column(name = "comments", nullable = true, length = 500)
private String comments;
public GLClosure(final Office office, final LocalDate closingDate, final String comments) {
this.office = office;
this.deleted = false;
this.closingDate = closingDate;
this.comments = StringUtils.defaultIfEmpty(comments, null);
if (this.comments != null) {
this.comments = this.comments.trim();
}
}
public static GLClosure fromJson(final Office office, final JsonCommand command) {
final LocalDate closingDate = command.localDateValueOfParameterNamed(GLClosureJsonInputParams.CLOSING_DATE.getValue());
final String comments = command.stringValueOfParameterNamed(GLClosureJsonInputParams.COMMENTS.getValue());
return new GLClosure(office, closingDate, comments);
}
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(5);
handlePropertyUpdate(command, actualChanges, GLClosureJsonInputParams.COMMENTS.getValue(), this.comments);
return actualChanges;
}
private void handlePropertyUpdate(final JsonCommand command, final Map<String, Object> actualChanges, final String paramName,
final String propertyToBeUpdated) {
if (command.isChangeInStringParameterNamed(paramName, propertyToBeUpdated)) {
final String newValue = command.stringValueOfParameterNamed(paramName);
actualChanges.put(paramName, newValue);
// now update actual property
if (paramName.equals(GLClosureJsonInputParams.COMMENTS.getValue())) {
this.comments = newValue;
}
}
}
}
@@ -0,0 +1,30 @@
/**
* 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.accounting.closure.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface GLClosureRepository extends JpaRepository<GLClosure, Long>, JpaSpecificationExecutor<GLClosure> {
@Query("select closure from GLClosure closure where closure.closingDate = (select max(closure1.closingDate) from GLClosure closure1 where closure1.office.id=:officeId) and closure.office.id= :officeId")
GLClosure getLatestGLClosureByBranch(@Param("officeId") Long officeId);
}
@@ -0,0 +1,35 @@
/**
* 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.accounting.closure.exception;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when a GL Closure for a given date and Office combination is already present
*/
public class GLClosureDuplicateException extends AbstractPlatformDomainRuleException {
public GLClosureDuplicateException(final Long officeId, final LocalDate closureDate) {
super("error.msg.glclosure.glcode.duplicate",
"An accounting closure for branch with Id " + officeId + " already exists for the date " + closureDate, officeId,
closureDate);
}
}
@@ -0,0 +1,33 @@
/**
* 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.accounting.closure.exception;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when a GL Closure Delte command is invalid
*/
public class GLClosureInvalidDeleteException extends AbstractPlatformDomainRuleException {
public GLClosureInvalidDeleteException(final Long officeId, final String officeName, final LocalDate latestclosureDate) {
super("error.msg.glclosure.invalid.delete", "The latest closure for office with Id " + officeId + " and name " + officeName
+ " is on " + latestclosureDate.toString() + ", please delete this closure first", latestclosureDate);
}
}
@@ -0,0 +1,56 @@
/**
* 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.accounting.closure.exception;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when a GL Closure is Invalid
*/
public class GLClosureInvalidException extends AbstractPlatformDomainRuleException {
/*** enum of reasons for invalid Accounting Closure **/
public enum GlClosureInvalidReason {
FUTURE_DATE, ACCOUNTING_CLOSED;
public String errorMessage() {
if (name().toString().equalsIgnoreCase("FUTURE_DATE")) {
return "Accounting closures cannot be made for a future date";
} else if (name().toString().equalsIgnoreCase("ACCOUNTING_CLOSED")) {
return "Accounting Closure for this branch has already been defined for a greater date";
}
return name().toString();
}
public String errorCode() {
if (name().toString().equalsIgnoreCase("FUTURE_DATE")) {
return "error.msg.glclosure.invalid.future.date";
} else if (name().toString().equalsIgnoreCase("ACCOUNTING_CLOSED")) {
return "error.msg.glclosure.invalid.accounting.closed";
}
return name().toString();
}
}
public GLClosureInvalidException(final GlClosureInvalidReason reason, final LocalDate date) {
super(reason.errorCode(), reason.errorMessage(), date);
}
}
@@ -0,0 +1,36 @@
/**
* 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.accounting.closure.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
/**
* A {@link RuntimeException} thrown when GL account closure resources are not found.
*/
public class GLClosureNotFoundException extends AbstractPlatformResourceNotFoundException {
public GLClosureNotFoundException(final Long id) {
super("error.msg.glclosure.id.invalid", "Accounting Closure with identifier " + id + " does not exist", id);
}
public GLClosureNotFoundException(final Long id, EmptyResultDataAccessException e) {
super("error.msg.glclosure.id.invalid", "Accounting Closure with identifier " + id + " does not exist", id, e);
}
}
@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.closure.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.service.GLClosureWritePlatformService;
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.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "GLCLOSURE", action = "CREATE")
@RequiredArgsConstructor
public class CreateGLClosureCommandHandler implements NewCommandSourceHandler {
private final GLClosureWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createGLClosure(command);
}
}
@@ -0,0 +1,42 @@
/**
* 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.accounting.closure.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.service.GLClosureWritePlatformService;
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.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "GLCLOSURE", action = "DELETE")
@RequiredArgsConstructor
public class DeleteGLClosureCommandHandler implements NewCommandSourceHandler {
private final GLClosureWritePlatformService closureWritePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.closureWritePlatformService.deleteGLClosure(command.entityId());
}
}
@@ -0,0 +1,42 @@
/**
* 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.accounting.closure.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.service.GLClosureWritePlatformService;
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.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "GLCLOSURE", action = "UPDATE")
@RequiredArgsConstructor
public class UpdateGLClosureCommandHandler implements NewCommandSourceHandler {
private final GLClosureWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateGLClosure(command.entityId(), command);
}
}
@@ -0,0 +1,66 @@
/**
* 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.accounting.closure.serialization;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.closure.api.GLClosureJsonInputParams;
import org.apache.fineract.accounting.closure.command.GLClosureCommand;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.serialization.AbstractFromApiJsonDeserializer;
import org.apache.fineract.infrastructure.core.serialization.FromApiJsonDeserializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
/**
* Implementation of {@link FromApiJsonDeserializer}
*/
@Component
@RequiredArgsConstructor
public final class GLClosureCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<GLClosureCommand> {
private final FromJsonHelper fromApiJsonHelper;
@Override
public GLClosureCommand commandFromApiJson(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
final Set<String> supportedParameters = GLClosureJsonInputParams.getAllValues();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Long id = this.fromApiJsonHelper.extractLongNamed(GLClosureJsonInputParams.ID.getValue(), element);
final Long officeId = this.fromApiJsonHelper.extractLongNamed(GLClosureJsonInputParams.OFFICE_ID.getValue(), element);
final String comments = this.fromApiJsonHelper.extractStringNamed(GLClosureJsonInputParams.COMMENTS.getValue(), element);
final LocalDate closingDate = this.fromApiJsonHelper.extractLocalDateNamed(GLClosureJsonInputParams.CLOSING_DATE.getValue(),
element);
return new GLClosureCommand(id, officeId, closingDate, comments);
}
}
@@ -0,0 +1,30 @@
/**
* 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.accounting.closure.service;
import java.util.List;
import org.apache.fineract.accounting.closure.data.GLClosureData;
public interface GLClosureReadPlatformService {
List<GLClosureData> retrieveAllGLClosures(Long OfficeId);
GLClosureData retrieveGLClosureById(long glClosureId);
}
@@ -0,0 +1,108 @@
/**
* 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.accounting.closure.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.data.GLClosureData;
import org.apache.fineract.accounting.closure.exception.GLClosureNotFoundException;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class GLClosureReadPlatformServiceImpl implements GLClosureReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private static final class GLClosureMapper implements RowMapper<GLClosureData> {
public String schema() {
return " glClosure.id as id, glClosure.office_id as officeId,office.name as officeName ,glClosure.closing_date as closingDate,"
+ " glClosure.is_deleted as isDeleted, creatingUser.id as creatingUserId,creatingUser.username as creatingUserName,"
+ " updatingUser.id as updatingUserId,updatingUser.username as updatingUserName, glClosure.created_date as createdDate,"
+ " glClosure.lastmodified_date as updatedDate, glClosure.comments as comments "
+ " from acc_gl_closure as glClosure, m_appuser as creatingUser, m_appuser as updatingUser,m_office as office"
+ " where glClosure.createdby_id=creatingUser.id and "
+ " glClosure.lastmodifiedby_id=updatingUser.id and glClosure.office_id=office.id";
}
@Override
public GLClosureData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final Long officeId = rs.getLong("officeId");
final String officeName = rs.getString("officeName");
final LocalDate closingDate = JdbcSupport.getLocalDate(rs, "closingDate");
final Boolean deleted = rs.getBoolean("isDeleted");
final LocalDate createdDate = JdbcSupport.getLocalDate(rs, "createdDate");
final LocalDate lastUpdatedDate = JdbcSupport.getLocalDate(rs, "updatedDate");
final Long creatingByUserId = rs.getLong("creatingUserId");
final String createdByUserName = rs.getString("creatingUserName");
final Long lastUpdatedByUserId = rs.getLong("updatingUserId");
final String lastUpdatedByUserName = rs.getString("updatingUserName");
final String comments = rs.getString("comments");
return new GLClosureData(id, officeId, officeName, closingDate, deleted, createdDate, lastUpdatedDate, creatingByUserId,
createdByUserName, lastUpdatedByUserId, lastUpdatedByUserName, comments);
}
}
@Override
public List<GLClosureData> retrieveAllGLClosures(final Long officeId) {
final GLClosureMapper rm = new GLClosureMapper();
String sql = "select " + rm.schema() + " and glClosure.is_deleted = false";
final Object[] objectArray = new Object[1];
int arrayPos = 0;
if (officeId != null && officeId != 0) {
sql += " and glClosure.office_id = ?";
objectArray[arrayPos] = officeId;
arrayPos = arrayPos + 1;
}
sql = sql + " order by glClosure.closing_date desc";
final Object[] finalObjectArray = Arrays.copyOf(objectArray, arrayPos);
return this.jdbcTemplate.query(sql, rm, finalObjectArray); // NOSONAR
}
@Override
public GLClosureData retrieveGLClosureById(final long glClosureId) {
try {
final GLClosureMapper rm = new GLClosureMapper();
final String sql = "select " + rm.schema() + " and glClosure.id = ?";
final GLClosureData glAccountData = this.jdbcTemplate.queryForObject(sql, rm, new Object[] { glClosureId }); // NOSONAR
return glAccountData;
} catch (final EmptyResultDataAccessException e) {
throw new GLClosureNotFoundException(glClosureId, e);
}
}
}
@@ -0,0 +1,32 @@
/**
* 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.accounting.closure.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface GLClosureWritePlatformService {
CommandProcessingResult createGLClosure(JsonCommand command);
CommandProcessingResult updateGLClosure(Long glClosureId, JsonCommand command);
CommandProcessingResult deleteGLClosure(Long glClosureId);
}
@@ -0,0 +1,150 @@
/**
* 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.accounting.closure.service;
import java.time.LocalDate;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.closure.api.GLClosureJsonInputParams;
import org.apache.fineract.accounting.closure.command.GLClosureCommand;
import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.closure.domain.GLClosureRepository;
import org.apache.fineract.accounting.closure.exception.GLClosureDuplicateException;
import org.apache.fineract.accounting.closure.exception.GLClosureInvalidDeleteException;
import org.apache.fineract.accounting.closure.exception.GLClosureInvalidException;
import org.apache.fineract.accounting.closure.exception.GLClosureInvalidException.GlClosureInvalidReason;
import org.apache.fineract.accounting.closure.exception.GLClosureNotFoundException;
import org.apache.fineract.accounting.closure.serialization.GLClosureCommandFromApiJsonDeserializer;
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.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Slf4j
public class GLClosureWritePlatformServiceJpaRepositoryImpl implements GLClosureWritePlatformService {
private final GLClosureRepository glClosureRepository;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final GLClosureCommandFromApiJsonDeserializer fromApiJsonDeserializer;
@Transactional
@Override
public CommandProcessingResult createGLClosure(final JsonCommand command) {
try {
final GLClosureCommand closureCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json());
closureCommand.validateForCreate();
// check office is valid
final Long officeId = command.longValueOfParameterNamed(GLClosureJsonInputParams.OFFICE_ID.getValue());
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
// TODO: Get Tenant specific date
// ensure closure date is not in the future
final LocalDate todaysDate = DateUtils.getBusinessLocalDate();
final LocalDate closureDate = command.localDateValueOfParameterNamed(GLClosureJsonInputParams.CLOSING_DATE.getValue());
if (closureDate.isAfter(todaysDate)) {
throw new GLClosureInvalidException(GlClosureInvalidReason.FUTURE_DATE, closureDate);
}
// shouldn't be before an existing accounting closure
final GLClosure latestGLClosure = this.glClosureRepository.getLatestGLClosureByBranch(officeId);
if (latestGLClosure != null) {
if (latestGLClosure.getClosingDate().isAfter(closureDate)) {
throw new GLClosureInvalidException(GlClosureInvalidReason.ACCOUNTING_CLOSED, latestGLClosure.getClosingDate());
}
}
final GLClosure glClosure = GLClosure.fromJson(office, command);
this.glClosureRepository.saveAndFlush(glClosure);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withOfficeId(officeId)
.withEntityId(glClosure.getId()).build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleGLClosureIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
}
@Transactional
@Override
public CommandProcessingResult updateGLClosure(final Long glClosureId, final JsonCommand command) {
final GLClosureCommand closureCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json());
closureCommand.validateForUpdate();
// is the glClosure valid
final GLClosure glClosure = this.glClosureRepository.findById(glClosureId)
.orElseThrow(() -> new GLClosureNotFoundException(glClosureId));
final Map<String, Object> changesOnly = glClosure.update(command);
if (!changesOnly.isEmpty()) {
this.glClosureRepository.saveAndFlush(glClosure);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withOfficeId(glClosure.getOffice().getId())
.withEntityId(glClosure.getId()).with(changesOnly).build();
}
@Transactional
@Override
public CommandProcessingResult deleteGLClosure(final Long glClosureId) {
final GLClosure glClosure = this.glClosureRepository.findById(glClosureId)
.orElseThrow(() -> new GLClosureNotFoundException(glClosureId));
/**
* check if any closures are present for this branch at a later date than this closure date
**/
final LocalDate closureDate = glClosure.getClosingDate();
final GLClosure latestGLClosure = this.glClosureRepository.getLatestGLClosureByBranch(glClosure.getOffice().getId());
if (latestGLClosure.getClosingDate().isAfter(closureDate)) {
throw new GLClosureInvalidDeleteException(latestGLClosure.getOffice().getId(), latestGLClosure.getOffice().getName(),
latestGLClosure.getClosingDate());
}
this.glClosureRepository.delete(glClosure);
return new CommandProcessingResultBuilder().withOfficeId(glClosure.getOffice().getId()).withEntityId(glClosure.getId()).build();
}
/**
* @param command
* @param dve
* @param dve
*/
private void handleGLClosureIntegrityIssues(final JsonCommand command, final Throwable realCause, NonTransientDataAccessException dve) {
if (realCause.getMessage().contains("office_id_closing_date")) {
throw new GLClosureDuplicateException(command.longValueOfParameterNamed(GLClosureJsonInputParams.OFFICE_ID.getValue()),
command.localDateValueOfParameterNamed(GLClosureJsonInputParams.CLOSING_DATE.getValue()));
}
log.error("Error occured.", dve);
throw new PlatformDataIntegrityException("error.msg.glClosure.unknown.data.integrity.issue",
"Unknown data integrity issue with resource GL Closure: " + realCause.getMessage());
}
}
@@ -0,0 +1,50 @@
/**
* 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.accounting.common;
import java.util.List;
import java.util.Map;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
public interface AccountingDropdownReadPlatformService {
List<EnumOptionData> retrieveGLAccountTypeOptions();
List<EnumOptionData> retrieveGLAccountUsageOptions();
List<EnumOptionData> retrieveJournalEntryTypeOptions();
List<EnumOptionData> retrieveAccountingRuleTypeOptions();
Map<String, List<GLAccountData>> retrieveAccountMappingOptionsForLoanProducts();
Map<String, List<GLAccountData>> retrieveAccountMappingOptionsForSavingsProducts();
Map<String, List<GLAccountData>> retrieveAccountMappingOptionsForCharges();
Map<String, List<GLAccountData>> retrieveAccountMappingOptions();
Map<String, List<GLAccountData>> retrieveAccountMappingOptionsForShareProducts();
List<GLAccountData> retrieveExpenseAccountOptions();
List<GLAccountData> retrieveAssetAccountOptions();
}
@@ -0,0 +1,178 @@
/**
* 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.accounting.financialactivityaccount.api;
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.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.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.financialactivityaccount.data.FinancialActivityAccountData;
import org.apache.fineract.accounting.financialactivityaccount.service.FinancialActivityAccountReadPlatformService;
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.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
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.springframework.stereotype.Component;
@Path("/v1/financialactivityaccounts")
@Component
@Tag(name = "Mapping Financial Activities to Accounts", description = "Organization Level Financial Activities like Asset and Liability Transfer can be mapped to GL Account. Integrated accounting takes these accounts into consideration when an Account transfer is made between a savings to loan/savings account and vice-versa\n"
+ "\n" + "Field Descriptions\n" + "financialActivityId\n" + "The identifier of the Financial Activity\n" + "glAccountId\n"
+ "The identifier of a GL Account ( Ledger Account) which shall be used as the default account for the selected Financial Activity")
@RequiredArgsConstructor
public class FinancialActivityAccountsApiResource {
private final PlatformSecurityContext context;
private final FinancialActivityAccountReadPlatformService financialActivityAccountReadPlatformService;
private final DefaultToApiJsonSerializer<FinancialActivityAccountData> apiJsonSerializerService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String retrieveTemplate(@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(FinancialActivityAccountsConstants.resourceNameForPermission);
FinancialActivityAccountData financialActivityAccountData = this.financialActivityAccountReadPlatformService
.getFinancialActivityAccountTemplate();
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serialize(settings, financialActivityAccountData,
FinancialActivityAccountsConstants.RESPONSE_DATA_PARAMETERS);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Financial Activities to Accounts Mappings", description = "Example Requests:\n" + "\n"
+ "financialactivityaccounts")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.GetFinancialActivityAccountsResponse.class)))) })
public String retrieveAll(@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(FinancialActivityAccountsConstants.resourceNameForPermission);
final List<FinancialActivityAccountData> financialActivityAccounts = this.financialActivityAccountReadPlatformService.retrieveAll();
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serialize(settings, financialActivityAccounts,
FinancialActivityAccountsConstants.RESPONSE_DATA_PARAMETERS);
}
@GET
@Path("{mappingId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a Financial Activity to Account Mapping\n", description = "Example Requests:\n" + "\n"
+ "financialactivityaccounts/1")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.GetFinancialActivityAccountsResponse.class))) })
public String retreive(@PathParam("mappingId") @Parameter(description = "mappingId") final Long mappingId,
@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(FinancialActivityAccountsConstants.resourceNameForPermission);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
FinancialActivityAccountData financialActivityAccountData = this.financialActivityAccountReadPlatformService.retrieve(mappingId);
if (settings.isTemplate()) {
financialActivityAccountData = this.financialActivityAccountReadPlatformService
.addTemplateDetails(financialActivityAccountData);
}
return this.apiJsonSerializerService.serialize(settings, financialActivityAccountData,
FinancialActivityAccountsConstants.RESPONSE_DATA_PARAMETERS);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create a new Financial Activity to Accounts Mapping", description = "Mandatory Fields\n"
+ "financialActivityId, glAccountId")
@RequestBody(content = @Content(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.PostFinancialActivityAccountsRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.PostFinancialActivityAccountsResponse.class))) })
public String createGLAccount(@Parameter(hidden = true) final String jsonRequestBody) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createOfficeToGLAccountMapping().withJson(jsonRequestBody)
.build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.apiJsonSerializerService.serialize(result);
}
@PUT
@Path("{mappingId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a Financial Activity to Account Mapping", description = "the API updates the Ledger account linked to a Financial Activity \n")
@RequestBody(content = @Content(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.PostFinancialActivityAccountsRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.PutFinancialActivityAccountsResponse.class))) })
public String updateGLAccount(@PathParam("mappingId") @Parameter(description = "mappingId") final Long mappingId,
@Parameter(hidden = true) final String jsonRequestBody) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateOfficeToGLAccountMapping(mappingId)
.withJson(jsonRequestBody).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.apiJsonSerializerService.serialize(result);
}
@DELETE
@Path("{mappingId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a Financial Activity to Account Mapping")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FinancialActivityAccountsApiResourceSwagger.DeleteFinancialActivityAccountsResponse.class))) })
public String deleteGLAccount(@PathParam("mappingId") @Parameter(description = "mappingId") final Long mappingId) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteOfficeToGLAccountMapping(mappingId).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.apiJsonSerializerService.serialize(result);
}
}
@@ -0,0 +1,114 @@
/**
* 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.accounting.financialactivityaccount.api;
import io.swagger.v3.oas.annotations.media.Schema;
import org.apache.fineract.accounting.financialactivityaccount.data.FinancialActivityData;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
/**
* Created by sanyam on 24/7/17.
*/
final class FinancialActivityAccountsApiResourceSwagger {
private FinancialActivityAccountsApiResourceSwagger() {}
@Schema(description = "GetFinancialActivityAccountsResponse")
public static final class GetFinancialActivityAccountsResponse {
private GetFinancialActivityAccountsResponse() {
}
@Schema(example = "1")
public Long id;
public FinancialActivityData financialActivityData;
public GLAccountData glAccountData;
}
@Schema(description = "PostFinancialActivityAccountsRequest")
public static final class PostFinancialActivityAccountsRequest {
private PostFinancialActivityAccountsRequest() {
}
@Schema(example = "200")
public Long financialActivityId;
@Schema(example = "2")
public Long glAccountId;
}
@Schema(description = "PostFinancialActivityAccountsResponse")
public static final class PostFinancialActivityAccountsResponse {
private PostFinancialActivityAccountsResponse() {
}
@Schema(example = "1")
public Long resourceId;
}
@Schema(description = "PutFinancialActivityAccountsRequest")
public static final class PutFinancialActivityAccountsRequest {
private PutFinancialActivityAccountsRequest() {
}
@Schema(example = "200")
public Long financialActivityId;
@Schema(example = "3")
public Long glAccountId;
}
@Schema(description = "PutFinancialActivityAccountsResponse")
public static final class PutFinancialActivityAccountsResponse {
private PutFinancialActivityAccountsResponse() {
}
public static final class PutFinancialActivityAccountscommentsSwagger {
private PutFinancialActivityAccountscommentsSwagger() {}
@Schema(example = "1")
public Long glAccountId;
}
@Schema(example = "1")
public Long resourceId;
public PutFinancialActivityAccountscommentsSwagger comments;
}
@Schema(description = "DeleteFinancialActivityAccountsResponse")
public static final class DeleteFinancialActivityAccountsResponse {
private DeleteFinancialActivityAccountsResponse() {
}
@Schema(example = "1")
public Long resourceId;
}
}
@@ -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.accounting.financialactivityaccount.api;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public final class FinancialActivityAccountsConstants {
private FinancialActivityAccountsConstants() {
}
private static final String idParamName = "id";
private static final String factivityDataParamName = "financialActivityData";
private static final String glAccountDataParamName = "glAccountData";
private static final String glAccountOptionsParamName = "glAccountOptions";
private static final String financialActivityOptionsParamName = "financialActivityOptions";
public static final String resourceNameForPermission = "FINANCIALACTIVITYACCOUNT";
static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(idParamName, factivityDataParamName,
glAccountDataParamName, glAccountOptionsParamName, financialActivityOptionsParamName));
}
@@ -0,0 +1,57 @@
/**
* 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.accounting.financialactivityaccount.api;
import java.util.HashSet;
import java.util.Set;
/***
* Enum of all parameters passed in while creating/updating a GL Account
***/
public enum FinancialActivityAccountsJsonInputParams {
FINANCIAL_ACTIVITY_ID("financialActivityId"), GL_ACCOUNT_ID("glAccountId");
private final String value;
FinancialActivityAccountsJsonInputParams(final String value) {
this.value = value;
}
private static final Set<String> values = new HashSet<>();
static {
for (final FinancialActivityAccountsJsonInputParams type : FinancialActivityAccountsJsonInputParams.values()) {
values.add(type.value);
}
}
public static Set<String> getAllValues() {
return values;
}
@Override
public String toString() {
return name().toString().replaceAll("_", " ");
}
public String getValue() {
return this.value;
}
}
@@ -0,0 +1,44 @@
/**
* 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.accounting.financialactivityaccount.data;
import java.util.List;
import java.util.Map;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
@RequiredArgsConstructor
@Data
public class FinancialActivityAccountData {
private final Long id;
private final FinancialActivityData financialActivityData;
private final GLAccountData glAccountData;
private Map<String, List<GLAccountData>> glAccountOptions;
private List<FinancialActivityData> financialActivityOptions;
public FinancialActivityAccountData() {
this.id = null;
this.glAccountData = null;
this.financialActivityData = null;
this.glAccountOptions = null;
this.financialActivityOptions = null;
}
}
@@ -0,0 +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.accounting.financialactivityaccount.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 lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
@Entity
@Table(name = "acc_gl_financial_activity_account")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
public class FinancialActivityAccount extends AbstractPersistableCustom {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "gl_account_id")
private GLAccount glAccount;
@Column(name = "financial_activity_type", nullable = false)
private Integer financialActivityType;
public static FinancialActivityAccount createNew(final GLAccount glAccount, final Integer financialAccountType) {
return new FinancialActivityAccount(glAccount, financialAccountType);
}
public void updateGlAccount(final GLAccount glAccount) {
this.glAccount = glAccount;
}
public void updateFinancialActivityType(final Integer financialActivityType) {
this.financialActivityType = financialActivityType;
}
}
@@ -0,0 +1,32 @@
/**
* 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.accounting.financialactivityaccount.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface FinancialActivityAccountRepository
extends JpaRepository<FinancialActivityAccount, Long>, JpaSpecificationExecutor<FinancialActivityAccount> {
@Query("select faa from FinancialActivityAccount faa where faa.financialActivityType = :financialActivityType")
FinancialActivityAccount findByFinancialActivityType(@Param("financialActivityType") int financialAccountType);
}
@@ -0,0 +1,68 @@
/**
* 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.accounting.financialactivityaccount.domain;
import java.util.List;
import org.apache.fineract.accounting.financialactivityaccount.exception.FinancialActivityAccountNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* Wrapper for {@link FinancialActivityAccountRepository} that adds NULL checking and Error handling capabilities
* </p>
*/
@Service
public class FinancialActivityAccountRepositoryWrapper {
private final FinancialActivityAccountRepository repository;
@Autowired
public FinancialActivityAccountRepositoryWrapper(final FinancialActivityAccountRepository repository) {
this.repository = repository;
}
public FinancialActivityAccount findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new FinancialActivityAccountNotFoundException(id));
}
public FinancialActivityAccount findByFinancialActivityTypeWithNotFoundDetection(final int financialActivityType) {
FinancialActivityAccount financialActivityAccount = this.repository.findByFinancialActivityType(financialActivityType);
if (financialActivityAccount == null) {
throw new FinancialActivityAccountNotFoundException(financialActivityType);
}
return financialActivityAccount;
}
public List<FinancialActivityAccount> findAll() {
return this.repository.findAll();
}
public void save(final FinancialActivityAccount entity) {
this.repository.save(entity);
}
public void saveAndFlush(final FinancialActivityAccount entity) {
this.repository.saveAndFlush(entity);
}
public void delete(final FinancialActivityAccount entity) {
this.repository.delete(entity);
}
}
@@ -0,0 +1,38 @@
/**
* 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.accounting.financialactivityaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when product to GL account mapping are not found.
*/
public class DuplicateFinancialActivityAccountFoundException extends AbstractPlatformDomainRuleException {
private static final String errorCode = "error.msg.financialActivityAccount.exists";
public DuplicateFinancialActivityAccountFoundException(final Integer financialActivityType) {
super(errorCode, "Mapping for activity already exists " + financialActivityType, financialActivityType);
}
public static String getErrorcode() {
return errorCode;
}
}
@@ -0,0 +1,45 @@
/**
* 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.accounting.financialactivityaccount.exception;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when product to GL account mapping are not found.
*/
public class FinancialActivityAccountInvalidException extends AbstractPlatformDomainRuleException {
private static final String errorCode = "error.msg.financialActivityAccount.invalid";
public FinancialActivityAccountInvalidException(final FinancialActivity financialActivity, final GLAccount glAccount) {
super(errorCode,
"Financial Activity '" + financialActivity.getCode() + "' with Id :" + financialActivity.getValue()
+ "' can only be associated with a Ledger Account of Type " + financialActivity.getMappedGLAccountType().getCode()
+ " the provided Ledger Account '" + glAccount.getName() + "(" + glAccount.getGlCode()
+ ")' does not of the required type",
financialActivity.getCode(), financialActivity.getValue(), financialActivity.getMappedGLAccountType().getCode(),
glAccount.getName(), glAccount.getGlCode());
}
public static String getErrorcode() {
return errorCode;
}
}
@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.financialactivityaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
/**
* A {@link RuntimeException} thrown when product to GL account mapping are not found.
*/
public class FinancialActivityAccountNotFoundException extends AbstractPlatformResourceNotFoundException {
public FinancialActivityAccountNotFoundException(final Long id) {
super("error.msg.financialActivityAccount.not.found", "Financial Activity account with Id " + id + " does not exist", id);
}
public FinancialActivityAccountNotFoundException(final Long id, EmptyResultDataAccessException e) {
super("error.msg.financialActivityAccount.not.found", "Financial Activity account with Id " + id + " does not exist", id, e);
}
public FinancialActivityAccountNotFoundException(final Integer financialActivityType) {
super("error.msg.financialActivityAccount.not.found",
"Financial Activity account with for the financial Activity with Id " + financialActivityType + " does not exist",
financialActivityType);
}
}
@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.financialactivityaccount.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.financialactivityaccount.service.FinancialActivityAccountWritePlatformService;
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.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "FINANCIALACTIVITYACCOUNT", action = "CREATE")
@RequiredArgsConstructor
public class CreateFinancialActivityAccountHandler implements NewCommandSourceHandler {
private final FinancialActivityAccountWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createFinancialActivityAccountMapping(command);
}
}
@@ -0,0 +1,42 @@
/**
* 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.accounting.financialactivityaccount.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.financialactivityaccount.service.FinancialActivityAccountWritePlatformService;
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.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "FINANCIALACTIVITYACCOUNT", action = "DELETE")
@RequiredArgsConstructor
public class DeleteFinancialActivityAccountCommandHandler implements NewCommandSourceHandler {
private final FinancialActivityAccountWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deleteGLAccountActivityMapping(command.entityId(), command);
}
}
@@ -0,0 +1,42 @@
/**
* 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.accounting.financialactivityaccount.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.financialactivityaccount.service.FinancialActivityAccountWritePlatformService;
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.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "FINANCIALACTIVITYACCOUNT", action = "UPDATE")
@RequiredArgsConstructor
public class UpdateFinancialActivityAccountCommandHandler implements NewCommandSourceHandler {
private final FinancialActivityAccountWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateGLAccountActivityMapping(command.entityId(), command);
}
}
@@ -0,0 +1,120 @@
/**
* 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.accounting.financialactivityaccount.serialization;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.accounting.financialactivityaccount.api.FinancialActivityAccountsJsonInputParams;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public final class FinancialActivityAccountDataValidator {
/**
* The parameters supported for this command.
*/
private final Set<String> supportedParameters = FinancialActivityAccountsJsonInputParams.getAllValues();
private final String paramNameForFinancialActivity = FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue();
private final String paramNameForGLAccount = FinancialActivityAccountsJsonInputParams.GL_ACCOUNT_ID.getValue();
private final FromJsonHelper fromApiJsonHelper;
public void validateForCreate(final String json) {
validateJSONAndCheckForUnsupportedParams(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = getDataValidator(dataValidationErrors);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Integer financialActivityId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(paramNameForFinancialActivity, element);
baseDataValidator.reset().parameter(paramNameForFinancialActivity).value(financialActivityId).notNull().isOneOfTheseValues(
FinancialActivity.ASSET_TRANSFER.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(),
FinancialActivity.CASH_AT_MAINVAULT.getValue(), FinancialActivity.CASH_AT_TELLER.getValue(),
FinancialActivity.OPENING_BALANCES_TRANSFER_CONTRA.getValue(), FinancialActivity.ASSET_FUND_SOURCE.getValue(),
FinancialActivity.PAYABLE_DIVIDENDS.getValue());
final Long glAccountId = this.fromApiJsonHelper.extractLongNamed(paramNameForGLAccount, element);
baseDataValidator.reset().parameter(paramNameForGLAccount).value(glAccountId).notNull().integerGreaterThanZero();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private DataValidatorBuilder getDataValidator(final List<ApiParameterError> dataValidationErrors) {
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("financialactivityaccount");
return baseDataValidator;
}
public void validateForUpdate(final String json) {
validateJSONAndCheckForUnsupportedParams(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = getDataValidator(dataValidationErrors);
final JsonElement element = this.fromApiJsonHelper.parse(json);
if (this.fromApiJsonHelper.parameterExists(paramNameForFinancialActivity, element)) {
final Integer financialActivityId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(paramNameForFinancialActivity,
element);
baseDataValidator.reset().parameter(paramNameForFinancialActivity).value(financialActivityId).ignoreIfNull().isOneOfTheseValues(
FinancialActivity.ASSET_TRANSFER.getValue(), FinancialActivity.LIABILITY_TRANSFER.getValue(),
FinancialActivity.OPENING_BALANCES_TRANSFER_CONTRA.getValue(), FinancialActivity.ASSET_FUND_SOURCE.getValue(),
FinancialActivity.PAYABLE_DIVIDENDS.getValue());
}
if (this.fromApiJsonHelper.parameterExists(paramNameForGLAccount, element)) {
final Long glAccountId = this.fromApiJsonHelper.extractLongNamed(paramNameForGLAccount, element);
baseDataValidator.reset().parameter(paramNameForGLAccount).value(glAccountId).ignoreIfNull().integerGreaterThanZero();
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void validateJSONAndCheckForUnsupportedParams(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.supportedParameters);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -0,0 +1,34 @@
/**
* 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.accounting.financialactivityaccount.service;
import java.util.List;
import org.apache.fineract.accounting.financialactivityaccount.data.FinancialActivityAccountData;
public interface FinancialActivityAccountReadPlatformService {
List<FinancialActivityAccountData> retrieveAll();
FinancialActivityAccountData retrieve(Long mappingId);
FinancialActivityAccountData addTemplateDetails(FinancialActivityAccountData financialActivityAccountData);
FinancialActivityAccountData getFinancialActivityAccountTemplate();
}
@@ -0,0 +1,115 @@
/**
* 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.accounting.financialactivityaccount.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.accounting.common.AccountingDropdownReadPlatformService;
import org.apache.fineract.accounting.financialactivityaccount.data.FinancialActivityAccountData;
import org.apache.fineract.accounting.financialactivityaccount.data.FinancialActivityData;
import org.apache.fineract.accounting.financialactivityaccount.exception.FinancialActivityAccountNotFoundException;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class FinancialActivityAccountReadPlatformServiceImpl implements FinancialActivityAccountReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private final AccountingDropdownReadPlatformService accountingDropdownReadPlatformService;
private final FinancialActivityAccountMapper financialActivityAccountMapper = new FinancialActivityAccountMapper();
@Override
public List<FinancialActivityAccountData> retrieveAll() {
String sql = "select " + financialActivityAccountMapper.schema();
return this.jdbcTemplate.query(sql, financialActivityAccountMapper, new Object[] {}); // NOSONAR
}
@Override
public FinancialActivityAccountData retrieve(Long financialActivityAccountId) {
try {
StringBuilder sqlBuilder = new StringBuilder(200);
sqlBuilder.append("select ");
sqlBuilder.append(this.financialActivityAccountMapper.schema());
sqlBuilder.append(" where faa.id=?");
return this.jdbcTemplate.queryForObject(sqlBuilder.toString(), this.financialActivityAccountMapper,
new Object[] { financialActivityAccountId });
} catch (final EmptyResultDataAccessException e) {
throw new FinancialActivityAccountNotFoundException(financialActivityAccountId, e);
}
}
@Override
public FinancialActivityAccountData addTemplateDetails(FinancialActivityAccountData financialActivityAccountData) {
final Map<String, List<GLAccountData>> accountOptions = this.accountingDropdownReadPlatformService.retrieveAccountMappingOptions();
financialActivityAccountData.setGlAccountOptions(accountOptions);
financialActivityAccountData.setFinancialActivityOptions(FinancialActivity.getAllFinancialActivities());
return financialActivityAccountData;
}
@Override
public FinancialActivityAccountData getFinancialActivityAccountTemplate() {
FinancialActivityAccountData financialActivityAccountData = new FinancialActivityAccountData();
return addTemplateDetails(financialActivityAccountData);
}
private static final class FinancialActivityAccountMapper implements RowMapper<FinancialActivityAccountData> {
private final String sql;
FinancialActivityAccountMapper() {
StringBuilder sb = new StringBuilder(300);
sb.append(
" faa.id as id, faa.financial_activity_type as financialActivityId, glaccount.id as glAccountId,glaccount.name as glAccountName,glaccount.gl_code as glCode ");
sb.append(" from acc_gl_financial_activity_account faa ");
sb.append(" join acc_gl_account glaccount on glaccount.id = faa.gl_account_id");
sql = sb.toString();
}
public String schema() {
return sql;
}
@Override
public FinancialActivityAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = JdbcSupport.getLong(rs, "id");
final Long glAccountId = JdbcSupport.getLong(rs, "glAccountId");
final Integer financialActivityId = JdbcSupport.getInteger(rs, "financialActivityId");
final String glAccountName = rs.getString("glAccountName");
final String glCode = rs.getString("glCode");
final GLAccountData glAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode);
final FinancialActivityData financialActivityData = FinancialActivity.toFinancialActivityData(financialActivityId);
final FinancialActivityAccountData financialActivityAccountData = new FinancialActivityAccountData(id, financialActivityData,
glAccountData);
return financialActivityAccountData;
}
}
}
@@ -0,0 +1,32 @@
/**
* 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.accounting.financialactivityaccount.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface FinancialActivityAccountWritePlatformService {
CommandProcessingResult createFinancialActivityAccountMapping(JsonCommand command);
CommandProcessingResult updateGLAccountActivityMapping(Long mappingId, JsonCommand command);
CommandProcessingResult deleteGLAccountActivityMapping(Long mappingId, JsonCommand command);
}
@@ -0,0 +1,190 @@
/**
* 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.accounting.financialactivityaccount.service;
import jakarta.persistence.PersistenceException;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.accounting.financialactivityaccount.api.FinancialActivityAccountsJsonInputParams;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
import org.apache.fineract.accounting.financialactivityaccount.exception.DuplicateFinancialActivityAccountFoundException;
import org.apache.fineract.accounting.financialactivityaccount.exception.FinancialActivityAccountInvalidException;
import org.apache.fineract.accounting.financialactivityaccount.serialization.FinancialActivityAccountDataValidator;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
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.PlatformDataIntegrityException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class FinancialActivityAccountWritePlatformServiceImpl implements FinancialActivityAccountWritePlatformService {
private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository;
private final FinancialActivityAccountDataValidator fromApiJsonDeserializer;
private final GLAccountRepositoryWrapper glAccountRepositoryWrapper;
@Override
public CommandProcessingResult createFinancialActivityAccountMapping(JsonCommand command) {
try {
this.fromApiJsonDeserializer.validateForCreate(command.json());
final Integer financialActivityId = command
.integerValueSansLocaleOfParameterNamed(FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue());
final Long accountId = command.longValueOfParameterNamed(FinancialActivityAccountsJsonInputParams.GL_ACCOUNT_ID.getValue());
final GLAccount glAccount = glAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
FinancialActivityAccount financialActivityAccount = FinancialActivityAccount.createNew(glAccount, financialActivityId);
validateFinancialActivityAndAccountMapping(financialActivityAccount);
this.financialActivityAccountRepository.saveAndFlush(financialActivityAccount);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(financialActivityAccount.getId()) //
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleFinancialActivityAccountDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return CommandProcessingResult.empty();
} catch (final PersistenceException ee) {
final Throwable throwable = ExceptionUtils.getRootCause(ee.getCause());
handleFinancialActivityAccountDataIntegrityIssues(command, throwable, ee);
return CommandProcessingResult.empty();
}
}
/**
* Validate that the GL Account is appropriate for the particular Financial Activity Type
**/
private void validateFinancialActivityAndAccountMapping(FinancialActivityAccount financialActivityAccount) {
FinancialActivity financialActivity = FinancialActivity.fromInt(financialActivityAccount.getFinancialActivityType());
GLAccount glAccount = financialActivityAccount.getGlAccount();
if (!financialActivity.getMappedGLAccountType().getValue().equals(glAccount.getType())) {
throw new FinancialActivityAccountInvalidException(financialActivity, glAccount);
}
}
@Override
public CommandProcessingResult updateGLAccountActivityMapping(Long financialActivityAccountId, JsonCommand command) {
try {
this.fromApiJsonDeserializer.validateForUpdate(command.json());
final FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
.findOneWithNotFoundDetection(financialActivityAccountId);
Map<String, Object> changes = findChanges(command, financialActivityAccount);
if (changes.containsKey(FinancialActivityAccountsJsonInputParams.GL_ACCOUNT_ID.getValue())) {
final Long accountId = command.longValueOfParameterNamed(FinancialActivityAccountsJsonInputParams.GL_ACCOUNT_ID.getValue());
final GLAccount glAccount = glAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
financialActivityAccount.updateGlAccount(glAccount);
}
if (changes.containsKey(FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue())) {
final Integer financialActivityId = command
.integerValueSansLocaleOfParameterNamed(FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue());
financialActivityAccount.updateFinancialActivityType(financialActivityId);
}
if (!changes.isEmpty()) {
validateFinancialActivityAndAccountMapping(financialActivityAccount);
this.financialActivityAccountRepository.saveAndFlush(financialActivityAccount);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(financialActivityAccountId) //
.with(changes) //
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleFinancialActivityAccountDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return CommandProcessingResult.empty();
} catch (final PersistenceException ee) {
Throwable throwable = ExceptionUtils.getRootCause(ee.getCause());
handleFinancialActivityAccountDataIntegrityIssues(command, throwable, ee);
return CommandProcessingResult.empty();
}
}
@Override
public CommandProcessingResult deleteGLAccountActivityMapping(Long financialActivityAccountId, JsonCommand command) {
final FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
.findOneWithNotFoundDetection(financialActivityAccountId);
this.financialActivityAccountRepository.delete(financialActivityAccount);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(financialActivityAccountId) //
.build();
}
private void handleFinancialActivityAccountDataIntegrityIssues(final JsonCommand command, final Throwable realCause,
final NonTransientDataAccessException dve) {
if (realCause.getMessage().contains("financial_activity_type")) {
final Integer financialActivityId = command
.integerValueSansLocaleOfParameterNamed(FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue());
throw new DuplicateFinancialActivityAccountFoundException(financialActivityId);
}
log.error("Error occured.", dve);
throw new PlatformDataIntegrityException("error.msg.glAccount.unknown.data.integrity.issue",
"Unknown data integrity issue with resource GL Account: " + realCause.getMessage());
}
private void handleFinancialActivityAccountDataIntegrityIssues(JsonCommand command, Throwable realCause, PersistenceException dve) {
if (realCause.getMessage().contains("financial_activity_type")) {
final Integer financialActivityId = command
.integerValueSansLocaleOfParameterNamed(FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue());
throw new DuplicateFinancialActivityAccountFoundException(financialActivityId);
}
log.error("Error occured.", dve);
throw new PlatformDataIntegrityException("error.msg.glAccount.unknown.data.integrity.issue",
"Unknown data integrity issue with resource GL Account: " + realCause.getMessage());
}
public Map<String, Object> findChanges(JsonCommand command, FinancialActivityAccount financialActivityAccount) {
Map<String, Object> changes = new HashMap<>();
Long existingGLAccountId = financialActivityAccount.getGlAccount().getId();
Integer financialActivityType = financialActivityAccount.getFinancialActivityType();
// is the account Id changed?
if (command.isChangeInLongParameterNamed(FinancialActivityAccountsJsonInputParams.GL_ACCOUNT_ID.getValue(), existingGLAccountId)) {
final Long newValue = command.longValueOfParameterNamed(FinancialActivityAccountsJsonInputParams.GL_ACCOUNT_ID.getValue());
changes.put(FinancialActivityAccountsJsonInputParams.GL_ACCOUNT_ID.getValue(), newValue);
}
// is the financial Activity changed
if (command.isChangeInIntegerSansLocaleParameterNamed(FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue(),
financialActivityType)) {
final Integer newValue = command
.integerValueSansLocaleOfParameterNamed(FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue());
changes.put(FinancialActivityAccountsJsonInputParams.FINANCIAL_ACTIVITY_ID.getValue(), newValue);
}
return changes;
}
}
@@ -0,0 +1,291 @@
/**
* 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.accounting.glaccount.api;
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.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.HashSet;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingConstants;
import org.apache.fineract.accounting.common.AccountingDropdownReadPlatformService;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.glaccount.service.GLAccountReadPlatformService;
import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData;
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.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.data.UploadRequest;
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.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.springframework.stereotype.Component;
@Path("/v1/glaccounts")
@Component
@Tag(name = "General Ledger Account", description = "Ledger accounts represent an Individual account within an Organizations Chart Of Accounts(COA) and are assigned a name and unique number by which they can be identified. \n"
+ "All transactions relating to a company's assets, liabilities, owners' equity, revenue and expenses are recorded against these accounts")
@RequiredArgsConstructor
public class GLAccountsApiResource {
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList("id", "name", "parentId", "glCode", "disabled",
"manualEntriesAllowed", "type", "usage", "description", "assetHeaderAccountOptions", "liabilityHeaderAccountOptions",
"equityHeaderAccountOptions", "incomeHeaderAccountOptions", "expenseHeaderAccountOptions", "nameDecorated", "tagId",
"allowedAssetsTagOptions", "allowedLiabilitiesTagOptions", "allowedEquityTagOptions", "allowedIncomeTagOptions",
"allowedExpensesTagOptions", "creditAccounts", "debitAccounts"));
private final String resourceNameForPermission = "GLACCOUNT";
private final PlatformSecurityContext context;
private final GLAccountReadPlatformService glAccountReadPlatformService;
private final DefaultToApiJsonSerializer<GLAccountData> apiJsonSerializerService;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final AccountingDropdownReadPlatformService dropdownReadPlatformService;
private final CodeValueReadPlatformService codeValueReadPlatformService;
private final BulkImportWorkbookService bulkImportWorkbookService;
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = {
"General Ledger Account" }, summary = "Retrieve GL Accounts Template", description = "This is a convenience resource. It can be useful when building maintenance user interface screens for client applications. The template data returned consists of any or all of:\n"
+ "\n" + "Field Defaults\n" + "Allowed Value Lists\n" + "Example Request:\n" + "\n" + "glaccounts/template\n"
+ "glaccounts/template?type=1\n" + "\n" + "type is optional and integer value from 1 to 5.\n" + "\n" + "1.Assets \n"
+ "2.Liabilities \n" + "3.Equity \n" + "4.Income \n" + "5.Expenses")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.GetGLAccountsTemplateResponse.class))) })
public String retrieveNewAccountDetails(@Context final UriInfo uriInfo,
@QueryParam("type") @Parameter(description = "type") final Integer type) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission);
GLAccountData glAccountData = this.glAccountReadPlatformService.retrieveNewGLAccountDetails(type);
glAccountData = handleTemplate(glAccountData);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serialize(settings, glAccountData, RESPONSE_DATA_PARAMETERS);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = { "General Ledger Account" }, summary = "List General Ledger Accounts", description = "ARGUMENTS\n"
+ "type Integer optional manualEntriesAllowed boolean optional usage Integer optional disabled boolean optional parentId Long optional tagId Long optional\n"
+ "Example Requests:\n" + "\n" + "glaccounts\n" + "\n" + "\n"
+ "glaccounts?type=1&manualEntriesAllowed=true&usage=1&disabled=false\n" + "\n" + "glaccounts?fetchRunningBalance=true")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = GLAccountsApiResourceSwagger.GetGLAccountsResponse.class)))) })
public String retrieveAllAccounts(@Context final UriInfo uriInfo,
@QueryParam("type") @Parameter(description = "type") final Integer type,
@QueryParam("searchParam") @Parameter(description = "searchParam") final String searchParam,
@QueryParam("usage") @Parameter(description = "usage") final Integer usage,
@QueryParam("manualEntriesAllowed") @Parameter(description = "manualEntriesAllowed") final Boolean manualEntriesAllowed,
@QueryParam("disabled") @Parameter(description = "disabled") final Boolean disabled,
@QueryParam("fetchRunningBalance") @Parameter(description = "fetchRunningBalance") final boolean runningBalance) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission);
JournalEntryAssociationParametersData associationParametersData = new JournalEntryAssociationParametersData(false, runningBalance);
final List<GLAccountData> glAccountDatas = this.glAccountReadPlatformService.retrieveAllGLAccounts(type, searchParam, usage,
manualEntriesAllowed, disabled, associationParametersData);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serialize(settings, glAccountDatas, RESPONSE_DATA_PARAMETERS);
}
@GET
@Path("{glAccountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = { "General Ledger Account" }, summary = "Retrieve a General Ledger Account", description = "Example Requests:\n"
+ "\n" + "glaccounts/1\n" + "\n" + "\n" + "glaccounts/1?template=true\n" + "\n" + "\n" + "glaccounts/1?fields=name,glCode\n"
+ "\n" + "\n" + "glaccounts/1?fetchRunningBalance=true")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.GetGLAccountsResponse.class))) })
public String retreiveAccount(@PathParam("glAccountId") @Parameter(description = "glAccountId") final Long glAccountId,
@Context final UriInfo uriInfo,
@QueryParam("fetchRunningBalance") @Parameter(description = "fetchRunningBalance") final boolean runningBalance) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
JournalEntryAssociationParametersData associationParametersData = new JournalEntryAssociationParametersData(false, runningBalance);
GLAccountData glAccountData = this.glAccountReadPlatformService.retrieveGLAccountById(glAccountId, associationParametersData);
if (settings.isTemplate()) {
glAccountData = handleTemplate(glAccountData);
}
return this.apiJsonSerializerService.serialize(settings, glAccountData, RESPONSE_DATA_PARAMETERS);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = {
"General Ledger Account" }, summary = "Create a General Ledger Account", description = "Note: You may optionally create Hierarchical Chart of Accounts by using the \"parentId\" property of an Account\n"
+ "Mandatory Fields: \n" + "name, glCode, type, usage and manualEntriesAllowed")
@RequestBody(content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.PostGLAccountsRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.PostGLAccountsResponse.class))) })
public String createGLAccount(@Parameter(hidden = true) final String jsonRequestBody) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createGLAccount().withJson(jsonRequestBody).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.apiJsonSerializerService.serialize(result);
}
@PUT
@Path("{glAccountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = { "General Ledger Account" }, summary = "Update a GL Account", description = "Updates a GL Account")
@RequestBody(content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.PutGLAccountsRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.PutGLAccountsResponse.class))) })
public String updateGLAccount(@PathParam("glAccountId") @Parameter(description = "glAccountId") final Long glAccountId,
@Parameter(hidden = true) final String jsonRequestBody) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateGLAccount(glAccountId).withJson(jsonRequestBody).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.apiJsonSerializerService.serialize(result);
}
@DELETE
@Path("{glAccountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(tags = { "General Ledger Account" }, summary = "Delete a GL Account", description = "Deletes a GL Account")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = GLAccountsApiResourceSwagger.DeleteGLAccountsRequest.class))) })
public String deleteGLAccount(@PathParam("glAccountId") @Parameter(description = "glAccountId") final Long glAccountId) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteGLAccount(glAccountId).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.apiJsonSerializerService.serialize(result);
}
private GLAccountData handleTemplate(final GLAccountData glAccountData) {
final List<EnumOptionData> accountTypeOptions = this.dropdownReadPlatformService.retrieveGLAccountTypeOptions();
final List<EnumOptionData> usageOptions = this.dropdownReadPlatformService.retrieveGLAccountUsageOptions();
final List<GLAccountData> assetHeaderAccountOptions = defaultIfEmpty(
this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.ASSET));
final List<GLAccountData> liabilityHeaderAccountOptions = defaultIfEmpty(
this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.LIABILITY));
final List<GLAccountData> equityHeaderAccountOptions = defaultIfEmpty(
this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.EQUITY));
final List<GLAccountData> incomeHeaderAccountOptions = defaultIfEmpty(
this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.INCOME));
final List<GLAccountData> expenseHeaderAccountOptions = defaultIfEmpty(
this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.EXPENSE));
final Collection<CodeValueData> allowedAssetsTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedLiabilitiesTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedEquityTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedIncomeTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedExpensesTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME);
return new GLAccountData().setId(glAccountData.getId()).setName(glAccountData.getName()).setParentId(glAccountData.getParentId())
.setGlCode(glAccountData.getGlCode()).setDisabled(glAccountData.getDisabled())
.setManualEntriesAllowed(glAccountData.getManualEntriesAllowed()).setType(glAccountData.getType())
.setUsage(glAccountData.getUsage()).setDescription(glAccountData.getDescription())
.setNameDecorated(glAccountData.getNameDecorated()).setTagId(glAccountData.getTagId())
.setOrganizationRunningBalance(glAccountData.getOrganizationRunningBalance()).setAccountTypeOptions(accountTypeOptions)
.setUsageOptions(usageOptions).setAssetHeaderAccountOptions(assetHeaderAccountOptions)
.setLiabilityHeaderAccountOptions(liabilityHeaderAccountOptions).setEquityHeaderAccountOptions(equityHeaderAccountOptions)
.setIncomeHeaderAccountOptions(incomeHeaderAccountOptions).setExpenseHeaderAccountOptions(expenseHeaderAccountOptions)
.setAllowedAssetsTagOptions(allowedAssetsTagOptions).setAllowedLiabilitiesTagOptions(allowedLiabilitiesTagOptions)
.setAllowedEquityTagOptions(allowedEquityTagOptions).setAllowedIncomeTagOptions(allowedIncomeTagOptions)
.setAllowedExpensesTagOptions(allowedExpensesTagOptions);
}
private List<GLAccountData> defaultIfEmpty(final List<GLAccountData> list) {
List<GLAccountData> returnList = null;
if (list != null && !list.isEmpty()) {
returnList = list;
}
return returnList;
}
@GET
@Path("downloadtemplate")
@Produces("application/vnd.ms-excel")
public Response getGlAccountsTemplate(@QueryParam("dateFormat") final String dateFormat) {
return bulkImportWorkbookPopulatorService.getTemplate(GlobalEntityType.CHART_OF_ACCOUNTS.toString(), null, null, dateFormat);
}
@POST
@Path("uploadtemplate")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@RequestBody(description = "Upload GL accounts template", content = {
@Content(mediaType = MediaType.MULTIPART_FORM_DATA, schema = @Schema(implementation = UploadRequest.class)) })
public String postGlAccountsTemplate(@FormDataParam("file") InputStream uploadedInputStream,
@FormDataParam("file") FormDataContentDisposition fileDetail, @FormDataParam("locale") final String locale,
@FormDataParam("dateFormat") final String dateFormat) {
Long importDocumentId = bulkImportWorkbookService.importWorkbook(GlobalEntityType.CHART_OF_ACCOUNTS.toString(), uploadedInputStream,
fileDetail, locale, dateFormat);
return this.apiJsonSerializerService.serialize(importDocumentId);
}
}
@@ -0,0 +1,225 @@
/**
* 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.accounting.glaccount.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
/**
* Created by sanyam on 24/7/17.
*/
final class GLAccountsApiResourceSwagger {
private GLAccountsApiResourceSwagger() {
}
@Schema(description = "GetGLAccountsResponse")
public static final class GetGLAccountsResponse {
private GetGLAccountsResponse() {
}
@Schema(example = "16")
public Long id;
@Schema(example = "Cash")
public String name;
@Schema(example = "1")
public Long parentId;
@Schema(example = "100001")
public String glCode;
@Schema(example = "false")
public Boolean disabled;
@Schema(example = "true")
public Boolean manualEntriesAllowed;
public EnumOptionData type;
public EnumOptionData usage;
@Schema(example = "Desc")
public String description;
@Schema(example = "....Cash")
public String nameDecorated;
public CodeValueData tagId;
@Schema(example = "118437")
public Long organizationRunningBalance;
}
@Schema(description = "GetGLAccountsTemplateResponse")
public static final class GetGLAccountsTemplateResponse {
private GetGLAccountsTemplateResponse() {
}
@Schema(example = "false")
public Boolean disabled;
@Schema(example = "true")
public Boolean manualEntriesAllowed;
public EnumOptionData type;
public EnumOptionData usage;
public List<EnumOptionData> accountTypeOptions;
public List<EnumOptionData> usageOptions;
public List<GLAccountData> assetHeaderAccountOptions;
public List<GLAccountData> liabilityHeaderAccountOptions;
public List<GLAccountData> equityHeaderAccountOptions;
public List<GLAccountData> expenseHeaderAccountOptions;
public Collection<CodeValueData> allowedAssetsTagOptions;
public Collection<CodeValueData> allowedLiabilitiesTagOptions;
public Collection<CodeValueData> allowedEquityTagOptions;
public Collection<CodeValueData> allowedIncomeTagOptions;
public Collection<CodeValueData> allowedExpensesTagOptions;
}
@Schema(description = "PostGLAccountsRequest")
public static final class PostGLAccountsRequest {
private PostGLAccountsRequest() {
}
@Schema(example = "Cash at Bangalore")
public String name;
@Schema(example = "100001")
public String glCode;
@Schema(example = "true")
public Boolean manualEntriesAllowed;
@Schema(example = "1")
public Integer type;
@Schema(example = "10")
public Long tagId;
@Schema(example = "1")
public Long parentId;
@Schema(example = "1")
public Integer usage;
@Schema(example = "Desc")
public String description;
}
@Schema(description = "PostGLAccountsResponse")
public static final class PostGLAccountsResponse {
private PostGLAccountsResponse() {
}
@Schema(example = "22")
public Long resourceId;
}
@Schema(description = "PutGLAccountsRequest")
public static final class PutGLAccountsRequest {
private PutGLAccountsRequest() {
}
@Schema(example = "Cash at Bangalore")
public String name;
@Schema(example = "100001")
public String glCode;
@Schema(example = "true")
public Boolean manualEntriesAllowed;
@Schema(example = "1")
public Integer type;
@Schema(example = "10")
public Long tagId;
@Schema(example = "1")
public Long parentId;
@Schema(example = "1")
public Integer usage;
@Schema(example = "Desc")
public String description;
@Schema(example = "false")
public Boolean disabled;
}
@Schema(description = "PutGLAccountsResponse")
public static final class PutGLAccountsResponse {
private PutGLAccountsResponse() {
}
@Schema
public static final class PutGLAccountsResponsechangesSwagger {
private PutGLAccountsResponsechangesSwagger() {}
@Schema(example = "Cash at Bangalore")
public String name;
}
@Schema(example = "1")
public Long resourceId;
public PutGLAccountsResponsechangesSwagger changes;
}
@Schema(description = "DeleteGLAccountsRequest")
public static final class DeleteGLAccountsRequest {
private DeleteGLAccountsRequest() {
}
private static final class DeleteGLAccountsRequestchangesSwagger {
private DeleteGLAccountsRequestchangesSwagger() {}
}
@Schema(example = "1")
public Long resourceId;
private DeleteGLAccountsRequestchangesSwagger changes;
}
}
@@ -0,0 +1,123 @@
/**
* 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.accounting.glaccount.command;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.glaccount.api.GLAccountJsonInputParams;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.glaccount.domain.GLAccountUsage;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
/**
* Immutable command for adding a general Ledger Account
*/
@RequiredArgsConstructor
@Getter
public class GLAccountCommand {
private final Long id;
private final String name;
private final Long parentId;
private final String glCode;
private final Boolean disabled;
private final Boolean manualEntriesAllowed;
private final Integer type;
private final Integer usage;
private final String description;
private final Long tagId;
public void validateForCreate() {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("GLAccount");
baseDataValidator.reset().parameter(GLAccountJsonInputParams.NAME.getValue()).value(this.name).notBlank().notExceedingLengthOf(200);
baseDataValidator.reset().parameter(GLAccountJsonInputParams.GL_CODE.getValue()).value(this.glCode).notBlank()
.notExceedingLengthOf(45);
baseDataValidator.reset().parameter(GLAccountJsonInputParams.PARENT_ID.getValue()).value(this.parentId).ignoreIfNull()
.integerGreaterThanZero();
baseDataValidator.reset().parameter(GLAccountJsonInputParams.TYPE.getValue()).value(this.type).notNull()
.inMinMaxRange(GLAccountType.getMinValue(), GLAccountType.getMaxValue());
baseDataValidator.reset().parameter(GLAccountJsonInputParams.USAGE.getValue()).value(this.usage)
.inMinMaxRange(GLAccountUsage.getMinValue(), GLAccountUsage.getMaxValue());
baseDataValidator.reset().parameter(GLAccountJsonInputParams.DESCRIPTION.getValue()).value(this.description).ignoreIfNull()
.notExceedingLengthOf(500);
baseDataValidator.reset().parameter(GLAccountJsonInputParams.MANUAL_ENTRIES_ALLOWED.getValue()).value(this.manualEntriesAllowed)
.notBlank();
baseDataValidator.reset().parameter(GLAccountJsonInputParams.TAGID.getValue()).value(this.tagId).ignoreIfNull()
.longGreaterThanZero();
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
public void validateForUpdate() {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("GLAccount");
baseDataValidator.reset().parameter(GLAccountJsonInputParams.NAME.getValue()).value(this.name).ignoreIfNull().notBlank()
.notExceedingLengthOf(200);
baseDataValidator.reset().parameter(GLAccountJsonInputParams.GL_CODE.getValue()).ignoreIfNull().value(this.glCode).notBlank()
.notExceedingLengthOf(45);
baseDataValidator.reset().parameter(GLAccountJsonInputParams.PARENT_ID.getValue()).value(this.parentId).ignoreIfNull()
.integerGreaterThanZero();
baseDataValidator.reset().parameter(GLAccountJsonInputParams.TYPE.getValue()).value(this.type).ignoreIfNull()
.inMinMaxRange(GLAccountType.getMinValue(), GLAccountType.getMaxValue());
baseDataValidator.reset().parameter(GLAccountJsonInputParams.USAGE.getValue()).value(this.usage).ignoreIfNull()
.inMinMaxRange(GLAccountUsage.getMinValue(), GLAccountUsage.getMaxValue());
baseDataValidator.reset().parameter(GLAccountJsonInputParams.DESCRIPTION.getValue()).value(this.description).ignoreIfNull()
.notBlank().notExceedingLengthOf(500);
baseDataValidator.reset().parameter(GLAccountJsonInputParams.DISABLED.getValue()).value(this.disabled).ignoreIfNull();
baseDataValidator.reset().anyOfNotNull(this.name, this.glCode, this.parentId, this.type, this.description, this.disabled);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
baseDataValidator.reset().parameter(GLAccountJsonInputParams.TAGID.getValue()).value(this.tagId).ignoreIfNull()
.longGreaterThanZero();
}
public boolean isHeaderAccount() {
return GLAccountUsage.HEADER.getValue().equals(this.usage);
}
}
@@ -0,0 +1,36 @@
/**
* 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.accounting.glaccount.data;
import java.io.Serializable;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class GLAccountDataForLookup implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String glCode;
}
@@ -0,0 +1,30 @@
/**
* 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.accounting.glaccount.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface GLAccountRepository extends JpaRepository<GLAccount, Long>, JpaSpecificationExecutor<GLAccount> {
// no added behaviour
// adding behaviour to fetch id by glcode for opening balance bulk import
Optional<GLAccount> findOneByGlCode(String glCode);
}
@@ -0,0 +1,49 @@
/**
* 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.accounting.glaccount.domain;
import org.apache.fineract.accounting.glaccount.exception.GLAccountNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* Wrapper for {@link GLAccountRepository} .
* </p>
*/
@Service
public class GLAccountRepositoryWrapper {
private final GLAccountRepository repository;
@Autowired
public GLAccountRepositoryWrapper(final GLAccountRepository repository) {
this.repository = repository;
}
public GLAccount findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new GLAccountNotFoundException(id));
}
// finding account id by glcode for opening balance bulk import
public GLAccount findOneByGlCodeWithNotFoundDetection(final String glCode) {
return this.repository.findOneByGlCode(glCode).orElseThrow(() -> new GLAccountNotFoundException(glCode));
}
}
@@ -0,0 +1,82 @@
/**
* 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.accounting.glaccount.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Objects;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
@Entity
@Table(name = "m_trial_balance")
@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class TrialBalance extends AbstractPersistableCustom {
@Column(name = "office_id", nullable = false)
private Long officeId;
@Column(name = "account_id", nullable = false)
private Long glAccountId;
@Column(name = "amount", nullable = false)
private BigDecimal amount;
@Column(name = "entry_date", nullable = false)
private LocalDate entryDate;
@Column(name = "created_date", nullable = true)
private LocalDate transactionDate;
@Column(name = "closing_balance", nullable = false)
private BigDecimal closingBalance;
public static TrialBalance getInstance(final Long officeId, final Long glAccountId, final BigDecimal amount, final LocalDate entryDate,
final LocalDate transactionDate) {
return new TrialBalance().setOfficeId(officeId).setGlAccountId(glAccountId).setAmount(amount).setEntryDate(entryDate)
.setTransactionDate(transactionDate);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TrialBalance)) {
return false;
}
TrialBalance other = (TrialBalance) obj;
return Objects.equals(other.officeId, officeId) && Objects.equals(other.glAccountId, glAccountId)
&& Objects.equals(other.amount, amount) && other.entryDate.compareTo(entryDate) == 0 ? Boolean.TRUE
: Boolean.FALSE && other.transactionDate.compareTo(transactionDate) == 0 ? Boolean.TRUE
: Boolean.FALSE && Objects.equals(other.closingBalance, closingBalance);
}
@Override
public int hashCode() {
return Objects.hash(officeId, glAccountId, amount, entryDate, transactionDate, closingBalance);
}
}
@@ -0,0 +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.accounting.glaccount.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface TrialBalanceRepository extends JpaRepository<TrialBalance, Long>, JpaSpecificationExecutor<TrialBalance> {
@Query(value = "select * from m_trial_balance where office_id=:officeId and account_id=:accountId and closing_balance is null order by created_date, entry_date", nativeQuery = true)
List<TrialBalance> findNewByOfficeAndAccount(@Param("officeId") Long officeId, @Param("accountId") Long accountId);
}
@@ -0,0 +1,48 @@
/**
* 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.accounting.glaccount.domain;
import java.util.List;
import org.apache.fineract.accounting.trialbalance.exception.TrialBalanceNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TrialBalanceRepositoryWrapper {
private final TrialBalanceRepository repository;
@Autowired
public TrialBalanceRepositoryWrapper(final TrialBalanceRepository repository) {
this.repository = repository;
}
public List<TrialBalance> findNewByOfficeAndAccount(final Long officeId, final Long accountId) {
final List<TrialBalance> trialBalanceList = this.repository.findNewByOfficeAndAccount(officeId, accountId);
if (trialBalanceList == null) {
throw new TrialBalanceNotFoundException(officeId, accountId);
}
return trialBalanceList;
}
public void save(final List<TrialBalance> tbRows) {
this.repository.saveAll(tbRows);
}
}
@@ -0,0 +1,32 @@
/**
* 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.accounting.glaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
*
* when gl account is attached with any product(loan/saving) then we can not disable the gl account
*/
public class GLAccountDisableException extends AbstractPlatformDomainRuleException {
public GLAccountDisableException() {
super("error.msg.glaccount.attached.to.product", "General Ledger Account is already attached to product.");
}
}
@@ -0,0 +1,32 @@
/**
* 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.accounting.glaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when a GL Account with a given GL Code of the particular type is already present
*/
public class GLAccountDuplicateException extends AbstractPlatformDomainRuleException {
public GLAccountDuplicateException(final String glCode) {
super("error.msg.glaccount.glcode.duplicate", "General Ledger Account with GL code " + glCode + " is already present", glCode);
}
}
@@ -0,0 +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.accounting.glaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when trying to fetch accounts belonging to an Invalid Usage Type
*/
public class GLAccountInvalidClassificationException extends AbstractPlatformDomainRuleException {
public GLAccountInvalidClassificationException(final Integer usage) {
super("error.msg.glaccount.usage.invalid", "The following COA usage is invalid: " + usage);
}
}
@@ -0,0 +1,55 @@
/**
* 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.accounting.glaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when a GL Account with a given GL Code of the particular type is already present
*/
public class GLAccountInvalidDeleteException extends AbstractPlatformDomainRuleException {
/*** Enum of reasons for invalid delete **/
public enum GlAccountInvalidDeleteReason {
TRANSANCTIONS_LOGGED, HAS_CHILDREN;
public String errorMessage() {
if (name().toString().equalsIgnoreCase("TRANSANCTIONS_LOGGED")) {
return "This GL Account cannot be deleted as it has transactions logged against it";
} else if (name().toString().equalsIgnoreCase("HAS_CHILDREN")) {
return "Cannot delete this Header GL Account without first deleting or reassinging its children";
}
return name().toString();
}
public String errorCode() {
if (name().toString().equalsIgnoreCase("TRANSANCTIONS_LOGGED")) {
return "error.msg.glaccount.glcode.invalid.delete.transactions.logged";
} else if (name().toString().equalsIgnoreCase("HAS_CHILDREN")) {
return "error.msg.glaccount.glcode.invalid.delete.has.children";
}
return name().toString();
}
}
public GLAccountInvalidDeleteException(final GlAccountInvalidDeleteReason reason, final Long glAccountId) {
super(reason.errorCode(), reason.errorMessage(), glAccountId);
}
}
@@ -0,0 +1,32 @@
/**
* 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.accounting.glaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when trying to map invalid parents to a GL account
*/
public class GLAccountInvalidParentException extends AbstractPlatformDomainRuleException {
public GLAccountInvalidParentException(final long glAccountId) {
super("error.msg.glaccount.parent.invalid",
"The account with id " + glAccountId + " is a 'Detail' account and cannot be used as a parent", glAccountId);
}
}
@@ -0,0 +1,51 @@
/**
* 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.accounting.glaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when an error is encountered during updating a GL Account
*/
public class GLAccountInvalidUpdateException extends AbstractPlatformDomainRuleException {
/*** Enum of reasons for invalid delete **/
public enum GlAccountInvalidUpdateReason {
TRANSANCTIONS_LOGGED;
public String errorMessage() {
if (name().toString().equalsIgnoreCase("TRANSANCTIONS_LOGGED")) {
return "This Usage of this (detail) GL Account as it already has transactions logged against it";
}
return name().toString();
}
public String errorCode() {
if (name().toString().equalsIgnoreCase("TRANSANCTIONS_LOGGED")) {
return "error.msg.glaccount.glcode.invalid.update.transactions.logged";
}
return name().toString();
}
}
public GLAccountInvalidUpdateException(final GlAccountInvalidUpdateReason reason, final Long glAccountId) {
super(reason.errorCode(), reason.errorMessage(), glAccountId);
}
}
@@ -0,0 +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.accounting.glaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when trying to fetch accounts belonging to an Invalid Type
*/
public class GLAccountInvalidUsageException extends AbstractPlatformDomainRuleException {
public GLAccountInvalidUsageException(final Integer type) {
super("error.msg.glaccount.classification.invalid", "The following COA type is invalid: " + type);
}
}
@@ -0,0 +1,40 @@
/**
* 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.accounting.glaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
/**
* A {@link RuntimeException} thrown when GL account resources are not found.
*/
public class GLAccountNotFoundException extends AbstractPlatformResourceNotFoundException {
public GLAccountNotFoundException(final Long id) {
super("error.msg.glaccount.id.invalid", "General Ledger account with identifier " + id + " does not exist ", id);
}
public GLAccountNotFoundException(final Long id, EmptyResultDataAccessException e) {
super("error.msg.glaccount.id.invalid", "General Ledger account with identifier " + id + " does not exist ", id, e);
}
public GLAccountNotFoundException(final String glCode) {
super("error.msg.glaccount.code.invalid", "General Ledger account with GlCode " + glCode + " does not exist ", glCode);
}
}
@@ -0,0 +1,30 @@
/**
* 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.accounting.glaccount.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class InvalidParentGLAccountHeadException extends AbstractPlatformDomainRuleException {
public InvalidParentGLAccountHeadException(final Long glAccountId, final Long parentId) {
super("error.msg.glaccount.id.and.parentid.must.not.same", "parentId:" + parentId + ", id" + glAccountId + " should not be same",
glAccountId, parentId);
}
}
@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.glaccount.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.glaccount.service.GLAccountWritePlatformService;
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.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "GLACCOUNT", action = "CREATE")
@RequiredArgsConstructor
public class CreateGLAccountCommandHandler implements NewCommandSourceHandler {
private final GLAccountWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createGLAccount(command);
}
}
@@ -0,0 +1,42 @@
/**
* 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.accounting.glaccount.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.glaccount.service.GLAccountWritePlatformService;
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.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "GLACCOUNT", action = "DELETE")
@RequiredArgsConstructor
public class DeleteGLAccountCommandHandler implements NewCommandSourceHandler {
private final GLAccountWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deleteGLAccount(command.entityId());
}
}
@@ -0,0 +1,42 @@
/**
* 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.accounting.glaccount.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.glaccount.service.GLAccountWritePlatformService;
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.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "GLACCOUNT", action = "UPDATE")
@RequiredArgsConstructor
public class UpdateGLAccountCommandHandler implements NewCommandSourceHandler {
private final GLAccountWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateGLAccount(command.entityId(), command);
}
}
@@ -0,0 +1,63 @@
/**
* 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.accounting.glaccount.jobs.updatetrialbalancedetails;
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepositoryWrapper;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class UpdateTrialBalanceDetailsConfig {
@Autowired
private JobRepository jobRepository;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private RoutingDataSourceServiceFactory dataSourceServiceFactory;
@Autowired
private TrialBalanceRepositoryWrapper trialBalanceRepositoryWrapper;
@Bean
protected Step updateTrialBalanceDetailsStep() {
return new StepBuilder(JobName.UPDATE_TRIAL_BALANCE_DETAILS.name(), jobRepository)
.tasklet(updateTrialBalanceDetailsTasklet(), transactionManager).build();
}
@Bean
public Job updateTrialBalanceDetailsJob() {
return new JobBuilder(JobName.UPDATE_TRIAL_BALANCE_DETAILS.name(), jobRepository).start(updateTrialBalanceDetailsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public UpdateTrialBalanceDetailsTasklet updateTrialBalanceDetailsTasklet() {
return new UpdateTrialBalanceDetailsTasklet(dataSourceServiceFactory, trialBalanceRepositoryWrapper);
}
}
@@ -0,0 +1,100 @@
/**
* 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.accounting.glaccount.jobs.updatetrialbalancedetails;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.glaccount.domain.TrialBalance;
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepositoryWrapper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.core.service.database.RoutingDataSourceServiceFactory;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.CollectionUtils;
@Slf4j
@RequiredArgsConstructor
public class UpdateTrialBalanceDetailsTasklet implements Tasklet {
private final RoutingDataSourceServiceFactory dataSourceServiceFactory;
private final TrialBalanceRepositoryWrapper trialBalanceRepositoryWrapper;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
final StringBuilder tbGapSqlBuilder = new StringBuilder(500);
tbGapSqlBuilder.append("select distinct(je.transaction_date) ").append("from acc_gl_journal_entry je ")
.append("where je.transaction_date > (select coalesce(MAX(created_date),'2010-01-01') from m_trial_balance)");
final List<LocalDate> tbGaps = jdbcTemplate.queryForList(tbGapSqlBuilder.toString(), LocalDate.class);
for (LocalDate tbGap : tbGaps) {
int days = Math.toIntExact(ChronoUnit.DAYS.between(tbGap, DateUtils.getBusinessLocalDate()));
if (days < 1) {
continue;
}
final StringBuilder sqlBuilder = new StringBuilder(600);
sqlBuilder.append("Insert Into m_trial_balance(office_id, account_id, Amount, entry_date, created_date,closing_balance) ")
.append("Select je.office_id, je.account_id, SUM(CASE WHEN je.type_enum=1 THEN (-1) * je.amount ELSE je.amount END) ")
.append("as Amount, Date(je.entry_date) as Entry_Date, je.transaction_date as Created_Date,sum(je.amount) as closing_balance ")
.append("from acc_gl_journal_entry je WHERE je.transaction_date = ? ")
.append("group by je.account_id, je.office_id, je.transaction_date, Date(je.entry_date)");
final int result = jdbcTemplate.update(sqlBuilder.toString(), tbGap);
log.debug("{}: Records affected by updateTrialBalanceDetails: {}", ThreadLocalContextUtil.getTenant().getName(), result);
}
String distinctOfficeQuery = "select distinct(office_id) from m_trial_balance where closing_balance is null group by office_id";
final List<Long> officeIds = jdbcTemplate.queryForList(distinctOfficeQuery, Long.class);
for (Long officeId : officeIds) {
String distinctAccountQuery = "select distinct(account_id) from m_trial_balance where office_id=? and closing_balance is null group by account_id";
final List<Long> accountIds = jdbcTemplate.queryForList(distinctAccountQuery, Long.class, officeId);
for (Long accountId : accountIds) {
final String closingBalanceQuery = "select closing_balance from m_trial_balance where office_id=? and account_id=? and closing_balance "
+ "is not null order by created_date desc, entry_date desc limit 1";
List<BigDecimal> closingBalanceData = jdbcTemplate.queryForList(closingBalanceQuery, BigDecimal.class, officeId, accountId);
List<TrialBalance> tbRows = trialBalanceRepositoryWrapper.findNewByOfficeAndAccount(officeId, accountId);
BigDecimal closingBalance = null;
if (!CollectionUtils.isEmpty(closingBalanceData)) {
closingBalance = closingBalanceData.get(0);
}
if (CollectionUtils.isEmpty(closingBalanceData)) {
closingBalance = BigDecimal.ZERO;
for (TrialBalance row : tbRows) {
closingBalance = closingBalance.add(row.getAmount());
row.setClosingBalance(closingBalance);
}
} else {
for (TrialBalance tbRow : tbRows) {
if (closingBalance != null) {
closingBalance = closingBalance.add(tbRow.getAmount());
}
tbRow.setClosingBalance(closingBalance);
}
}
trialBalanceRepositoryWrapper.save(tbRows);
}
}
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,71 @@
/**
* 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.accounting.glaccount.serialization;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.glaccount.api.GLAccountJsonInputParams;
import org.apache.fineract.accounting.glaccount.command.GLAccountCommand;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.serialization.AbstractFromApiJsonDeserializer;
import org.apache.fineract.infrastructure.core.serialization.FromApiJsonDeserializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
/**
* Implementation of {@link FromApiJsonDeserializer}
*/
@Component
@RequiredArgsConstructor
public final class GLAccountCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<GLAccountCommand> {
private final FromJsonHelper fromApiJsonHelper;
@Override
public GLAccountCommand commandFromApiJson(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
final Set<String> supportedParameters = GLAccountJsonInputParams.getAllValues();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Long id = this.fromApiJsonHelper.extractLongNamed(GLAccountJsonInputParams.ID.getValue(), element);
final String name = this.fromApiJsonHelper.extractStringNamed(GLAccountJsonInputParams.NAME.getValue(), element);
final Long parentId = this.fromApiJsonHelper.extractLongNamed(GLAccountJsonInputParams.PARENT_ID.getValue(), element);
final String glCode = this.fromApiJsonHelper.extractStringNamed(GLAccountJsonInputParams.GL_CODE.getValue(), element);
final Boolean disabled = this.fromApiJsonHelper.extractBooleanNamed(GLAccountJsonInputParams.DISABLED.getValue(), element);
final Boolean manualEntriesAllowed = this.fromApiJsonHelper
.extractBooleanNamed(GLAccountJsonInputParams.MANUAL_ENTRIES_ALLOWED.getValue(), element);
final Integer type = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(GLAccountJsonInputParams.TYPE.getValue(), element);
final Integer usage = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(GLAccountJsonInputParams.USAGE.getValue(), element);
final String description = this.fromApiJsonHelper.extractStringNamed(GLAccountJsonInputParams.DESCRIPTION.getValue(), element);
final Long tagId = this.fromApiJsonHelper.extractLongNamed(GLAccountJsonInputParams.TAGID.getValue(), element);
return new GLAccountCommand(id, name, parentId, glCode, disabled, manualEntriesAllowed, type, usage, description, tagId);
}
}
@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.glaccount.service;
import java.util.List;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.glaccount.data.GLAccountDataForLookup;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData;
public interface GLAccountReadPlatformService {
List<GLAccountData> retrieveAllGLAccounts(Integer accountClassification, String searchParam, Integer usage,
Boolean manualTransactionsAllowed, Boolean disabled, JournalEntryAssociationParametersData associationParametersData);
GLAccountData retrieveGLAccountById(long glAccountId, JournalEntryAssociationParametersData associationParametersData);
List<GLAccountData> retrieveAllEnabledDetailGLAccounts();
List<GLAccountData> retrieveAllEnabledDetailGLAccounts(GLAccountType accountType);
List<GLAccountData> retrieveAllEnabledHeaderGLAccounts(GLAccountType accountType);
GLAccountData retrieveNewGLAccountDetails(Integer type);
List<GLAccountDataForLookup> retrieveAccountsByTagId(Long ruleId, Integer transactionType);
}
@@ -0,0 +1,283 @@
/**
* 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.accounting.glaccount.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.common.AccountingEnumerations;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.glaccount.data.GLAccountDataForLookup;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.glaccount.domain.GLAccountUsage;
import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidClassificationException;
import org.apache.fineract.accounting.glaccount.exception.GLAccountNotFoundException;
import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformService {
private static final String nameDecoratedBaseOnHierarchy = "concat(substring('........................................', 1, ((LENGTH(hierarchy) - LENGTH(REPLACE(hierarchy, '.', '')) - 1) * 4)), name)";
private final JdbcTemplate jdbcTemplate;
private static final class GLAccountMapper implements RowMapper<GLAccountData> {
private final JournalEntryAssociationParametersData associationParametersData;
GLAccountMapper(final JournalEntryAssociationParametersData associationParametersData) {
if (associationParametersData == null) {
this.associationParametersData = new JournalEntryAssociationParametersData();
} else {
this.associationParametersData = associationParametersData;
}
}
public String schema() {
StringBuilder sb = new StringBuilder();
sb.append(
" gl.id as id, name as name, parent_id as parentId, gl_code as glCode, disabled as disabled, manual_journal_entries_allowed as manualEntriesAllowed, ")
.append("classification_enum as classification, account_usage as accountUsage, gl.description as description, ")
.append(nameDecoratedBaseOnHierarchy).append(" as nameDecorated, ")
.append("cv.id as codeId, cv.code_value as codeValue ");
if (this.associationParametersData.isRunningBalanceRequired()) {
sb.append(",gl_j.organization_running_balance as organizationRunningBalance ");
}
sb.append("from acc_gl_account gl left join m_code_value cv on tag_id=cv.id ");
if (this.associationParametersData.isRunningBalanceRequired()) {
sb.append("left outer Join acc_gl_journal_entry gl_j on gl_j.account_id = gl.id");
}
return sb.toString();
}
@Override
public GLAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String name = rs.getString("name");
final Long parentId = JdbcSupport.getLong(rs, "parentId");
final String glCode = rs.getString("glCode");
final boolean disabled = rs.getBoolean("disabled");
final boolean manualEntriesAllowed = rs.getBoolean("manualEntriesAllowed");
final int accountTypeId = JdbcSupport.getInteger(rs, "classification");
final EnumOptionData accountType = AccountingEnumerations.gLAccountType(accountTypeId);
final int usageId = JdbcSupport.getInteger(rs, "accountUsage");
final EnumOptionData usage = AccountingEnumerations.gLAccountUsage(usageId);
final String description = rs.getString("description");
final String nameDecorated = rs.getString("nameDecorated");
final Long codeId = rs.wasNull() ? null : rs.getLong("codeId");
final String codeValue = rs.getString("codeValue");
final CodeValueData tagId = CodeValueData.instance(codeId, codeValue);
Long organizationRunningBalance = null;
if (associationParametersData.isRunningBalanceRequired()) {
organizationRunningBalance = rs.getLong("organizationRunningBalance");
}
return new GLAccountData().setId(id).setName(name).setParentId(parentId).setGlCode(glCode).setDisabled(disabled)
.setManualEntriesAllowed(manualEntriesAllowed).setType(accountType).setUsage(usage).setDescription(description)
.setNameDecorated(nameDecorated).setTagId(tagId).setOrganizationRunningBalance(organizationRunningBalance);
}
}
@Override
public List<GLAccountData> retrieveAllGLAccounts(final Integer accountClassification, final String searchParam, final Integer usage,
final Boolean manualTransactionsAllowed, final Boolean disabled,
JournalEntryAssociationParametersData associationParametersData) {
if (accountClassification != null) {
if (!checkValidGLAccountType(accountClassification)) {
throw new GLAccountInvalidClassificationException(accountClassification);
}
}
if (usage != null) {
if (!checkValidGLAccountUsage(usage)) {
throw new GLAccountInvalidClassificationException(accountClassification);
}
}
final GLAccountMapper rm = new GLAccountMapper(associationParametersData);
String sql = "select " + rm.schema();
// append SQL statement for fetching account totals
if (associationParametersData != null) {
if (associationParametersData.isRunningBalanceRequired()) {
sql = sql + " and gl_j.id in (select t1.id from (select t2.account_id, max(t2.id) as id from "
+ "(select id, max(entry_date) as entry_date, account_id from acc_gl_journal_entry where is_running_balance_calculated = true "
+ "group by account_id desc, id) t3 inner join acc_gl_journal_entry t2 on t2.account_id = t3.account_id and t2.entry_date = t3.entry_date "
+ "group by t2.account_id desc) t1)";
}
}
final Object[] parameterArray = new Object[3];
int arrayPos = 0;
boolean filtersPresent = false;
if ((accountClassification != null) || StringUtils.isNotBlank(searchParam) || (usage != null) || (manualTransactionsAllowed != null)
|| (disabled != null)) {
filtersPresent = true;
sql += " where";
}
if (filtersPresent) {
boolean firstWhereConditionAdded = false;
if (accountClassification != null) {
sql += " classification_enum = ?";
parameterArray[arrayPos] = accountClassification.shortValue();
arrayPos = arrayPos + 1;
firstWhereConditionAdded = true;
}
if (StringUtils.isNotBlank(searchParam)) {
if (firstWhereConditionAdded) {
sql += " and ";
}
sql += " ( name like %?% or gl_code like %?% )";
parameterArray[arrayPos] = searchParam;
arrayPos = arrayPos + 1;
parameterArray[arrayPos] = searchParam;
arrayPos = arrayPos + 1;
firstWhereConditionAdded = true;
}
if (usage != null) {
if (firstWhereConditionAdded) {
sql += " and ";
}
if (GLAccountUsage.HEADER.getValue().equals(usage)) {
sql += " account_usage = 2 ";
} else if (GLAccountUsage.DETAIL.getValue().equals(usage)) {
sql += " account_usage = 1 ";
}
firstWhereConditionAdded = true;
}
if (manualTransactionsAllowed != null) {
if (firstWhereConditionAdded) {
sql += " and ";
}
sql += " manual_journal_entries_allowed = " + manualTransactionsAllowed;
firstWhereConditionAdded = true;
}
if (disabled != null) {
if (firstWhereConditionAdded) {
sql += " and ";
}
sql += " disabled = " + disabled;
firstWhereConditionAdded = true;
}
}
sql += " ORDER BY gl_code ASC";
final Object[] finalObjectArray = Arrays.copyOf(parameterArray, arrayPos);
return this.jdbcTemplate.query(sql, rm, finalObjectArray); // NOSONAR
}
@Override
public GLAccountData retrieveGLAccountById(final long glAccountId, JournalEntryAssociationParametersData associationParametersData) {
try {
final GLAccountMapper rm = new GLAccountMapper(associationParametersData);
final StringBuilder sql = new StringBuilder();
sql.append("select ").append(rm.schema());
if (associationParametersData.isRunningBalanceRequired()) {
sql.append(" and gl_j.is_running_balance_calculated = true ");
}
sql.append("where gl.id = ?");
if (associationParametersData.isRunningBalanceRequired()) {
sql.append(" ORDER BY gl_j.entry_date DESC,gl_j.id DESC LIMIT 1");
}
final GLAccountData glAccountData = this.jdbcTemplate.queryForObject(sql.toString(), rm, new Object[] { glAccountId });
return glAccountData;
} catch (final EmptyResultDataAccessException e) {
throw new GLAccountNotFoundException(glAccountId, e);
}
}
@Override
public List<GLAccountData> retrieveAllEnabledDetailGLAccounts(final GLAccountType accountType) {
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.DETAIL.getValue(), null, false,
new JournalEntryAssociationParametersData());
}
@Override
public List<GLAccountData> retrieveAllEnabledDetailGLAccounts() {
return retrieveAllGLAccounts(null, null, GLAccountUsage.DETAIL.getValue(), null, false,
new JournalEntryAssociationParametersData());
}
private static boolean checkValidGLAccountType(final int type) {
for (final GLAccountType accountType : GLAccountType.values()) {
if (accountType.getValue().equals(type)) {
return true;
}
}
return false;
}
private static boolean checkValidGLAccountUsage(final int type) {
for (final GLAccountUsage accountUsage : GLAccountUsage.values()) {
if (accountUsage.getValue().equals(type)) {
return true;
}
}
return false;
}
@Override
public GLAccountData retrieveNewGLAccountDetails(final Integer type) {
return GLAccountData.sensibleDefaultsForNewGLAccountCreation(type);
}
@Override
public List<GLAccountData> retrieveAllEnabledHeaderGLAccounts(final GLAccountType accountType) {
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.HEADER.getValue(), null, false,
new JournalEntryAssociationParametersData());
}
@Override
public List<GLAccountDataForLookup> retrieveAccountsByTagId(final Long ruleId, final Integer transactionType) {
final GLAccountDataLookUpMapper mapper = new GLAccountDataLookUpMapper();
final String sql = "Select " + mapper.schema() + " where rule.id=? and tags.acc_type_enum=?";
return this.jdbcTemplate.query(sql, mapper, new Object[] { ruleId, transactionType }); // NOSONAR
}
private static final class GLAccountDataLookUpMapper implements RowMapper<GLAccountDataForLookup> {
public String schema() {
return " gl.id as id, gl.name as name, gl.gl_code as glCode from acc_accounting_rule rule join acc_rule_tags tags on tags.acc_rule_id = rule.id join acc_gl_account gl on gl.tag_id=tags.tag_id";
}
@Override
public GLAccountDataForLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = JdbcSupport.getLong(rs, "id");
final String name = rs.getString("name");
final String glCode = rs.getString("glCode");
return new GLAccountDataForLookup().setId(id).setName(name).setGlCode(glCode);
}
}
}
@@ -0,0 +1,32 @@
/**
* 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.accounting.glaccount.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface GLAccountWritePlatformService {
CommandProcessingResult createGLAccount(JsonCommand command);
CommandProcessingResult updateGLAccount(Long glAccountId, JsonCommand command);
CommandProcessingResult deleteGLAccount(Long glAccountId);
}
@@ -0,0 +1,258 @@
/**
* 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.accounting.glaccount.service;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingConstants;
import org.apache.fineract.accounting.glaccount.api.GLAccountJsonInputParams;
import org.apache.fineract.accounting.glaccount.command.GLAccountCommand;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.glaccount.exception.GLAccountDisableException;
import org.apache.fineract.accounting.glaccount.exception.GLAccountDuplicateException;
import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidDeleteException;
import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidDeleteException.GlAccountInvalidDeleteReason;
import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidParentException;
import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidUpdateException;
import org.apache.fineract.accounting.glaccount.exception.GLAccountInvalidUpdateException.GlAccountInvalidUpdateReason;
import org.apache.fineract.accounting.glaccount.exception.GLAccountNotFoundException;
import org.apache.fineract.accounting.glaccount.exception.InvalidParentGLAccountHeadException;
import org.apache.fineract.accounting.glaccount.serialization.GLAccountCommandFromApiJsonDeserializer;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
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.PlatformDataIntegrityException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class GLAccountWritePlatformServiceJpaRepositoryImpl implements GLAccountWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(GLAccountWritePlatformServiceJpaRepositoryImpl.class);
private final GLAccountRepository glAccountRepository;
private final JournalEntryRepository glJournalEntryRepository;
private final GLAccountCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final CodeValueRepositoryWrapper codeValueRepositoryWrapper;
private final JdbcTemplate jdbcTemplate;
@Transactional
@Override
public CommandProcessingResult createGLAccount(final JsonCommand command) {
try {
final GLAccountCommand accountCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json());
accountCommand.validateForCreate();
// check parent is valid
final Long parentId = command.longValueOfParameterNamed(GLAccountJsonInputParams.PARENT_ID.getValue());
GLAccount parentGLAccount = null;
if (parentId != null) {
parentGLAccount = validateParentGLAccount(parentId);
}
CodeValue glAccountTagType = null;
final Long tagId = command.longValueOfParameterNamed(GLAccountJsonInputParams.TAGID.getValue());
final Long type = command.longValueOfParameterNamed(GLAccountJsonInputParams.TYPE.getValue());
final GLAccountType accountType = GLAccountType.fromInt(type.intValue());
if (tagId != null) {
glAccountTagType = retrieveTagId(tagId, accountType);
}
final GLAccount glAccount = GLAccount.fromJson(parentGLAccount, command, glAccountTagType);
this.glAccountRepository.saveAndFlush(glAccount);
glAccount.generateHierarchy();
this.glAccountRepository.saveAndFlush(glAccount);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(glAccount.getId()).build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleGLAccountDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
}
@Transactional
@Override
public CommandProcessingResult updateGLAccount(final Long glAccountId, final JsonCommand command) {
try {
final GLAccountCommand accountCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json());
accountCommand.validateForUpdate();
if (command.hasParameter(GLAccountJsonInputParams.DISABLED.getValue())
&& command.booleanPrimitiveValueOfParameterNamed(GLAccountJsonInputParams.DISABLED.getValue())) {
validateForAttachedProduct(glAccountId);
}
final Long parentId = command.longValueOfParameterNamed(GLAccountJsonInputParams.PARENT_ID.getValue());
if (glAccountId.equals(parentId)) {
throw new InvalidParentGLAccountHeadException(glAccountId, parentId);
}
// is the glAccount valid
final GLAccount glAccount = this.glAccountRepository.findById(glAccountId)
.orElseThrow(() -> new GLAccountNotFoundException(glAccountId));
final Map<String, Object> changesOnly = glAccount.update(command);
// is the new parent valid
if (changesOnly.containsKey(GLAccountJsonInputParams.PARENT_ID.getValue())) {
final GLAccount parentAccount = validateParentGLAccount(parentId);
glAccount.setParent(parentAccount);
glAccount.generateHierarchy();
}
if (changesOnly.containsKey(GLAccountJsonInputParams.TAGID.getValue())) {
final Long tagIdLongValue = command.longValueOfParameterNamed(GLAccountJsonInputParams.TAGID.getValue());
final GLAccountType accountType = GLAccountType.fromInt(glAccount.getType());
CodeValue tagID = null;
if (tagIdLongValue != null) {
tagID = retrieveTagId(tagIdLongValue, accountType);
}
glAccount.setTagId(tagID);
}
/**
* a detail account cannot be changed to a header account if transactions are already logged against it
**/
if (changesOnly.containsKey(GLAccountJsonInputParams.USAGE.getValue())) {
if (glAccount.isHeaderAccount()) {
final boolean journalEntriesForAccountExist = this.glJournalEntryRepository
.exists((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("glAccountId"), glAccountId));
if (journalEntriesForAccountExist) {
throw new GLAccountInvalidUpdateException(GlAccountInvalidUpdateReason.TRANSANCTIONS_LOGGED, glAccountId);
}
}
}
if (!changesOnly.isEmpty()) {
this.glAccountRepository.saveAndFlush(glAccount);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(glAccount.getId()).with(changesOnly)
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleGLAccountDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
}
private void validateForAttachedProduct(Long glAccountId) {
String sql = "select count(*) from acc_product_mapping acc where acc.gl_account_id = ?";
try {
int count = this.jdbcTemplate.queryForObject(sql, Integer.class, glAccountId);
if (count > 0) {
throw new GLAccountDisableException();
}
} catch (EmptyResultDataAccessException e) {
LOG.error("Problem encountered in validateForAttachedProduct()", e);
}
}
@Transactional
@Override
public CommandProcessingResult deleteGLAccount(final Long glAccountId) {
final GLAccount glAccount = this.glAccountRepository.findById(glAccountId)
.orElseThrow(() -> new GLAccountNotFoundException(glAccountId));
// validate this isn't a header account that has children
if (glAccount.isHeaderAccount() && glAccount.getChildren().size() > 0) {
throw new GLAccountInvalidDeleteException(GlAccountInvalidDeleteReason.HAS_CHILDREN, glAccountId);
}
// does this account have transactions logged against it
final boolean journalEntriesForAccountExist = this.glJournalEntryRepository
.exists((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("glAccountId"), glAccountId));
if (journalEntriesForAccountExist) {
throw new GLAccountInvalidDeleteException(GlAccountInvalidDeleteReason.TRANSANCTIONS_LOGGED, glAccountId);
}
this.glAccountRepository.delete(glAccount);
return new CommandProcessingResultBuilder().withEntityId(glAccountId).build();
}
/**
* @param command
* @return
*/
private GLAccount validateParentGLAccount(final Long parentAccountId) {
GLAccount parentGLAccount = null;
if (parentAccountId != null) {
parentGLAccount = this.glAccountRepository.findById(parentAccountId)
.orElseThrow(() -> new GLAccountNotFoundException(parentAccountId));
// ensure parent is not a detail account
if (parentGLAccount.isDetailAccount()) {
throw new GLAccountInvalidParentException(parentAccountId);
}
}
return parentGLAccount;
}
/**
* @param command
* @param dve
*/
private void handleGLAccountDataIntegrityIssues(final JsonCommand command, final Throwable realCause,
final NonTransientDataAccessException dve) {
if (realCause.getMessage().contains("acc_gl_code")) {
final String glCode = command.stringValueOfParameterNamed(GLAccountJsonInputParams.GL_CODE.getValue());
throw new GLAccountDuplicateException(glCode);
}
LOG.error("Error occured.", dve);
throw new PlatformDataIntegrityException("error.msg.glAccount.unknown.data.integrity.issue",
"Unknown data integrity issue with resource GL Account: " + realCause.getMessage());
}
private CodeValue retrieveTagId(final Long tagId, final GLAccountType accountType) {
CodeValue glAccountTagType = null;
if (accountType.isAssetType()) {
glAccountTagType = this.codeValueRepositoryWrapper
.findOneByCodeNameAndIdWithNotFoundDetection(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME, tagId);
} else if (accountType.isLiabilityType()) {
glAccountTagType = this.codeValueRepositoryWrapper
.findOneByCodeNameAndIdWithNotFoundDetection(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME, tagId);
} else if (accountType.isEquityType()) {
glAccountTagType = this.codeValueRepositoryWrapper
.findOneByCodeNameAndIdWithNotFoundDetection(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME, tagId);
} else if (accountType.isIncomeType()) {
glAccountTagType = this.codeValueRepositoryWrapper
.findOneByCodeNameAndIdWithNotFoundDetection(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME, tagId);
} else if (accountType.isExpenseType()) {
glAccountTagType = this.codeValueRepositoryWrapper
.findOneByCodeNameAndIdWithNotFoundDetection(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME, tagId);
}
return glAccountTagType;
}
}
@@ -0,0 +1,106 @@
/**
* 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.accounting.journalentry;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.accounting.producttoaccountmapping.domain.PortfolioProductType;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
@Mapper(config = MapstructMapperConfig.class)
public interface JournalEntryMapper {
@Mapping(target = "id", source = "id")
@Mapping(target = "officeId", source = "office.id")
@Mapping(target = "officeName", source = "office.name")
@Mapping(target = "glAccountId", source = "glAccount.id")
@Mapping(target = "glAccountCode", source = "glAccount.glCode")
@Mapping(target = "glAccountName", source = "glAccount.name")
@Mapping(target = "glAccountType", source = "glAccount.type", qualifiedByName = "glAccountType")
@Mapping(target = "transactionDate", source = "transactionDate")
@Mapping(target = "entryType", source = "type", qualifiedByName = "journalEntryType")
@Mapping(target = "amount", source = "amount")
@Mapping(target = "entityType", source = "entityType", qualifiedByName = "entityType")
@Mapping(target = "entityId", source = "entityId")
@Mapping(target = "submittedOnDate", source = "submittedOnDate")
@Mapping(target = "transactionId", source = "transactionId")
@Mapping(target = "currency", source = "currencyCode")
@Mapping(target = "manualEntry", source = "manualEntry")
@Mapping(target = "reversed", source = "reversed")
@Mapping(target = "referenceNumber", source = "referenceNumber")
@Mapping(target = "paymentTypeId", source = "paymentDetail.paymentType.id")
@Mapping(target = "accountNumber", source = "paymentDetail.accountNumber")
@Mapping(target = "checkNumber", source = "paymentDetail.checkNumber")
@Mapping(target = "routingCode", source = "paymentDetail.routingCode")
@Mapping(target = "receiptNumber", source = "paymentDetail.receiptNumber")
@Mapping(target = "bankNumber", source = "paymentDetail.bankNumber")
@Mapping(target = "createdDate", ignore = true)
@Mapping(target = "createdByUserId", ignore = true)
@Mapping(target = "createdByUserName", ignore = true)
@Mapping(target = "comments", ignore = true)
@Mapping(target = "officeRunningBalance", ignore = true)
@Mapping(target = "organizationRunningBalance", ignore = true)
@Mapping(target = "runningBalanceComputed", ignore = true)
@Mapping(target = "rowIndex", ignore = true)
@Mapping(target = "dateFormat", ignore = true)
@Mapping(target = "locale", ignore = true)
@Mapping(target = "credits", ignore = true)
@Mapping(target = "debits", ignore = true)
@Mapping(target = "transactionDetails", ignore = true)
@Mapping(target = "savingTransactionId", ignore = true)
JournalEntryData map(JournalEntry journalEntry);
@Named("entityType")
default PortfolioProductType mapEntityType(Integer entityTypeId) {
return PortfolioProductType.fromInt(entityTypeId);
}
@Named("glAccountType")
default GLAccountType mapGlAccountType(Integer typeId) {
return GLAccountType.fromInt(typeId);
}
@Named("journalEntryType")
default JournalEntryType mapJournalEntryType(Integer typeId) {
return JournalEntryType.fromInt(typeId);
}
default EnumOptionData mapGlAccountType(GLAccountType accountType) {
return new EnumOptionData((long) accountType.getValue(), accountType.getCode(), accountType.name());
}
default EnumOptionData mapJournalEntryType(JournalEntryType journalEntryType) {
return new EnumOptionData((long) journalEntryType.getValue(), journalEntryType.getCode(), journalEntryType.name());
}
default EnumOptionData mapEntityType(PortfolioProductType entityType) {
return new EnumOptionData((long) entityType.getValue(), entityType.getCode(), entityType.name());
}
default CurrencyData mapCurrency(String currencyCode) {
return new CurrencyData(currencyCode);
}
}
@@ -0,0 +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.accounting.journalentry.data;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public class CreditDebit {
private final Long glAccountId;
private final BigDecimal amount;
}
@@ -0,0 +1,35 @@
/**
* 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.accounting.journalentry.data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public class JournalEntryAssociationParametersData {
private final boolean transactionDetailsRequired;
private final boolean runningBalanceRequired;
public JournalEntryAssociationParametersData() {
this.transactionDetailsRequired = false;
this.runningBalanceRequired = false;
}
}
@@ -0,0 +1,284 @@
/**
* 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.accounting.journalentry.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import lombok.Data;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
/**
* Immutable object representing a General Ledger Account
*
* Note: no getter/setters required as google will produce json from fields of object.
*/
@Data
public class JournalEntryData {
private Long id;
private Long officeId;
@SuppressWarnings("unused")
private String officeName;
@SuppressWarnings("unused")
private String glAccountName;
private Long glAccountId;
@SuppressWarnings("unused")
private String glAccountCode;
private EnumOptionData glAccountType;
@SuppressWarnings("unused")
private LocalDate transactionDate;
private EnumOptionData entryType;
private BigDecimal amount;
@SuppressWarnings("unused")
private CurrencyData currency;
private String transactionId;
@SuppressWarnings("unused")
private Boolean manualEntry;
@SuppressWarnings("unused")
private EnumOptionData entityType;
@SuppressWarnings("unused")
private Long entityId;
@SuppressWarnings("unused")
private Long createdByUserId;
@SuppressWarnings("unused")
private LocalDate createdDate;
@SuppressWarnings("unused")
private String createdByUserName;
@SuppressWarnings("unused")
private String comments;
@SuppressWarnings("unused")
private Boolean reversed;
@SuppressWarnings("unused")
private String referenceNumber;
@SuppressWarnings("unused")
private BigDecimal officeRunningBalance;
@SuppressWarnings("unused")
private BigDecimal organizationRunningBalance;
@SuppressWarnings("unused")
private Boolean runningBalanceComputed;
@SuppressWarnings("unused")
private TransactionDetailData transactionDetails;
@SuppressWarnings("unused")
private LocalDate submittedOnDate;
// import fields
private transient Integer rowIndex;
private String dateFormat;
private String locale;
private List<CreditDebit> credits;
private List<CreditDebit> debits;
private Long paymentTypeId;
private String currencyCode;
private String accountNumber;
private String checkNumber;
private String routingCode;
private String receiptNumber;
private String bankNumber;
private transient Long savingTransactionId;
public JournalEntryData() {}
// for opening bal bulk import
public JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, List<CreditDebit> credits,
List<CreditDebit> debits, String locale, String dateFormat) {
this.officeId = officeId;
this.dateFormat = dateFormat;
this.locale = locale;
this.transactionDate = transactionDate;
this.currencyCode = currencyCode;
this.credits = credits;
this.debits = debits;
this.rowIndex = null;
this.paymentTypeId = null;
this.accountNumber = null;
this.checkNumber = null;
this.routingCode = null;
this.receiptNumber = null;
this.bankNumber = null;
this.comments = null;
this.id = null;
this.officeName = null;
this.glAccountName = null;
this.glAccountId = null;
this.glAccountCode = null;
this.glAccountType = null;
this.entryType = null;
this.amount = null;
this.currency = null;
this.transactionId = null;
this.manualEntry = null;
this.entityType = null;
this.entityId = null;
this.createdByUserId = null;
this.createdDate = null;
this.createdByUserName = null;
this.reversed = null;
this.referenceNumber = null;
this.officeRunningBalance = null;
this.organizationRunningBalance = null;
this.runningBalanceComputed = null;
this.transactionDetails = null;
this.submittedOnDate = null;
}
private JournalEntryData(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId, Integer rowIndex,
List<CreditDebit> credits, List<CreditDebit> debits, String accountNumber, String checkNumber, String routingCode,
String receiptNumber, String bankNumber, String comments, String locale, String dateFormat) {
this.officeId = officeId;
this.dateFormat = dateFormat;
this.locale = locale;
this.transactionDate = transactionDate;
this.currencyCode = currencyCode;
this.rowIndex = rowIndex;
this.credits = credits;
this.debits = debits;
this.paymentTypeId = paymentTypeId;
this.accountNumber = accountNumber;
this.checkNumber = checkNumber;
this.routingCode = routingCode;
this.receiptNumber = receiptNumber;
this.bankNumber = bankNumber;
this.comments = comments;
this.id = null;
this.officeName = null;
this.glAccountName = null;
this.glAccountId = null;
this.glAccountCode = null;
this.glAccountType = null;
this.entryType = null;
this.amount = null;
this.currency = null;
this.transactionId = null;
this.manualEntry = null;
this.entityType = null;
this.entityId = null;
this.createdByUserId = null;
this.submittedOnDate = null;
this.createdDate = null;
this.createdByUserName = null;
this.reversed = null;
this.referenceNumber = null;
this.officeRunningBalance = null;
this.organizationRunningBalance = null;
this.runningBalanceComputed = null;
this.transactionDetails = null;
}
public JournalEntryData(final Long id, final Long officeId, final String officeName, final String glAccountName, final Long glAccountId,
final String glAccountCode, final EnumOptionData glAccountClassification, final LocalDate transactionDate,
final EnumOptionData entryType, final BigDecimal amount, final String transactionId, final Boolean manualEntry,
final EnumOptionData entityType, final Long entityId, final Long createdByUserId, final LocalDate submittedOnDate,
final String createdByUserName, final String comments, final Boolean reversed, final String referenceNumber,
final BigDecimal officeRunningBalance, final BigDecimal organizationRunningBalance, final Boolean runningBalanceComputed,
final TransactionDetailData transactionDetailData, final CurrencyData currency) {
this.id = id;
this.officeId = officeId;
this.officeName = officeName;
this.glAccountName = glAccountName;
this.glAccountId = glAccountId;
this.glAccountCode = glAccountCode;
this.glAccountType = glAccountClassification;
this.transactionDate = transactionDate;
this.entryType = entryType;
this.amount = amount;
this.transactionId = transactionId;
this.manualEntry = manualEntry;
this.entityType = entityType;
this.entityId = entityId;
this.createdByUserId = createdByUserId;
this.createdDate = submittedOnDate;
this.submittedOnDate = submittedOnDate;
this.createdByUserName = createdByUserName;
this.comments = comments;
this.reversed = reversed;
this.referenceNumber = referenceNumber;
this.officeRunningBalance = officeRunningBalance;
this.organizationRunningBalance = organizationRunningBalance;
this.runningBalanceComputed = runningBalanceComputed;
this.transactionDetails = transactionDetailData;
this.currency = currency;
}
public static JournalEntryData importInstance(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId,
Integer rowIndex, List<CreditDebit> credits, List<CreditDebit> debits, String accountNumber, String checkNumber,
String routingCode, String receiptNumber, String bankNumber, String comments, String locale, String dateFormat) {
return new JournalEntryData(officeId, transactionDate, currencyCode, paymentTypeId, rowIndex, credits, debits, accountNumber,
checkNumber, routingCode, receiptNumber, bankNumber, comments, locale, dateFormat);
}
public static JournalEntryData importInstance1(Long officeId, LocalDate transactionDate, String currencyCode, List<CreditDebit> credits,
List<CreditDebit> debits, String locale, String dateFormat) {
return new JournalEntryData(officeId, transactionDate, currencyCode, credits, debits, locale, dateFormat);
}
public static JournalEntryData fromGLAccountData(final GLAccountData glAccountData) {
final Long id = null;
final Long officeId = null;
final String officeName = null;
final String glAccountName = glAccountData.getName();
final Long glAccountId = glAccountData.getId();
final String glAccountCode = glAccountData.getGlCode();
final EnumOptionData glAccountClassification = glAccountData.getType();
final LocalDate transactionDate = null;
final EnumOptionData entryType = null;
final BigDecimal amount = null;
final String transactionId = null;
final Boolean manualEntry = null;
final EnumOptionData entityType = null;
final Long entityId = null;
final Long createdByUserId = null;
final LocalDate submittedOnDate = null;
final String createdByUserName = null;
final String comments = null;
final Boolean reversed = null;
final String referenceNumber = null;
final BigDecimal officeRunningBalance = null;
final BigDecimal organizationRunningBalance = null;
final Boolean runningBalanceComputed = null;
final TransactionDetailData transactionDetailData = null;
final CurrencyData currency = null;
return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glAccountCode, glAccountClassification,
transactionDate, entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate,
createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance,
runningBalanceComputed, transactionDetailData, currency);
}
public Integer getRowIndex() {
return rowIndex;
}
public LocalDate getTransactionDate() {
return transactionDate;
}
public void addDebits(CreditDebit debit) {
this.debits.add(debit);
}
public void addCredits(CreditDebit credit) {
this.credits.add(credit);
}
}
@@ -0,0 +1,41 @@
/**
* 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.accounting.journalentry.data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.note.data.NoteData;
import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData;
@RequiredArgsConstructor
@Getter
public class TransactionDetailData {
private final Long transactionId;
private final PaymentDetailData paymentDetails;
private final NoteData noteData;
private final TransactionTypeEnumData transactionType;
public TransactionDetailData(final Long transactionId, final PaymentDetailData paymentDetails, final NoteData noteData) {
this.transactionId = transactionId;
this.paymentDetails = paymentDetails;
this.noteData = noteData;
this.transactionType = null;
}
}
@@ -0,0 +1,34 @@
/**
* 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.accounting.journalentry.data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* Immutable data object represent loan and Savings status enumerations.
*/
@RequiredArgsConstructor
@Getter
public class TransactionTypeEnumData {
private final Long id;
private final String code;
private final String value;
}
@@ -0,0 +1,157 @@
/**
* 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.accounting.journalentry.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 lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
@Entity
@Getter
@Table(name = "acc_gl_journal_entry")
public class JournalEntry extends AbstractAuditableWithUTCDateTimeCustom {
@ManyToOne
@JoinColumn(name = "office_id", nullable = false)
private Office office;
@ManyToOne()
@JoinColumn(name = "payment_details_id")
private PaymentDetail paymentDetail;
@ManyToOne
@JoinColumn(name = "account_id", nullable = false)
private GLAccount glAccount;
@Column(name = "currency_code", length = 3, nullable = false)
private String currencyCode;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reversal_id")
private JournalEntry reversalJournalEntry;
@Column(name = "transaction_id", nullable = false, length = 50)
private String transactionId;
@Column(name = "loan_transaction_id")
private Long loanTransactionId;
@Column(name = "savings_transaction_id")
private Long savingsTransactionId;
@Column(name = "client_transaction_id")
private Long clientTransactionId;
@Column(name = "share_transaction_id")
private Long shareTransactionId;
@Column(name = "reversed", nullable = false)
private boolean reversed = false;
@Column(name = "manual_entry", nullable = false)
private boolean manualEntry = false;
@Column(name = "entry_date")
private LocalDate transactionDate;
@Column(name = "type_enum", nullable = false)
private Integer type;
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
@Column(name = "description", length = 500)
private String description;
@Column(name = "entity_type_enum", length = 50)
private Integer entityType;
@Column(name = "entity_id")
private Long entityId;
@Column(name = "ref_num")
private String referenceNumber;
@Column(name = "submitted_on_date", nullable = false)
private LocalDate submittedOnDate;
protected JournalEntry() {
//
}
public JournalEntry(final Office office, final PaymentDetail paymentDetail, final GLAccount glAccount, final String currencyCode,
final String transactionId, final boolean manualEntry, final LocalDate transactionDate, final Integer type,
final BigDecimal amount, final String description, final Integer entityType, final Long entityId, final String referenceNumber,
final Long loanTransactionId, final Long savingsTransactionId, final Long clientTransactionId, final Long shareTransactionId) {
this.office = office;
this.glAccount = glAccount;
this.reversalJournalEntry = null;
this.transactionId = transactionId;
this.reversed = false;
this.manualEntry = manualEntry;
this.transactionDate = transactionDate;
this.type = type;
this.amount = amount;
this.description = StringUtils.defaultIfEmpty(description, null);
this.entityType = entityType;
this.entityId = entityId;
this.referenceNumber = referenceNumber;
this.currencyCode = currencyCode;
this.loanTransactionId = loanTransactionId;
this.savingsTransactionId = savingsTransactionId;
this.clientTransactionId = clientTransactionId;
this.paymentDetail = paymentDetail;
this.shareTransactionId = shareTransactionId;
this.submittedOnDate = DateUtils.getBusinessLocalDate();
}
public static JournalEntry createNew(final Office office, final PaymentDetail paymentDetail, final GLAccount glAccount,
final String currencyCode, final String transactionId, final boolean manualEntry, final LocalDate transactionDate,
final JournalEntryType journalEntryType, final BigDecimal amount, final String description, final Integer entityType,
final Long entityId, final String referenceNumber, final Long loanTransaction, final Long savingsTransaction,
final Long clientTransaction, Long shareTransactionId) {
return new JournalEntry(office, paymentDetail, glAccount, currencyCode, transactionId, manualEntry, transactionDate,
journalEntryType.getValue(), amount, description, entityType, entityId, referenceNumber, loanTransaction,
savingsTransaction, clientTransaction, shareTransactionId);
}
public boolean isDebitEntry() {
return JournalEntryType.DEBIT.getValue().equals(this.type);
}
public void setReversalJournalEntry(final JournalEntry reversalJournalEntry) {
this.reversalJournalEntry = reversalJournalEntry;
}
public void setReversed(final boolean reversed) {
this.reversed = reversed;
}
}
@@ -0,0 +1,44 @@
/**
* 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.accounting.journalentry.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface JournalEntryRepository extends JpaRepository<JournalEntry, Long>, JpaSpecificationExecutor<JournalEntry> {
@Query("select journalEntry from JournalEntry journalEntry where journalEntry.transactionId= :transactionId and journalEntry.reversed=false and journalEntry.manualEntry=true")
List<JournalEntry> findUnReversedManualJournalEntriesByTransactionId(@Param("transactionId") String transactionId);
@Query("select DISTINCT j.transactionId from JournalEntry j where j.transactionId not in (select DISTINCT je.transactionId from JournalEntry je where je.glAccount.id = :contraId)")
List<String> findNonContraTransactionIds(@Param("contraId") Long contraId);
@Query("select DISTINCT j.transactionId from JournalEntry j where j.office.id = :officeId and j.glAccount.id = :contraId and j.reversed=false and j.transactionId not in (select DISTINCT je.reversalJournalEntry.transactionId from JournalEntry je where je.reversed=true)")
List<String> findNonReversedContraTransactionIds(@Param("contraId") Long contraId, @Param("officeId") Long officeId);
@Query("select journalEntry from JournalEntry journalEntry where journalEntry.entityId= :entityId and journalEntry.entityType = :entityType")
List<JournalEntry> findProvisioningJournalEntriesByEntityId(@Param("entityId") Long entityId, @Param("entityType") Integer entityType);
@Query("select journalEntry from JournalEntry journalEntry where journalEntry.transactionId= :transactionId and journalEntry.reversed=false and journalEntry.entityType = :entityType")
List<JournalEntry> findJournalEntries(@Param("transactionId") String transactionId, @Param("entityType") Integer entityType);
}
@@ -0,0 +1,42 @@
/**
* 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.accounting.journalentry.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.dao.EmptyResultDataAccessException;
/**
* A {@link RuntimeException} thrown when journal entry resources are not found.
*/
public class JournalEntriesNotFoundException extends AbstractPlatformResourceNotFoundException {
public JournalEntriesNotFoundException(final String transactionId) {
super("error.msg.journalEntries.transactionId.invalid",
"Journal Entries with transaction Identifier " + transactionId + " does not exist or are not system generated/reversible ",
transactionId);
}
public JournalEntriesNotFoundException(final Long entryId) {
super("error.msg.journalEntries.id.invalid", "Journal Entry with entry Id " + entryId + " does not exist ", entryId);
}
public JournalEntriesNotFoundException(final Long entryId, EmptyResultDataAccessException e) {
super("error.msg.journalEntries.id.invalid", "Journal Entry with entry Id " + entryId + " does not exist ", entryId, e);
}
}
@@ -0,0 +1,83 @@
/**
* 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.accounting.journalentry.exception;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* A {@link RuntimeException} thrown when a GL Journal Entry is Invalid
*/
public class JournalEntryInvalidException extends AbstractPlatformDomainRuleException {
/*** enum of reasons for invalid Journal Entry **/
public enum GlJournalEntryInvalidReason {
FUTURE_DATE, ACCOUNTING_CLOSED, NO_DEBITS_OR_CREDITS, DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT, DEBIT_CREDIT_SUM_MISMATCH, DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, GL_ACCOUNT_DISABLED, GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED, INVALID_DEBIT_OR_CREDIT_ACCOUNTS;
public String errorMessage() {
if (name().equalsIgnoreCase("FUTURE_DATE")) {
return "The journal entry cannot be made for a future date";
} else if (name().equalsIgnoreCase("ACCOUNTING_CLOSED")) {
return "Journal entry cannot be made prior to last account closing date for the branch";
} else if (name().equalsIgnoreCase("NO_DEBITS_OR_CREDITS")) {
return "Journal Entry must have at least one Debit and one Credit";
} else if (name().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT")) {
return "Sum of All Debits OR Credits must equal the Amount for a Journal Entry";
} else if (name().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH")) {
return "Sum of All Debits must equal the sum of all Credits for a Journal Entry";
} else if (name().equalsIgnoreCase("DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY")) {
return "Both account and amount must be specified for all Debits and Credits";
} else if (name().equalsIgnoreCase("GL_ACCOUNT_DISABLED")) {
return "Target account has been disabled";
} else if (name().equalsIgnoreCase("INVALID_DEBIT_OR_CREDIT_ACCOUNTS")) {
return "Invalid debit or credit accounts are passed";
} else if (name().equalsIgnoreCase("GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED")) {
return "Target account does not allow manual adjustments";
}
return name();
}
public String errorCode() {
if (name().equalsIgnoreCase("FUTURE_DATE")) {
return "error.msg.glJournalEntry.invalid.future.date";
} else if (name().equalsIgnoreCase("ACCOUNTING_CLOSED")) {
return "error.msg.glJournalEntry.invalid.accounting.closed";
} else if (name().equalsIgnoreCase("NO_DEBITS_OR_CREDITS")) {
return "error.msg.glJournalEntry.invalid.no.debits.or.credits";
} else if (name().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH")) {
return "error.msg.glJournalEntry.invalid.mismatch.debits.credits";
} else if (name().equalsIgnoreCase("DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY")) {
return "error.msg.glJournalEntry.invalid.empty.account.or.amount";
} else if (name().equalsIgnoreCase("GL_ACCOUNT_DISABLED")) {
return "error.msg.glJournalEntry.invalid.account.disabled";
} else if (name().equalsIgnoreCase("INVALID_DEBIT_OR_CREDIT_ACCOUNTS")) {
return "error.msg.glJournalEntry.invalid.debit.or.credit.accounts";
} else if (name().equalsIgnoreCase("GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED")) {
return "error.msg.glJournalEntry.invalid.account.manual.adjustments.not.permitted";
}
return name();
}
}
public JournalEntryInvalidException(final GlJournalEntryInvalidReason reason, final LocalDate date, final String accountName,
final String accountGLCode) {
super(reason.errorCode(), reason.errorMessage(), date, accountName, accountGLCode);
}
}
@@ -0,0 +1,37 @@
/**
* 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.accounting.journalentry.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
/**
* A {@link RuntimeException} thrown when journal entry resources are not found.
*/
public class JournalEntryNotFoundException extends AbstractPlatformResourceNotFoundException {
public JournalEntryNotFoundException(final String transactionId) {
super("error.msg.journalEntries.transactionId.invalid",
"Journal Entries with transaction Identifier " + transactionId + " does not exist or are not system generated/reversible ",
transactionId);
}
public JournalEntryNotFoundException(final Long entryId) {
super("error.msg.journalEntries.id.invalid", "Journal Entry with entry Id " + entryId + " does not exist ", entryId);
}
}
@@ -0,0 +1,28 @@
/**
* 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.accounting.journalentry.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class JournalEntryRuntimeException extends AbstractPlatformDomainRuleException {
public JournalEntryRuntimeException(String errorCode, String errorMessage) {
super(errorCode, errorMessage);
}
}
@@ -0,0 +1,30 @@
/**
* 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.accounting.trialbalance.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class TrialBalanceNotFoundException extends AbstractPlatformResourceNotFoundException {
public TrialBalanceNotFoundException(final Long officeId, final Long accountId) {
super("error.msg.trial.balance.invalid",
"Trial balance with officeId " + officeId + "and accountId" + accountId + " does not exist");
}
}
@@ -0,0 +1,146 @@
/**
* 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.batch.api;
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.DefaultValue;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
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 java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.batch.serialization.BatchRequestJsonHelper;
import org.apache.fineract.batch.service.BatchApiService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.exception.InvalidInstanceTypeMethodException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.stereotype.Component;
/**
* Provides a REST resource for Batch Requests. This class acts as a proxy to
* {@link org.apache.fineract.batch.service.BatchApiService} and de-serializes the incoming JSON string to a list of
* {@link org.apache.fineract.batch.domain .BatchRequest} type. This list is forwarded to BatchApiService which finally
* returns a list of {@link org.apache.fineract.batch.domain.BatchResponse} type which is then serialized into JSON
* response by this Resource class.
*
* @author Rishabh Shukla
*
* @see org.apache.fineract.batch.service.BatchApiService
* @see org.apache.fineract.batch.domain.BatchRequest
* @see org.apache.fineract.batch.domain.BatchResponse
*/
@Path("/v1/batches")
@Component
@Tag(name = "Batch API", description = "The Apache Fineract Batch API enables a consumer to access significant amounts of data in a single call or to make changes to several objects at once. Batching allows a consumer to pass instructions for several operations in a single HTTP request. A consumer can also specify dependencies between related operations. Once all operations have been completed, a consolidated response will be passed back and the HTTP connection will be closed.\n"
+ "\n"
+ "The Batch API takes in an array of logical HTTP requests represented as JSON arrays - each request has a requestId (the id of a request used to specify the sequence and as a dependency between requests), a method (corresponding to HTTP method GET/PUT/POST/DELETE etc.), a relativeUrl (the portion of the URL after https://example.org/api/v2/), optional headers array (corresponding to HTTP headers), optional reference parameter if a request is dependent on another request and an optional body (for POST and PUT requests). The Batch API returns an array of logical HTTP responses represented as JSON arrays - each response has a requestId, a status code, an optional headers array and an optional body (which is a JSON encoded string).\n"
+ "\n"
+ "Batch API uses Json Path to handle dependent parameters. For example, if request '2' is referencing request '1' and in the \"body\" or in \"relativeUrl\" of request '2', there is a dependent parameter (which will look like \"$.parameter_name\"), then Batch API will internally substitute this dependent parameter from the response body of request '1'.\n"
+ "\n"
+ "Batch API is able to handle deeply nested dependent requests as well nested parameters. As shown in the example, requests are dependent on each other as, 1<--2<--6, i.e a nested dependency, where request '6' is not directly dependent on request '1' but still it is one of the nested child of request '1'. In the same way Batch API could handle a deeply nested dependent value, such as {..[..{..,$.parameter_name,..}..]}.")
@RequiredArgsConstructor
public class BatchApiResource {
private final PlatformSecurityContext context;
private final ToApiJsonSerializer<BatchResponse> toApiJsonSerializer;
private final BatchApiService service;
private final BatchRequestJsonHelper batchRequestJsonHelper;
private final FineractProperties fineractProperties;
/**
* Rest assured POST method to get {@link BatchRequest} and returns back the consolidated {@link BatchResponse}
*
* @param jsonRequestString
* @param enclosingTransaction
* @param uriInfo
* @return serialized JSON
*/
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Batch requests in a single transaction", description = "The Apache Fineract Batch API is also capable of executing all the requests in a single transaction, by setting a Query Parameter, \"enclosingTransaction=true\". So, if one or more of the requests in a batch returns an erroneous response all of the Data base transactions made by other successful requests will be rolled back.\n"
+ "\n"
+ "If there has been a rollback in a transaction then a single response will be provided, with a '400' status code and a body consisting of the error details of the first failed request.")
@RequestBody(required = true, content = @Content(array = @ArraySchema(schema = @Schema(implementation = BatchRequest.class, description = "request body"))))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Success", content = @Content(array = @ArraySchema(schema = @Schema(implementation = BatchResponse.class)))) })
public String handleBatchRequests(
@DefaultValue("false") @QueryParam("enclosingTransaction") @Parameter(description = "enclosingTransaction", required = false) final boolean enclosingTransaction,
@Parameter(hidden = true) final String jsonRequestString, @Context UriInfo uriInfo) {
// Handles user authentication
this.context.authenticatedUser();
// Converts request array into BatchRequest List
final List<BatchRequest> requestList = this.batchRequestJsonHelper.extractList(jsonRequestString);
validateRequestMethodsAllowedOnInstanceType(requestList);
// Gets back the consolidated BatchResponse from BatchApiservice
List<BatchResponse> result = new ArrayList<>();
// If the request is to be handled as a Transaction. All requests will
// be rolled back on error
if (enclosingTransaction) {
result = service.handleBatchRequestsWithEnclosingTransaction(requestList, uriInfo);
} else {
result = service.handleBatchRequestsWithoutEnclosingTransaction(requestList, uriInfo);
}
return this.toApiJsonSerializer.serialize(result);
}
/**
* Validates to make sure the request methods are allowed on currently running instance mode (type).
*
* @param requestList
* the list of {@link BatchRequest}s
*/
private void validateRequestMethodsAllowedOnInstanceType(final List<BatchRequest> requestList) {
// Throw exception if instance is read only and any of the batch requests are trying to write/update data.
if (fineractProperties.getMode().isReadOnlyMode()) {
final Optional<BatchRequest> nonGetRequest = requestList.stream()
.filter(batchRequest -> !HttpMethod.GET.equals(batchRequest.getMethod())).findFirst();
if (nonGetRequest.isPresent()) {
throw new InvalidInstanceTypeMethodException(nonGetRequest.get().getMethod());
}
}
}
}
@@ -0,0 +1,79 @@
/**
* 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.batch.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Set;
import org.apache.fineract.batch.domain.Header;
/**
* Created by sanyam on 26/7/17.
*/
final class BatchApiResourceSwagger {
private BatchApiResourceSwagger() {
}
@Schema(description = "PostBatchesRequest")
public static final class PostBatchesRequest {
private PostBatchesRequest() {
}
public static final class PostBodyRequestSwagger {
private PostBodyRequestSwagger() {
}
@Schema(example = "1")
public Long officeId;
@Schema(example = "\"Petra\"")
public String firstname;
@Schema(example = "\"Yton\"")
public String lastname;
@Schema(example = "\"ex_externalId1\"")
public String externalId;
@Schema(example = "\"dd MMMM yyyy\"")
public String dateFormat;
@Schema(example = "\"en\"")
public String locale;
@Schema(example = "true")
public boolean active;
@Schema(example = "\"04 March 2009\"")
public String activationDate;
@Schema(example = "\"04 March 2009\"")
public String submittedOnDate;
}
@Schema(example = "1")
public Long requestId;
@Schema(example = "clients")
public String relativeUrl;
@Schema(example = "POST")
public String method;
public Set<Header> headers;
@Schema(example = "1")
public Long reference;
public PostBodyRequestSwagger body;
}
}
@@ -0,0 +1,124 @@
/**
* 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.batch.command;
import lombok.Getter;
/**
* Provides an object to {@link org.apache.fineract.batch.service.BatchApiService} to get the proper commandStrategy for
* each request in BatchRequest. It uses Builder pattern to create object of this type.
*
* @author Rishabh Shukla
*
* @see org.apache.fineract.batch.service.BatchApiService
*/
@Getter
public final class CommandContext {
/**
* Static Builder class to provide a Build method for CommandContext.
*
* @author Rishabh Shukla
*/
public static final class Builder {
private String resource;
private String method;
private Builder(final String resource) {
this.resource = resource;
}
public Builder method(final String method) {
this.method = method;
return this;
}
public CommandContext build() {
return new CommandContext(this.resource, this.method);
}
}
private final String resource;
private final String method;
private CommandContext(final String resource, final String method) {
this.resource = resource;
this.method = method;
}
public static Builder resource(final String resource) {
return new Builder(resource);
}
/**
* Returns a boolean value if the relativeUrl 'matches' one of the regex keys in the available commandStrategies. It
* take CommandContext object as parameter which contains a 'resource' member as a regex key for available
* commandStrategies.
*
* @param other
* @return boolean
*/
public boolean matcher(CommandContext other) {
if (this.resource.matches(other.resource) && this.method.equals(other.method)) {
return true;
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.method == null) ? 0 : this.method.hashCode());
result = prime * result + ((this.resource == null) ? 0 : this.resource.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof CommandContext)) {
return false;
}
CommandContext other = (CommandContext) obj;
if (this.method == null) {
if (other.method != null) {
return false;
}
} else if (!this.method.equals(other.method)) {
return false;
}
if (this.resource == null) {
if (other.resource != null) {
return false;
}
} else if (!this.resource.equals(other.resource)) {
return false;
}
return true;
}
}
@@ -0,0 +1,44 @@
/**
* 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.batch.command;
import jakarta.ws.rs.core.UriInfo;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
/**
* An interface for various Command Strategies. It contains a single function which returns appropriate response from a
* particular command strategy.
*
* @author Rishabh Shukla
*
* @see org.apache.fineract.batch.command.internal.UnknownCommandStrategy
*/
public interface CommandStrategy {
/**
* Returns an object of type {@link org.apache.fineract.batch.domain.BatchResponse}. This takes
* {@link org.apache.fineract.batch.domain.BatchRequest} as it's single argument and provides appropriate response.
*
* @param batchRequest
* @param uriInfo
* @return BatchResponse
*/
BatchResponse execute(BatchRequest batchRequest, UriInfo uriInfo);
}
@@ -0,0 +1,227 @@
/**
* 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.batch.command;
import static jakarta.ws.rs.HttpMethod.GET;
import static jakarta.ws.rs.HttpMethod.POST;
import static jakarta.ws.rs.HttpMethod.PUT;
import static org.apache.fineract.batch.command.CommandStrategyUtils.isResourceVersioned;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.fineract.batch.command.internal.UnknownCommandStrategy;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* Provides an appropriate CommandStrategy using the 'method' and 'resourceUrl'. CommandStrategy bean is created using
* Spring Application Context.
*
* @author Rishabh Shukla
*
* @see org.apache.fineract.batch.command.internal.UnknownCommandStrategy
*/
@Component
public class CommandStrategyProvider {
private final ApplicationContext applicationContext;
private static final Map<CommandContext, String> commandStrategies = new ConcurrentHashMap<>();
/**
* Regex pattern for specifying any number of query params or not specific any query param
*/
private static final String OPTIONAL_QUERY_PARAM_REGEX = "(\\?(\\w+(?:\\=[\\w,]+|&)+)+)?";
/**
* Regex pattern for specifying query params
*/
private static final String MANDATORY_QUERY_PARAM_REGEX = "(\\?(\\w+(?:\\=[\\w\\-,]+|&)+)+)";
/**
* Regex pattern for specifying any query param that has key = 'command' or not specific anything.
*/
private static final String OPTIONAL_COMMAND_PARAM_REGEX = "(\\?command=[\\w]+)?";
/**
* Regex pattern for specifying a mandatory query param that has key = 'command'.
*/
private static final String MANDATORY_COMMAND_PARAM_REGEX = "\\?command=[\\w]+";
/**
* Regex pattern for specifying a UUID param.
*/
private static final String UUID_PARAM_REGEX = "[\\w\\d-]+";
/**
* Regex pattern for specifying a param that's should be a number.
*/
private static final String NUMBER_REGEX = "\\d+";
/**
* Regex pattern for specifying a param that contains case in-sensitive alphanumeric characters with underscores.
*/
private static final String ALPHANUMBERIC_WITH_UNDERSCORE_REGEX = "[a-zA-Z0-9_]*";
/**
* Constructs a CommandStrategyProvider with argument of ApplicationContext type. It also initializes
* commandStrategies using init() function by filling it with available CommandStrategies in
* {@link org.apache.fineract.batch.command.internal}.
*
* @param applicationContext
*/
public CommandStrategyProvider(final ApplicationContext applicationContext) {
// calls init() function of this class.
init();
this.applicationContext = applicationContext;
}
/**
* Returns an appropriate commandStrategy after determining it using the CommandContext of the request. If no such
* Strategy is found then a default strategy is returned.
*
* @param commandContext
* @return CommandStrategy
* @see org.apache.fineract.batch.command.internal.UnknownCommandStrategy
*/
public CommandStrategy getCommandStrategy(final CommandContext commandContext) {
if (isResourceVersioned(commandContext)) {
return internalGetCommandStrategy(commandContext);
} else {
// for backward compatibility, support non-versioned relative paths too
CommandContext alteredCommandContext = CommandContext.resource("v1/" + commandContext.getResource())
.method(commandContext.getMethod()).build();
return internalGetCommandStrategy(alteredCommandContext);
}
}
private CommandStrategy internalGetCommandStrategy(CommandContext commandContext) {
if (commandStrategies.containsKey(commandContext)) {
return (CommandStrategy) this.applicationContext.getBean(commandStrategies.get(commandContext));
}
for (Map.Entry<CommandContext, String> entry : commandStrategies.entrySet()) {
if (commandContext.matcher(entry.getKey())) {
return (CommandStrategy) applicationContext.getBean(commandStrategies.get(entry.getKey()));
}
}
return new UnknownCommandStrategy();
}
/**
* Contains various available command strategies in {@link org.apache.fineract.batch.command.internal}. Any new
* command Strategy will have to be added within this function in order to initiate it within the constructor.
*/
private static void init() {
commandStrategies.put(CommandContext.resource("v1\\/clients").method(POST).build(), "createClientCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/clients\\/" + NUMBER_REGEX).method(PUT).build(), "updateClientCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans").method(POST).build(), "applyLoanCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + OPTIONAL_QUERY_PARAM_REGEX).method(GET).build(),
"getLoanByIdCommandStrategy");
commandStrategies.put(
CommandContext.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + OPTIONAL_QUERY_PARAM_REGEX).method(GET).build(),
"getLoanByExternalIdCommandStrategy");
commandStrategies.put(
CommandContext.resource("v1\\/savingsaccounts\\/" + NUMBER_REGEX + OPTIONAL_QUERY_PARAM_REGEX).method(GET).build(),
"getSavingsAccountByIdCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/savingsaccounts").method(POST).build(), "applySavingsCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/savingsaccounts\\/" + NUMBER_REGEX + "\\/transactions" + OPTIONAL_COMMAND_PARAM_REGEX).method(POST).build(),
"savingsAccountTransactionCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/savingsaccounts\\/" + NUMBER_REGEX + "\\/transactions\\/" + NUMBER_REGEX + OPTIONAL_COMMAND_PARAM_REGEX)
.method(POST).build(), "savingsAccountAdjustTransactionCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\/charges").method(POST).build(),
"createChargeCommandStrategy");
commandStrategies.put(
CommandContext.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + "\\/charges" + OPTIONAL_COMMAND_PARAM_REGEX + "")
.method(POST).build(),
"createChargeByLoanExternalIdCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\/charges").method(GET).build(),
"collectChargesCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + "\\/charges").method(GET).build(),
"collectChargesByLoanExternalIdCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\/charges\\/" + NUMBER_REGEX).method(GET).build(),
"getChargeByIdCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + "\\/charges\\/external-id\\/"
+ UUID_PARAM_REGEX + OPTIONAL_QUERY_PARAM_REGEX).method(GET).build(), "getChargeByChargeExternalIdCommandStrategy");
commandStrategies.put(
CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\/charges\\/" + NUMBER_REGEX + MANDATORY_COMMAND_PARAM_REGEX)
.method(POST).build(),
"adjustChargeCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + "\\/charges\\/external-id\\/"
+ UUID_PARAM_REGEX + MANDATORY_COMMAND_PARAM_REGEX).method(POST).build(), "adjustChargeByChargeExternalIdCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\/transactions" + MANDATORY_COMMAND_PARAM_REGEX)
.method(POST).build(), "createTransactionLoanCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + "\\/transactions" + MANDATORY_COMMAND_PARAM_REGEX).method(POST)
.build(), "createTransactionByLoanExternalIdCommandStrategy");
commandStrategies.put(
CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\/transactions\\/" + NUMBER_REGEX + OPTIONAL_COMMAND_PARAM_REGEX)
.method(POST).build(),
"adjustTransactionCommandStrategy");
commandStrategies.put(
CommandContext.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + "\\/transactions\\/external-id\\/"
+ UUID_PARAM_REGEX + OPTIONAL_COMMAND_PARAM_REGEX).method(POST).build(),
"adjustTransactionByExternalIdCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/clients\\/" + NUMBER_REGEX + "\\?command=activate").method(POST).build(),
"activateClientCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\?command=approve").method(POST).build(),
"approveLoanCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\?command=disburse").method(POST).build(),
"disburseLoanCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + MANDATORY_COMMAND_PARAM_REGEX)
.method(POST).build(), "loanStateTransistionsByExternalIdCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/rescheduleloans").method(POST).build(),
"createLoanRescheduleRequestCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/rescheduleloans\\/" + NUMBER_REGEX + "\\?command=approve").method(POST).build(),
"approveLoanRescheduleCommandStrategy");
commandStrategies.put(
CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\/transactions\\/" + NUMBER_REGEX).method(GET).build(),
"getTransactionByIdCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + "\\/transactions\\/external-id\\/"
+ UUID_PARAM_REGEX + OPTIONAL_QUERY_PARAM_REGEX).method(GET).build(), "getTransactionByExternalIdCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX)
.method(POST).build(), "createDatatableEntryCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX + "\\/" + NUMBER_REGEX)
.method(PUT).build(), "updateDatatableEntryOneToManyCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX)
.method(PUT).build(), "updateDatatableEntryOneToOneCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX + "\\/" + NUMBER_REGEX)
.method(PUT).build(), "updateDatatableEntryOneToManyCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX + OPTIONAL_QUERY_PARAM_REGEX)
.method(GET).build(), "getDatatableEntryByAppTableIdCommandStrategy");
commandStrategies.put(
CommandContext.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX + "\\/"
+ NUMBER_REGEX + OPTIONAL_QUERY_PARAM_REGEX).method(GET).build(),
"getDatatableEntryByAppTableIdAndDataTableIdCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + OPTIONAL_COMMAND_PARAM_REGEX).method(PUT).build(),
"modifyLoanApplicationCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + OPTIONAL_COMMAND_PARAM_REGEX)
.method(PUT).build(), "modifyLoanApplicationByExternalIdCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/query" + MANDATORY_QUERY_PARAM_REGEX).method(GET)
.build(), "getDatatableEntryByQueryCommandStrategy");
}
}
@@ -0,0 +1,89 @@
/**
* 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.batch.command;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.infrastructure.core.api.MutableUriInfo;
public final class CommandStrategyUtils {
private static final Pattern VERSIONED_RELATIVE_URL_PATTERN = Pattern.compile("^(v[1-9][0-9]*/)(.*)$");
private CommandStrategyUtils() {
}
/**
* Get query parameters from relative URL.
*
* @param relativeUrl
* the relative URL
* @return the query parameters in a map
*/
public static Map<String, String> getQueryParameters(final String relativeUrl) {
final String queryParameterStr = StringUtils.substringAfter(relativeUrl, "?");
final String[] queryParametersArray = StringUtils.split(queryParameterStr, "&");
final Map<String, String> queryParametersMap = new HashMap<>();
for (String parameterStr : queryParametersArray) {
String[] keyValue = StringUtils.split(parameterStr, "=");
queryParametersMap.put(keyValue[0], keyValue[1]);
}
return queryParametersMap;
}
/**
* Add query parameters(received in the relative URL) to URI info query parameters.
*
* @param uriInfo
* the URI info
* @param queryParameters
* the query parameters
*/
public static void addQueryParametersToUriInfo(final MutableUriInfo uriInfo, final Map<String, String> queryParameters) {
for (Map.Entry<String, String> entry : queryParameters.entrySet()) {
uriInfo.addAdditionalQueryParameter(entry.getKey(), entry.getValue());
}
}
public static String relativeUrlWithoutVersion(BatchRequest request) {
String relativeUrl = request.getRelativeUrl();
Matcher m = VERSIONED_RELATIVE_URL_PATTERN.matcher(relativeUrl);
if (m.matches()) {
return m.group(2);
} else {
return relativeUrl;
}
}
public static boolean isResourceVersioned(CommandContext commandContext) {
String relativeUrl = commandContext.getResource();
return isRelativeUrlVersioned(relativeUrl);
}
public static boolean isRelativeUrlVersioned(String relativeUrl) {
Matcher m = VERSIONED_RELATIVE_URL_PATTERN.matcher(relativeUrl);
return m.matches();
}
}
@@ -0,0 +1,49 @@
/**
* 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.batch.command.internal;
import jakarta.ws.rs.core.UriInfo;
import org.apache.fineract.batch.command.CommandStrategy;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.springframework.stereotype.Component;
/**
* Provides a default CommandStrategy by implementing {@link org.apache.fineract.batch.command.CommandStrategy} in case
* there is no appropriate command strategy with requested 'method' and 'resoureUrl'.
*
* @author Rishabh Shukla
*/
@Component
public class UnknownCommandStrategy implements CommandStrategy {
@Override
public BatchResponse execute(BatchRequest batchRequest, @SuppressWarnings("unused") UriInfo uriInfo) {
final BatchResponse batchResponse = new BatchResponse();
batchResponse.setRequestId(batchRequest.getRequestId());
batchResponse.setStatusCode(501);
batchResponse.setBody("Resource with method " + batchRequest.getMethod() + " and relativeUrl " + batchRequest.getRelativeUrl()
+ " doesn't exist");
return batchResponse;
}
}
@@ -0,0 +1,47 @@
/**
* 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.batch.domain;
import java.util.Set;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* Provides an object for separate HTTP requests in the Batch Request for Batch API. A requestId is also included as
* data field which takes care of dependency issues among various requests. This class also provides getter and setter
* functions to access Batch Request data fields.
*
* @author Rishabh Shukla
*
* @see org.apache.fineract.batch.api.BatchApiResource
* @see Header
*/
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class BatchRequest {
private Long requestId;
private String relativeUrl;
private String method;
private Set<Header> headers;
private Long reference;
private String body;
}
@@ -0,0 +1,46 @@
/**
* 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.batch.domain;
import java.util.Set;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* Provides an object for separate HTTP responses in the Batch Response for Batch API. It contains all the information
* about a particular HTTP response in the Batch Response. Getter and Setter functions are also included to access
* response data fields.
*
* @author Rishabh Shukla
*
* @see org.apache.fineract.batch.api.BatchApiResource
* @see org.apache.fineract.batch.service.BatchApiService
* @see Header
*/
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class BatchResponse {
private Long requestId;
private Integer statusCode;
private Set<Header> headers;
private String body;
}
@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.batch.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* Provides an object to handle HTTP headers as name and value pairs for Batch API. It is used in {@link BatchRequest}
* and {@link BatchResponse} to store the information regarding the headers in incoming and outgoing JSON Strings.
*
* @author Rishabh Shukla
*
* @see BatchRequest
* @see BatchResponse
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Accessors(chain = true)
public class Header {
private String name;
private String value;
}
@@ -0,0 +1,29 @@
/**
* 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.batch.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ClientDetailsNotFoundException extends AbstractPlatformDomainRuleException {
public ClientDetailsNotFoundException() {
super("validation.msg.batch.jlg.no.clients.defined", "No Client details found", "");
}
}
@@ -0,0 +1,114 @@
/**
* 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.batch.exception;
import com.google.gson.Gson;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.exception.PlatformInternalServerException;
import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformApiDataValidationExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformDataIntegrityExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformDomainRuleExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformInternalServerExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.PlatformResourceNotFoundExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.UnsupportedParameterExceptionMapper;
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
import org.apache.http.HttpStatus;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.transaction.TransactionException;
/**
* Provides an Error Handler method that returns an object of type {@link ErrorInfo} to the CommandStrategy which raised
* the exception. This class uses various subclasses of RuntimeException to check the kind of exception raised and
* provide appropriate status and error codes for each one of the raised exception.
*
* @author Rishabh Shukla
* @see org.apache.fineract.batch.command.CommandStrategy
*/
public final class ErrorHandler {
private static final Gson JSON_HELPER = GoogleGsonSerializerHelper.createGsonBuilder(true).create();
private static final LinkedHashMap<Class<? extends Exception>, Function<RuntimeException, ErrorInfo>> EXCEPTION_HANDLERS = new LinkedHashMap<>();
private static final Map.Entry<Class<? extends Exception>, Function<RuntimeException, ErrorInfo>> DEFAULT_ERROR_HANDLER = Map.entry(
RuntimeException.class,
runtimeException -> new ErrorInfo(500, 9999, "{\"Exception\": %s}".formatted(runtimeException.getMessage())));
static {
EXCEPTION_HANDLERS.put(AbstractPlatformResourceNotFoundException.class,
runtimeException -> handleException(runtimeException, new PlatformResourceNotFoundExceptionMapper(), 1001));
EXCEPTION_HANDLERS.put(UnsupportedParameterException.class,
runtimeException -> handleException(runtimeException, new UnsupportedParameterExceptionMapper(), 2001));
EXCEPTION_HANDLERS.put(PlatformApiDataValidationException.class,
runtimeException -> handleException(runtimeException, new PlatformApiDataValidationExceptionMapper(), 2002));
EXCEPTION_HANDLERS.put(PlatformDataIntegrityException.class,
runtimeException -> handleException(runtimeException, new PlatformDataIntegrityExceptionMapper(), 3001));
EXCEPTION_HANDLERS.put(AbstractPlatformDomainRuleException.class,
runtimeException -> handleException(runtimeException, new PlatformDomainRuleExceptionMapper(), 9999));
EXCEPTION_HANDLERS.put(TransactionException.class, runtimeException -> new ErrorInfo(HttpStatus.SC_BAD_REQUEST, 4001,
"{\"Exception\": %s}".formatted(runtimeException.getMessage())));
EXCEPTION_HANDLERS.put(PlatformInternalServerException.class,
runtimeException -> handleException(runtimeException, new PlatformInternalServerExceptionMapper(), 5001));
EXCEPTION_HANDLERS.put(NonTransientDataAccessException.class, runtimeException -> new ErrorInfo(HttpStatus.SC_BAD_REQUEST, 4002,
"{\"Exception\": %s}".formatted(runtimeException.getMessage())));
EXCEPTION_HANDLERS.put(DEFAULT_ERROR_HANDLER.getKey(), DEFAULT_ERROR_HANDLER.getValue());
}
private ErrorHandler() {}
public static void registerNewErrorHandler(final Class<? extends RuntimeException> exceptionClass, final ExceptionMapper mapper,
int errorCode) {
LinkedHashMap<Class<? extends Exception>, Function<RuntimeException, ErrorInfo>> newHandlers = new LinkedHashMap<>();
newHandlers.put(exceptionClass, runtimeException -> handleException(runtimeException, mapper, errorCode));
EXCEPTION_HANDLERS.forEach(newHandlers::putIfAbsent);
}
public static void registerNewErrorHandler(final Class<? extends RuntimeException> exceptionClass,
Function<RuntimeException, ErrorInfo> function) {
LinkedHashMap<Class<? extends Exception>, Function<RuntimeException, ErrorInfo>> newHandlers = new LinkedHashMap<>();
newHandlers.put(exceptionClass, function);
EXCEPTION_HANDLERS.forEach(newHandlers::putIfAbsent);
}
private static ErrorInfo handleException(final RuntimeException exception, final ExceptionMapper mapper, final int errorCode) {
final Response response = mapper.toResponse(exception);
final String errorBody = JSON_HELPER.toJson(response.getEntity());
return new ErrorInfo(response.getStatus(), errorCode, errorBody);
}
/**
* Returns an object of ErrorInfo type containing the information regarding the raised error.
*
* @param exception
* @return ErrorInfo
*/
public static ErrorInfo handler(final RuntimeException exception) {
Map.Entry<Class<? extends Exception>, Function<RuntimeException, ErrorInfo>> errorhandler = EXCEPTION_HANDLERS.entrySet().stream()
.filter(e -> e.getKey().isAssignableFrom(exception.getClass())).findFirst().orElse(DEFAULT_ERROR_HANDLER);
return errorhandler.getValue().apply(exception);
}
}
@@ -0,0 +1,108 @@
/**
* 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.batch.exception;
/**
* Provides members to hold the basic information about the exceptions raised in commandStrategy classes.
*
* @author Rishabh Shukla
*
* @see ErrorHandler
*/
public final class ErrorInfo {
private Integer statusCode;
private Integer errorCode;
private String message;
/**
* Constructor to initialize the members of this class.
*
* @param statusCode
* @param errorCode
* @param message
*/
public ErrorInfo(final Integer statusCode, final Integer errorCode, final String message) {
this.statusCode = statusCode;
this.errorCode = errorCode;
this.message = message;
}
/**
* Constructor so JSON serialization will work with out special Serialiazer
*/
ErrorInfo() {
}
/**
* Getter method to provide the statusCode for an object of this type.
*
* @return Integer
*/
public Integer getStatusCode() {
return this.statusCode;
}
/**
* Setter method to set the statusCode for an object of this type.
*
* @param statusCode
*/
public void setStatusCode(final Integer statusCode) {
this.statusCode = statusCode;
}
/**
* Getter method to provide the errorCode for an object of this type.
*
* @return Integer
*/
public Integer getErrorCode() {
return this.errorCode;
}
/**
* Setter method to set the errorCode for an object of this type.
*
* @param errorCode
*/
public void setErrorCode(final Integer errorCode) {
this.errorCode = errorCode;
}
/**
* Getter method to provide the message of the error for an object of this type.
*
* @return String
*/
public String getMessage() {
return this.message;
}
/**
* Setter method to set the message of the error for an object of this type.
*
* @param message
*/
public void setMessage(final String message) {
this.message = message;
}
}
@@ -0,0 +1,51 @@
/**
* 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.batch.serialization;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
/**
* Extends {@link org.apache.fineract.infrastructure.core.serialization.FromJsonHelper} to de-serialize the incoming
* String into a JSON List of type {@link org.apache.fineract.batch.domain.BatchRequest}
*
* @author Rishabh Shukla
*
* @see org.apache.fineract.batch.domain.BatchRequest
* @see org.apache.fineract.infrastructure.core.serialization.FromJsonHelper
*/
@Component
public class BatchRequestJsonHelper extends FromJsonHelper {
/**
* Returns a list of batchRequests after de-serializing it from the input JSON string.
*
* @param json
* @return List&lt;BatchRequest&gt;
*/
public List<BatchRequest> extractList(final String json) {
final Type listType = new TypeToken<List<BatchRequest>>() {}.getType();
final List<BatchRequest> requests = super.getGsonConverter().fromJson(json, listType);
return requests;
}
}
@@ -0,0 +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.batch.service;
import jakarta.ws.rs.core.UriInfo;
import java.util.List;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
/**
* Provides an interface for service class, that implements the method to handle separate Batch Requests.
*
* @author Rishabh Shukla
*
* @see org.apache.fineract.batch.domain.BatchRequest
* @see org.apache.fineract.batch.domain.BatchResponse
* @see BatchApiServiceImpl
*/
public interface BatchApiService {
/**
* Returns a list of {@link org.apache.fineract.batch.domain.BatchResponse}s by getting the appropriate
* CommandStrategy for every {@link org.apache.fineract.batch.domain.BatchRequest}. It will be used when the Query
* Parameter "enclosingTransaction "is set to 'false'.
*
* @param requestList
* @param uriInfo
* @return List&lt;BatchResponse&gt;
*/
List<BatchResponse> handleBatchRequestsWithoutEnclosingTransaction(List<BatchRequest> requestList, UriInfo uriInfo);
/**
* returns a list of {@link org.apache.fineract.batch.domain.BatchResponse}s by getting the appropriate
* CommandStrategy for every {@link org.apache.fineract.batch.domain.BatchRequest}. It will be used when the Query
* Parameter "enclosingTransaction "is set to 'true'. If one or more of the requests are not completed properly then
* whole of the transaction will be rolled back properly.
*
* @param requestList
* @param uriInfo
* @return List&lt;BatchResponse&gt;
*/
List<BatchResponse> handleBatchRequestsWithEnclosingTransaction(List<BatchRequest> requestList, UriInfo uriInfo);
}
@@ -0,0 +1,382 @@
/**
* 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.batch.service;
import com.google.gson.Gson;
import io.github.resilience4j.core.functions.Either;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.UriInfo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.batch.command.CommandContext;
import org.apache.fineract.batch.command.CommandStrategy;
import org.apache.fineract.batch.command.CommandStrategyProvider;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.batch.domain.Header;
import org.apache.fineract.batch.exception.ClientDetailsNotFoundException;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.batch.service.ResolutionHelper.BatchRequestNode;
import org.apache.fineract.infrastructure.core.domain.BatchRequestContextHolder;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException;
import org.apache.fineract.infrastructure.core.filters.BatchCallHandler;
import org.apache.fineract.infrastructure.core.filters.BatchFilter;
import org.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionExecution;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Implementation for {@link BatchApiService} to iterate through all the incoming requests and obtain the appropriate
* CommandStrategy from CommandStrategyProvider.
*
* @author Rishabh Shukla
* @see org.apache.fineract.batch.domain.BatchRequest
* @see org.apache.fineract.batch.domain.BatchResponse
* @see org.apache.fineract.batch.command.CommandStrategyProvider
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class BatchApiServiceImpl implements BatchApiService {
private final CommandStrategyProvider strategyProvider;
private final ResolutionHelper resolutionHelper;
private final PlatformTransactionManager transactionManager;
private final List<BatchFilter> batchFilters;
private final List<BatchRequestPreprocessor> batchPreprocessors;
@PersistenceContext
private final EntityManager entityManager;
/**
* Returns the response list by getting a proper {@link org.apache.fineract.batch.command.CommandStrategy}.
* execute() method of acquired commandStrategy is then provided with the separate Request.
*
* @param requestList
* @param uriInfo
* @return {@code List<BatchResponse>}
*/
private List<BatchResponse> handleBatchRequests(boolean enclosingTransaction, final List<BatchRequest> requestList,
final UriInfo uriInfo) {
final List<BatchResponse> responseList = new ArrayList<>(requestList.size());
final List<BatchRequestNode> batchRequestNodes = this.resolutionHelper.getDependingRequests(requestList);
if (batchRequestNodes.isEmpty()) {
final BatchResponse response = new BatchResponse();
ErrorInfo ex = ErrorHandler.handler(new ClientDetailsNotFoundException());
response.setStatusCode(500);
response.setBody(ex.getMessage());
responseList.add(response);
return responseList;
}
for (BatchRequestNode rootNode : batchRequestNodes) {
if (enclosingTransaction) {
this.callRequestRecursive(rootNode.getRequest(), rootNode, responseList, uriInfo, enclosingTransaction);
} else {
List<BatchResponse> localResponseList = new ArrayList<>();
this.callRequestRecursive(rootNode.getRequest(), rootNode, localResponseList, uriInfo, enclosingTransaction);
responseList.addAll(localResponseList);
}
}
Collections.sort(responseList, Comparator.comparing(BatchResponse::getRequestId));
return responseList;
}
/**
* Executes the request and call child requests recursively.
*
* @param request
* the current batch request
* @param requestNode
* the batch request holder node
* @param responseList
* the collected responses
* @return {@code BatchResponse}
*/
private void callRequestRecursive(BatchRequest request, BatchRequestNode requestNode, List<BatchResponse> responseList, UriInfo uriInfo,
boolean enclosingTransaction) {
// 1. run current node
BatchResponse response;
if (enclosingTransaction) {
response = executeRequest(request, uriInfo);
} else {
List<BatchResponse> transactionResponse = callInTransaction(
transactionTemplate -> transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
() -> List.of(executeRequest(request, uriInfo)));
response = transactionResponse.get(0);
}
responseList.add(response);
if (response.getStatusCode() != null && response.getStatusCode() == 200) {
requestNode.getChildRequests().forEach(childNode -> {
BatchRequest resolvedChildRequest;
try {
resolvedChildRequest = this.resolutionHelper.resoluteRequest(childNode.getRequest(), response);
} catch (RuntimeException ex) {
throw new BatchExecutionException(childNode.getRequest(), ex);
}
callRequestRecursive(resolvedChildRequest, childNode, responseList, uriInfo, enclosingTransaction);
});
} else {
responseList.addAll(parentRequestFailedRecursive(request, requestNode));
}
// If the current request fails, then all the child requests are not executed. If we want to write out all the
// child requests, here is the place.
}
/**
* All requests recursively are set to status 409 if the parent request fails.
*
* @param request
* the current request
* @param requestNode
* the current request node
* @return {@code BatchResponse} list of the generated batch responses
*/
private List<BatchResponse> parentRequestFailedRecursive(BatchRequest request, BatchRequestNode requestNode) {
List<BatchResponse> responseList = new ArrayList<>();
BatchRequestContextHolder.getEnclosingTransaction().ifPresent(TransactionExecution::setRollbackOnly);
BatchResponse errorResponse = new BatchResponse();
errorResponse.setRequestId(request.getRequestId());
errorResponse.setStatusCode(Status.CONFLICT.getStatusCode());
// Some detail information about the error
final ErrorInfo conflictError = new ErrorInfo(Status.CONFLICT.getStatusCode(), 8001,
"Parent request with id " + request.getRequestId() + " was erroneous!");
errorResponse.setBody(conflictError.getMessage());
requestNode.getChildRequests().stream()
.flatMap(childNode -> parentRequestFailedRecursive(childNode.getRequest(), childNode).stream()).forEach(responseList::add);
return responseList;
}
/**
* Execute the request
*
* @param request
* @param uriInfo
* @return
*/
private BatchResponse executeRequest(BatchRequest request, UriInfo uriInfo) {
final CommandStrategy commandStrategy = this.strategyProvider
.getCommandStrategy(CommandContext.resource(request.getRelativeUrl()).method(request.getMethod()).build());
log.debug("Batch request: method [{}], relative url [{}]", request.getMethod(), request.getRelativeUrl());
Either<RuntimeException, BatchRequest> preprocessorResult = runPreprocessors(request);
if (preprocessorResult.isLeft()) {
throw new BatchExecutionException(request, preprocessorResult.getLeft());
} else {
request = preprocessorResult.get();
}
try {
BatchRequestContextHolder.setRequestAttributes(new HashMap<>(Optional.ofNullable(request.getHeaders())
.map(list -> list.stream().collect(Collectors.toMap(Header::getName, Header::getValue)))
.orElse(Collections.emptyMap())));
BatchCallHandler callHandler = new BatchCallHandler(this.batchFilters, commandStrategy::execute);
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
if (BatchRequestContextHolder.getEnclosingTransaction().get().isRollbackOnly()) {
BatchResponse br = new BatchResponse();
br.setBody("Parent request was erroneous!");
br.setRequestId(request.getRequestId());
return br;
}
entityManager.flush();
}
final BatchResponse rootResponse = callHandler.serviceCall(request, uriInfo);
log.debug("Batch response: status code [{}], method [{}], relative url [{}]", rootResponse.getStatusCode(), request.getMethod(),
request.getRelativeUrl());
return rootResponse;
} catch (AbstractIdempotentCommandException idempotentException) {
return handleIdempotentRequests(idempotentException, request);
} catch (RuntimeException ex) {
throw new BatchExecutionException(request, ex);
} finally {
BatchRequestContextHolder.resetRequestAttributes();
}
}
private Either<RuntimeException, BatchRequest> runPreprocessors(BatchRequest request) {
return runPreprocessor(batchPreprocessors, request);
}
private Either<RuntimeException, BatchRequest> runPreprocessor(List<BatchRequestPreprocessor> remainingPreprocessor,
BatchRequest request) {
if (remainingPreprocessor.isEmpty()) {
return Either.right(request);
} else {
BatchRequestPreprocessor preprocessor = remainingPreprocessor.get(0);
Either<RuntimeException, BatchRequest> processingResult = preprocessor.preprocess(request);
if (processingResult.isLeft()) {
return processingResult;
} else {
return runPreprocessor(remainingPreprocessor.subList(1, remainingPreprocessor.size()), processingResult.get());
}
}
}
/**
* Return the idempotent response when idempotent exception raised
*
* @param idempotentException
* the idempotent exception
* @param request
* the called request
* @return
*/
private BatchResponse handleIdempotentRequests(AbstractIdempotentCommandException idempotentException, BatchRequest request) {
BatchResponse response = new BatchResponse();
response.setRequestId(request.getRequestId());
response.setHeaders(Optional.ofNullable(request.getHeaders()).orElse(new HashSet<>()));
response.getHeaders().add(new Header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true"));
response.setBody(idempotentException.getResponse());
if (idempotentException instanceof IdempotentCommandProcessSucceedException) {
response.setStatusCode(200);
} else if (idempotentException instanceof IdempotentCommandProcessUnderProcessingException) {
response.setStatusCode(409);
} else if (idempotentException instanceof IdempotentCommandProcessFailedException) {
response.setStatusCode(((IdempotentCommandProcessFailedException) idempotentException).getStatusCode());
} else {
response.setStatusCode(500);
}
return response;
}
/**
* Run each request root step in a separated transaction
*
* @param requestList
* @param uriInfo
* @return
*/
@Override
public List<BatchResponse> handleBatchRequestsWithoutEnclosingTransaction(final List<BatchRequest> requestList, UriInfo uriInfo) {
BatchRequestContextHolder.setEnclosingTransaction(Optional.empty());
return handleBatchRequests(false, requestList, uriInfo);
}
/**
* Run the batch request in transaction
*
* @param requestList
* @param uriInfo
* @return
*/
@Override
public List<BatchResponse> handleBatchRequestsWithEnclosingTransaction(final List<BatchRequest> requestList, final UriInfo uriInfo) {
return callInTransaction(() -> handleBatchRequests(true, requestList, uriInfo));
}
@NotNull
private List<BatchResponse> createErrorResponse(List<BatchResponse> responseList, int statusCode) {
BatchResponse errResponse = new BatchResponse();
for (BatchResponse res : responseList) {
if (res.getStatusCode() == null || !res.getStatusCode().equals(200)) {
errResponse.setBody("Transaction is being rolled back. First erroneous request: \n" + new Gson().toJson(res));
errResponse.setRequestId(res.getRequestId());
if (statusCode == -1) {
if (res.getStatusCode() != null) {
statusCode = res.getStatusCode();
} else {
statusCode = Status.INTERNAL_SERVER_ERROR.getStatusCode();
}
}
break;
}
}
errResponse.setStatusCode(statusCode);
return Arrays.asList(errResponse);
}
private List<BatchResponse> callInTransaction(Supplier<List<BatchResponse>> request) {
return callInTransaction(Function.identity()::apply, request);
}
/**
* Helper method to run the command in transaction
*
* @param request
* the enclosing supplier of the command
* @param transactionConfigurator
* consumer to configure the transaction behavior and isolation
* @return
*/
private List<BatchResponse> callInTransaction(Consumer<TransactionTemplate> transactionConfigurator,
Supplier<List<BatchResponse>> request) {
List<BatchResponse> responseList = new ArrayList<>();
try {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionConfigurator.accept(transactionTemplate);
return transactionTemplate.execute(status -> {
BatchRequestContextHolder.setEnclosingTransaction(Optional.of(status));
try {
responseList.addAll(request.get());
if (status.isRollbackOnly()) {
return createErrorResponse(responseList, -1);
}
return responseList;
} catch (BatchExecutionException ex) {
status.setRollbackOnly();
BatchResponse errResponse = new BatchResponse();
errResponse.setStatusCode(ex.getErrorInfo().getStatusCode());
errResponse.setRequestId(ex.getRequest().getRequestId());
errResponse.setBody(ex.getErrorInfo().getMessage());
return Arrays.asList(errResponse);
} catch (RuntimeException ex) {
status.setRollbackOnly();
ErrorInfo e = ErrorHandler.handler(ex);
BatchResponse errResponse = new BatchResponse();
errResponse.setStatusCode(e.getStatusCode());
errResponse.setBody(e.getMessage());
return Arrays.asList(errResponse);
}
});
} catch (TransactionException | NonTransientDataAccessException ex) {
ErrorInfo e = ErrorHandler.handler(ex);
return createErrorResponse(responseList, e.getStatusCode());
}
}
}
@@ -0,0 +1,37 @@
/**
* 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.batch.service;
import lombok.Getter;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo;
@Getter
public class BatchExecutionException extends RuntimeException {
private final BatchRequest request;
private final ErrorInfo errorInfo;
public BatchExecutionException(BatchRequest request, RuntimeException ex) {
super("Error executing batch request: " + request, ex);
this.request = request;
this.errorInfo = ErrorHandler.handler(ex);
}
}
@@ -0,0 +1,226 @@
/**
* 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.batch.service;
import com.google.common.base.Splitter;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.ReadContext;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
/**
* Provides methods to create dependency map among the various batchRequests. It also provides method that takes care of
* dependency resolution among related requests.
*
* @author Rishabh Shukla
* @see BatchApiServiceImpl
*/
@Component
@RequiredArgsConstructor
public class ResolutionHelper {
/**
* Provides a Node like object for the request tree.
*
* @author Rishabh shukla
*
*/
public static class BatchRequestNode {
private BatchRequest request;
private final List<BatchRequestNode> childRequests = new ArrayList<>();
public BatchRequestNode() {
}
public BatchRequest getRequest() {
return this.request;
}
public void setRequest(BatchRequest request) {
this.request = request;
}
public List<BatchRequestNode> getChildRequests() {
return this.childRequests;
}
public void addChildRequest(final BatchRequestNode batchRequest) {
this.childRequests.add(batchRequest);
}
}
private final FromJsonHelper fromJsonHelper;
/**
* Returns a map containing requests that are divided in accordance of dependency relations among them. Each
* different list is identified with a "Key" which is the "requestId" of the request at topmost level in dependency
* hierarchy of that particular list.
*
* @param batchRequests
* @return List&lt;ArrayList&lt;BatchRequestNode&gt;&gt;
*/
public List<BatchRequestNode> getDependingRequests(final List<BatchRequest> batchRequests) {
final List<BatchRequestNode> rootRequests = new ArrayList<>();
for (BatchRequest batchRequest : batchRequests) {
if (batchRequest.getReference() == null) {
final BatchRequestNode node = new BatchRequestNode();
node.setRequest(batchRequest);
rootRequests.add(node);
} else {
this.addDependingRequest(batchRequest, rootRequests);
}
}
return rootRequests;
}
private void addDependingRequest(final BatchRequest batchRequest, final List<BatchRequestNode> parentRequests) {
for (BatchRequestNode batchRequestNode : parentRequests) {
if (batchRequestNode.getRequest().getRequestId().equals(batchRequest.getReference())) {
final BatchRequestNode dependingRequest = new BatchRequestNode();
dependingRequest.setRequest(batchRequest);
batchRequestNode.addChildRequest(dependingRequest);
} else {
addDependingRequest(batchRequest, batchRequestNode.getChildRequests());
}
}
}
/**
* Returns a BatchRequest after dependency resolution. It takes a request and the response of the request it is
* dependent upon as its arguments and change the body or relativeUrl of the request according to parent Request.
*
* @param request
* @param parentResponse
* @return BatchRequest
*/
public BatchRequest resoluteRequest(final BatchRequest request, final BatchResponse parentResponse) {
// Create a duplicate request
final BatchRequest br = request;
final ReadContext responseCtx = JsonPath.parse(parentResponse.getBody());
// Gets the body from current Request as a JsonObject
final JsonObject jsonRequestBody = this.fromJsonHelper.parse(request.getBody()).getAsJsonObject();
JsonObject jsonResultBody = new JsonObject();
// Iterate through each element in the requestBody to find dependent
// parameter
for (Map.Entry<String, JsonElement> element : jsonRequestBody.entrySet()) {
final String key = element.getKey();
final JsonElement value = resolveDependentVariables(element, responseCtx);
jsonResultBody.add(key, value);
}
// Set the body after dependency resolution
br.setBody(jsonResultBody.toString());
// Also check the relativeUrl for any dependency resolution
String relativeUrl = request.getRelativeUrl();
if (relativeUrl.contains("$.")) {
String queryParams = "";
if (relativeUrl.contains("?")) {
queryParams = relativeUrl.substring(relativeUrl.indexOf("?"));
relativeUrl = relativeUrl.substring(0, relativeUrl.indexOf("?"));
}
final Iterable<String> parameters = Splitter.on('/').split(relativeUrl);
for (String parameter : parameters) {
if (parameter.contains("$.")) {
final String resParamValue = responseCtx.read(parameter).toString();
relativeUrl = relativeUrl.replace(parameter, resParamValue);
br.setRelativeUrl(relativeUrl + queryParams);
}
}
}
return br;
}
private JsonElement resolveDependentVariables(final Map.Entry<String, JsonElement> entryElement, final ReadContext responseCtx) {
JsonElement value = null;
final JsonElement element = entryElement.getValue();
if (element.isJsonObject()) {
final JsonObject jsObject = element.getAsJsonObject();
value = processJsonObject(jsObject, responseCtx);
} else if (element.isJsonArray()) {
final JsonArray jsElementArray = element.getAsJsonArray();
value = processJsonArray(jsElementArray, responseCtx);
} else {
value = resolveDependentVariable(element, responseCtx);
}
return value;
}
private JsonElement processJsonObject(final JsonObject jsObject, final ReadContext responseCtx) {
JsonObject valueObj = new JsonObject();
for (Map.Entry<String, JsonElement> element : jsObject.entrySet()) {
final String key = element.getKey();
final JsonElement value = resolveDependentVariable(element.getValue(), responseCtx);
valueObj.add(key, value);
}
return valueObj;
}
private JsonArray processJsonArray(final JsonArray elementArray, final ReadContext responseCtx) {
JsonArray valueArr = new JsonArray();
for (JsonElement element : elementArray) {
if (element.isJsonObject()) {
final JsonObject jsObject = element.getAsJsonObject();
valueArr.add(processJsonObject(jsObject, responseCtx));
}
}
return valueArr;
}
private JsonElement resolveDependentVariable(final JsonElement element, final ReadContext responseCtx) {
JsonElement value = element;
String paramVal = element.getAsString();
if (paramVal.contains("$.")) {
// Get the value of the parameter from parent response
final String resParamValue = responseCtx.read(paramVal).toString();
value = this.fromJsonHelper.parse(resParamValue);
}
return value;
}
}
@@ -0,0 +1,21 @@
/**
* 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.cob.exceptions;
public class BusinessStepNotBelongsToJobException extends RuntimeException {}
@@ -0,0 +1,37 @@
/**
* 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.commands.domain;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum CommandProcessingResultType {
INVALID(0, "commandProcessingResultType.invalid"), //
PROCESSED(1, "commandProcessingResultType.processed"), //
AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), //
REJECTED(3, "commandProcessingResultType.rejected"), //
UNDER_PROCESSING(4, "commandProcessingResultType.underProcessing"), //
ERROR(5, "commandProcessingResultType.error");
private final Integer value;
private final String code;
}

Some files were not shown because too many files have changed in this diff Show More