InventoryController.java

/*
 * Copyright 2019 Global Crop Diversity Trust
 *
 * Licensed 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.gringlobal.api.v1.impl;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.apache.commons.lang3.ArrayUtils;
import org.genesys.blocks.auditlog.model.AuditLog;
import org.genesys.blocks.auditlog.model.filters.AuditLogFilter;
import org.genesys.blocks.auditlog.service.AuditTrailService;
import org.genesys.blocks.model.filters.NumberFilter;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.gringlobal.api.v1.ActionController;
import org.gringlobal.api.v1.ApiBaseController;
import org.gringlobal.api.v1.CRUDController;
import org.gringlobal.api.v1.FilteredCRUDController;
import org.gringlobal.api.v1.FilteredPage;
import org.gringlobal.api.v1.Pagination;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.AccessionInvAttach;
import org.gringlobal.model.Cooperator;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.InventoryAction;
import org.gringlobal.model.InventoryQualityStatus;
import org.gringlobal.model.OrderRequest;
import org.gringlobal.model.QInventory;
import org.gringlobal.model.QInventoryAction;
import org.gringlobal.model.SeedInventoryExtra;
import org.gringlobal.model.Site;
import org.gringlobal.persistence.InventoryRepositoryCustom;
import org.gringlobal.service.InventoryActionService;
import org.gringlobal.service.InventoryActionService.InventoryActionRequest;
import org.gringlobal.service.InventoryActionService.InventoryActionScheduleFilter;
import org.gringlobal.service.InventoryService.ComparedSitesResponse;
import org.gringlobal.service.InventoryAttachmentService;
import org.gringlobal.service.InventoryExtraService;
import org.gringlobal.service.InventoryQualityStatusService;
import org.gringlobal.service.InventoryService;
import org.gringlobal.service.filter.AccessionFilter;
import org.gringlobal.service.filter.InventoryActionFilter;
import org.gringlobal.service.filter.InventoryFilter;
import org.gringlobal.service.filter.InventoryQualityStatusFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.querydsl.core.types.OrderSpecifier;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;

@RestController("inventoryApi1")
@RequestMapping(InventoryController.API_URL)
@PreAuthorize("isAuthenticated()")
@Tag(name = "Inventory")
public class InventoryController extends FilteredCRUDController<Inventory, InventoryService, InventoryFilter> {

	/** The Constant API_URL. */
	public static final String API_URL = ApiBaseController.APIv1_BASE + "/i";

	@Autowired
	private InventoryAttachmentService attachmentFileService;

	@Autowired
	private AuditTrailService auditService;
	
	@Autowired
	protected InventoryExtraService inventoryExtraService;

	@Override
	protected OrderSpecifier<?>[] defaultSort() {
		return new OrderSpecifier[] { QInventory.inventory.id.desc() };
	}

	@Override
	protected Class<InventoryFilter> filterType() {
		return InventoryFilter.class;
	}

	/**
	 * Retrieve inventory details by id
	 *
	 * @param id the id
	 * @return the inventory details
	 */
	@GetMapping(value = "/details/{id}", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "getDetails", description = "Retrieve inventory details by ID", summary = "Details")
	public InventoryService.InventoryDetails details(@PathVariable("id") final long id) {
		return crudService.getInventoryDetails(crudService.get(id));
	}

	/**
	 * Retrieve inventory details by barcode
	 *
	 * @param barcode the barcode
	 * @return the inventory details
	 */
	@GetMapping(value = "/details", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "getDetails", description = "Retrieve inventory details by barcode", summary = "Details")
	public InventoryService.InventoryDetails details(@RequestParam("barcode") final String barcode) {
		return crudService.getInventoryDetails(crudService.getByBarcode(barcode));
	}

	/**
	 * Retrieve a list of audit logs for the specified inventory
	 *
	 * @param inventoryId the inventoryId
	 * @param page the page request
	 * @return the list of all log entries
	 */
	@GetMapping("/auditlog/{id}")
	public Page<AuditLog> inventoryAuditLogs(@PathVariable(value = "id") final Long inventoryId, @Parameter(hidden = true) final Pagination page) {
		var filter = new AuditLogFilter();
		filter
			.entityId(new NumberFilter<Long>().eq(Set.of(crudService.get(inventoryId).getId())))
			.classname(Inventory.class.getName());

		return auditService.listAuditLogs(filter, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE));
	}

	/**
	 * Generate and assign the barcode for Inventory based on a template
	 *
	 * @param inventoryId the ID of inventory
	 * @return the barcode
	 */
	@PostMapping(value = "/{id}/barcode", produces = { MediaType.TEXT_PLAIN_VALUE })
	public String assignBarcode(@PathVariable(value = "id") final Long inventoryId) {
		return crudService.assignBarcode(crudService.get(inventoryId));
	}

	@PostMapping("/quantity")
	@Operation(summary = "Set inventory quantity on hand", description = "Update current quantity on hand.")
	public Inventory setInventoryQuantity(@RequestBody @Valid InventoryService.InventoryQuantityRequest inventoryQuantity) {
		return crudService.setInventoryQuantity(inventoryQuantity);
	}

	@PostMapping("/discard-material")
	@Operation(summary = "Update inventory quantity on hand", description = "Update current quantity on hand.")
	public ResponseEntity<HttpStatus> discardMaterial(@RequestBody final Map<Long, Integer> discardQuantities) {
		crudService.discardMaterial(discardQuantities);
		return ResponseEntity.ok().build();
	}

	@Override
	@Operation(operationId = "list")
	public FilteredPage<Inventory, InventoryFilter> list(@Parameter(hidden = true) final Pagination page, @RequestBody InventoryFilter filter) throws SearchException, IOException {
		return super.list(page, filter);
	}

	@PostMapping(value = "/aggregate-quantity", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "aggregateQuantity", description = "Returns the aggregate of inventory quantities.", summary = "Sum inventory quantities.")
	public FilteredPage<InventoryRepositoryCustom.AggregatedInventoryQuantity, InventoryFilter> aggregateQuantity(@Parameter(hidden = true) final Pagination page, @RequestBody InventoryFilter filter) throws IOException {
		InventoryFilter normalizedFilter = shortFilterService.normalizeFilter(filter, filterType());
		Pageable pageable = ArrayUtils.isEmpty(page.getS()) ? page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE, QInventory.inventory.formTypeCode.asc()) : page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE);
		return new FilteredPage<>(normalizedFilter, crudService.aggregateQuantity(filter, pageable));
	}

	@Override
	@Operation(operationId = "filter", description = "Retrieve list of records matching the filter or filter code", summary = "List by filter code or filter")
	public FilteredPage<Inventory, InventoryFilter> filter(@RequestParam(name = "f", required = false) String filterCode, @Parameter(hidden = true) final Pagination page, @RequestBody InventoryFilter filter) throws IOException, SearchException {
		return super.filter(filterCode, page, filter);
	}

	@PostMapping(value = "/overview/{groupBy:.+}", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "inventoryOverview", description = "Get inventory statistics", summary = "Overview")
	public Map<?, ?> inventoryOverview(@PathVariable(name = "groupBy", required = true) String groupBy, @RequestBody InventoryFilter filter) {
		return crudService.inventoryOverview(groupBy, filter);
	}

	@PostMapping(value = "/attach/{inventoryId}", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "uploadFile", description = "Attach accession file", summary = "Attach file")
	public AccessionInvAttach uploadFile(@PathVariable(name = "inventoryId") final Long inventoryId, @RequestPart(name = "file") final MultipartFile file,
			@RequestPart(name = "metadata") final InventoryAttachmentService.InventoryAttachmentRequest metadata) throws InvalidRepositoryPathException, InvalidRepositoryFileDataException, IOException {

		return attachmentFileService.uploadFile(crudService.get(inventoryId), file, metadata);
	}

	@DeleteMapping(value = "/attach/{inventoryId}/{attachmentId}", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "removeFile", description = "Remove attached file", summary = "Remove file")
	public AccessionInvAttach removeFile(@PathVariable(name = "inventoryId") final Long inventoryId, @PathVariable(name = "attachmentId") final Long attachmentId) {
		return attachmentFileService.removeFile(crudService.get(inventoryId), attachmentId);
	}

	@PostMapping(value = "/attach/{attachId}/share", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "shareAttachment", description = "Share attachment to inventories", summary = "Share attachment file")
	public void shareAttachment(@PathVariable(name = "attachId") final Long attachId, @RequestBody @NotEmpty List<Long> inventoryIds) {
		crudService.shareAttachment(attachId, inventoryIds);
	}

	@PostMapping(value = "/split", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "split", description = "Split inventory", summary = "split")
	public List<Inventory> splitInventory(@RequestBody @Valid InventoryService.SplitInventoryRequest splitInventoryRequest) {
		return crudService.splitInventory(splitInventoryRequest);
	}

	@PostMapping(value = "/assign-location", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "assignLocation", description = "Assign location to inventories", summary = "Assign location")
	public List<Inventory> assignLocation(@RequestBody InventoryService.AssignLocationRequest assignLocationRequest) {
		return crudService.assignLocation(assignLocationRequest);
	}

	@PostMapping(value = "/assign-locations", produces = { MediaType.APPLICATION_JSON_VALUE })
	@Operation(operationId = "assignLocations", description = "Assign multiple location to inventories", summary = "Assign locations")
	public List<Inventory> assignLocations(@RequestBody List<InventoryService.AssignLocationRequest> assignLocationRequests) {
		return crudService.assignLocations(assignLocationRequests);
	}

	@PostMapping("/multiplication/order")
	@Operation(summary = "Create multiplication OrderRequest", description = "Create new OrderRequest for multiplication")
	public OrderRequest multiplicationOrder(@RequestBody @Valid MultiplicationOrderRequest request) {
		return crudService.multiplicationOrder(request.site, request.inventoryIds, request.cooperator, request.orderTypeCode, request.intendedUseCode);
	}

	public static class MultiplicationOrderRequest {
		public Site site;
		@NotNull
		public Cooperator cooperator;
		@NotNull
		@Size(min = 1)
		public Set<Long> inventoryIds;
		@NotNull
		public String orderTypeCode;
		@NotNull
		public String intendedUseCode;
	}

	@PostMapping("/extra/{extraId}/moisture-content")
	@Operation(summary = "Update moisture content", description = "Update moisture content in inventory extra")
	public SeedInventoryExtra update(@PathVariable(name = "extraId") final Long extraId, @RequestBody @Valid InventoryService.MoistureContentRequest request) {
		return crudService.updateMoistureContent((SeedInventoryExtra) inventoryExtraService.load(extraId), request);
	}

	@PostMapping("/site/compare")
	@Operation(summary = "Compare sites", description = "Compare the number of inventories across sites")
	public Page<ComparedSitesResponse> compareSites(@Parameter(hidden = true) final Pagination page, @RequestBody @Valid CompareSitesRequest request) {
		return crudService.compareSites(request.filter, request.sites, page.toPageRequest(MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE));
	}

	public static class CompareSitesRequest {
		public AccessionFilter filter;
		@NotNull
		@Size(min = 2)
		public Map<Long, NumberFilter<Long>> sites;
	}

	@RestController("accessionInvAttachApi1")
	@RequestMapping(AccessionInvAttachController.API_URL)
	@PreAuthorize("isAuthenticated()")
	@Tag(name = "Inventory")
	public static class AccessionInvAttachController extends CRUDController<AccessionInvAttach, InventoryAttachmentService> {
		/** The Constant API_URL. */
		public static final String API_URL = InventoryController.API_URL + "/attach/meta";

		@Override
		@Operation(operationId = "createAccessionInvAttach", description = "Create AccessionInvAttach", summary = "Create")
		public AccessionInvAttach create(@RequestBody AccessionInvAttach entity) {
			// Throws UnsupportedOperationException
			return super.create(entity);
		}

		@Override
		@Operation(operationId = "updateAccessionInvAttach", description = "Update an existing record", summary = "Update")
		public AccessionInvAttach update(@RequestBody AccessionInvAttach entity) {
			return super.update(entity);
		}

		@Override
		@Operation(operationId = "getAccessionInvAttach", description = "Get record by ID", summary = "Get")
		public AccessionInvAttach get(@PathVariable long id) {
			return super.get(id);
		}

		@Override
		@Operation(operationId = "deleteAccessionInvAttach", description = "Delete existing record by ID", summary = "Delete")
		public AccessionInvAttach remove(@PathVariable long id) {
			return super.remove(id);
		}
	}

	@RestController("inventoryActionApi1")
	@RequestMapping(InventoryActionController.API_URL)
	@PreAuthorize("isAuthenticated()")
	@Tag(name = "Inventory")
	public static class InventoryActionController extends ActionController<InventoryAction, InventoryActionFilter, InventoryActionRequest, InventoryActionScheduleFilter, InventoryActionService> {
		public static final String API_URL = InventoryController.API_URL;

		@Override
		protected Class<InventoryActionFilter> filterType() {
			return InventoryActionFilter.class;
		}

		@Override
		protected OrderSpecifier<?>[] defaultSort() {
			return new OrderSpecifier[] { QInventoryAction.inventoryAction.createdDate.desc() };
		}
	}

	@RestController("inventoryQualityStatusApi1")
	@RequestMapping(InventoryQualityStatusController.API_URL)
	@PreAuthorize("isAuthenticated()")
	@Tag(name = "Inventory")
	public static class InventoryQualityStatusController extends FilteredCRUDController<InventoryQualityStatus, InventoryQualityStatusService, InventoryQualityStatusFilter> {
		public static final String API_URL = InventoryController.API_URL + "/quality";

	}
}