Skip to content
Snippets Groups Projects
Commit 9338b099 authored by GAYDAMAKHA MIKHAIL's avatar GAYDAMAKHA MIKHAIL
Browse files

:sparkles: add stock update endpoint

parent 58d4ee7b
Branches
2 merge requests!28Category and doc update to Master.,!26:sparkles: add stock update endpoint
Showing
with 303 additions and 150 deletions
......@@ -67,7 +67,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
/**
* API Mapping for retrieving stocks.
*/
public static final String MAPPING_GETSTOCKS = API_FULL_PREFIX +
public static final String MAPPING_STOCKS = API_FULL_PREFIX +
"/stock";
/**
......
......@@ -4,7 +4,7 @@
*/
package fr.unistra.sil.erp.back.controller.api;
import static fr.unistra.sil.erp.back.WebMvcConfig.MAPPING_GETSTOCKS;
import static fr.unistra.sil.erp.back.WebMvcConfig.MAPPING_STOCKS;
import fr.unistra.sil.erp.back.controller.IRetrieveStocks;
import fr.unistra.sil.erp.back.model.Stock;
......@@ -41,7 +41,7 @@ public class ApiRetrieveStocks implements IRetrieveStocks {
* @return the response.
* @throws ApiServerErrorException if the request could not be served.
*/
@GetMapping(MAPPING_GETSTOCKS)
@GetMapping(MAPPING_STOCKS)
@Override
public ResponseEntity<Object> retrieveStocks(HttpServletRequest request, HttpServletResponse response) throws ApiServerErrorException {
List<Stock> res = repository.getStocks();
......
/*
* CONTRAT DE LICENCE DE LOGICIEL LIBRE CeCILL-B
* https://cecill.info/licences/Licence_CeCILL-B_V1-fr.html
*/
package fr.unistra.sil.erp.back.controller.api;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import static fr.unistra.sil.erp.back.WebMvcConfig.MAPPING_SUBTRANSAC;
import fr.unistra.sil.erp.back.controller.ISubmitTransactionController;
import fr.unistra.sil.erp.back.repository.DatabaseConnectionException;
import fr.unistra.sil.erp.back.repository.DatabaseResourceNotFoundException;
import fr.unistra.sil.erp.back.repository.DatabaseUpdateException;
import fr.unistra.sil.erp.back.model.Stock;
import fr.unistra.sil.erp.back.model.Transaction;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import fr.unistra.sil.erp.back.repository.IStocksRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* REST Controller for submitting transactions.
*
* @author BEAUVAIS ANTOINE
*/
@RestController
public class ApiSubmitTransactionController implements ISubmitTransactionController {
private final IStocksRepository repository;
public ApiSubmitTransactionController(IStocksRepository repository) {
this.repository = repository;
}
/**
* Handler for transaction submissions.
*
* @param request the HTTP Servlet Request provided by Spring.
* @param response the HTTP Servlet Response provided by Spring.
* @return the response for the user.
* @throws ApiBadRequestException if the query failed.
* @throws ApiServerErrorException Server error.
* @throws ApiResourceNotFoundException Stock not found.
*/
@RequestMapping(value = MAPPING_SUBTRANSAC, method = RequestMethod.POST)
@Override
public ResponseEntity<Object> submitTransaction(HttpServletRequest request,
HttpServletResponse response)
throws ApiBadRequestException, ApiServerErrorException,
ApiResourceNotFoundException {
Gson gson = new Gson();
String body;
try {
body = request.getReader().lines()
.collect(Collectors.joining(System.lineSeparator()));
} catch (IOException ex) {
Logger.getLogger(ApiSubmitTransactionController.class.getName())
.log(Level.SEVERE, "Unparseable body.", ex);
throw new ApiBadRequestException("Unparseable body.");
}
if (body == null)
throw new ApiBadRequestException("Missing JSON body.");
Transaction t;
try {
t = gson.fromJson(body, Transaction.class);
} catch (JsonParseException ex) {
throw new ApiBadRequestException("Invalid JSON: syntax error.");
}
if (t == null)
throw new ApiBadRequestException("Missing JSON body.");
if (!t.checkIfValid())
throw new ApiBadRequestException("Invalid JSON schema.");
Stock s;
try {
s = repository.getStockForItem(t.getItem());
if (s == null)
throw new ApiServerErrorException("Database failure.");
int newQuantity = s.getQuantity() + t.getQuantity();
if (newQuantity < 0)
newQuantity = 0;
repository.updateStock(s.getId(), newQuantity);
} catch (DatabaseConnectionException ex) {
Logger.getLogger(ApiSubmitTransactionController.class.getName())
.log(Level.SEVERE, ex.getMessage(), ex);
throw new ApiServerErrorException("Database failure.");
} catch (DatabaseResourceNotFoundException ex) {
throw new ApiResourceNotFoundException("No stock found for item " +
t.getItem());
} catch (DatabaseUpdateException ex) {
Logger.getLogger(ApiSubmitTransactionController.class.getName())
.log(Level.SEVERE, ex.getMessage(), ex);
throw new ApiServerErrorException("Database update failure.");
}
return new ResponseEntity<>(t, HttpStatus.CREATED);
}
}
package fr.unistra.sil.erp.back.controller.api;
import fr.unistra.sil.erp.back.model.Stock;
import fr.unistra.sil.erp.back.service.CantUpdateStockException;
import fr.unistra.sil.erp.back.service.UpdateStockRequest;
import fr.unistra.sil.erp.back.service.UpdateStockService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import static fr.unistra.sil.erp.back.WebMvcConfig.MAPPING_STOCKS;
@Controller
public class ApiUpdateStockController {
private final UpdateStockService service;
public ApiUpdateStockController(UpdateStockService service) {
this.service = service;
}
/**
* Creates a product.
*
* @param requestBody request body
* @return a JSON response.
* @throws ApiBadRequestException if the request could not be served.
*/
@PutMapping(MAPPING_STOCKS)
public ResponseEntity<Stock> putStock(
@Validated @RequestBody PutStockDTO requestBody
) throws ApiBadRequestException {
Stock stock;
try {
stock = service.update(new UpdateStockRequest(
requestBody.getItemId(),
requestBody.getQuantity()
));
} catch (CantUpdateStockException e) {
throw new ApiBadRequestException(e.getMessage());
}
return new ResponseEntity<>(stock, HttpStatus.CREATED);
}
}
package fr.unistra.sil.erp.back.controller.api;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
public class PutStockDTO {
/**
* This product's category identifier.
*/
@NotNull
@Positive
private final int itemId;
/**
* This product's category identifier.
*/
@NotNull
private final int quantity;
public PutStockDTO(@JsonProperty("item_id") int itemId, @JsonProperty("quantity") int quantity) {
this.itemId = itemId;
this.quantity = quantity;
}
public int getItemId() {
return itemId;
}
public int getQuantity() {
return quantity;
}
}
......@@ -20,7 +20,7 @@ public class RegistryEntry {
/**
* The entry's ID.
*/
private final int id;
private final Integer id;
/**
* The transaction's type.
......@@ -62,7 +62,7 @@ public class RegistryEntry {
* @param credit money for the credit.
* @param remarks arbitrary remarks.
*/
public RegistryEntry(int id, int transactionType, Date dt,
public RegistryEntry(Integer id, int transactionType, Date dt,
int accountId, BigDecimal debit, BigDecimal credit,
String remarks)
{
......@@ -79,7 +79,7 @@ public class RegistryEntry {
* Returns the entry's ID.
* @return the entry's ID.
*/
public int getId()
public Integer getId()
{
return this.id;
}
......
......@@ -4,6 +4,8 @@
*/
package fr.unistra.sil.erp.back.model;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Representation of a stock entry.
*
......@@ -23,17 +25,19 @@ public class Stock {
/**
* The stock's item ID.
*/
@JsonProperty("item_id")
private final int itemId;
/**
* The stock's item name.
*/
@JsonProperty("item_name")
private final String itemName;
/**
* The quantity for the item.
*/
private final int quantity;
private int quantity;
/**
* Class constructor.
......@@ -85,4 +89,11 @@ public class Stock {
{
return this.quantity;
}
public void addQuantity(int quantity) {
this.quantity += quantity;
if (this.quantity < 0) {
this.quantity = 0;
}
}
}
......@@ -20,4 +20,6 @@ public interface IItemsRepository {
List<Item> getItemsFromCategory(int category);
Item store(Item item);
Item findById(int id);
}
......@@ -9,4 +9,6 @@ public interface IRegistryRepository {
* Returns all registry entries.
*/
List<RegistryEntry> getRegistry() throws DatabaseConnectionException;
RegistryEntry store(RegistryEntry entry);
}
......@@ -17,19 +17,11 @@ public interface IStocksRepository {
*
* @param itemId the item's ID.
* @return the stock line.
* @throws DatabaseResourceNotFoundException when the query fails.
*/
Stock getStockForItem(int itemId)
throws DatabaseResourceNotFoundException;
Stock getStockForItem(int itemId);
/**
* Updates the specified stock's quantity.
*
* @param id the stock entry's ID.
* @param quantity the new quantity.
* @throws DatabaseConnectionException when the database is unreachable.
* @throws DatabaseUpdateException when the query fails.
*/
void updateStock(int id, int quantity)
throws DatabaseConnectionException, DatabaseUpdateException;
void updateStock(Stock stock) throws DatabaseConnectionException, DatabaseUpdateException;
}
package fr.unistra.sil.erp.back.repository.item;
import fr.unistra.sil.erp.back.model.Item;
import fr.unistra.sil.erp.back.model.Stock;
import fr.unistra.sil.erp.back.repository.IItemsRepository;
import fr.unistra.sil.erp.back.repository.SqliteRepository;
import org.springframework.stereotype.Repository;
......@@ -20,6 +21,11 @@ public class SqliteItemsRepository extends SqliteRepository implements IItemsRep
private static final String SQL_GETALLITEMS =
"SELECT id, name, price, subscriberPrice, category FROM items";
/**
* Query used to find an item by id
*/
private static final String SQL_GET_ITEM = "SELECT * FROM items WHERE id = ?";
/**
* Query used to get all items stored in the database from a specific
* category.
......@@ -151,4 +157,48 @@ public class SqliteItemsRepository extends SqliteRepository implements IItemsRep
item.getCategory()
);
}
@Override
public Item findById(int id) {
PreparedStatement ps;
try {
ps = this.conn.prepareStatement(SQL_GET_ITEM);
} catch (SQLException ex) {
Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Failed to connect to database.", ex);
return null;
}
try {
ps.setInt(1, id);
} catch (SQLException ex) {
Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Failed to set query parameter.", ex);
return null;
}
ResultSet rs;
try {
rs = ps.executeQuery();
} catch (SQLException ex) {
Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Failed to fetch item");
return null;
}
Item item;
try {
if (rs.next())
item = new Item(
rs.getInt("id"),
rs.getString("name"),
rs.getBigDecimal("price"),
rs.getBigDecimal("subscriberPrice"),
rs.getInt("category")
);
else
return null;
} catch (SQLException ex) {
Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Failed to parse results.", ex);
return null;
}
return item;
}
}
......@@ -5,9 +5,10 @@ import fr.unistra.sil.erp.back.repository.IRegistryRepository;
import fr.unistra.sil.erp.back.repository.SqliteRepository;
import org.springframework.stereotype.Repository;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
......@@ -55,4 +56,57 @@ public class SqliteRegistryRepository extends SqliteRepository implements IRegis
return res;
}
@Override
public RegistryEntry store(RegistryEntry entry) {
String query = "INSERT INTO registry (dt, type, account_id, debit, credit, remarks) VALUES (?, ?, ?, ?, ?, ?);";
PreparedStatement ps;
LocalDateTime ldt = entry.getDatetime().toInstant()
.atZone(ZoneId.of("CET"))
.toLocalDateTime();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
try {
ps = this.conn.prepareStatement(query);
ps.setString(1, ldt.format(formatter));
ps.setInt(2, entry.getTransactionType());
ps.setInt(3, entry.getAccountId());
ps.setBigDecimal(4, entry.getDebit());
ps.setBigDecimal(5, entry.getCredit());
ps.setString(6, entry.getRemarks());
} catch (SQLException ex) {
Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Failed to connect to database.", ex);
return null;
}
try {
ps.executeUpdate();
} catch (SQLException ex) {
Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Failed to create registry: " + ex.getMessage());
return null;
}
int id;
query = "SELECT MAX(id) AS max_id FROM registry";
ResultSet rs;
try {
ps = this.conn.prepareStatement(query);
rs = ps.executeQuery();
} catch (SQLException ex) {
Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Failed to find registry id: " + ex.getMessage());
return null;
}
try {
id = rs.getInt("max_id");
} catch (SQLException ex) {
Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Failed to parse registry id: " + ex.getMessage());
return null;
}
return new RegistryEntry(
id,
entry.getTransactionType(),
entry.getDatetime(),
entry.getAccountId(),
entry.getDebit(),
entry.getCredit(),
entry.getRemarks()
);
}
}
......@@ -74,11 +74,9 @@ public class SqliteStocksRepository extends SqliteRepository implements IStocksR
*
* @param itemId the item's ID.
* @return the Stock entry.
* @throws DatabaseResourceNotFoundException if the stock doesn't exist.
*/
@Override
public Stock getStockForItem(int itemId)
throws DatabaseResourceNotFoundException {
public Stock getStockForItem(int itemId) {
PreparedStatement ps;
try {
ps = this.conn.prepareStatement(SQL_GETSTOCKFORITEM);
......@@ -113,7 +111,7 @@ public class SqliteStocksRepository extends SqliteRepository implements IStocksR
rs.getString("name"),
rs.getInt("quantity"));
else
throw new DatabaseResourceNotFoundException("Stock not found.");
return null;
} catch (SQLException ex) {
Logger.getLogger(this.getClass().getName()).log(
Level.SEVERE, "Failed to parse results.", ex);
......@@ -125,16 +123,9 @@ public class SqliteStocksRepository extends SqliteRepository implements IStocksR
/**
* Updates the specified stock entry with the new quantity.
*
* @param id the stock's ID.
* @param quantity the associated quantity.
* @throws DatabaseConnectionException when the connection to the DB fails.
* @throws DatabaseUpdateException when the update fails.
*/
@Override
public void updateStock(int id, int quantity)
throws DatabaseConnectionException, DatabaseUpdateException {
public void updateStock(Stock stock) throws DatabaseConnectionException, DatabaseUpdateException {
PreparedStatement ps;
try {
ps = this.conn.prepareStatement(SQL_UPDATESTOCK);
......@@ -145,8 +136,8 @@ public class SqliteStocksRepository extends SqliteRepository implements IStocksR
}
try {
ps.setInt(1, quantity);
ps.setInt(2, id);
ps.setInt(1, stock.getQuantity());
ps.setInt(2, stock.getId());
} catch (SQLException ex) {
Logger.getLogger(this.getClass().getName()).log(
Level.SEVERE, "Failed to set query parameter.", ex);
......
package fr.unistra.sil.erp.back.service;
public class CantUpdateStockException extends Exception
{
public CantUpdateStockException(String message) {
super(message);
}
}
package fr.unistra.sil.erp.back.service;
public class UpdateStockRequest {
private final int itemId;
private final int quantity;
public UpdateStockRequest(int itemId, int quantity) {
this.itemId = itemId;
this.quantity = quantity;
}
public int getItemId() {
return itemId;
}
public int getQuantity() {
return quantity;
}
}
package fr.unistra.sil.erp.back.service;
import fr.unistra.sil.erp.back.model.Item;
import fr.unistra.sil.erp.back.model.RegistryEntry;
import fr.unistra.sil.erp.back.model.Stock;
import fr.unistra.sil.erp.back.repository.*;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Date;
@Service
public class UpdateStockService {
private final IStocksRepository stocksRepository;
private final IItemsRepository itemsRepository;
private final IRegistryRepository registryRepository;
public UpdateStockService(IStocksRepository repository, IItemsRepository itemsRepository, IRegistryRepository registryRepository) {
this.stocksRepository = repository;
this.itemsRepository = itemsRepository;
this.registryRepository = registryRepository;
}
public Stock update(UpdateStockRequest request) throws CantUpdateStockException {
Stock stock = stocksRepository.getStockForItem(request.getItemId());
if (stock == null)
throw new CantUpdateStockException("Stock not found for item " + request.getItemId());
int oldValue = stock.getQuantity();
stock.addQuantity(request.getQuantity());
if (oldValue == stock.getQuantity()) {
// Skip stock update and registry update if quantity is not changed
return stock;
}
try {
stocksRepository.updateStock(stock);
} catch (DatabaseConnectionException | DatabaseUpdateException e) {
throw new CantUpdateStockException("Internal error");
}
Item item = itemsRepository.findById(stock.getItemId());
if (item == null) {
throw new CantUpdateStockException("Item not found");
}
BigDecimal totalPrice = item.getPrice().multiply(BigDecimal.valueOf(Math.abs(request.getQuantity())));
RegistryEntry entry = new RegistryEntry(
null,
request.getQuantity() > 0 ? 1 : 3,
new Date(),
request.getQuantity() > 0 ? 3 : 5,
request.getQuantity() > 0 ? totalPrice : null,
request.getQuantity() > 0 ? null : totalPrice,
null
);
entry = registryRepository.store(entry);
if (entry == null) {
throw new CantUpdateStockException("Registry entry is not stored");
}
return stock;
}
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment