diff --git a/backend/app/src/test/java/eu/viandeendirect/service/TestOrderService.java b/backend/app/src/test/java/eu/viandeendirect/service/TestOrderService.java index 7094ac9..89dcc58 100644 --- a/backend/app/src/test/java/eu/viandeendirect/service/TestOrderService.java +++ b/backend/app/src/test/java/eu/viandeendirect/service/TestOrderService.java @@ -5,6 +5,7 @@ import eu.viandeendirect.repository.OrderItemRepository; import eu.viandeendirect.repository.PackageLotRepository; import eu.viandeendirect.repository.SaleRepository; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -16,6 +17,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD; import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_METHOD; @@ -55,6 +57,7 @@ void createOrder_should_persist_order_in_database() { item.setUnitPrice(16f); item.setPackageLot(packageLot); order.setItems(List.of(item)); + int quantitySoldBeforeOrderCreation = packageLot.getQuantitySold(); // when Order orderCreated = orderService.createOrder(order).getBody(); @@ -64,6 +67,29 @@ void createOrder_should_persist_order_in_database() { assertThat(orderCreated.getId()).isNotNull(); List itemsCreated = orderItemRepository.findByOrder(orderCreated); assertThat(itemsCreated).hasSize(1); + assertThat(packageLot.getQuantitySold()).isEqualTo(quantitySoldBeforeOrderCreation + 1); + } + + @Test + void createOrder_should_raise_an_exception_if_not_enough_quantity_to_sell() { + // given + Sale sale = saleRepository.findById(1000).get(); + Customer customer = customerRepository.findById(3000).get(); + PackageLot packageLot = packageLotRepository.findById(10001).get(); + Order order = new Order(); + order.setCustomer(customer); + order.setSale(sale); + OrderItem item = new OrderItem(); + item.setQuantity(100); + item.setUnitPrice(16f); + item.setPackageLot(packageLot); + order.setItems(List.of(item)); + int quantitySoldBeforeOrderCreation = packageLot.getQuantitySold(); + + // when / then + assertThatThrownBy(() -> orderService.createOrder(order)) + .isNotNull(); + assertThat(packageLot.getQuantitySold()).isEqualTo(quantitySoldBeforeOrderCreation); } @Test diff --git a/backend/model/src/main/java/eu/viandeendirect/model/Order.java b/backend/model/src/main/java/eu/viandeendirect/model/Order.java index 2a2d819..68bc0da 100644 --- a/backend/model/src/main/java/eu/viandeendirect/model/Order.java +++ b/backend/model/src/main/java/eu/viandeendirect/model/Order.java @@ -2,6 +2,8 @@ import java.net.URI; import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonCreator; import eu.viandeendirect.model.Customer; @@ -63,8 +65,8 @@ public Order id(Integer id) { * * @return id */ - @NotNull - @Schema(name = "id", description = "", required = true) + + @Schema(name = "id", description = "", required = false) public Integer getId() { return id; } diff --git a/backend/model/src/main/java/eu/viandeendirect/model/OrderItem.java b/backend/model/src/main/java/eu/viandeendirect/model/OrderItem.java index c55cdca..7f6ff5b 100644 --- a/backend/model/src/main/java/eu/viandeendirect/model/OrderItem.java +++ b/backend/model/src/main/java/eu/viandeendirect/model/OrderItem.java @@ -2,6 +2,8 @@ import java.net.URI; import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonCreator; import eu.viandeendirect.model.Order; @@ -38,6 +40,7 @@ public class OrderItem { @JsonProperty("order") @ManyToOne + @JsonIgnore private Order order; @JsonProperty("packageLot") diff --git a/backend/model/src/main/java/eu/viandeendirect/model/Producer.java b/backend/model/src/main/java/eu/viandeendirect/model/Producer.java index dbbc9fc..3acd3fb 100644 --- a/backend/model/src/main/java/eu/viandeendirect/model/Producer.java +++ b/backend/model/src/main/java/eu/viandeendirect/model/Producer.java @@ -55,6 +55,7 @@ public class Producer { private List productions = null; @JsonProperty("sales") + @JsonIgnore @JsonManagedReference("salesSeller") @jakarta.persistence.OneToMany(mappedBy = "seller") @Valid diff --git a/backend/model/src/main/java/eu/viandeendirect/model/Sale.java b/backend/model/src/main/java/eu/viandeendirect/model/Sale.java index 36cba69..fd7f39e 100644 --- a/backend/model/src/main/java/eu/viandeendirect/model/Sale.java +++ b/backend/model/src/main/java/eu/viandeendirect/model/Sale.java @@ -45,6 +45,7 @@ public class Sale { @JsonProperty("orders") @jakarta.persistence.OneToMany(mappedBy = "sale") @Valid + @JsonIgnore private List orders = null; @JsonProperty("deliveryStart") diff --git a/frontend/app/src/domains/customer/CustomerController.js b/frontend/app/src/domains/customer/CustomerController.js index 86170f3..929db3e 100644 --- a/frontend/app/src/domains/customer/CustomerController.js +++ b/frontend/app/src/domains/customer/CustomerController.js @@ -1,7 +1,7 @@ import { useState } from 'react' import CustomersList from './views/CustomersList.tsx' -export default function CustomerController() { +export default function CustomerController({producer: producer}) { const NONE = 'NONE' diff --git a/frontend/app/src/domains/customer/views/CustomersList.tsx b/frontend/app/src/domains/customer/views/CustomersList.tsx index 0ee27ca..e892da1 100644 --- a/frontend/app/src/domains/customer/views/CustomersList.tsx +++ b/frontend/app/src/domains/customer/views/CustomersList.tsx @@ -5,31 +5,34 @@ import { DataGrid, GridRowsProp, GridColDef, GridToolbar } from '@mui/x-data-gri import { useKeycloak } from '@react-keycloak/web' import { ApiBuilder } from '../../../api/ApiBuilder.ts' +import { ApiInvoker } from '../../../api/ApiInvoker.ts'; export default function CustomersList() { const [customers, setCustomers] = useState([]) const { keycloak, initialized } = useKeycloak() - const apiBuilder = new ApiBuilder() - + const apiInvoker = new ApiInvoker() useEffect(() => { loadCustomers() }, [keycloak]) function loadCustomers() { - apiBuilder.getAuthenticatedApi(keycloak).then(api => { - apiBuilder.invokeAuthenticatedApi(() => { - api.getProducerCustomers((error, data, response) => { - if (error) { - console.error(error) - } else { - console.log('api.getProducerCustomers called successfully. Returned data: ' + data) - setCustomers(data) - } - }) - }, keycloak) - }) + + apiInvoker.callApiAuthenticatedly(keycloak, api => api.getProducerCustomers, null, setCustomers, console.error) + + // apiBuilder.getAuthenticatedApi(keycloak).then(api => { + // apiBuilder.invokeAuthenticatedApi(() => { + // api.getProducerCustomers((error, data, response) => { + // if (error) { + // console.error(error) + // } else { + // console.log('api.getProducerCustomers called successfully. Returned data: ' + data) + // setCustomers(data) + // } + // }) + // }, keycloak) + // }) } const rows: GridRowsProp = customers.map(customer => { diff --git a/frontend/app/src/domains/production/ProductionController.tsx b/frontend/app/src/domains/production/ProductionController.tsx index 15d5cc3..966c2a3 100644 --- a/frontend/app/src/domains/production/ProductionController.tsx +++ b/frontend/app/src/domains/production/ProductionController.tsx @@ -5,7 +5,7 @@ import PackageLotsCreator from './views/PackageLotsCreator.tsx' import ProductionsList from './views/ProductionsList.tsx' import BeefProductionView from './views/beefProduction/BeefProductionView.tsx' -export default function ProductionController() { +export default function ProductionController({producer: producer}) { const BEEF_PRODUCTION_CREATION = 'BEEF_PRODUCTION_CREATION' const BEEF_PRODUCTION_VIEW = 'BEEF_PRODUCTION_VIEW' diff --git a/frontend/app/src/domains/sale/SaleController.tsx b/frontend/app/src/domains/sale/SaleController.tsx index 6fb7673..4ae8fd1 100644 --- a/frontend/app/src/domains/sale/SaleController.tsx +++ b/frontend/app/src/domains/sale/SaleController.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { useState } from 'react' +import { useState, useEffect } from 'react' import SalesList from './views/SalesList.tsx' import SaleForm from './views/SaleForm.tsx' @@ -7,7 +7,7 @@ import OrdersList from './views/OrdersList.tsx' import Sale from 'viandeendirect_eu/dist/model/Sale' import Order from 'viandeendirect_eu/dist/model/Order' import OrderView from './views/OrderView.tsx' -import OrderForm from './views/OrderForm.tsx' +import ProducerOrderForm from './views/ProducerOrderForm.tsx' export default function SaleController({producer: producer}) { @@ -28,7 +28,7 @@ export default function SaleController({producer: producer}) { case SALE_CREATION_VIEW: return case ORDERS_LIST_VIEW: return createOrder(context)}/> case ORDER_VIEW: return - case ORDER_CREATION_VIEW: return + case ORDER_CREATION_VIEW: return } } diff --git a/frontend/app/src/domains/sale/components/CustomerSelector.tsx b/frontend/app/src/domains/sale/components/CustomerSelector.tsx index 170f2fd..fde330c 100644 --- a/frontend/app/src/domains/sale/components/CustomerSelector.tsx +++ b/frontend/app/src/domains/sale/components/CustomerSelector.tsx @@ -1,7 +1,7 @@ import React from 'react' import { useState } from 'react' -import { Switch, Autocomplete, TextField, Button } from "@mui/material" +import { Switch, Button } from "@mui/material" import { AutocompleteElement, FormContainer, TextFieldElement } from 'react-hook-form-mui' import Customer from "viandeendirect_eu/dist/model/Customer" diff --git a/frontend/app/src/domains/sale/components/SaleCard.tsx b/frontend/app/src/domains/sale/components/SaleCard.tsx index 648db60..e10ebb5 100644 --- a/frontend/app/src/domains/sale/components/SaleCard.tsx +++ b/frontend/app/src/domains/sale/components/SaleCard.tsx @@ -1,11 +1,29 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { Button, ButtonGroup, Card, CardActions, CardContent, Typography } from "@mui/material" import dayjs from 'dayjs' import SaleCardBeefProduction from './SaleCardBeefProduction.js'; +import { ApiInvoker } from '../../../api/ApiInvoker.ts'; +import { useKeycloak } from '@react-keycloak/web'; +import Order from 'viandeendirect_eu/dist/model/Order' +import Production from 'viandeendirect_eu/dist/model/Production' + export default function SaleCard({ sale: sale, manageOrdersCallback: manageOrdersCallback}) { + const apiInvoker = new ApiInvoker() + const [orders, setOrders] = useState>([]) + const [productions, setProductions] = useState>([]) + const {keycloak} = useKeycloak() + + useEffect(() => { + apiInvoker.callApiAuthenticatedly(keycloak, api => api.getSaleOrders, sale.id, setOrders, console.error) + }, [keycloak, sale]) + + useEffect(() => { + apiInvoker.callApiAuthenticatedly(keycloak, api => api.getSaleProductions, sale.id, setProductions, console.error) + }, [keycloak, sale]) + return ( @@ -29,7 +47,7 @@ export default function SaleCard({ sale: sale, manageOrdersCallback: manageOrder Commandes - {sale.orders.length} commandes client + {orders.length} commandes client {getQuantitySold()} kg commandés @@ -44,7 +62,7 @@ export default function SaleCard({ sale: sale, manageOrdersCallback: manageOrder Productions mises en vente - {sale.productions.map(getProduction)} + {productions.map(getProduction)} @@ -69,14 +87,14 @@ export default function SaleCard({ sale: sale, manageOrdersCallback: manageOrder } function getQuantitySold() { - return sale.orders + return orders .flatMap(order => order.items) .map(item => item.packageLot.netWeight * item.quantity) .reduce((totalQuantity, orderItemQuantity) => totalQuantity + orderItemQuantity, 0) } function getAmountSold() { - return sale.orders + return orders .flatMap(order => order.items) .map(item => item.unitPrice * item.quantity) .reduce((totalAmout, orderItemAmout) => totalAmout + orderItemAmout, 0) diff --git a/frontend/app/src/domains/sale/components/SaleCardBeefProduction.js b/frontend/app/src/domains/sale/components/SaleCardBeefProduction.js index 2877468..d565b2e 100644 --- a/frontend/app/src/domains/sale/components/SaleCardBeefProduction.js +++ b/frontend/app/src/domains/sale/components/SaleCardBeefProduction.js @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useKeycloak } from '@react-keycloak/web' import { Typography } from "@mui/material" -import { ApiBuilder } from '../../../api/ApiBuilder.ts' +import { ApiInvoker } from '../../../api/ApiInvoker.ts' import { AnimalTypeUtils } from '../../../enum/AnimalType.ts'; import PieChart from '../../commons/components/PieChart.tsx' import styles from './SaleCard.css' @@ -9,28 +9,17 @@ import styles from './SaleCard.css' export default function SaleCardBeefProduction({production: production}) { const [productionPercentageSold, setProductionPercentageSold] = useState([]) - const { keycloak, initialized } = useKeycloak() - const apiBuilder = new ApiBuilder() + const { keycloak } = useKeycloak() + const apiInvoker = new ApiInvoker() useEffect(() => { - loadProductionPercentageSold() - }, [keycloak]) - - - function loadProductionPercentageSold() { - apiBuilder.getAuthenticatedApi(keycloak).then(api => { - apiBuilder.invokeAuthenticatedApi(() => { - api.getProductionPercentageSold(production.id, (error, data, response) => { - if (error) { - console.error(error) - } else { - console.log('api.getProductionPercentageSold called successfully. Returned data: ' + data) - setProductionPercentageSold(data) - } - }) - }, keycloak) - }) - } + apiInvoker.callApiAuthenticatedly( + keycloak, + api => api.getProductionPercentageSold, + production.id, + setProductionPercentageSold, + console.error) + }, [keycloak, production]) return <>
diff --git a/frontend/app/src/domains/sale/views/OrderForm.tsx b/frontend/app/src/domains/sale/views/ProducerOrderForm.tsx similarity index 65% rename from frontend/app/src/domains/sale/views/OrderForm.tsx rename to frontend/app/src/domains/sale/views/ProducerOrderForm.tsx index 76df4c2..13e2a60 100644 --- a/frontend/app/src/domains/sale/views/OrderForm.tsx +++ b/frontend/app/src/domains/sale/views/ProducerOrderForm.tsx @@ -17,14 +17,16 @@ import Sale from "viandeendirect_eu/dist/model/Sale" import PackageSelector from '../components/PackageSelector.tsx' import CustomerSelector from '../components/CustomerSelector.tsx' import OrderSummary from '../components/OrderSummary.tsx' +import { ApiInvoker } from '../../../api/ApiInvoker.ts' -export default function OrderForm({ sale: sale, returnCallback: returnCallback }) { +export default function ProducerOrderForm({ producer: producer, sale: sale, returnCallback: returnCallback }) { const SET_ITEMS_STEP = 1 const SET_CUSTOMER_STEP = 2 const CONFIRMATION_STEP = 3 const { keycloak, initialized } = useKeycloak() + const apiInvoker = new ApiInvoker() const apiBuilder = new ApiBuilder() const [productions, setProductions] = useState>([]) @@ -39,35 +41,44 @@ export default function OrderForm({ sale: sale, returnCallback: returnCallback } }, [keycloak]) function loadProductions() { - apiBuilder.getAuthenticatedApi(keycloak).then(api => { - apiBuilder.invokeAuthenticatedApi(() => { - api.getSaleProductions(sale.id, (error, data, response) => { - if (error) { - console.error(error) - } else { - console.log('api.getSaleProductions called successfully. Returned data: ' + data) - setProductions(data) - } - }) - }, keycloak) - }) + apiInvoker.callApiAuthenticatedly( + keycloak, + api => api.getSaleProductions, + sale.id, setProductions, + console.error) } function loadCustomers() { - apiBuilder.getAuthenticatedApi(keycloak).then(api => { - apiBuilder.invokeAuthenticatedApi(() => { - api.getProducerCustomers((error, data, response) => { - if (error) { - console.error(error) - } else { - console.log('api.getCustomers called successfully. Returned data: ' + data) - setCustomers(data) - } - }) - }, keycloak) - }) + apiInvoker.callApiAuthenticatedly( + keycloak, + api => api.getProducerCustomers, + producer.id, + setCustomers, + console.error) } + function createCustomerAndOrder(customer: Customer){ + apiInvoker.callApiAuthenticatedly( + keycloak, + api => api.createCustomer, + customer, + customer => { + const updatedOrder = {...order, customer: customer} + setOrder(updatedOrder) + createOrder(updatedOrder) + returnCallback(sale) + }, + console.error) + } + + function createOrder(order: Order) { + apiInvoker.callApiAuthenticatedly( + keycloak, + api => api.createOrder, + order, + order => console.log('api.createOrder called successfully. Returned data: ' + order), + console.error) + } return <> Creation d'une commande pour la vente du {dayjs(sale.deliveryStart).format('DD/MM/YYYY')} - {sale.deliveryAddressName} @@ -134,38 +145,10 @@ export default function OrderForm({ sale: sale, returnCallback: returnCallback } function validateOrder() { if(!order.customer.id) { - order.customer = createCustomer(order.customer) + createCustomerAndOrder(order.customer) + } else { + createOrder(order) + returnCallback(sale) } - createOrder(order) - returnCallback(sale) - } - - function createCustomer(customer: Customer){ - apiBuilder.getAuthenticatedApi(keycloak).then(api => { - apiBuilder.invokeAuthenticatedApi(() => { - api.createCustomer(customer, (error, data, response) => { - if (error) { - console.error(error) - } else { - console.log('api.createCustomer called successfully. Returned data: ' + data) - setOrder({...order, customer: data}) - } - }) - }, keycloak) - }) - } - - function createOrder(order: Order) { - apiBuilder.getAuthenticatedApi(keycloak).then(api => { - apiBuilder.invokeAuthenticatedApi(() => { - api.createOrder(order, (error, data, response) => { - if (error) { - console.error(error) - } else { - console.log('api.createOrder called successfully. Returned data: ' + data) - } - }) - }, keycloak) - }) } } diff --git a/frontend/app/src/domains/sale/views/SaleForm.tsx b/frontend/app/src/domains/sale/views/SaleForm.tsx index b6a24fb..26b6b34 100644 --- a/frontend/app/src/domains/sale/views/SaleForm.tsx +++ b/frontend/app/src/domains/sale/views/SaleForm.tsx @@ -5,7 +5,6 @@ import { Button, ButtonGroup, Stepper, Step, StepLabel, StepContent, Typography, import { ApiBuilder } from '../../../api/ApiBuilder.ts' import { DatePickerElement, TextFieldElement, FormContainer, TimePickerElement } from 'react-hook-form-mui' -import Production from 'viandeendirect_eu/dist/model/Production' import Sale from 'viandeendirect_eu/dist/model/Sale' import SaleProductionSelector from '../components/SaleProductionSelector.js' diff --git a/frontend/app/src/domains/sale/views/SalesList.tsx b/frontend/app/src/domains/sale/views/SalesList.tsx index c275456..7850f56 100644 --- a/frontend/app/src/domains/sale/views/SalesList.tsx +++ b/frontend/app/src/domains/sale/views/SalesList.tsx @@ -3,35 +3,20 @@ import { useEffect, useState } from 'react' import { Typography, Button } from "@mui/material" import { useKeycloak } from '@react-keycloak/web' -import { ApiBuilder } from '../../../api/ApiBuilder.ts' import SaleCard from '../components/SaleCard.tsx' +import { ApiInvoker } from '../../../api/ApiInvoker.ts' export default function SalesList({producer: producer, manageSaleOrdersCallback: manageSaleOrdersCallback, createSaleCallback: createSaleCallback}) { - const { keycloak, initialized } = useKeycloak() - const apiBuilder = new ApiBuilder() + const { keycloak } = useKeycloak() const [sales, setSales] = useState([]) + const apiInvoker = new ApiInvoker() useEffect(() => { - loadSales() - }, [keycloak]) - - function loadSales() { - apiBuilder.getAuthenticatedApi(keycloak).then(api => { - apiBuilder.invokeAuthenticatedApi(() => { - api.getProducerSales(producer.id, (error, data, response) => { - if (error) { - console.error(error) - } else { - console.log('api.getSales called successfully. Returned data: ' + data) - setSales(data) - } - }) - }, keycloak) - }) - } + apiInvoker.callApiAuthenticatedly(keycloak, api => api.getProducerSales, producer.id, setSales, console.error) + }, [keycloak, producer]) return <> Ventes diff --git a/frontend/app/src/layouts/producer/AuthenticatedLayout.tsx b/frontend/app/src/layouts/producer/AuthenticatedLayout.tsx index 4d1de59..e49b9e4 100644 --- a/frontend/app/src/layouts/producer/AuthenticatedLayout.tsx +++ b/frontend/app/src/layouts/producer/AuthenticatedLayout.tsx @@ -12,23 +12,29 @@ import CustomerController from '../../domains/customer/CustomerController.js'; import GrowerAccount from '../../domains/producer/ProducerAccount.js' import ProductionController from '../../domains/production/ProductionController.tsx' import SaleController from '../../domains/sale/SaleController.tsx' -import SideMenu from './SideMenu.js' import Producer from 'viandeendirect_eu/dist/model/Producer.js'; +import SideMenu from './SideMenu.js' import { AuthenticationService } from '../../authentication/AuthenticationService.ts'; function AuthenticatedLayout() { const { keycloak, initialized } = useKeycloak() - const apiInvoker = new ApiInvoker() const [sideMenuOpen, setSideMenuOpen] = useState(false) const [mainContent, setMainContent] = useState('DASHBOARD') const [producer, setProducer] = useState() + const apiInvoker = new ApiInvoker() const authenticationService = new AuthenticationService(keycloak) useEffect(() => { - apiInvoker.callApiAuthenticatedly(keycloak, api => api.getProducer, {'email': authenticationService.getCurrentUserEmail()}, setProducer) + apiInvoker.callApiAuthenticatedly( + keycloak, + api => api.getProducer, + {'email': authenticationService.getCurrentUserEmail()}, + setProducer, + console.error) }, [keycloak]) + const sideMenuWidth = 240; const handleSideMenuToggle = () => { @@ -44,8 +50,8 @@ function AuthenticatedLayout() { switch (mainContent) { case 'DASHBOARD' : return case 'SALES' : return - case 'PRODUCTIONS' : return - case 'CUSTOMERS' : return + case 'PRODUCTIONS' : return + case 'CUSTOMERS' : return case 'GROWER_ACCOUNT' : return } } diff --git a/openapi/openapi.yml b/openapi/openapi.yml index faaf9b4..3977d55 100644 --- a/openapi/openapi.yml +++ b/openapi/openapi.yml @@ -680,7 +680,6 @@ components: description: "" required: - customer - - id type: object properties: id: