From 85143520617aee2bdb231ad5db929d954d85826e Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Tue, 30 Nov 2021 16:21:45 +0200 Subject: [PATCH 01/71] Ignore .tomcat folders --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7f0b1dd..16fb120 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,6 @@ dist/ log/ # Skip VisualStudio Code folders -.vscode/ \ No newline at end of file +.vscode/ + +.tomcat/ \ No newline at end of file From a6fe4ac6e4f301370aed41cd78ece18608479b7b Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Tue, 30 Nov 2021 16:23:01 +0200 Subject: [PATCH 02/71] Join Success and Error message columns in table with Upload results together --- cm-frontend/src/pages/UploadPage.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cm-frontend/src/pages/UploadPage.js b/cm-frontend/src/pages/UploadPage.js index 27277c6..21653ec 100644 --- a/cm-frontend/src/pages/UploadPage.js +++ b/cm-frontend/src/pages/UploadPage.js @@ -93,7 +93,7 @@ export default function Upload() { setLoading(false); } ) - }; + } function handleUpload() { if (loading) { @@ -180,8 +180,7 @@ export default function Upload() { Version Action ID - Success - Error message + Result Line count Add Update @@ -195,8 +194,7 @@ export default function Upload() { {u.productCatalogUpdate?.documentVersion} {u.productCatalogUpdate?.document?.actionCode} {u.productCatalogUpdate?.document?.id} - {u.success} - {u.errorMessage} + {u.success ? 'OK' : ('ERROR: ' + u.errorMessage)} {u.lineCount} {u.lineActionStat.ADD} {u.lineActionStat.UPDATE} From 9c0b8deb69922a9bec7e66fc580668963ea06a05 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Tue, 30 Nov 2021 16:59:20 +0200 Subject: [PATCH 03/71] Clean pom - global java version and project encoding --- pom.xml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index 7e38376..b841899 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,11 @@ 4.12 1.22 1.2.14 + + 1.8 + ${java.version} + ${java.version} + UTF-8 @@ -68,23 +73,11 @@ 3 - - - org.apache.maven.wagon - wagon-http - 3.0.0 - - org.apache.maven.plugins maven-compiler-plugin 2.3.2 - - 1.8 - 1.8 - UTF-8 - org.apache.maven.plugins From 77769f57911c26a7aac49ae6de584a200bd8acaa Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Tue, 30 Nov 2021 16:59:54 +0200 Subject: [PATCH 04/71] Fix Idea warnings --- cm-frontend/src/components/ProductDetail.js | 41 +++++++++++---------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/cm-frontend/src/components/ProductDetail.js b/cm-frontend/src/components/ProductDetail.js index 0ff9dee..bfec7ef 100644 --- a/cm-frontend/src/components/ProductDetail.js +++ b/cm-frontend/src/components/ProductDetail.js @@ -1,3 +1,5 @@ +// noinspection JSUnresolvedVariable + import { makeStyles } from "@material-ui/core"; import { Fragment } from "react"; import ItemDetailsService from '../services/ItemDetailsService'; @@ -26,6 +28,7 @@ const useStyles = makeStyles(theme => ({ function DataView(props) { const _isValueDefined = (value) => value ? true : false; + // noinspection JSUnusedLocalSymbols const _renderValue = (v, i) => { return v }; const { name, value, isValueDefined = _isValueDefined, renderValue = _renderValue } = props; @@ -104,29 +107,29 @@ export default function ProductView(props) { <> { showTech && ( <> - - - - - - - - + + + + + + + + )} - - - - {return e.id}}> - {return e?.partyName?.name}}> - - - - + + + + {return e.id}}/> + {return e?.partyName?.name}}/> + + + + - - + + ) From d2a131fdced38246079ac3fb6e94c664370bc858 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Tue, 30 Nov 2021 18:23:03 +0200 Subject: [PATCH 05/71] chore: typo, warnings --- cm-frontend/src/components/ProductListContainer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cm-frontend/src/components/ProductListContainer.js b/cm-frontend/src/components/ProductListContainer.js index b773174..741aa6e 100644 --- a/cm-frontend/src/components/ProductListContainer.js +++ b/cm-frontend/src/components/ProductListContainer.js @@ -22,7 +22,7 @@ const currentPosition = (list, id) => { for (var i = 0; i < list.length; i++) if (list[i].id === id) { list._cachedPos[id] = (i + 1); return i; - }; + } return 0; }; @@ -35,7 +35,7 @@ const currentPosition = (list, id) => { } } -export function ProductListContainer(props) { +export function ProductListContainer() { const [showBanner, setShowBanner] = useStickyState(true, 'dcm-banner'); const [productList, setProductList] = React.useState([]); @@ -74,7 +74,7 @@ export function ProductListContainer(props) { setProductListLoading(false); } ).catch(error => { - console.log('Error occured: ' + error.message); + console.log('Error occurred: ' + error.message); setProductListLoading(false); }); } From d0d1d4b43d5bc6d5c3a73c6a6de67c8a842e2ec2 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Tue, 30 Nov 2021 18:24:49 +0200 Subject: [PATCH 06/71] start to add button --- cm-frontend/src/components/AddToBasket.js | 26 ++++++++++++++++++ cm-frontend/src/components/ProductDetail.js | 29 +++++++++++++++++---- 2 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 cm-frontend/src/components/AddToBasket.js diff --git a/cm-frontend/src/components/AddToBasket.js b/cm-frontend/src/components/AddToBasket.js new file mode 100644 index 0000000..b6f36c0 --- /dev/null +++ b/cm-frontend/src/components/AddToBasket.js @@ -0,0 +1,26 @@ +import {Button} from "@material-ui/core"; +import React, {useEffect, useRef} from "react"; + +export default function AddToBasket() { + + const [state, setState] = React.useState('empty'); + + const timerRef = useRef(null); + + function handleAdd() { + setState('adding'); + timerRef.current = setTimeout(() => { + setState('added') + }, 500); + } + + useEffect(() => { + return () => clearTimeout(timerRef.current) + }, []); + + return ( + <> + + + ) +} \ No newline at end of file diff --git a/cm-frontend/src/components/ProductDetail.js b/cm-frontend/src/components/ProductDetail.js index bfec7ef..699e4c6 100644 --- a/cm-frontend/src/components/ProductDetail.js +++ b/cm-frontend/src/components/ProductDetail.js @@ -5,6 +5,7 @@ import { Fragment } from "react"; import ItemDetailsService from '../services/ItemDetailsService'; import CatalogBadge from "./CatalogBadge"; import ProductPictureList from './ProductPictureList'; +import AddToBasket from "./AddToBasket"; const useStyles = makeStyles(theme => ({ row: { @@ -25,6 +26,19 @@ const useStyles = makeStyles(theme => ({ }, })); +function DataRow(props) { + const { name, children } = props; + + const classes = useStyles(); + + return ( +
+
{name}
+
{children}
+
+ ) +} + function DataView(props) { const _isValueDefined = (value) => value ? true : false; @@ -32,14 +46,10 @@ function DataView(props) { const _renderValue = (v, i) => { return v }; const { name, value, isValueDefined = _isValueDefined, renderValue = _renderValue } = props; - const classes = useStyles(); return ( <> {isValueDefined(value) ? ( -
-
{name}
-
{renderValue(value)}
-
+ {renderValue(value)} ) : (<>)} ) @@ -101,10 +111,19 @@ export default function ProductView(props) { const showTech = false; + const showOrdering = true; + const { product } = props; return ( <> + { showOrdering && ( + <> + + + + + )} { showTech && ( <> From c8093cb27ebdbba7583fb0316dfe35a830766988 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 11:58:09 +0200 Subject: [PATCH 07/71] AddToBasket - use enums with state, change button name on state change, show progress on adding --- cm-frontend/src/components/AddToBasket.js | 48 +++++++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/cm-frontend/src/components/AddToBasket.js b/cm-frontend/src/components/AddToBasket.js index b6f36c0..9b3f657 100644 --- a/cm-frontend/src/components/AddToBasket.js +++ b/cm-frontend/src/components/AddToBasket.js @@ -1,17 +1,44 @@ import {Button} from "@material-ui/core"; import React, {useEffect, useRef} from "react"; +import CircularProgress from "@material-ui/core/CircularProgress"; + + +const ProductBasketStatus = { + Empty: 'empty', + Adding: 'adding', + Added: 'added', +} export default function AddToBasket() { - const [state, setState] = React.useState('empty'); + const [state, setState] = React.useState(ProductBasketStatus.Empty); - const timerRef = useRef(null); + const getButtonTitle = () => { + switch (state) { + case ProductBasketStatus.Adding: + return 'Adding to basket'; + case ProductBasketStatus.Added: + return 'Remove from basket'; + default: + return 'Add to basket'; + } + } - function handleAdd() { - setState('adding'); - timerRef.current = setTimeout(() => { - setState('added') - }, 500); + const isProgress = () => { + return state === ProductBasketStatus.Adding; + } + + // TODO: Remove - temporary code to imitate slow adding + const timerRef = useRef(null); + const handleClick = () => { + if (state === ProductBasketStatus.Empty) { + setState(ProductBasketStatus.Adding); + timerRef.current = setTimeout(() => { + setState(ProductBasketStatus.Added) + }, 800); + } else if (state === ProductBasketStatus.Added) { + setState(ProductBasketStatus.Empty); + } } useEffect(() => { @@ -20,7 +47,12 @@ export default function AddToBasket() { return ( <> - + ) } \ No newline at end of file From 0e128ec0135f8f2c6659f20a5098aae2f968b2a1 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 11:59:03 +0200 Subject: [PATCH 08/71] Add dummy BasketBar to Top navigation --- cm-frontend/src/components/BasketBar.js | 26 +++++++++++++++++++++++++ cm-frontend/src/components/TopNav.js | 9 +++++++++ 2 files changed, 35 insertions(+) create mode 100644 cm-frontend/src/components/BasketBar.js diff --git a/cm-frontend/src/components/BasketBar.js b/cm-frontend/src/components/BasketBar.js new file mode 100644 index 0000000..28c8394 --- /dev/null +++ b/cm-frontend/src/components/BasketBar.js @@ -0,0 +1,26 @@ +import React from 'react'; +import {makeStyles} from '@material-ui/core/styles'; +import ShoppingBasketIcon from '@material-ui/icons/ShoppingBasket'; + +const useStyles = makeStyles((theme) => ({ + basket: { + padding: theme.spacing(0, 2), + }, + basketIcon: { + color: theme.palette.common.white + }, +})); + +export default function BasketBar() { + const classes = useStyles(); + + return ( + +
+
+ +
+
+ + ) +} \ No newline at end of file diff --git a/cm-frontend/src/components/TopNav.js b/cm-frontend/src/components/TopNav.js index 26a8fa5..9aaaf4f 100644 --- a/cm-frontend/src/components/TopNav.js +++ b/cm-frontend/src/components/TopNav.js @@ -7,6 +7,7 @@ import Button from '@material-ui/core/Button'; import { Link } from 'react-router-dom'; import SearchBar from './SearchBar'; import './TopNav.css'; +import BasketBar from "./BasketBar"; const useStyles = makeStyles((theme) => ({ root: { @@ -28,6 +29,11 @@ link: { color: "#d3d3d3" } }, + basketBar: { + [theme.breakpoints.down('xs')]: { + margin: theme.spacing(2), + } + }, searchBar: { [theme.breakpoints.down('xs')]: { margin: theme.spacing(2), @@ -60,6 +66,9 @@ export default function TopNav(props) {
+
+ +
From 064306b0503e5c4ccff7f96fe4d0884940983ab2 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 12:56:11 +0200 Subject: [PATCH 09/71] Change visibility of BasketBar depending on number of items in BasketData, add/remove/change quantity per product. Tried to use a data class for this - but had issues with React.useState on object class... --- cm-frontend/src/components/AddToBasket.js | 8 ++++-- cm-frontend/src/components/BasketData.js | 15 +++++++++++ cm-frontend/src/components/ProductDetail.js | 4 +-- .../src/components/ProductListContainer.js | 25 ++++++++++++++++--- cm-frontend/src/components/TopNav.js | 4 ++- cm-frontend/src/pages/ProductDetailPage.js | 4 +-- 6 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 cm-frontend/src/components/BasketData.js diff --git a/cm-frontend/src/components/AddToBasket.js b/cm-frontend/src/components/AddToBasket.js index 9b3f657..273c800 100644 --- a/cm-frontend/src/components/AddToBasket.js +++ b/cm-frontend/src/components/AddToBasket.js @@ -9,7 +9,9 @@ const ProductBasketStatus = { Added: 'added', } -export default function AddToBasket() { +export default function AddToBasket(props) { + + const {changeBasket, product} = props; const [state, setState] = React.useState(ProductBasketStatus.Empty); @@ -34,9 +36,11 @@ export default function AddToBasket() { if (state === ProductBasketStatus.Empty) { setState(ProductBasketStatus.Adding); timerRef.current = setTimeout(() => { + changeBasket(product.id, 1); setState(ProductBasketStatus.Added) - }, 800); + }, 400); } else if (state === ProductBasketStatus.Added) { + changeBasket(product.id, 0); setState(ProductBasketStatus.Empty); } } diff --git a/cm-frontend/src/components/BasketData.js b/cm-frontend/src/components/BasketData.js new file mode 100644 index 0000000..3ce7aba --- /dev/null +++ b/cm-frontend/src/components/BasketData.js @@ -0,0 +1,15 @@ +export function createBasketData() { + + class BasketData { + constructor() { + this.orderLines = {}; + this.orderLinesCount = 0; + } + + isEmpty() { + return this.orderLines.length === 0; + } + } + + return new BasketData(); +} \ No newline at end of file diff --git a/cm-frontend/src/components/ProductDetail.js b/cm-frontend/src/components/ProductDetail.js index 699e4c6..6a09134 100644 --- a/cm-frontend/src/components/ProductDetail.js +++ b/cm-frontend/src/components/ProductDetail.js @@ -113,14 +113,14 @@ export default function ProductView(props) { const showOrdering = true; - const { product } = props; + const { product, changeBasket } = props; return ( <> { showOrdering && ( <> - + )} diff --git a/cm-frontend/src/components/ProductListContainer.js b/cm-frontend/src/components/ProductListContainer.js index 741aa6e..6b12280 100644 --- a/cm-frontend/src/components/ProductListContainer.js +++ b/cm-frontend/src/components/ProductListContainer.js @@ -19,7 +19,7 @@ const currentPosition = (list, id) => { if (!list) { return 0; } - for (var i = 0; i < list.length; i++) if (list[i].id === id) { + for (let i = 0; i < list.length; i++) if (list[i].id === id) { list._cachedPos[id] = (i + 1); return i; } @@ -44,6 +44,25 @@ export function ProductListContainer() { const [productListTotal, setProductListTotal] = React.useState(0); const [productListLoading, setProductListLoading] = React.useState(false); + const [basketData, setBasketData] = React.useState({orderLines: {}, orderLinesCount: 0}); + const changeBasket = (productId, quantity) => { + let newOrderLines = {...basketData.orderLines}; + let newOrderLinesCount = basketData.orderLinesCount; + if (productId in newOrderLines) { + if (quantity > 0) { + newOrderLines[productId] += quantity; + } else { + delete newOrderLines[productId] + newOrderLinesCount--; + } + } else { + newOrderLines[productId] = quantity; + newOrderLinesCount++; + } + console.log(newOrderLines); + setBasketData({orderLines: newOrderLines, orderLinesCount: newOrderLinesCount}); + } + const setBannerClosed = () => { setShowBanner(false); }; @@ -81,14 +100,14 @@ export function ProductListContainer() { return ( <> - + - + ); diff --git a/cm-frontend/src/components/TopNav.js b/cm-frontend/src/components/TopNav.js index 9aaaf4f..21c999c 100644 --- a/cm-frontend/src/components/TopNav.js +++ b/cm-frontend/src/components/TopNav.js @@ -52,7 +52,7 @@ link: { export default function TopNav(props) { const classes = useStyles(); - const { aboutAction, searchAction } = props; + const { aboutAction, searchAction, showBasketBar } = props; return (
@@ -66,9 +66,11 @@ export default function TopNav(props) {
+ { showBasketBar && (
+ )}
diff --git a/cm-frontend/src/pages/ProductDetailPage.js b/cm-frontend/src/pages/ProductDetailPage.js index 8e22953..69f49a3 100644 --- a/cm-frontend/src/pages/ProductDetailPage.js +++ b/cm-frontend/src/pages/ProductDetailPage.js @@ -35,7 +35,7 @@ function ViewToggle(props) { export default function ProductDetailPage(props) { - const { navigator } = props; + const { navigator, changeBasket } = props; let { id } = useParams(); @@ -78,7 +78,7 @@ export default function ProductDetailPage(props) { {viewMode === "json" ? (
{JSON.stringify(data, null, 2)}
) : ( - + ) } From fa8c8732f89f6a7c49076cbfd1666418fa0e2268 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 13:11:18 +0200 Subject: [PATCH 10/71] Use external class BasketData to manage basket state and reference it in React.useState --- cm-frontend/src/components/BasketData.js | 25 ++- .../src/components/ProductListContainer.js | 161 +++++++++--------- 2 files changed, 99 insertions(+), 87 deletions(-) diff --git a/cm-frontend/src/components/BasketData.js b/cm-frontend/src/components/BasketData.js index 3ce7aba..5a2e78a 100644 --- a/cm-frontend/src/components/BasketData.js +++ b/cm-frontend/src/components/BasketData.js @@ -1,13 +1,30 @@ export function createBasketData() { class BasketData { - constructor() { - this.orderLines = {}; - this.orderLinesCount = 0; + constructor(orderLines = {}, orderLinesCount = 0) { + this.orderLines = orderLines; + this.orderLinesCount = orderLinesCount; } isEmpty() { - return this.orderLines.length === 0; + return this.orderLinesCount === 0; + } + + changeBasket(productId, quantity) { + let newOrderLines = {...this.orderLines}; + let newOrderLinesCount = this.orderLinesCount; + if (productId in newOrderLines) { + if (quantity > 0) { + newOrderLines[productId] += quantity; + } else { + delete newOrderLines[productId] + newOrderLinesCount--; + } + } else { + newOrderLines[productId] = quantity; + newOrderLinesCount++; + } + return new BasketData(newOrderLines, newOrderLinesCount); } } diff --git a/cm-frontend/src/components/ProductListContainer.js b/cm-frontend/src/components/ProductListContainer.js index 6b12280..ba0361d 100644 --- a/cm-frontend/src/components/ProductListContainer.js +++ b/cm-frontend/src/components/ProductListContainer.js @@ -1,5 +1,5 @@ import React from "react"; -import { Route, useHistory } from "react-router-dom"; +import {Route, useHistory} from "react-router-dom"; import ProductListPage from "../pages/ProductListPage"; import Banner from "./Banner"; import UploadPage from "../pages/UploadPage"; @@ -7,108 +7,103 @@ import ProductDetailPage from "../pages/ProductDetailPage"; import TopNav from "./TopNav"; import DataService from "../services/DataService"; import useStickyState from '../utils/useStickyState'; +import {createBasketData} from "./BasketData"; const currentPosition = (list, id) => { if (list._cachedPos) { - if (list._cachedPos[id]) { - return list._cachedPos[id] - 1; // Cache position with + 1 - so 0 is not considered as absent - } + if (list._cachedPos[id]) { + return list._cachedPos[id] - 1; // Cache position with + 1 - so 0 is not considered as absent + } } else { - list._cachedPos = {}; + list._cachedPos = {}; } if (!list) { - return 0; + return 0; } for (let i = 0; i < list.length; i++) if (list[i].id === id) { - list._cachedPos[id] = (i + 1); - return i; + list._cachedPos[id] = (i + 1); + return i; } return 0; - }; - - export const listNavigator = (list) => { +}; + +export const listNavigator = (list) => { return { - hasNext: (id) => { return currentPosition(list, id) < list.length - 1}, - hasPrevious: (id) => { return currentPosition(list, id) > 0}, - getNext: (id) => { return '/product/view/'+ list[currentPosition(list, id)+1].id}, - getPrevious: (id) => { return '/product/view/'+ list[currentPosition(list, id)-1].id}, + hasNext: (id) => { + return currentPosition(list, id) < list.length - 1 + }, + hasPrevious: (id) => { + return currentPosition(list, id) > 0 + }, + getNext: (id) => { + return '/product/view/' + list[currentPosition(list, id) + 1].id + }, + getPrevious: (id) => { + return '/product/view/' + list[currentPosition(list, id) - 1].id + }, } - } +} export function ProductListContainer() { - const [showBanner, setShowBanner] = useStickyState(true, 'dcm-banner'); - const [productList, setProductList] = React.useState([]); - const [productListPage, setProductListPage] = React.useState(0); - const [productListPageSize, setProductListPageSize] = React.useState(20); - const [productListTotal, setProductListTotal] = React.useState(0); - const [productListLoading, setProductListLoading] = React.useState(false); + const [showBanner, setShowBanner] = useStickyState(true, 'dcm-banner'); + const [productList, setProductList] = React.useState([]); + const [productListPage, setProductListPage] = React.useState(0); + const [productListPageSize, setProductListPageSize] = React.useState(20); + const [productListTotal, setProductListTotal] = React.useState(0); + const [productListLoading, setProductListLoading] = React.useState(false); - const [basketData, setBasketData] = React.useState({orderLines: {}, orderLinesCount: 0}); - const changeBasket = (productId, quantity) => { - let newOrderLines = {...basketData.orderLines}; - let newOrderLinesCount = basketData.orderLinesCount; - if (productId in newOrderLines) { - if (quantity > 0) { - newOrderLines[productId] += quantity; - } else { - delete newOrderLines[productId] - newOrderLinesCount--; - } - } else { - newOrderLines[productId] = quantity; - newOrderLinesCount++; - } - console.log(newOrderLines); - setBasketData({orderLines: newOrderLines, orderLinesCount: newOrderLinesCount}); - } + const [basketData, setBasketData] = React.useState(createBasketData()); + const changeBasket = (productId, quantity) => { + setBasketData(basketData.changeBasket(productId, quantity)); + } - const setBannerClosed = () => { - setShowBanner(false); - }; - const setBannerOpened = () => { - setShowBanner(true); - }; + const setBannerClosed = () => { + setShowBanner(false); + }; + const setBannerOpened = () => { + setShowBanner(true); + }; - const history = useHistory(); - const searchAction = (...params) => { - history.push('/'); - loadProducts(...params); - }; + const history = useHistory(); + const searchAction = (...params) => { + history.push('/'); + loadProducts(...params); + }; - React.useEffect(() => { - loadProducts(); - }, []); + React.useEffect(() => { + loadProducts(); + }, []); - async function loadProducts(search = null, page = 0, size = 20) { - console.log("Load products: "+search+" "+page+" "+size); - setProductListLoading(true); - await DataService.fetchProducts(search, page, size).then(response => { - let responseData = response.data; - console.log(responseData); - setProductList(responseData.content); - setProductListPage(responseData.number); - setProductListPageSize(responseData.size); - setProductListTotal(responseData.totalElements); - setProductListLoading(false); + async function loadProducts(search = null, page = 0, size = 20) { + console.log("Load products: " + search + " " + page + " " + size); + setProductListLoading(true); + await DataService.fetchProducts(search, page, size).then(response => { + let responseData = response.data; + console.log(responseData); + setProductList(responseData.content); + setProductListPage(responseData.number); + setProductListPageSize(responseData.size); + setProductListTotal(responseData.totalElements); + setProductListLoading(false); + } + ).catch(error => { + console.log('Error occurred: ' + error.message); + setProductListLoading(false); + }); } - ).catch(error => { - console.log('Error occurred: ' + error.message); - setProductListLoading(false); - }); - } - return ( - <> - - - - - - - - - - - ); + return ( + <> + + + + + + + + + + + ); } From f38ed4b38333abbdc906c6e9fd671e5362d05c3d Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 13:28:14 +0200 Subject: [PATCH 11/71] One loading product, show correct basket status; move ProductBasketStatus to BasketData; pass changeBasket and basketData through intermediate components to AddToBasket via ...props without explicitly naming parameters. --- cm-frontend/src/components/AddToBasket.js | 11 +++-------- cm-frontend/src/components/BasketData.js | 10 ++++++++++ cm-frontend/src/components/ProductDetail.js | 4 ++-- cm-frontend/src/components/ProductListContainer.js | 2 +- cm-frontend/src/pages/ProductDetailPage.js | 4 ++-- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/cm-frontend/src/components/AddToBasket.js b/cm-frontend/src/components/AddToBasket.js index 273c800..fbfba0f 100644 --- a/cm-frontend/src/components/AddToBasket.js +++ b/cm-frontend/src/components/AddToBasket.js @@ -1,19 +1,14 @@ import {Button} from "@material-ui/core"; import React, {useEffect, useRef} from "react"; import CircularProgress from "@material-ui/core/CircularProgress"; +import {ProductBasketStatus} from "./BasketData"; -const ProductBasketStatus = { - Empty: 'empty', - Adding: 'adding', - Added: 'added', -} - export default function AddToBasket(props) { - const {changeBasket, product} = props; + const {changeBasket, basketData, product} = props; - const [state, setState] = React.useState(ProductBasketStatus.Empty); + const [state, setState] = React.useState(basketData.getProductBasketStatus(product.id)); const getButtonTitle = () => { switch (state) { diff --git a/cm-frontend/src/components/BasketData.js b/cm-frontend/src/components/BasketData.js index 5a2e78a..a78fa8d 100644 --- a/cm-frontend/src/components/BasketData.js +++ b/cm-frontend/src/components/BasketData.js @@ -1,3 +1,9 @@ +export const ProductBasketStatus = { + Empty: 'empty', + Adding: 'adding', + Added: 'added', +} + export function createBasketData() { class BasketData { @@ -26,6 +32,10 @@ export function createBasketData() { } return new BasketData(newOrderLines, newOrderLinesCount); } + + getProductBasketStatus(productId) { + return productId in this.orderLines ? ProductBasketStatus.Added : ProductBasketStatus.Empty; + } } return new BasketData(); diff --git a/cm-frontend/src/components/ProductDetail.js b/cm-frontend/src/components/ProductDetail.js index 6a09134..180ec75 100644 --- a/cm-frontend/src/components/ProductDetail.js +++ b/cm-frontend/src/components/ProductDetail.js @@ -113,14 +113,14 @@ export default function ProductView(props) { const showOrdering = true; - const { product, changeBasket } = props; + const { product } = props; return ( <> { showOrdering && ( <> - + )} diff --git a/cm-frontend/src/components/ProductListContainer.js b/cm-frontend/src/components/ProductListContainer.js index ba0361d..490c487 100644 --- a/cm-frontend/src/components/ProductListContainer.js +++ b/cm-frontend/src/components/ProductListContainer.js @@ -102,7 +102,7 @@ export function ProductListContainer() { - + ); diff --git a/cm-frontend/src/pages/ProductDetailPage.js b/cm-frontend/src/pages/ProductDetailPage.js index 69f49a3..2138073 100644 --- a/cm-frontend/src/pages/ProductDetailPage.js +++ b/cm-frontend/src/pages/ProductDetailPage.js @@ -35,7 +35,7 @@ function ViewToggle(props) { export default function ProductDetailPage(props) { - const { navigator, changeBasket } = props; + const { navigator } = props; let { id } = useParams(); @@ -78,7 +78,7 @@ export default function ProductDetailPage(props) { {viewMode === "json" ? (
{JSON.stringify(data, null, 2)}
) : ( - + ) } From b67255ced31c37baf5717af149754355a1577f15 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 13:53:46 +0200 Subject: [PATCH 12/71] Add basket page with product ids and quantities --- .../src/components/ProductListContainer.js | 4 + cm-frontend/src/components/TopNav.js | 4 +- cm-frontend/src/pages/BasketPage.js | 101 ++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 cm-frontend/src/pages/BasketPage.js diff --git a/cm-frontend/src/components/ProductListContainer.js b/cm-frontend/src/components/ProductListContainer.js index 490c487..690e5e7 100644 --- a/cm-frontend/src/components/ProductListContainer.js +++ b/cm-frontend/src/components/ProductListContainer.js @@ -8,6 +8,7 @@ import TopNav from "./TopNav"; import DataService from "../services/DataService"; import useStickyState from '../utils/useStickyState'; import {createBasketData} from "./BasketData"; +import BasketPage from "../pages/BasketPage"; const currentPosition = (list, id) => { if (list._cachedPos) { @@ -100,6 +101,9 @@ export function ProductListContainer() { + + + diff --git a/cm-frontend/src/components/TopNav.js b/cm-frontend/src/components/TopNav.js index 21c999c..7969fbf 100644 --- a/cm-frontend/src/components/TopNav.js +++ b/cm-frontend/src/components/TopNav.js @@ -67,9 +67,9 @@ export default function TopNav(props) {
{ showBasketBar && ( -
+ -
+ )}
diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js new file mode 100644 index 0000000..3808f3b --- /dev/null +++ b/cm-frontend/src/pages/BasketPage.js @@ -0,0 +1,101 @@ +import React from "react"; +import {makeStyles, withStyles} from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import Paper from "@material-ui/core/Paper"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import {useHistory} from "react-router"; +import ProductListHeader from '../components/ProductListHeader'; + +const useStyles = makeStyles(theme => ({ + table: { + minWidth: 600, + }, + header: { + marginBottom: '1em' + }, + paper: { + padding: theme.spacing(2), + paddingBottom: theme.spacing(5), + marginBottom: theme.spacing(3), + } +})); + +const StyledTableCell = withStyles(() => ({ + head: { + fontWeight: 'bold', + }, +}))(TableCell); + +const StyledTableRow = withStyles((theme) => ({ + root: { + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover, + }, + '&:hover': { + backgroundColor: 'rgba(0,0,0,.08)', + }, + cursor: 'pointer' + }, +}))(TableRow); + +export default function BasketPage(props) { + + const {basketData, changeBasket} = props; + + const [isLoading, setLoading] = React.useState(false); + + const classes = useStyles(); + + const {push} = useHistory(); + + const refreshAction = () => { + + } + + const showRowDetails = (productId) => { + push('/product/view/' + productId); + } + + return ( + <> + + + {(isLoading || false) ? ( + + ) : ( + <> + + + + + # + Product id + Quantity + + + + {(!basketData.isEmpty()) ? Object.keys(basketData.orderLines).map((productId, index) => ( + showRowDetails(productId)}> + {(index + 1)} + {productId} + {basketData.orderLines[productId]} + + )) : ( + + Basket is empty + + )} + +
+
+ + )} +
+ + ); +} From 467664b285490d285c703c9ba92559868c69d355 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 14:13:35 +0200 Subject: [PATCH 13/71] Rename ProductListHeader to PageHeader, add support for nested action buttons, add mouse-over titles to buttons --- .../{ProductListHeader.js => PageHeader.js} | 101 +++++++++--------- .../src/components/ProductDetailHeader.js | 41 ++++--- cm-frontend/src/pages/BasketPage.js | 17 +-- cm-frontend/src/pages/ProductListPage.js | 5 +- 4 files changed, 91 insertions(+), 73 deletions(-) rename cm-frontend/src/components/{ProductListHeader.js => PageHeader.js} (84%) diff --git a/cm-frontend/src/components/ProductListHeader.js b/cm-frontend/src/components/PageHeader.js similarity index 84% rename from cm-frontend/src/components/ProductListHeader.js rename to cm-frontend/src/components/PageHeader.js index 6e965d1..2621d44 100644 --- a/cm-frontend/src/components/ProductListHeader.js +++ b/cm-frontend/src/components/PageHeader.js @@ -1,50 +1,53 @@ -import { Card, CardContent, Fab, makeStyles, Typography } from "@material-ui/core"; -import RefreshIcon from '@material-ui/icons/Refresh'; - -const useStyles = makeStyles(theme => ({ - cardContent: { - '&:last-child': { - paddingBottom: theme.spacing(2), - }, - }, - row: { - display: "flex", - }, - header: { - flex: '1', - }, - buttons: { - flex: '1', - display: "flex", - placeContent: 'stretch flex-end', - alignItems: 'stretch', - - '& button': { - marginLeft: theme.spacing(4), - } - } -})); - -export default function ProductListHeader(prop) { - const { name, refreshAction } = prop; - - const classes = useStyles(); - - return ( - - -
-
- {name} -
- -
- - refreshAction()}/> - -
-
-
-
- ) +import { Card, CardContent, Fab, makeStyles, Typography } from "@material-ui/core"; +import RefreshIcon from '@material-ui/icons/Refresh'; + +const useStyles = makeStyles(theme => ({ + cardContent: { + '&:last-child': { + paddingBottom: theme.spacing(2), + }, + }, + row: { + display: "flex", + }, + header: { + flex: '1', + }, + buttons: { + flex: '1', + display: "flex", + placeContent: 'stretch flex-end', + alignItems: 'stretch', + + '& button': { + marginLeft: theme.spacing(4), + } + } +})); + +export default function PageHeader(prop) { + const { name, refreshAction, children } = prop; + + const classes = useStyles(); + + return ( + + +
+
+ {name} +
+ +
+ {refreshAction && ( + + refreshAction()}/> + + )} + {children} +
+
+
+
+ ) } \ No newline at end of file diff --git a/cm-frontend/src/components/ProductDetailHeader.js b/cm-frontend/src/components/ProductDetailHeader.js index 085484a..2f8d823 100644 --- a/cm-frontend/src/components/ProductDetailHeader.js +++ b/cm-frontend/src/components/ProductDetailHeader.js @@ -1,7 +1,7 @@ -import { Card, CardContent, Fab, makeStyles, Typography } from "@material-ui/core"; +import {Card, CardContent, Fab, makeStyles, Typography} from "@material-ui/core"; import ArrowIcon from '@material-ui/icons/KeyboardBackspaceOutlined'; import RefreshIcon from '@material-ui/icons/Refresh'; -import { useHistory } from "react-router"; +import {useHistory} from "react-router"; const useStyles = makeStyles(theme => ({ @@ -31,16 +31,25 @@ const useStyles = makeStyles(theme => ({ } })); +// noinspection JSUnusedLocalSymbols const _emptyNavigator = { - hasNext: (id) => {return false}, - hasPrevious: (id) => {return false}, - getNext: (id) => { return null}, - getPrevious: (id) => { return null}, + hasNext: (id) => { + return false + }, + hasPrevious: (id) => { + return false + }, + getNext: (id) => { + return null + }, + getPrevious: (id) => { + return null + }, } export default function ProductDetailHeader(prop) { - const { name, navigator = _emptyNavigator, id, refreshAction } = prop; + const {name, navigator = _emptyNavigator, id, refreshAction} = prop; const classes = useStyles(); @@ -55,24 +64,24 @@ export default function ProductDetailHeader(prop) { } return ( - + -
+
{name}
- navigateTo(navigator.getPrevious(id)) } > - + navigateTo(navigator.getPrevious(id))}> + - navigateTo(navigator.getNext(id)) } > - + navigateTo(navigator.getNext(id))}> + - refreshAction(id)}> + refreshAction(id)}> - - + +
diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index 3808f3b..067f668 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -9,7 +9,9 @@ import TableRow from "@material-ui/core/TableRow"; import Paper from "@material-ui/core/Paper"; import CircularProgress from "@material-ui/core/CircularProgress"; import {useHistory} from "react-router"; -import ProductListHeader from '../components/ProductListHeader'; +import PageHeader from '../components/PageHeader'; +import {Fab} from "@material-ui/core"; +import SendIcon from "@material-ui/icons/Send"; const useStyles = makeStyles(theme => ({ table: { @@ -53,9 +55,8 @@ export default function BasketPage(props) { const {push} = useHistory(); - const refreshAction = () => { - - } + const refreshAction = () => {} + const sendAction = () => {} const showRowDetails = (productId) => { push('/product/view/' + productId); @@ -63,7 +64,11 @@ export default function BasketPage(props) { return ( <> - + + + sendAction()}/> + + {(isLoading || false) ? ( @@ -87,7 +92,7 @@ export default function BasketPage(props) { )) : ( - Basket is empty + Basket is empty )} diff --git a/cm-frontend/src/pages/ProductListPage.js b/cm-frontend/src/pages/ProductListPage.js index 53f1ee4..8795b44 100644 --- a/cm-frontend/src/pages/ProductListPage.js +++ b/cm-frontend/src/pages/ProductListPage.js @@ -11,7 +11,7 @@ import CircularProgress from "@material-ui/core/CircularProgress"; import TablePagination from "@material-ui/core/TablePagination"; import { useHistory } from "react-router"; import ItemDetailsService from "../services/ItemDetailsService"; -import ProductListHeader from '../components/ProductListHeader'; +import PageHeader from '../components/PageHeader'; const useStyles = makeStyles(theme => ({ table: { @@ -27,6 +27,7 @@ const useStyles = makeStyles(theme => ({ } })); +// noinspection JSUnusedLocalSymbols const StyledTableCell = withStyles((theme) => ({ head: { fontWeight: 'bold', @@ -66,7 +67,7 @@ export default function ProductListPage(props) { return ( <> - + {(isLoading || false) ? ( From d8bf5533e29973fae310b8bb2df6931f5c4c1592 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 14:14:10 +0200 Subject: [PATCH 14/71] Fix small screen basket icon position --- cm-frontend/src/components/TopNav.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cm-frontend/src/components/TopNav.js b/cm-frontend/src/components/TopNav.js index 7969fbf..d11e57f 100644 --- a/cm-frontend/src/components/TopNav.js +++ b/cm-frontend/src/components/TopNav.js @@ -30,9 +30,9 @@ link: { } }, basketBar: { - [theme.breakpoints.down('xs')]: { + [theme.breakpoints.up('xs')]: { margin: theme.spacing(2), - } + }, }, searchBar: { [theme.breakpoints.down('xs')]: { @@ -65,12 +65,12 @@ export default function TopNav(props) { -
{ showBasketBar && ( )} +
From 8aec03101bf7195a0ed101b0dfc9883701fc8537 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 15:03:44 +0200 Subject: [PATCH 15/71] Hide internal implementation of BasketData - add getOrderLineList() method --- cm-frontend/src/components/BasketData.js | 6 ++++++ cm-frontend/src/pages/BasketPage.js | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cm-frontend/src/components/BasketData.js b/cm-frontend/src/components/BasketData.js index a78fa8d..06b9fbd 100644 --- a/cm-frontend/src/components/BasketData.js +++ b/cm-frontend/src/components/BasketData.js @@ -16,6 +16,12 @@ export function createBasketData() { return this.orderLinesCount === 0; } + getOrderLineList() { + return Object.keys(this.orderLines).map((productId) => { + return {productId: productId, quantity: this.orderLines[productId]} + }); + } + changeBasket(productId, quantity) { let newOrderLines = {...this.orderLines}; let newOrderLinesCount = this.orderLinesCount; diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index 067f668..d809a1a 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -84,11 +84,11 @@ export default function BasketPage(props) { - {(!basketData.isEmpty()) ? Object.keys(basketData.orderLines).map((productId, index) => ( - showRowDetails(productId)}> + {!basketData.isEmpty() ? basketData.getOrderLineList().map((orderLine, index) => ( + showRowDetails(orderLine.productId)}> {(index + 1)} - {productId} - {basketData.orderLines[productId]} + {orderLine.productId} + {orderLine.quantity} )) : ( From 47dedac6ecc1ad025ab42875fd12ebe933aea0cc Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 15:36:23 +0200 Subject: [PATCH 16/71] Add plus/minus controls on basket page per product --- cm-frontend/src/components/BasketData.js | 2 +- cm-frontend/src/pages/BasketPage.js | 40 +++++++++++++++++++++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/cm-frontend/src/components/BasketData.js b/cm-frontend/src/components/BasketData.js index 06b9fbd..63f20d8 100644 --- a/cm-frontend/src/components/BasketData.js +++ b/cm-frontend/src/components/BasketData.js @@ -26,7 +26,7 @@ export function createBasketData() { let newOrderLines = {...this.orderLines}; let newOrderLinesCount = this.orderLinesCount; if (productId in newOrderLines) { - if (quantity > 0) { + if (quantity !== 0) { newOrderLines[productId] += quantity; } else { delete newOrderLines[productId] diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index d809a1a..06a29cc 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -12,6 +12,8 @@ import {useHistory} from "react-router"; import PageHeader from '../components/PageHeader'; import {Fab} from "@material-ui/core"; import SendIcon from "@material-ui/icons/Send"; +import RemoveIcon from "@material-ui/icons/Remove"; +import AddIcon from "@material-ui/icons/Add"; const useStyles = makeStyles(theme => ({ table: { @@ -45,6 +47,32 @@ const StyledTableRow = withStyles((theme) => ({ }, }))(TableRow); +function QuantityControl(props) { + const {productId, quantity, changeBasket} = props; + + const changeQuantity = (e, quantityChange) => { + e.stopPropagation(); + if (quantity + quantityChange === 0) { + changeBasket(productId, 0); + } else { + changeBasket(productId, quantityChange); + } + return false; + } + + return ( +
{e.stopPropagation(); return false;}}> + + changeQuantity(e, -1)}/> + + {quantity} + + changeQuantity(e, 1)}/> + +
+ ) +} + export default function BasketPage(props) { const {basketData, changeBasket} = props; @@ -55,8 +83,10 @@ export default function BasketPage(props) { const {push} = useHistory(); - const refreshAction = () => {} - const sendAction = () => {} + const refreshAction = () => { + } + const sendAction = () => { + } const showRowDetails = (productId) => { push('/product/view/' + productId); @@ -66,7 +96,7 @@ export default function BasketPage(props) { <> - sendAction()}/> + sendAction()}/> @@ -80,7 +110,7 @@ export default function BasketPage(props) { # Product id - Quantity + Quantity @@ -88,7 +118,7 @@ export default function BasketPage(props) { showRowDetails(orderLine.productId)}> {(index + 1)} {orderLine.productId} - {orderLine.quantity} + )) : ( From dca57a4f15331cec9d7e07a8c88d5e99848c1dfb Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 16:34:57 +0200 Subject: [PATCH 17/71] Load products details on basket page --- .../dk/erst/cm/api/item/ProductService.java | 172 +++++++++--------- cm-frontend/src/pages/BasketPage.js | 52 +++++- cm-frontend/src/services/DataService.js | 71 ++++---- .../src/services/ItemDetailsService.js | 23 ++- .../dk/erst/cm/webapi/ProductController.java | 26 ++- 5 files changed, 211 insertions(+), 133 deletions(-) diff --git a/cm-api/src/main/java/dk/erst/cm/api/item/ProductService.java b/cm-api/src/main/java/dk/erst/cm/api/item/ProductService.java index 2563011..1262e33 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/item/ProductService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/item/ProductService.java @@ -1,9 +1,10 @@ package dk.erst.cm.api.item; -import java.time.Instant; -import java.util.List; -import java.util.Optional; - +import dk.erst.cm.api.dao.mongo.ProductRepository; +import dk.erst.cm.api.data.Product; +import dk.erst.cm.api.data.ProductCatalogUpdate; +import dk.erst.cm.xml.ubl21.model.CatalogueLine; +import dk.erst.cm.xml.ubl21.model.NestedSchemeID; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -11,91 +12,88 @@ import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.stereotype.Service; -import dk.erst.cm.api.dao.mongo.ProductRepository; -import dk.erst.cm.api.data.Product; -import dk.erst.cm.api.data.ProductCatalogUpdate; -import dk.erst.cm.xml.ubl21.model.CatalogueLine; -import dk.erst.cm.xml.ubl21.model.NestedSchemeID; +import java.time.Instant; +import java.util.List; +import java.util.Optional; @Service public class ProductService { - private ProductRepository productRepository; - - @Autowired - public ProductService(ProductRepository itemRepository) { - this.productRepository = itemRepository; - } - - public Product saveCatalogUpdateItem(ProductCatalogUpdate catalog, CatalogueLine line) { - String lineLogicalId = line.getLogicalId(); - String productCatalogId = catalog.getProductCatalogId(); - - String itemLogicalId = productCatalogId + "_" + lineLogicalId; - - boolean deleteAction = line.getActionCode() != null && "Delete".equals(line.getActionCode().getId()); - - Product product; - Optional optional = productRepository.findById(itemLogicalId); - if (optional.isPresent()) { - product = optional.get(); - product.setUpdateTime(Instant.now()); - product.setVersion(product.getVersion() + 1); - } else { - product = new Product(); - product.setId(itemLogicalId); - product.setCreateTime(Instant.now()); - product.setUpdateTime(null); - product.setVersion(1); - } - product.setDocumentVersion(ProductDocumentVersion.PEPPOL_CATALOGUE_3_1); - product.setProductCatalogId(productCatalogId); - product.setStandardNumber(getLineStandardNumber(line)); - product.setDocument(line); - - if (deleteAction) { - if (optional.isPresent()) { - productRepository.delete(product); - } - return null; - } - productRepository.save(product); - - return product; - } - - private String getLineStandardNumber(CatalogueLine line) { - if (line != null && line.getItem() != null) { - if (line.getItem().getStandardItemIdentification() != null) { - NestedSchemeID sn = line.getItem().getStandardItemIdentification(); - if (sn.getId() != null && sn.getId().getId() != null) { - return sn.getId().getId().toUpperCase(); - } - } - } - return null; - } - - public long countItems() { - return productRepository.count(); - } - - public Page findAll(String searchParam, Pageable pageable) { - Page productList; - if (!StringUtils.isEmpty(searchParam)) { - TextCriteria textCriteria = TextCriteria.forDefaultLanguage().matching(searchParam); - productList = productRepository.findAllBy(textCriteria, pageable); - } else { - productList = productRepository.findAll(pageable); - } - return productList; - } - - public Optional findById(String id) { - return productRepository.findById(id); - } - - public List findByStandardNumber(String standardNumber) { - return productRepository.findByStandardNumber(standardNumber); - } + private final ProductRepository productRepository; + + @Autowired + public ProductService(ProductRepository itemRepository) { + this.productRepository = itemRepository; + } + + public Product saveCatalogUpdateItem(ProductCatalogUpdate catalog, CatalogueLine line) { + String lineLogicalId = line.getLogicalId(); + String productCatalogId = catalog.getProductCatalogId(); + String itemLogicalId = productCatalogId + "_" + lineLogicalId; + boolean deleteAction = line.getActionCode() != null && "Delete".equals(line.getActionCode().getId()); + Product product; + Optional optional = productRepository.findById(itemLogicalId); + if (optional.isPresent()) { + product = optional.get(); + product.setUpdateTime(Instant.now()); + product.setVersion(product.getVersion() + 1); + } else { + product = new Product(); + product.setId(itemLogicalId); + product.setCreateTime(Instant.now()); + product.setUpdateTime(null); + product.setVersion(1); + } + product.setDocumentVersion(ProductDocumentVersion.PEPPOL_CATALOGUE_3_1); + product.setProductCatalogId(productCatalogId); + product.setStandardNumber(getLineStandardNumber(line)); + product.setDocument(line); + if (deleteAction) { + if (optional.isPresent()) { + productRepository.delete(product); + } + return null; + } + productRepository.save(product); + return product; + } + + private String getLineStandardNumber(CatalogueLine line) { + if (line != null && line.getItem() != null) { + if (line.getItem().getStandardItemIdentification() != null) { + NestedSchemeID sn = line.getItem().getStandardItemIdentification(); + if (sn.getId() != null && sn.getId().getId() != null) { + return sn.getId().getId().toUpperCase(); + } + } + } + return null; + } + + public long countItems() { + return productRepository.count(); + } + + public Page findAll(String searchParam, Pageable pageable) { + Page productList; + if (!StringUtils.isEmpty(searchParam)) { + TextCriteria textCriteria = TextCriteria.forDefaultLanguage().matching(searchParam); + productList = productRepository.findAllBy(textCriteria, pageable); + } else { + productList = productRepository.findAll(pageable); + } + return productList; + } + + public Optional findById(String id) { + return productRepository.findById(id); + } + + public Iterable findAllByIds(Iterable ids) { + return productRepository.findAllById(ids); + } + + public List findByStandardNumber(String standardNumber) { + return productRepository.findByStandardNumber(standardNumber); + } } diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index 06a29cc..5849d79 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -14,6 +14,8 @@ import {Fab} from "@material-ui/core"; import SendIcon from "@material-ui/icons/Send"; import RemoveIcon from "@material-ui/icons/Remove"; import AddIcon from "@material-ui/icons/Add"; +import DataService from "../services/DataService"; +import ItemDetailsService from "../services/ItemDetailsService"; const useStyles = makeStyles(theme => ({ table: { @@ -61,7 +63,10 @@ function QuantityControl(props) { } return ( -
{e.stopPropagation(); return false;}}> +
{ + e.stopPropagation(); + return false; + }}> changeQuantity(e, -1)}/> @@ -78,16 +83,51 @@ export default function BasketPage(props) { const {basketData, changeBasket} = props; const [isLoading, setLoading] = React.useState(false); + const [productList, setProductList] = React.useState({}); const classes = useStyles(); const {push} = useHistory(); const refreshAction = () => { + loadProducts().then(() => setLoading(false)); } const sendAction = () => { } + const productItem = (productId) => { + if (productId in productList) { + return productList[productId].document.item; + } + return null; + } + + async function loadProducts() { + if (!basketData.isEmpty()) { + setLoading(true); + const productIdList = basketData.getOrderLineList().map((orderLine) => orderLine.productId); + + await DataService.fetchProductsByIds(productIdList).then(response => { + let responseData = response.data; + console.log(responseData); + const productMapById = {} + for (const index in responseData) { + const p = responseData[index]; + productMapById[p.id] = p; + } + console.log(productMapById); + setProductList(productMapById); + } + ).catch(error => { + console.log('Error occurred: ' + error.message); + }); + } + } + + React.useEffect(() => { + loadProducts().then(() => setLoading(false)); + }, []); + const showRowDetails = (productId) => { push('/product/view/' + productId); } @@ -109,20 +149,24 @@ export default function BasketPage(props) { # - Product id Quantity + Name + Standard number + Seller number {!basketData.isEmpty() ? basketData.getOrderLineList().map((orderLine, index) => ( showRowDetails(orderLine.productId)}> {(index + 1)} - {orderLine.productId} + {ItemDetailsService.itemName(productItem(orderLine.productId))} + {ItemDetailsService.itemStandardNumber(productItem(orderLine.productId))} + {ItemDetailsService.itemSellerNumber(productItem(orderLine.productId))} )) : ( - Basket is empty + Basket is empty )} diff --git a/cm-frontend/src/services/DataService.js b/cm-frontend/src/services/DataService.js index f5f7df4..5fd8df0 100644 --- a/cm-frontend/src/services/DataService.js +++ b/cm-frontend/src/services/DataService.js @@ -1,33 +1,40 @@ -import Axios from "axios"; - -//const apiUrl = "http://localhost:8080/api"; -const apiUrl = "/dcm/api"; - -const fetchProductDetails = (productId) => { - return fetch(apiUrl + "/products/" + productId); -} - -const fetchProducts = (search, page = 0, size = 20) => { - const params = { - page: page, - size: size, - } - if (search) { - params.search = search; - } - return Axios.get(apiUrl + "/products", { - params: params - }); -} - -const uploadFiles = (formData) => { - return Axios.post(apiUrl + "/upload", formData); -} - -const DataService = { - fetchProductDetails, - fetchProducts, - uploadFiles, -} - +import Axios from "axios"; + +//const apiUrl = "http://localhost:8080/api"; +const apiUrl = "/dcm/api"; + +const fetchProductDetails = (productId) => { + return fetch(apiUrl + "/products/" + productId); +} + +const fetchProducts = (search, page = 0, size = 20) => { + const params = { + page: page, + size: size, + } + if (search) { + params.search = search; + } + return Axios.get(apiUrl + "/products", { + params: params + }); +} + +const fetchProductsByIds = (productIdList = []) => { + return Axios.post(apiUrl + "/products_by_ids", { + ids: productIdList + }); +} + +const uploadFiles = (formData) => { + return Axios.post(apiUrl + "/upload", formData); +} + +const DataService = { + fetchProductDetails, + fetchProducts, + fetchProductsByIds, + uploadFiles, +} + export default DataService; \ No newline at end of file diff --git a/cm-frontend/src/services/ItemDetailsService.js b/cm-frontend/src/services/ItemDetailsService.js index c855354..4a544b7 100644 --- a/cm-frontend/src/services/ItemDetailsService.js +++ b/cm-frontend/src/services/ItemDetailsService.js @@ -1,4 +1,4 @@ -import { Box } from "@material-ui/core"; +import {Box} from "@material-ui/core"; const itemOriginCountry = (item) => { if (item && item.originCountry) { @@ -18,6 +18,12 @@ const itemUNSPSC = (item) => { } return null; } +const itemName = (item) => { + if (item) { + return item.name; + } + return null; +} const itemSellerNumber = (item) => { if (item) { if (item.sellersItemIdentification) { @@ -96,7 +102,7 @@ const renderItemCertificate = (cert) => { const renderItemSpecification = (s) => { return (
- {s.documentTypeCode && ({s.documentTypeCode})} + {s.documentTypeCode && ({s.documentTypeCode})} {renderUrl(s.attachment.externalReference.uri)}
) @@ -104,8 +110,8 @@ const renderItemSpecification = (s) => { const renderItemAdditionalProperty = (s) => { return (
- {s.name && ({s.name})} - {s.nameCode && ({' '}{s.nameCode.id})} + {s.name && ({s.name})} + {s.nameCode && ({' '}{s.nameCode.id})} {(s.name || s.nameCode) && (":")} {s.value && ({' '}{s.value})} {s.valueQuantity && ({' '}{s.valueQuantity.quantity}{' '} {s.valueQuantity.unitCode})} @@ -113,11 +119,16 @@ const renderItemAdditionalProperty = (s) => { ) } -const renderUrlListValue = (v) => { return (renderUrl(v.attachment.externalReference.uri)) }; +const renderUrlListValue = (v) => { + return (renderUrl(v.attachment.externalReference.uri)) +}; -const renderUrl = (v) => { return ({v}) }; +const renderUrl = (v) => { + return ({v}) +}; const ItemDetailsService = { + itemName, itemOriginCountry, itemUNSPSC, itemSellerNumber, diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/ProductController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/ProductController.java index 29a7f01..998114b 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/ProductController.java +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/ProductController.java @@ -1,9 +1,11 @@ package dk.erst.cm.webapi; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; +import lombok.Data; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -12,6 +14,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PathVariable; +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.RestController; @@ -25,8 +28,12 @@ @Slf4j public class ProductController { + private final ProductService productService; + @Autowired - private ProductService productService; + public ProductController(ProductService productService) { + this.productService = productService; + } @RequestMapping(value = "/api/products") public Page getProducts(@RequestParam(required = false) String search, Pageable pageable) { @@ -34,11 +41,22 @@ public Page getProducts(@RequestParam(required = false) String search, return productService.findAll(search, pageable); } + @Data + public static class IdList { + private String[] ids; + } + + @RequestMapping(value = "/api/products_by_ids") + public Iterable getProductsByIds(@RequestBody IdList query) { + log.info("Search products by ids " + query); + return productService.findAllByIds(Arrays.asList(query.ids)); + } + @RequestMapping(value = "/api/product/{id}") public ResponseEntity getProductById(@PathVariable("id") String id) { Optional findById = productService.findById(id); if (findById.isPresent()) { - return new ResponseEntity(findById.get(), HttpStatus.OK); + return new ResponseEntity<>(findById.get(), HttpStatus.OK); } return ResponseEntity.notFound().build(); } @@ -52,9 +70,9 @@ public ResponseEntity> getProductsById(@PathVariable("id") String if (!StringUtils.isEmpty(product.getStandardNumber())) { list = productService.findByStandardNumber(product.getStandardNumber()); } else { - list = Arrays.asList(product); + list = Collections.singletonList(product); } - return new ResponseEntity>(list, HttpStatus.OK); + return new ResponseEntity<>(list, HttpStatus.OK); } return ResponseEntity.notFound().build(); } From a4b3a1a7070d8e82d43ea5c1434f1730c66256c4 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 16:44:34 +0200 Subject: [PATCH 18/71] Funny automatic extraction of component OrderLineList by IntelliJ Idea... --- cm-frontend/src/pages/BasketPage.js | 66 +++++++++++++++++------------ 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index 5849d79..a57b053 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -16,6 +16,7 @@ import RemoveIcon from "@material-ui/icons/Remove"; import AddIcon from "@material-ui/icons/Add"; import DataService from "../services/DataService"; import ItemDetailsService from "../services/ItemDetailsService"; +import * as PropTypes from "prop-types"; const useStyles = makeStyles(theme => ({ table: { @@ -78,6 +79,34 @@ function QuantityControl(props) { ) } +function OrderLineList(props) { + return + + + + # + Quantity + Name + Standard number + Seller number + + + + {!props.basketData.isEmpty() ? props.basketData.getOrderLineList().map(props.callbackFn) : ( + + Basket is empty + + )} + +
+
; +} + +OrderLineList.propTypes = { + classes: PropTypes.string, + basketData: PropTypes.any, + callbackFn: PropTypes.func +}; export default function BasketPage(props) { const {basketData, changeBasket} = props; @@ -144,34 +173,15 @@ export default function BasketPage(props) { ) : ( <> - - - - - # - Quantity - Name - Standard number - Seller number - - - - {!basketData.isEmpty() ? basketData.getOrderLineList().map((orderLine, index) => ( - showRowDetails(orderLine.productId)}> - {(index + 1)} - - {ItemDetailsService.itemName(productItem(orderLine.productId))} - {ItemDetailsService.itemStandardNumber(productItem(orderLine.productId))} - {ItemDetailsService.itemSellerNumber(productItem(orderLine.productId))} - - )) : ( - - Basket is empty - - )} - -
-
+ ( + showRowDetails(orderLine.productId)}> + {(index + 1)} + + {ItemDetailsService.itemName(productItem(orderLine.productId))} + {ItemDetailsService.itemStandardNumber(productItem(orderLine.productId))} + {ItemDetailsService.itemSellerNumber(productItem(orderLine.productId))} + + )}/> )} From 33254ec99f8b214220a02c0a457bec5337488afd Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 17:07:44 +0200 Subject: [PATCH 19/71] fix: Warning: Failed prop type: Invalid prop `cellHeight` supplied to `ForwardRef(GridList)` - change from string "360" to {360} --- cm-frontend/src/components/ProductPictureList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cm-frontend/src/components/ProductPictureList.js b/cm-frontend/src/components/ProductPictureList.js index f283944..a600f33 100644 --- a/cm-frontend/src/components/ProductPictureList.js +++ b/cm-frontend/src/components/ProductPictureList.js @@ -23,7 +23,7 @@ export default function RenderPictureList(props) { const classes = useStyles(); return ( - + {props.specList.map((spec) => ( Product From ec37147f07257eb8e9e16bc4fdeaf78eedffa12d Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 17:13:56 +0200 Subject: [PATCH 20/71] fix: js error "Warning: Each child in a list should have a unique "key" prop." Add several keys to render objects --- cm-frontend/src/components/ProductDetail.js | 4 ++-- cm-frontend/src/services/ItemDetailsService.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cm-frontend/src/components/ProductDetail.js b/cm-frontend/src/components/ProductDetail.js index 180ec75..66edf9f 100644 --- a/cm-frontend/src/components/ProductDetail.js +++ b/cm-frontend/src/components/ProductDetail.js @@ -92,7 +92,7 @@ const renderCatalogs = (source) => { {source.length}{source.length > 1 ? ' catalogs: ':' catalog'} {source.map((s,i) => { return ( - + ) })} @@ -101,7 +101,7 @@ const renderCatalogs = (source) => { const renderSourcedValue = (v, extractValue = (e)=> {return e.value}) => { return ( -
+
{v._source ? (<>{' '}{extractValue(v)}) : <>{v}}
) diff --git a/cm-frontend/src/services/ItemDetailsService.js b/cm-frontend/src/services/ItemDetailsService.js index 4a544b7..3e6f98b 100644 --- a/cm-frontend/src/services/ItemDetailsService.js +++ b/cm-frontend/src/services/ItemDetailsService.js @@ -101,7 +101,7 @@ const renderItemCertificate = (cert) => { } const renderItemSpecification = (s) => { return ( -
+
{s.documentTypeCode && ({s.documentTypeCode})} {renderUrl(s.attachment.externalReference.uri)}
@@ -109,7 +109,7 @@ const renderItemSpecification = (s) => { } const renderItemAdditionalProperty = (s) => { return ( -
+
{s.name && ({s.name})} {s.nameCode && ({' '}{s.nameCode.id})} {(s.name || s.nameCode) && (":")} From bbcc4763d9ddeec14f0b56702e9c66eb675d1645 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 17:35:57 +0200 Subject: [PATCH 21/71] fix: tried to fix "Line 159:39: React Hook React.useEffect has a missing dependency: 'loadProducts'. Either include it or remove the dependency array react-hooks/exhaustive-deps" - but give up. --- cm-frontend/src/pages/BasketPage.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index a57b053..f5b5430 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -103,7 +103,7 @@ function OrderLineList(props) { } OrderLineList.propTypes = { - classes: PropTypes.string, + classes: PropTypes.any, basketData: PropTypes.any, callbackFn: PropTypes.func }; @@ -113,13 +113,14 @@ export default function BasketPage(props) { const [isLoading, setLoading] = React.useState(false); const [productList, setProductList] = React.useState({}); + const [reloadCount, setReloadCount] = React.useState(0); const classes = useStyles(); const {push} = useHistory(); const refreshAction = () => { - loadProducts().then(() => setLoading(false)); + setReloadCount(reloadCount + 1); } const sendAction = () => { } @@ -131,6 +132,8 @@ export default function BasketPage(props) { return null; } + const callLoadProducts = () => { loadProducts().finally(() => setLoading(false))}; + async function loadProducts() { if (!basketData.isEmpty()) { setLoading(true); @@ -153,9 +156,7 @@ export default function BasketPage(props) { } } - React.useEffect(() => { - loadProducts().then(() => setLoading(false)); - }, []); + React.useEffect(callLoadProducts, [reloadCount]); const showRowDetails = (productId) => { push('/product/view/' + productId); From ef8747f44eb955bc9945ea23bb319c7bf5bc3315 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 17:54:05 +0200 Subject: [PATCH 22/71] chore: move OrderLineList to a separate file from BasketPage before adding OrderHeader component to BasketPage --- cm-frontend/src/components/OrderLineList.js | 108 +++++++++++++++++++ cm-frontend/src/pages/BasketPage.js | 111 ++------------------ 2 files changed, 115 insertions(+), 104 deletions(-) create mode 100644 cm-frontend/src/components/OrderLineList.js diff --git a/cm-frontend/src/components/OrderLineList.js b/cm-frontend/src/components/OrderLineList.js new file mode 100644 index 0000000..2e77eff --- /dev/null +++ b/cm-frontend/src/components/OrderLineList.js @@ -0,0 +1,108 @@ +import {makeStyles, withStyles} from "@material-ui/core/styles"; +import TableContainer from "@material-ui/core/TableContainer"; +import Table from "@material-ui/core/Table"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import ItemDetailsService from "../services/ItemDetailsService"; +import React from "react"; +import {Fab} from "@material-ui/core"; +import RemoveIcon from "@material-ui/icons/Remove"; +import AddIcon from "@material-ui/icons/Add"; + +const StyledTableCell = withStyles(() => ({ + head: { + fontWeight: 'bold', + }, +}))(TableCell); + +const StyledTableRow = withStyles((theme) => ({ + root: { + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover, + }, + '&:hover': { + backgroundColor: 'rgba(0,0,0,.08)', + }, + cursor: 'pointer' + }, +}))(TableRow); + +function QuantityControl(props) { + const {productId, quantity, changeBasket} = props; + + const changeQuantity = (e, quantityChange) => { + e.stopPropagation(); + if (quantity + quantityChange === 0) { + changeBasket(productId, 0); + } else { + changeBasket(productId, quantityChange); + } + return false; + } + + return ( +
{ + e.stopPropagation(); + return false; + }}> + + changeQuantity(e, -1)}/> + + {quantity} + + changeQuantity(e, 1)}/> + +
+ ) +} + +export default function OrderLineList(props) { + + const {basketData, showRowDetails, changeBasket, productList} = props; + + const useStyles = makeStyles(() => ({ + table: { + minWidth: 600, + }, + })); + + const classes = useStyles(); + + const productItem = (productId) => { + if (productId in productList) { + return productList[productId].document.item; + } + return null; + } + + return + + + + # + Quantity + Name + Standard number + Seller number + + + + {!basketData.isEmpty() ? basketData.getOrderLineList().map((orderLine, index) => ( + showRowDetails(orderLine.productId)}> + {(index + 1)} + + {ItemDetailsService.itemName(productItem(orderLine.productId))} + {ItemDetailsService.itemStandardNumber(productItem(orderLine.productId))} + {ItemDetailsService.itemSellerNumber(productItem(orderLine.productId))} + + )) : ( + + Basket is empty + + )} + +
+
; +} diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index f5b5430..3ea5f73 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -1,22 +1,13 @@ import React from "react"; -import {makeStyles, withStyles} from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; +import {makeStyles} from "@material-ui/core/styles"; import Paper from "@material-ui/core/Paper"; import CircularProgress from "@material-ui/core/CircularProgress"; import {useHistory} from "react-router"; import PageHeader from '../components/PageHeader'; import {Fab} from "@material-ui/core"; import SendIcon from "@material-ui/icons/Send"; -import RemoveIcon from "@material-ui/icons/Remove"; -import AddIcon from "@material-ui/icons/Add"; import DataService from "../services/DataService"; -import ItemDetailsService from "../services/ItemDetailsService"; -import * as PropTypes from "prop-types"; +import OrderLineList from "../components/OrderLineList"; const useStyles = makeStyles(theme => ({ table: { @@ -32,81 +23,7 @@ const useStyles = makeStyles(theme => ({ } })); -const StyledTableCell = withStyles(() => ({ - head: { - fontWeight: 'bold', - }, -}))(TableCell); - -const StyledTableRow = withStyles((theme) => ({ - root: { - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.action.hover, - }, - '&:hover': { - backgroundColor: 'rgba(0,0,0,.08)', - }, - cursor: 'pointer' - }, -}))(TableRow); -function QuantityControl(props) { - const {productId, quantity, changeBasket} = props; - - const changeQuantity = (e, quantityChange) => { - e.stopPropagation(); - if (quantity + quantityChange === 0) { - changeBasket(productId, 0); - } else { - changeBasket(productId, quantityChange); - } - return false; - } - - return ( -
{ - e.stopPropagation(); - return false; - }}> - - changeQuantity(e, -1)}/> - - {quantity} - - changeQuantity(e, 1)}/> - -
- ) -} - -function OrderLineList(props) { - return - - - - # - Quantity - Name - Standard number - Seller number - - - - {!props.basketData.isEmpty() ? props.basketData.getOrderLineList().map(props.callbackFn) : ( - - Basket is empty - - )} - -
-
; -} - -OrderLineList.propTypes = { - classes: PropTypes.any, - basketData: PropTypes.any, - callbackFn: PropTypes.func -}; export default function BasketPage(props) { const {basketData, changeBasket} = props; @@ -125,14 +42,9 @@ export default function BasketPage(props) { const sendAction = () => { } - const productItem = (productId) => { - if (productId in productList) { - return productList[productId].document.item; - } - return null; - } - - const callLoadProducts = () => { loadProducts().finally(() => setLoading(false))}; + const callLoadProducts = () => { + loadProducts().finally(() => setLoading(false)) + }; async function loadProducts() { if (!basketData.isEmpty()) { @@ -169,21 +81,12 @@ export default function BasketPage(props) { sendAction()}/> + {(isLoading || false) ? ( ) : ( - <> - ( - showRowDetails(orderLine.productId)}> - {(index + 1)} - - {ItemDetailsService.itemName(productItem(orderLine.productId))} - {ItemDetailsService.itemStandardNumber(productItem(orderLine.productId))} - {ItemDetailsService.itemSellerNumber(productItem(orderLine.productId))} - - )}/> - + )} From 36b528d9fba8cf107901755be9c5442ea18bf267 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 17:54:29 +0200 Subject: [PATCH 23/71] chore: 400ms imtation looks too slow now, reduce to 300ms :) --- cm-frontend/src/components/AddToBasket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cm-frontend/src/components/AddToBasket.js b/cm-frontend/src/components/AddToBasket.js index fbfba0f..29ce636 100644 --- a/cm-frontend/src/components/AddToBasket.js +++ b/cm-frontend/src/components/AddToBasket.js @@ -33,7 +33,7 @@ export default function AddToBasket(props) { timerRef.current = setTimeout(() => { changeBasket(product.id, 1); setState(ProductBasketStatus.Added) - }, 400); + }, 300); } else if (state === ProductBasketStatus.Added) { changeBasket(product.id, 0); setState(ProductBasketStatus.Empty); From 134d9e145763561a163140bdfcddcde1c843833a Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 17:54:54 +0200 Subject: [PATCH 24/71] chore: start to add order header --- cm-frontend/src/components/OrderHeader.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 cm-frontend/src/components/OrderHeader.js diff --git a/cm-frontend/src/components/OrderHeader.js b/cm-frontend/src/components/OrderHeader.js new file mode 100644 index 0000000..b817d42 --- /dev/null +++ b/cm-frontend/src/components/OrderHeader.js @@ -0,0 +1,7 @@ +import {Paper} from "@material-ui/core"; + +export default function OrderHeader() { + return + Test + +} \ No newline at end of file From 2d9ff73059657bdb94a63f490637ce8f96ef16af Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 19:12:42 +0200 Subject: [PATCH 25/71] chore: utility function to test loading layout - delay --- cm-frontend/src/utils/delay.js | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 cm-frontend/src/utils/delay.js diff --git a/cm-frontend/src/utils/delay.js b/cm-frontend/src/utils/delay.js new file mode 100644 index 0000000..782424e --- /dev/null +++ b/cm-frontend/src/utils/delay.js @@ -0,0 +1,4 @@ +// Usage example: +// import delay from "../utils/delay" +// await delay(10000); +export default function delay( ms ) { return new Promise(res => setTimeout(res, ms)); } From e829f11ac0ebbc311a5289890663f68760917309 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 1 Dec 2021 19:14:11 +0200 Subject: [PATCH 26/71] feature: add dummy OrderHeader with buyer company info and contact info --- cm-frontend/src/components/OrderHeader.js | 60 +++++++++++++++++- cm-frontend/src/components/OrderLineList.js | 68 ++++++++++++--------- cm-frontend/src/pages/BasketPage.js | 17 +++--- 3 files changed, 104 insertions(+), 41 deletions(-) diff --git a/cm-frontend/src/components/OrderHeader.js b/cm-frontend/src/components/OrderHeader.js index b817d42..2d93935 100644 --- a/cm-frontend/src/components/OrderHeader.js +++ b/cm-frontend/src/components/OrderHeader.js @@ -1,7 +1,61 @@ -import {Paper} from "@material-ui/core"; +import {Grid, Paper, TextField, Typography} from "@material-ui/core"; +import {makeStyles} from "@material-ui/core/styles"; export default function OrderHeader() { - return - Test + + const useStyles = makeStyles((theme) => ({ + paper: { + padding: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + formHeader: { + paddingTop: theme.spacing(1), + paddingLeft: theme.spacing(2), + textAlign: "left", + fontSize: '1em', + }, + form: { + padding: theme.spacing(2), + display: "flex", + flex: "1", + flexDirection: "row", + justifyContent: "space-between", + }, + input: { + paddingInline: theme.spacing(0.5), + } + })); + + const classes = useStyles(); + + function DataInput(props) { + return + } + + function DataBlock(props) { + return + +
{props.name}
+
+ {props.children} +
+
+
+ + } + + return + + + + + + + + + + + + } \ No newline at end of file diff --git a/cm-frontend/src/components/OrderLineList.js b/cm-frontend/src/components/OrderLineList.js index 2e77eff..7ff1bc9 100644 --- a/cm-frontend/src/components/OrderLineList.js +++ b/cm-frontend/src/components/OrderLineList.js @@ -7,7 +7,7 @@ import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import ItemDetailsService from "../services/ItemDetailsService"; import React from "react"; -import {Fab} from "@material-ui/core"; +import {Fab, Paper} from "@material-ui/core"; import RemoveIcon from "@material-ui/icons/Remove"; import AddIcon from "@material-ui/icons/Add"; @@ -62,7 +62,12 @@ export default function OrderLineList(props) { const {basketData, showRowDetails, changeBasket, productList} = props; - const useStyles = makeStyles(() => ({ + const useStyles = makeStyles((theme) => ({ + paper: { + padding: theme.spacing(2), + paddingBottom: theme.spacing(5), + marginBottom: theme.spacing(3), + }, table: { minWidth: 600, }, @@ -77,32 +82,35 @@ export default function OrderLineList(props) { return null; } - return - - - - # - Quantity - Name - Standard number - Seller number - - - - {!basketData.isEmpty() ? basketData.getOrderLineList().map((orderLine, index) => ( - showRowDetails(orderLine.productId)}> - {(index + 1)} - - {ItemDetailsService.itemName(productItem(orderLine.productId))} - {ItemDetailsService.itemStandardNumber(productItem(orderLine.productId))} - {ItemDetailsService.itemSellerNumber(productItem(orderLine.productId))} - - )) : ( - - Basket is empty - - )} - -
-
; + return + + + + + # + Quantity + Name + Standard number + Seller number + + + + {!basketData.isEmpty() ? basketData.getOrderLineList().map((orderLine, index) => ( + showRowDetails(orderLine.productId)}> + {(index + 1)} + + {ItemDetailsService.itemName(productItem(orderLine.productId))} + {ItemDetailsService.itemStandardNumber(productItem(orderLine.productId))} + {ItemDetailsService.itemSellerNumber(productItem(orderLine.productId))} + + )) : ( + + Basket is empty + + )} + +
+
+
+ ; } diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index 3ea5f73..12856ec 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -8,6 +8,7 @@ import {Fab} from "@material-ui/core"; import SendIcon from "@material-ui/icons/Send"; import DataService from "../services/DataService"; import OrderLineList from "../components/OrderLineList"; +import OrderHeader from "../components/OrderHeader"; const useStyles = makeStyles(theme => ({ table: { @@ -18,8 +19,6 @@ const useStyles = makeStyles(theme => ({ }, paper: { padding: theme.spacing(2), - paddingBottom: theme.spacing(5), - marginBottom: theme.spacing(3), } })); @@ -50,7 +49,6 @@ export default function BasketPage(props) { if (!basketData.isEmpty()) { setLoading(true); const productIdList = basketData.getOrderLineList().map((orderLine) => orderLine.productId); - await DataService.fetchProductsByIds(productIdList).then(response => { let responseData = response.data; console.log(responseData); @@ -82,13 +80,16 @@ export default function BasketPage(props) { - - {(isLoading || false) ? ( + {(isLoading || false) ? ( + - ) : ( + + ) : ( + <> + - )} - + + )} ); } From 0bf4e52d898ee6548ce1757d6fe988b3963c141a Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Thu, 2 Dec 2021 12:41:56 +0200 Subject: [PATCH 27/71] feature: add OrderData class and use in orderheader to post on sending --- cm-frontend/src/components/BasketData.js | 43 +++++++++++++++++++++++ cm-frontend/src/components/OrderHeader.js | 16 +++++---- cm-frontend/src/pages/BasketPage.js | 5 ++- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/cm-frontend/src/components/BasketData.js b/cm-frontend/src/components/BasketData.js index 63f20d8..e8343d3 100644 --- a/cm-frontend/src/components/BasketData.js +++ b/cm-frontend/src/components/BasketData.js @@ -45,4 +45,47 @@ export function createBasketData() { } return new BasketData(); +} + +export function createOrderData() { + + class Company { + constructor() { + this.registrationName = null; + this.legalIdentifier = null; + this.partyIdentifier = null; + } + + setDefault() { + this.registrationName = "My Company ApS"; + this.legalIdentifier = "DK11223344"; + this.partyIdentifier = "7300010000001"; + } + } + + class Contact { + constructor() { + this.personName = null; + this.email = null; + this.telephone = null; + } + + setDefault() { + this.personName = "John Doe"; + this.email = "unexisting@email.com"; + this.telephone = "+45 11223344"; + } + } + + class OrderData { + constructor() { + this.buyerCompany = new Company(); + this.buyerCompany.setDefault(); + this.buyerContact = new Contact(); + this.buyerContact.setDefault(); + } + + } + + return new OrderData(); } \ No newline at end of file diff --git a/cm-frontend/src/components/OrderHeader.js b/cm-frontend/src/components/OrderHeader.js index 2d93935..d3004a9 100644 --- a/cm-frontend/src/components/OrderHeader.js +++ b/cm-frontend/src/components/OrderHeader.js @@ -1,7 +1,9 @@ import {Grid, Paper, TextField, Typography} from "@material-ui/core"; import {makeStyles} from "@material-ui/core/styles"; -export default function OrderHeader() { +export default function OrderHeader(props) { + + const {orderData: orderData} = props; const useStyles = makeStyles((theme) => ({ paper: { @@ -47,14 +49,14 @@ export default function OrderHeader() { return - - - + + + - - - + + + diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index 12856ec..a599c4b 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -9,6 +9,7 @@ import SendIcon from "@material-ui/icons/Send"; import DataService from "../services/DataService"; import OrderLineList from "../components/OrderLineList"; import OrderHeader from "../components/OrderHeader"; +import {createOrderData} from "../components/BasketData"; const useStyles = makeStyles(theme => ({ table: { @@ -31,6 +32,8 @@ export default function BasketPage(props) { const [productList, setProductList] = React.useState({}); const [reloadCount, setReloadCount] = React.useState(0); + const [orderData, setOrderData] = React.useState(createOrderData()); + const classes = useStyles(); const {push} = useHistory(); @@ -86,7 +89,7 @@ export default function BasketPage(props) {
) : ( <> - + )} From 05144c1a38c64fd9c81e4e0d5c8d1b0b956329ed Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Thu, 2 Dec 2021 16:13:40 +0200 Subject: [PATCH 28/71] feature: add basket sending controller and data, lock controls on sending, hide refresh on sending, show progress bar, temporary add delay seeing how it looks on long sending --- cm-frontend/src/components/BasketData.js | 4 ++ cm-frontend/src/components/OrderHeader.js | 4 +- cm-frontend/src/components/OrderLineList.js | 10 +-- cm-frontend/src/pages/BasketPage.js | 32 +++++++-- cm-frontend/src/services/DataService.js | 8 +++ .../dk/erst/cm/webapi/BasketController.java | 70 +++++++++++++++++++ 6 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 cm-webapi/src/main/java/dk/erst/cm/webapi/BasketController.java diff --git a/cm-frontend/src/components/BasketData.js b/cm-frontend/src/components/BasketData.js index e8343d3..6343770 100644 --- a/cm-frontend/src/components/BasketData.js +++ b/cm-frontend/src/components/BasketData.js @@ -85,6 +85,10 @@ export function createOrderData() { this.buyerContact.setDefault(); } + isEmpty() { + // TODO: Implement empty validation + return false; + } } return new OrderData(); diff --git a/cm-frontend/src/components/OrderHeader.js b/cm-frontend/src/components/OrderHeader.js index d3004a9..b0a3d1e 100644 --- a/cm-frontend/src/components/OrderHeader.js +++ b/cm-frontend/src/components/OrderHeader.js @@ -3,7 +3,7 @@ import {makeStyles} from "@material-ui/core/styles"; export default function OrderHeader(props) { - const {orderData: orderData} = props; + const {orderData: orderData, lockControls} = props; const useStyles = makeStyles((theme) => ({ paper: { @@ -31,7 +31,7 @@ export default function OrderHeader(props) { const classes = useStyles(); function DataInput(props) { - return + return } function DataBlock(props) { diff --git a/cm-frontend/src/components/OrderLineList.js b/cm-frontend/src/components/OrderLineList.js index 7ff1bc9..07d0b07 100644 --- a/cm-frontend/src/components/OrderLineList.js +++ b/cm-frontend/src/components/OrderLineList.js @@ -30,7 +30,7 @@ const StyledTableRow = withStyles((theme) => ({ }))(TableRow); function QuantityControl(props) { - const {productId, quantity, changeBasket} = props; + const {productId, quantity, changeBasket, lockControls} = props; const changeQuantity = (e, quantityChange) => { e.stopPropagation(); @@ -47,11 +47,11 @@ function QuantityControl(props) { e.stopPropagation(); return false; }}> - + changeQuantity(e, -1)}/> {quantity} - + changeQuantity(e, 1)}/>
@@ -60,7 +60,7 @@ function QuantityControl(props) { export default function OrderLineList(props) { - const {basketData, showRowDetails, changeBasket, productList} = props; + const {basketData, showRowDetails, changeBasket, productList, lockControls} = props; const useStyles = makeStyles((theme) => ({ paper: { @@ -98,7 +98,7 @@ export default function OrderLineList(props) { {!basketData.isEmpty() ? basketData.getOrderLineList().map((orderLine, index) => ( showRowDetails(orderLine.productId)}> {(index + 1)} - + {ItemDetailsService.itemName(productItem(orderLine.productId))} {ItemDetailsService.itemStandardNumber(productItem(orderLine.productId))} {ItemDetailsService.itemSellerNumber(productItem(orderLine.productId))} diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index a599c4b..003732a 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -10,6 +10,7 @@ import DataService from "../services/DataService"; import OrderLineList from "../components/OrderLineList"; import OrderHeader from "../components/OrderHeader"; import {createOrderData} from "../components/BasketData"; +import delay from "../utils/delay"; const useStyles = makeStyles(theme => ({ table: { @@ -29,6 +30,7 @@ export default function BasketPage(props) { const {basketData, changeBasket} = props; const [isLoading, setLoading] = React.useState(false); + const [isSending, setSending] = React.useState(false); const [productList, setProductList] = React.useState({}); const [reloadCount, setReloadCount] = React.useState(0); @@ -42,6 +44,22 @@ export default function BasketPage(props) { setReloadCount(reloadCount + 1); } const sendAction = () => { + sendBasket().finally(() => setSending(false)); + } + + async function sendBasket() { + if (!basketData.isEmpty() && !orderData.isEmpty()) { + setSending(true); + // TODO: Remove test delay + await delay(1000); + await DataService.sendBasket(basketData, orderData).then(response => { + let responseData = response.data; + console.log(responseData); + } + ).catch(error => { + console.log('Error occurred: ' + error.message); + }); + } } const callLoadProducts = () => { @@ -77,9 +95,13 @@ export default function BasketPage(props) { return ( <> - - - sendAction()}/> + + + {isSending ? ( + + ) : ( + sendAction()}/> + )} @@ -89,8 +111,8 @@ export default function BasketPage(props) { ) : ( <> - - + + )} diff --git a/cm-frontend/src/services/DataService.js b/cm-frontend/src/services/DataService.js index 5fd8df0..9ba7e47 100644 --- a/cm-frontend/src/services/DataService.js +++ b/cm-frontend/src/services/DataService.js @@ -20,6 +20,13 @@ const fetchProducts = (search, page = 0, size = 20) => { }); } +const sendBasket = (basketData, orderData) => { + return Axios.post(apiUrl + "/basket/send", { + basketData: basketData, + orderData: orderData, + }); +} + const fetchProductsByIds = (productIdList = []) => { return Axios.post(apiUrl + "/products_by_ids", { ids: productIdList @@ -34,6 +41,7 @@ const DataService = { fetchProductDetails, fetchProducts, fetchProductsByIds, + sendBasket, uploadFiles, } diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/BasketController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/BasketController.java new file mode 100644 index 0000000..e871b34 --- /dev/null +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/BasketController.java @@ -0,0 +1,70 @@ +package dk.erst.cm.webapi; + +import dk.erst.cm.api.item.ProductService; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@CrossOrigin(maxAge = 3600) +@RestController +@Slf4j +public class BasketController { + + private final ProductService productService; + + @Autowired + public BasketController(ProductService productService) { + this.productService = productService; + } + + @Data + public static class Contact { + private String personName; + private String email; + private String telephone; + } + + @Data + public static class Company { + private String registrationName; + private String legalIdentifier; + private String partyIdentifier; + } + + @Data + public static class OrderData { + private Company buyerCompany; + private Contact buyerContact; + } + + @Data + public static class BasketData { + private Map orderLines; + } + + @Data + public static class SendBasketData { + private BasketData basketData; + private OrderData orderData; + } + + @Data + public static class SendBasketResponse { + private boolean success; + } + + @RequestMapping(value = "/api/basket/send") + public SendBasketResponse basketSend(@RequestBody SendBasketData query) { + log.info("Send basket with data " + query); + SendBasketResponse r = new SendBasketResponse(); + r.success = true; + return r; + } + +} From 20f4a02e94e0660f188167b8c3f5204fbadd1a24 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 4 Dec 2021 16:27:40 +0200 Subject: [PATCH 29/71] Move Order data to a separate js file --- cm-frontend/src/components/BasketData.js | 47 -------------- cm-frontend/src/components/OrderData.js | 65 +++++++++++++++++++ .../src/components/ProductListContainer.js | 3 +- 3 files changed, 67 insertions(+), 48 deletions(-) create mode 100644 cm-frontend/src/components/OrderData.js diff --git a/cm-frontend/src/components/BasketData.js b/cm-frontend/src/components/BasketData.js index 6343770..00a7091 100644 --- a/cm-frontend/src/components/BasketData.js +++ b/cm-frontend/src/components/BasketData.js @@ -46,50 +46,3 @@ export function createBasketData() { return new BasketData(); } - -export function createOrderData() { - - class Company { - constructor() { - this.registrationName = null; - this.legalIdentifier = null; - this.partyIdentifier = null; - } - - setDefault() { - this.registrationName = "My Company ApS"; - this.legalIdentifier = "DK11223344"; - this.partyIdentifier = "7300010000001"; - } - } - - class Contact { - constructor() { - this.personName = null; - this.email = null; - this.telephone = null; - } - - setDefault() { - this.personName = "John Doe"; - this.email = "unexisting@email.com"; - this.telephone = "+45 11223344"; - } - } - - class OrderData { - constructor() { - this.buyerCompany = new Company(); - this.buyerCompany.setDefault(); - this.buyerContact = new Contact(); - this.buyerContact.setDefault(); - } - - isEmpty() { - // TODO: Implement empty validation - return false; - } - } - - return new OrderData(); -} \ No newline at end of file diff --git a/cm-frontend/src/components/OrderData.js b/cm-frontend/src/components/OrderData.js new file mode 100644 index 0000000..cf4728b --- /dev/null +++ b/cm-frontend/src/components/OrderData.js @@ -0,0 +1,65 @@ +const _orderData = createOrderData(); + +export const getOrderData = () => _orderData; + +function createOrderData() { + return { + buyerCompany: { + registrationName: "My Company ApS", + legalIdentifier: "DK11223344", + partyIdentifier: "7300010000001", + }, + buyerContact: { + personName: "John Doe", + email: "unexisting@email.com", + telephone: "+45 11223344", + }, + } +} + +function xcreateOrderData() { + + class Company { + constructor() { + this.registrationName = null; + this.legalIdentifier = null; + this.partyIdentifier = null; + } + + setDefault() { + this.registrationName = "My Company ApS"; + this.legalIdentifier = "DK11223344"; + this.partyIdentifier = "7300010000001"; + } + } + + class Contact { + constructor() { + this.personName = null; + this.email = null; + this.telephone = null; + } + + setDefault() { + this.personName = "John Doe"; + this.email = "unexisting@email.com"; + this.telephone = "+45 11223344"; + } + } + + class OrderData { + constructor() { + this.buyerCompany = new Company(); + this.buyerCompany.setDefault(); + this.buyerContact = new Contact(); + this.buyerContact.setDefault(); + } + + isEmpty() { + // TODO: Implement empty validation + return false; + } + } + + return new OrderData(); +} \ No newline at end of file diff --git a/cm-frontend/src/components/ProductListContainer.js b/cm-frontend/src/components/ProductListContainer.js index 690e5e7..7986d11 100644 --- a/cm-frontend/src/components/ProductListContainer.js +++ b/cm-frontend/src/components/ProductListContainer.js @@ -8,6 +8,7 @@ import TopNav from "./TopNav"; import DataService from "../services/DataService"; import useStickyState from '../utils/useStickyState'; import {createBasketData} from "./BasketData"; +import {getOrderData} from "./OrderData"; import BasketPage from "../pages/BasketPage"; const currentPosition = (list, id) => { @@ -102,7 +103,7 @@ export function ProductListContainer() { - + From fafd1317b97f5c2576d838497d1e03f6217406f8 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 4 Dec 2021 16:56:11 +0200 Subject: [PATCH 30/71] Move Order data to a separate js file as global variable, do not use state to keep it, but update global class object inside input onchange, use local state per each input, avoid parameter passing to each DataInput by using React.cloneElement with expanded list of parameters inside DataBlock; build label by field name - split by uppercase and convert first letter to upper. See comments in commit for reasons. --- cm-frontend/src/components/OrderData.js | 21 ++--- cm-frontend/src/components/OrderHeader.js | 99 ++++++++++++++++------- cm-frontend/src/pages/BasketPage.js | 5 +- 3 files changed, 76 insertions(+), 49 deletions(-) diff --git a/cm-frontend/src/components/OrderData.js b/cm-frontend/src/components/OrderData.js index cf4728b..76e473c 100644 --- a/cm-frontend/src/components/OrderData.js +++ b/cm-frontend/src/components/OrderData.js @@ -1,23 +1,12 @@ +// Global state for order data - because I want not finished input of sending form to be kept between page navigations, +// so changes should be applied without any submit. But when useState on the root component was used, any changes +// led to re-render of many components. So instead local useState per input is used, current global value is passed as +// parameter and is used as local state initial value in each input. const _orderData = createOrderData(); export const getOrderData = () => _orderData; function createOrderData() { - return { - buyerCompany: { - registrationName: "My Company ApS", - legalIdentifier: "DK11223344", - partyIdentifier: "7300010000001", - }, - buyerContact: { - personName: "John Doe", - email: "unexisting@email.com", - telephone: "+45 11223344", - }, - } -} - -function xcreateOrderData() { class Company { constructor() { @@ -42,7 +31,7 @@ function xcreateOrderData() { setDefault() { this.personName = "John Doe"; - this.email = "unexisting@email.com"; + this.email = "some@email.com"; this.telephone = "+45 11223344"; } } diff --git a/cm-frontend/src/components/OrderHeader.js b/cm-frontend/src/components/OrderHeader.js index b0a3d1e..02a7578 100644 --- a/cm-frontend/src/components/OrderHeader.js +++ b/cm-frontend/src/components/OrderHeader.js @@ -1,15 +1,38 @@ +import React from "react"; import {Grid, Paper, TextField, Typography} from "@material-ui/core"; import {makeStyles} from "@material-ui/core/styles"; -export default function OrderHeader(props) { +const buildDefaultLabelByName = (str) => { + return str.split(/(?=[A-Z])/).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ") +} + +const DataInput = (props) => { + const {name, label = buildDefaultLabelByName(name), lockControls, target, ...rest} = props; + + const [value, setValue] = React.useState(target[name]); - const {orderData: orderData, lockControls} = props; + const onChange = (e) => { + const newValue = e.target.value; + target[e.target.name] = newValue; + return setValue(newValue); + }; + + const useStyles = makeStyles((theme) => ({ + input: { + paddingInline: theme.spacing(0.5), + } + })); + + const classes = useStyles(); + + return +} + +const DataBlock = (props) => { + + const {name, target, lockControls} = props; const useStyles = makeStyles((theme) => ({ - paper: { - padding: theme.spacing(2), - marginBottom: theme.spacing(2), - }, formHeader: { paddingTop: theme.spacing(1), paddingLeft: theme.spacing(2), @@ -25,38 +48,56 @@ export default function OrderHeader(props) { }, input: { paddingInline: theme.spacing(0.5), - } + }, + })); const classes = useStyles(); - function DataInput(props) { - return - } - - function DataBlock(props) { - return - -
{props.name}
-
- {props.children} -
-
-
+ return + +
{name}
+
+ {/* + // Below trick is needed to avoid writing same target and lockControls attribute in each child + // - parent expands children with these equal parameters by cloning them. + // If it affects performance - just do copy/paste... + */} + {props.children.map((child) => ( +
+ {React.cloneElement(child, {target: target, lockControls: lockControls})} +
+ ))} +
+
+
+ +} + +export default function OrderHeader(props) { + + const {orderData, lockControls} = props; - } + const useStyles = makeStyles((theme) => ({ + paper: { + padding: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + })); + + const classes = useStyles(); return - - - - + + + + - - - - + + + + diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index 003732a..c990966 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -9,7 +9,6 @@ import SendIcon from "@material-ui/icons/Send"; import DataService from "../services/DataService"; import OrderLineList from "../components/OrderLineList"; import OrderHeader from "../components/OrderHeader"; -import {createOrderData} from "../components/BasketData"; import delay from "../utils/delay"; const useStyles = makeStyles(theme => ({ @@ -27,15 +26,13 @@ const useStyles = makeStyles(theme => ({ export default function BasketPage(props) { - const {basketData, changeBasket} = props; + const {basketData, changeBasket, orderData} = props; const [isLoading, setLoading] = React.useState(false); const [isSending, setSending] = React.useState(false); const [productList, setProductList] = React.useState({}); const [reloadCount, setReloadCount] = React.useState(0); - const [orderData, setOrderData] = React.useState(createOrderData()); - const classes = useStyles(); const {push} = useHistory(); From 44f9af12e739e6ed4103fb7d0bb7dd4122f5c137 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 4 Dec 2021 17:31:34 +0200 Subject: [PATCH 31/71] chore: some cleanup in maven plugins and dependencies definitions to avoid configuration duplication inside child projects --- cm-all/pom.xml | 11 ++++++++- cm-xml-codelist/pom.xml | 34 --------------------------- cm-xml-syntax/pom.xml | 34 --------------------------- pom.xml | 52 +++++++++++++++++++++++++++++++++-------- 4 files changed, 52 insertions(+), 79 deletions(-) diff --git a/cm-all/pom.xml b/cm-all/pom.xml index 63884e8..1268ea3 100644 --- a/cm-all/pom.xml +++ b/cm-all/pom.xml @@ -15,7 +15,7 @@ cm-all 1.0.0 - Catalog Manager :: Web API and Frontent together + Catalog Manager :: Web API and Frontend together 1.8 1.0.0 @@ -79,7 +79,13 @@ + org.apache.maven.plugins maven-resources-plugin + 2.6 + false + + UTF-8 + position-react-build @@ -101,7 +107,9 @@ + org.apache.maven.plugins maven-clean-plugin + 3.0.0 true @@ -110,6 +118,7 @@ org.springframework.boot spring-boot-maven-plugin + 2.4.0 dk.erst.cm.CatalogApiApplication diff --git a/cm-xml-codelist/pom.xml b/cm-xml-codelist/pom.xml index e183010..55fc4a2 100644 --- a/cm-xml-codelist/pom.xml +++ b/cm-xml-codelist/pom.xml @@ -22,19 +22,16 @@ org.apache.commons commons-lang3 - 3.7 org.junit.jupiter junit-jupiter - 5.7.0 test commons-io commons-io - 2.8.0 test @@ -79,37 +76,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - UTF-8 - - - - org.apache.maven.plugins - maven-resources-plugin - 2.6 - - UTF-8 - - - - - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - - jar - - - - diff --git a/cm-xml-syntax/pom.xml b/cm-xml-syntax/pom.xml index 530dad7..b8dba24 100644 --- a/cm-xml-syntax/pom.xml +++ b/cm-xml-syntax/pom.xml @@ -22,19 +22,16 @@ org.apache.commons commons-lang3 - 3.7 org.junit.jupiter junit-jupiter - 5.7.0 test commons-io commons-io - 2.8.0 test @@ -79,37 +76,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - UTF-8 - - - - org.apache.maven.plugins - maven-resources-plugin - 2.6 - - UTF-8 - - - - - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - - jar - - - - diff --git a/pom.xml b/pom.xml index b841899..5089901 100644 --- a/pom.xml +++ b/pom.xml @@ -60,9 +60,29 @@ ${lombok.version} provided - + + + org.junit.jupiter + junit-jupiter + 5.7.0 + test + + + + commons-io + commons-io + 2.8.0 + + + + org.apache.commons + commons-lang3 + 3.7 + + + @@ -79,6 +99,19 @@ maven-compiler-plugin 2.3.2 + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + attach-sources + + jar + + + + org.apache.maven.plugins maven-clean-plugin @@ -102,6 +135,14 @@ + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + UTF-8 + + org.codehaus.mojo sonar-maven-plugin @@ -134,15 +175,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - 1.8 - 1.8 - UTF-8 - - From 27f70a9a9c326052ceb3ebe4f30c345d71c57d12 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 5 Dec 2021 14:28:35 +0200 Subject: [PATCH 32/71] chore: exclude lombok from cm-xml-syntax - used only in unit test for single field, but was not compliant with maven plugin --- cm-xml-syntax/pom.xml | 8 -------- .../erst/cm/xml/syntax/StructureLoadServiceTest.java | 11 ++++++----- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/cm-xml-syntax/pom.xml b/cm-xml-syntax/pom.xml index b8dba24..b1ef52b 100644 --- a/cm-xml-syntax/pom.xml +++ b/cm-xml-syntax/pom.xml @@ -34,14 +34,6 @@ commons-io test - - - org.projectlombok - lombok - test - true - - diff --git a/cm-xml-syntax/src/test/java/dk/erst/cm/xml/syntax/StructureLoadServiceTest.java b/cm-xml-syntax/src/test/java/dk/erst/cm/xml/syntax/StructureLoadServiceTest.java index a3fd5a8..04ed8f6 100644 --- a/cm-xml-syntax/src/test/java/dk/erst/cm/xml/syntax/StructureLoadServiceTest.java +++ b/cm-xml-syntax/src/test/java/dk/erst/cm/xml/syntax/StructureLoadServiceTest.java @@ -18,8 +18,6 @@ import dk.erst.cm.xml.syntax.structure.AttributeType; import dk.erst.cm.xml.syntax.structure.ElementType; import dk.erst.cm.xml.syntax.structure.StructureType; -import lombok.Getter; -import lombok.Setter; public class StructureLoadServiceTest { @@ -58,7 +56,7 @@ public void testGenerateTxtSyntax() throws IOException { for (String[] strings : structures) { String pathname = rootPath + "/" + strings[0]; StructureType s; - try (InputStream is = new FileInputStream(new File(pathname))) { + try (InputStream is = new FileInputStream(pathname)) { s = service.loadStructure(is, pathname); } catch (Exception e) { throw new IllegalStateException("Failed to load Peppol Catalogue structure by path " + pathname, e); @@ -95,10 +93,13 @@ public void testSyntax() throws IOException { } private static class StructureDumpService { - @Getter - @Setter + private boolean removeTagNsAlias = false; + public void setRemoveTagNsAlias(boolean removeTagNsAlias) { + this.removeTagNsAlias = removeTagNsAlias; + } + public String dump(StructureType s) { StringBuilder sb = new StringBuilder(); this.dump(sb, s.getDocument(), 0); From 7863f4035856e71d295980729c2d26f3b0bdf733 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 5 Dec 2021 14:28:48 +0200 Subject: [PATCH 33/71] chore: add order only example --- cm-resources/examples/order/OrderOnly.xml | 191 ++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 cm-resources/examples/order/OrderOnly.xml diff --git a/cm-resources/examples/order/OrderOnly.xml b/cm-resources/examples/order/OrderOnly.xml new file mode 100644 index 0000000..742fcea --- /dev/null +++ b/cm-resources/examples/order/OrderOnly.xml @@ -0,0 +1,191 @@ + + + urn:fdc:peppol.eu:poacc:trns:order:3 + urn:fdc:peppol.eu:poacc:bis:order_only:3 + 1005 + 2021-12-01 + 05:10:10 + EUR + 12345678 + + 2021-12-02 + + + Contract0101 + + + + 5798009882806 + + 5798009882806 + + + Svensk Fyrtårn + + + Svensk Fyrtårn + 5798009882806 + + Stockholm + + SE + + + + + + + + 5798009882783 + + DK31261430 + + + Grønt Fyrtårn + + + Langelinie Alle 17 + Copenhagen + 2100 + + DK + + + + Grønt Fyrtårn + 5798009882783 + + + + + + 5798009882806 + + 5798009882806 + + + Svensk Fyrtårn + + + Stockholm + 2100 + + SE + + + + Svensk Fyrtårn + 5798009882806 + + Stockholm + + SE + + + + + + + + 5798009882806 + Svensk Fyrtårn + + Godsgatan 2 + Godsmottagningen + Stockholm + 0585 + + Portkod 1234 + + + SE + + + + + 2021-12-03 + 2021-12-04 + + + + 5798009882806 + + + Svensk Fyrtårn + + + Godsgatan 2 + Intern IT + Stockholm + 0585 + + 1. sal + + + SE + + + + Ole Hansen + +453158877523 + olemad@erst.dk + + + + + 72.50 + + + 290.00 + 362.50 + + + + 1 + 1 + 200.00 + + 200.00 + + + POLY STUDIO P21 + + 637557 + + + S + 25 + + VAT + + + + + + + + 2 + 1 + 90.00 + + 90.00 + + + POLY STUDIO P15 + + 637558 + + + S + 25 + + VAT + + + + + + From edb2ff216e9147901bc386b8d11b5c627ab1acef Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 5 Dec 2021 15:44:03 +0200 Subject: [PATCH 34/71] Add dependency on Philip Helger's library with UBL2.1 XML stubs for serialization/deserialization --- cm-api/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cm-api/pom.xml b/cm-api/pom.xml index 66b7376..5af6067 100644 --- a/cm-api/pom.xml +++ b/cm-api/pom.xml @@ -24,6 +24,13 @@ cm-ubl ${cm.version} + + + com.helger.ubl + ph-ubl21 + 6.6.3 + + org.springframework.boot spring-boot-starter-data-mongodb From 608241609a2176150f896730f05b4b0ee294c0da Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 5 Dec 2021 15:47:22 +0200 Subject: [PATCH 35/71] Unit test to learn com.helger.ubl.ph-ubl21 library - read and generate valid by schema/schematron Order BIS3 3.12.0. Add logback-test to hide debug logging from this library in tests --- .../erst/cm/api/order/OrderProducerTest.java | 168 ++++++++++++++++++ cm-api/src/test/resources/logback-test.xml | 11 ++ 2 files changed, 179 insertions(+) create mode 100644 cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerTest.java create mode 100644 cm-api/src/test/resources/logback-test.xml diff --git a/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerTest.java b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerTest.java new file mode 100644 index 0000000..f20e2cd --- /dev/null +++ b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerTest.java @@ -0,0 +1,168 @@ +package dk.erst.cm.api.order; + +import com.helger.commons.error.IError; +import com.helger.commons.error.list.IErrorList; +import com.helger.ubl21.UBL21Reader; +import com.helger.ubl21.UBL21Validator; +import com.helger.ubl21.UBL21Writer; +import lombok.Data; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.AddressType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.CountryType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.CustomerPartyType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.ItemIdentificationType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.ItemType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.LineItemType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.OrderLineType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.PartyIdentificationType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.PartyLegalEntityType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.PartyNameType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.PartyType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.PeriodType; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.SupplierPartyType; +import oasis.names.specification.ubl.schema.xsd.order_21.OrderType; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings("ConstantConditions") +class OrderProducerTest { + + @Test + void read() throws IOException { + try (InputStream is = new FileInputStream("../cm-resources/examples/order/OrderOnly.xml")) { + OrderType res = UBL21Reader.order().read(is); + assertEquals("1005", res.getIDValue()); + assertEquals("Contract0101", res.getContract().get(0).getIDValue()); + List orderLine = res.getOrderLine(); + for (int i = 0; i < orderLine.size(); i++) { + OrderLineType orderLineType = orderLine.get(i); + assertEquals(String.valueOf(i + 1), orderLineType.getLineItem().getIDValue()); + } + } + } + + @Test + void produce() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OrderType order = buildOrder(); + IErrorList errorList = UBL21Validator.order().validate(order); + if (errorList.isNotEmpty()) { + System.out.println("Found " + errorList.size() + " errors:"); + for (int i = 0; i < errorList.size(); i++) { + IError error = errorList.get(i); + System.out.println((i + 1) + "\t" + error.toString()); + } + } + assertTrue(errorList.isEmpty()); + UBL21Writer.order().write(order, out); + String xml = new String(out.toByteArray(), StandardCharsets.UTF_8); + System.out.println(xml); + assertTrue(xml.indexOf(order.getSellerSupplierParty().getParty().getPostalAddress().getCountry().getIdentificationCodeValue()) > 0); + } + + @SuppressWarnings("SpellCheckingInspection") + private OrderType buildOrder() { + OrderType order = new OrderType(); + order.setCustomizationID("urn:fdc:peppol.eu:poacc:trns:order:3"); + order.setProfileID("urn:fdc:peppol.eu:poacc:bis:order_only:3"); + order.setID("TEST"); + order.setIssueDate(LocalDate.now()); + order.setIssueTime(LocalTime.now()); + order.setDocumentCurrencyCode("DKK"); + CustomerPartyType buyerCustomerParty = new CustomerPartyType(); + buyerCustomerParty.setParty(buildParty(new PartyInfo("5798009882806", "0088", "Swedish Company"))); + order.setBuyerCustomerParty(buyerCustomerParty); + SupplierPartyType supplierPartyType = new SupplierPartyType(); + supplierPartyType.setParty(buildParty(new PartyInfo("5798009882783", "0088", "Danish Company"))); + order.setSellerSupplierParty(supplierPartyType); + AddressType addressType = new AddressType(); + addressType.setCityName("Stockholm"); + addressType.setPostalZone("2100"); + CountryType countryType = new CountryType(); + countryType.setIdentificationCode("SE"); + addressType.setCountry(countryType); + supplierPartyType.getParty().setPostalAddress(addressType); + ArrayList validityPeriodList = new ArrayList<>(); + PeriodType periodType = new PeriodType(); + periodType.setEndDate(LocalDate.now().plusDays(1)); + validityPeriodList.add(periodType); + order.setValidityPeriod(validityPeriodList); + ArrayList orderLineList = new ArrayList<>(); + order.setOrderLine(orderLineList); + OrderLineType line = new OrderLineType(); + LineItemType lineItem = new LineItemType(); + lineItem.setID("1"); + lineItem.setQuantity(BigDecimal.valueOf(1)); + lineItem.getQuantity().setUnitCode("EA"); + ItemType item = new ItemType(); + item.setName("Test"); + ItemIdentificationType itemIdentificationType = new ItemIdentificationType(); + itemIdentificationType.setID("1234"); + item.setSellersItemIdentification(itemIdentificationType); + lineItem.setItem(item); + line.setLineItem(lineItem); + orderLineList.add(line); + return order; + } + + public PartyType buildParty(PartyInfo partyInfo) { + PartyType buyerParty = new PartyType(); + buyerParty.setEndpointID(partyInfo.getEndpointID()); + buyerParty.getEndpointID().setSchemeID(partyInfo.getEndpointIdSchemeID()); + List list = new ArrayList<>(); + PartyIdentificationType partyIdentificationType = new PartyIdentificationType(); + partyIdentificationType.setID(partyInfo.getPartyIdentificationID()); + partyIdentificationType.getID().setSchemeID(partyInfo.getPartyIdentificationIDSchemeID()); + list.add(partyIdentificationType); + buyerParty.setPartyIdentification(list); + List partyNameList = new ArrayList<>(); + PartyNameType e = new PartyNameType(); + e.setName(partyInfo.getPartyName()); + partyNameList.add(e); + buyerParty.setPartyName(partyNameList); + List legalEntityList = new ArrayList<>(); + PartyLegalEntityType legalEntity = new PartyLegalEntityType(); + legalEntity.setRegistrationName(partyInfo.getLegalEntityRegistrationName()); + legalEntity.setCompanyID(partyInfo.getLegalEntityCompanyID()); + legalEntity.getCompanyID().setSchemeID(partyInfo.getLegalEntityCompanyIDSchemeID()); + legalEntityList.add(legalEntity); + buyerParty.setPartyLegalEntity(legalEntityList); + return buyerParty; + } + + @Data + private static class PartyInfo { + + private String endpointID; + private String endpointIdSchemeID; + private String partyIdentificationID; + private String partyIdentificationIDSchemeID; + private String partyName; + private String legalEntityRegistrationName; + private String legalEntityCompanyID; + private String legalEntityCompanyIDSchemeID; + + public PartyInfo(String defaultId, String defaultSchemeID, String defaultName) { + this.endpointID = defaultId; + this.endpointIdSchemeID = defaultSchemeID; + this.partyIdentificationID = defaultId; + this.partyIdentificationIDSchemeID = defaultSchemeID; + this.legalEntityCompanyID = defaultId; + this.legalEntityCompanyIDSchemeID = defaultSchemeID; + this.partyName = defaultName; + this.legalEntityRegistrationName = defaultName; + } + } +} \ No newline at end of file diff --git a/cm-api/src/test/resources/logback-test.xml b/cm-api/src/test/resources/logback-test.xml new file mode 100644 index 0000000..b2d8dca --- /dev/null +++ b/cm-api/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + + %d{dd.MM.yyyy HH:mm:ss.SSS} [%thread] %-5level %logger{20} - %msg%n + + + + + + \ No newline at end of file From 831d17ff419c2f3c2044d3f82b722903e37dd693 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Mon, 6 Dec 2021 10:12:54 +0200 Subject: [PATCH 36/71] chore: idea warnings... --- .../dk/erst/cm/webapi/UploadController.java | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/UploadController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/UploadController.java index 93901c4..21a93e5 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/UploadController.java +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/UploadController.java @@ -1,9 +1,12 @@ package dk.erst.cm.webapi; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - +import dk.erst.cm.api.data.ProductCatalogUpdate; +import dk.erst.cm.api.item.LoadCatalogService; +import dk.erst.cm.api.load.PeppolLoadService; +import dk.erst.cm.webapi.FileUploadConsumer.LineAction; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -12,15 +15,10 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import dk.erst.cm.api.data.ProductCatalogUpdate; -import dk.erst.cm.api.item.LoadCatalogService; -import dk.erst.cm.api.load.PeppolLoadService; -import dk.erst.cm.webapi.FileUploadConsumer.LineAction; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; @RestController @CrossOrigin(maxAge = 3600) @@ -34,8 +32,8 @@ public class UploadController { private LoadCatalogService loadCatalogService; @PostMapping(value = "/api/upload") - public ResponseEntity> upload(@RequestParam("files") MultipartFile files[], RedirectAttributes redirectAttributes) { - List uploadResultList = new ArrayList(); + public ResponseEntity> upload(@RequestParam("files") MultipartFile[] files) { + List uploadResultList = new ArrayList<>(); for (MultipartFile file : files) { UploadResult ur = new UploadResult(); ur.setFileName(file.getOriginalFilename()); From 71ecb2531f78b4df6d1f99a5b2ce4505a09be4b3 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Mon, 6 Dec 2021 13:14:35 +0200 Subject: [PATCH 37/71] Add some not finished info about ordering to About --- cm-frontend/src/components/Banner.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cm-frontend/src/components/Banner.js b/cm-frontend/src/components/Banner.js index 62a93dd..ee35914 100644 --- a/cm-frontend/src/components/Banner.js +++ b/cm-frontend/src/components/Banner.js @@ -54,6 +54,10 @@ export default function Banner(props) {
  • related items
  • + + Products can be added to basket and sent as BIS3 Orders for demo purposes, generated orders can be downloaded as XML, delivery status and potential OrderResponse + can be checked by direct link. + + + + + + + + ))} + + + ) +} diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index c990966..3a1fafd 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -10,6 +10,7 @@ import DataService from "../services/DataService"; import OrderLineList from "../components/OrderLineList"; import OrderHeader from "../components/OrderHeader"; import delay from "../utils/delay"; +import BasketSendResult from "../components/BasketSendResult"; const useStyles = makeStyles(theme => ({ table: { @@ -30,6 +31,7 @@ export default function BasketPage(props) { const [isLoading, setLoading] = React.useState(false); const [isSending, setSending] = React.useState(false); + const [sendResult, setSendResult] = React.useState(null); const [productList, setProductList] = React.useState({}); const [reloadCount, setReloadCount] = React.useState(0); @@ -48,10 +50,12 @@ export default function BasketPage(props) { if (!basketData.isEmpty() && !orderData.isEmpty()) { setSending(true); // TODO: Remove test delay - await delay(1000); + await delay(500); await DataService.sendBasket(basketData, orderData).then(response => { let responseData = response.data; console.log(responseData); + setSendResult(responseData); + changeBasket(null, 0); } ).catch(error => { console.log('Error occurred: ' + error.message); @@ -92,24 +96,32 @@ export default function BasketPage(props) { return ( <> - - - {isSending ? ( - - ) : ( - sendAction()}/> - )} - + + {(sendResult === null) && ( + + {isSending ? ( + + ) : ( + sendAction()}/> + )} + + )} - {(isLoading || false) ? ( + {(isLoading) ? ( ) : ( <> - - + {(sendResult === null) ? ( + <> + + + + ) : ( + + )} )} diff --git a/cm-frontend/src/pages/ProductListPage.js b/cm-frontend/src/pages/ProductListPage.js index 8795b44..e6c7dfe 100644 --- a/cm-frontend/src/pages/ProductListPage.js +++ b/cm-frontend/src/pages/ProductListPage.js @@ -28,13 +28,13 @@ const useStyles = makeStyles(theme => ({ })); // noinspection JSUnusedLocalSymbols -const StyledTableCell = withStyles((theme) => ({ +export const StyledTableCell = withStyles((theme) => ({ head: { fontWeight: 'bold', }, }))(TableCell); -const StyledTableRow = withStyles((theme) => ({ +export const StyledTableRow = withStyles((theme) => ({ root: { '&:nth-of-type(odd)': { backgroundColor: theme.palette.action.hover, From e7cd99339ab5ff82c42aa9e3233ba0af091fead7 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Wed, 8 Dec 2021 16:42:17 +0200 Subject: [PATCH 41/71] feature: rename BasketPage to SendPage, as BasketPage component will show details of already sent baskets, add to basket status sent date, id, order lines count, forward after successful basket sending to basket detail page --- .../src/components/BasketSendResult.js | 135 +++++++++--------- cm-frontend/src/components/ProductDetail.js | 6 +- .../src/components/ProductListContainer.js | 8 +- cm-frontend/src/components/TopNav.js | 2 +- cm-frontend/src/pages/BasketPage.js | 95 +----------- cm-frontend/src/pages/SendPage.js | 131 +++++++++++++++++ 6 files changed, 213 insertions(+), 164 deletions(-) create mode 100644 cm-frontend/src/pages/SendPage.js diff --git a/cm-frontend/src/components/BasketSendResult.js b/cm-frontend/src/components/BasketSendResult.js index 338678c..dc48184 100644 --- a/cm-frontend/src/components/BasketSendResult.js +++ b/cm-frontend/src/components/BasketSendResult.js @@ -1,6 +1,6 @@ import {makeStyles} from "@material-ui/core/styles"; import Paper from "@material-ui/core/Paper"; -import {Button, Card, CardActions, CardContent, CardHeader, Grid} from "@material-ui/core"; +import {Button} from "@material-ui/core"; import React from "react"; import {Alert, AlertTitle} from "@material-ui/lab"; import TableContainer from "@material-ui/core/TableContainer"; @@ -10,102 +10,103 @@ import TableRow from "@material-ui/core/TableRow"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import {StyledTableCell, StyledTableRow} from "../pages/ProductListPage"; +import {DataRow, DataView} from "./ProductDetail"; -export default function BasketSendResult() { +export default function BasketSendResult(props) { + + const {showSuccess = false} = props; const useStyles = makeStyles((theme) => ({ paper: { padding: theme.spacing(2), marginBottom: theme.spacing(2), }, - order: { - padding: theme.spacing(2), - textAlign: "center", - }, - links: { - padding: theme.spacing(2), - display: 'flex', - flexFlow: 'row wrap', - placeContent: 'space-evenly', - } })); const classes = useStyles(); + const basketStatus = { + sentDate: '01.12.2021 12:54:43', + id: 'vs094fj34f309jv340', + } + const orderDataList = [ { id: "b42f4f4f3gfegsfdgadsfasdfasdf", orderNumber: "20211205-131812-01", + orderLines: 1, supplierName: "Danish Supplier A/S", status: "Generated", }, { id: "b422434vfsdfdsfaf24ff", orderNumber: "20211205-131812-02", + orderLines: 1, supplierName: "Norwegian Supplier ApS", status: "Downloaded", }, ] return ( - - - Success - {orderDataList.length} order{orderDataList.length > 1 ? 's' : ''} are successfully generated - + <> + {showSuccess && ( + + Success +
    {orderDataList.length} order{orderDataList.length > 1 ? 's' : ''} in the basket are successfully generated and scheduled for sending.
    +
    You can either: +
      +
    • copy and save link to the whole basket with all orders to track their status together;
    • +
    • download all orders in XML format;
    • +
    • copy and save links to each order separately to track their status;
    • +
    • download each order in XML format separately.
    • +
    +
    +
    + )} - - - - - Order - Status - Supplier - Order number - Actions - - - - {orderDataList?.map((row, index) => ( - - {(index + 1)} - {row.status} - {row.supplierName} - {row.orderNumber} - - - - - - ))} - -
    -
    + - - {orderDataList.map((o, index) => ( - + + + prev + cur.orderLines, 0)}/> + + + + + + - - - -
    {o.supplierName}
    -
    - Number: - {o.orderNumber} -
    -
    Status: {o.status}
    -
    - - - - - -
    + + + + + Order + Status + Supplier + Order number + Lines + Actions + + + + {orderDataList?.map((row, index) => ( + + {(index + 1)} + {row.status} + {row.supplierName} + {row.orderNumber} + {row.orderLines} + + + + + + ))} + +
    +
    +
    + - - ))} - -
    ) } diff --git a/cm-frontend/src/components/ProductDetail.js b/cm-frontend/src/components/ProductDetail.js index 66edf9f..d570958 100644 --- a/cm-frontend/src/components/ProductDetail.js +++ b/cm-frontend/src/components/ProductDetail.js @@ -26,7 +26,7 @@ const useStyles = makeStyles(theme => ({ }, })); -function DataRow(props) { +export function DataRow(props) { const { name, children } = props; const classes = useStyles(); @@ -39,7 +39,7 @@ function DataRow(props) { ) } -function DataView(props) { +export function DataView(props) { const _isValueDefined = (value) => value ? true : false; // noinspection JSUnusedLocalSymbols @@ -55,7 +55,7 @@ function DataView(props) { ) } -const isListFilled = (list) => list && list.length > 0 ? true : false; +const isListFilled = (list) => list && list.length > 0; function DataListView(props) { const _isValueDefined = isListFilled; diff --git a/cm-frontend/src/components/ProductListContainer.js b/cm-frontend/src/components/ProductListContainer.js index 7986d11..891ae30 100644 --- a/cm-frontend/src/components/ProductListContainer.js +++ b/cm-frontend/src/components/ProductListContainer.js @@ -9,6 +9,7 @@ import DataService from "../services/DataService"; import useStickyState from '../utils/useStickyState'; import {createBasketData} from "./BasketData"; import {getOrderData} from "./OrderData"; +import SendPage from "../pages/SendPage"; import BasketPage from "../pages/BasketPage"; const currentPosition = (list, id) => { @@ -102,13 +103,16 @@ export function ProductListContainer() { - - + + + + + ); } diff --git a/cm-frontend/src/components/TopNav.js b/cm-frontend/src/components/TopNav.js index d11e57f..02cd118 100644 --- a/cm-frontend/src/components/TopNav.js +++ b/cm-frontend/src/components/TopNav.js @@ -66,7 +66,7 @@ export default function TopNav(props) { { showBasketBar && ( - + )} diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index 3a1fafd..593453f 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -2,14 +2,7 @@ import React from "react"; import {makeStyles} from "@material-ui/core/styles"; import Paper from "@material-ui/core/Paper"; import CircularProgress from "@material-ui/core/CircularProgress"; -import {useHistory} from "react-router"; import PageHeader from '../components/PageHeader'; -import {Fab} from "@material-ui/core"; -import SendIcon from "@material-ui/icons/Send"; -import DataService from "../services/DataService"; -import OrderLineList from "../components/OrderLineList"; -import OrderHeader from "../components/OrderHeader"; -import delay from "../utils/delay"; import BasketSendResult from "../components/BasketSendResult"; const useStyles = makeStyles(theme => ({ @@ -27,86 +20,12 @@ const useStyles = makeStyles(theme => ({ export default function BasketPage(props) { - const {basketData, changeBasket, orderData} = props; - const [isLoading, setLoading] = React.useState(false); - const [isSending, setSending] = React.useState(false); - const [sendResult, setSendResult] = React.useState(null); - const [productList, setProductList] = React.useState({}); - const [reloadCount, setReloadCount] = React.useState(0); - const classes = useStyles(); - const {push} = useHistory(); - - const refreshAction = () => { - setReloadCount(reloadCount + 1); - } - const sendAction = () => { - sendBasket().finally(() => setSending(false)); - } - - async function sendBasket() { - if (!basketData.isEmpty() && !orderData.isEmpty()) { - setSending(true); - // TODO: Remove test delay - await delay(500); - await DataService.sendBasket(basketData, orderData).then(response => { - let responseData = response.data; - console.log(responseData); - setSendResult(responseData); - changeBasket(null, 0); - } - ).catch(error => { - console.log('Error occurred: ' + error.message); - }); - } - } - - const callLoadProducts = () => { - loadProducts().finally(() => setLoading(false)) - }; - - async function loadProducts() { - if (!basketData.isEmpty()) { - setLoading(true); - const productIdList = basketData.getOrderLineList().map((orderLine) => orderLine.productId); - await DataService.fetchProductsByIds(productIdList).then(response => { - let responseData = response.data; - console.log(responseData); - const productMapById = {} - for (const index in responseData) { - const p = responseData[index]; - productMapById[p.id] = p; - } - console.log(productMapById); - setProductList(productMapById); - } - ).catch(error => { - console.log('Error occurred: ' + error.message); - }); - } - } - - React.useEffect(callLoadProducts, [reloadCount]); - - const showRowDetails = (productId) => { - push('/product/view/' + productId); - } - return ( <> - - {(sendResult === null) && ( - - {isSending ? ( - - ) : ( - sendAction()}/> - )} - - )} - + {(isLoading) ? ( @@ -114,16 +33,10 @@ export default function BasketPage(props) { ) : ( <> - {(sendResult === null) ? ( - <> - - - - ) : ( - - )} + )} ); -} + +} \ No newline at end of file diff --git a/cm-frontend/src/pages/SendPage.js b/cm-frontend/src/pages/SendPage.js new file mode 100644 index 0000000..7547326 --- /dev/null +++ b/cm-frontend/src/pages/SendPage.js @@ -0,0 +1,131 @@ +import React from "react"; +import {makeStyles} from "@material-ui/core/styles"; +import Paper from "@material-ui/core/Paper"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import {useHistory} from "react-router"; +import PageHeader from '../components/PageHeader'; +import {Fab} from "@material-ui/core"; +import SendIcon from "@material-ui/icons/Send"; +import DataService from "../services/DataService"; +import OrderLineList from "../components/OrderLineList"; +import OrderHeader from "../components/OrderHeader"; +import delay from "../utils/delay"; +import BasketSendResult from "../components/BasketSendResult"; + +const useStyles = makeStyles(theme => ({ + table: { + minWidth: 600, + }, + header: { + marginBottom: '1em' + }, + paper: { + padding: theme.spacing(2), + } +})); + + +export default function SendPage(props) { + + const {basketData, changeBasket, orderData} = props; + + const [isLoading, setLoading] = React.useState(false); + const [isSending, setSending] = React.useState(false); + const [sendResult, setSendResult] = React.useState(null); + const [productList, setProductList] = React.useState({}); + const [reloadCount, setReloadCount] = React.useState(0); + + const classes = useStyles(); + + const {push} = useHistory(); + + const refreshAction = () => { + setReloadCount(reloadCount + 1); + } + const sendAction = () => { + sendBasket().finally(() => setSending(false)); + } + + async function sendBasket() { + if (!basketData.isEmpty() && !orderData.isEmpty()) { + setSending(true); + // TODO: Remove test delay + await delay(500); + await DataService.sendBasket(basketData, orderData).then(response => { + let responseData = response.data; + console.log(responseData); + // setSendResult(responseData); + changeBasket(null, 0); + const basketId = "234f029dfj23d2"; + push("/basket/"+basketId); + } + ).catch(error => { + console.log('Error occurred: ' + error.message); + }); + } + } + + const callLoadProducts = () => { + loadProducts().finally(() => setLoading(false)) + }; + + async function loadProducts() { + if (!basketData.isEmpty()) { + setLoading(true); + const productIdList = basketData.getOrderLineList().map((orderLine) => orderLine.productId); + await DataService.fetchProductsByIds(productIdList).then(response => { + let responseData = response.data; + console.log(responseData); + const productMapById = {} + for (const index in responseData) { + const p = responseData[index]; + productMapById[p.id] = p; + } + console.log(productMapById); + setProductList(productMapById); + } + ).catch(error => { + console.log('Error occurred: ' + error.message); + }); + } + } + + React.useEffect(callLoadProducts, [reloadCount]); + + const showRowDetails = (productId) => { + push('/product/view/' + productId); + } + + return ( + <> + + {(sendResult === null) && ( + + {isSending ? ( + + ) : ( + sendAction()}/> + )} + + )} + + + {(isLoading) ? ( + + + + ) : ( + <> + {(sendResult === null) ? ( + <> + + + + ) : ( + + )} + + )} + + ); +} From 29a865938711ed691b5a44e2ea921923f7b51ff5 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 11 Dec 2021 15:11:34 +0200 Subject: [PATCH 42/71] feature: implement persistence of basket and orders, initial order generation and integrations configuration --- .../cm/api/dao/mongo/BasketRepository.java | 8 + .../cm/api/dao/mongo/OrderRepository.java | 8 + .../mongo/ProductCatalogUpdateRepository.java | 2 + .../main/java/dk/erst/cm/api/data/Basket.java | 23 ++ .../main/java/dk/erst/cm/api/data/Order.java | 37 ++ .../java/dk/erst/cm/api/data/OrderStatus.java | 7 + .../dk/erst/cm/api/item/CatalogService.java | 38 +++ .../cm/api/order/OrderProducerService.java} | 162 ++++----- .../dk/erst/cm/api/order/OrderService.java | 63 ++++ .../cm/api/order/data/CustomerOrderData.java | 25 ++ .../api/order/OrderProducerServiceTest.java | 89 +++++ cm-webapi/.gitignore | 1 + .../main/java/dk/erst/cm/AppProperties.java | 25 ++ .../dk/erst/cm/webapi/BasketController.java | 70 ---- .../dk/erst/cm/webapi/IndexController.java | 3 + .../dk/erst/cm/webapi/OrderController.java | 320 ++++++++++++++++++ .../src/main/resources/application.properties | 7 + 17 files changed, 723 insertions(+), 165 deletions(-) create mode 100644 cm-api/src/main/java/dk/erst/cm/api/dao/mongo/BasketRepository.java create mode 100644 cm-api/src/main/java/dk/erst/cm/api/dao/mongo/OrderRepository.java create mode 100644 cm-api/src/main/java/dk/erst/cm/api/data/Basket.java create mode 100644 cm-api/src/main/java/dk/erst/cm/api/data/Order.java create mode 100644 cm-api/src/main/java/dk/erst/cm/api/data/OrderStatus.java rename cm-api/src/{test/java/dk/erst/cm/api/order/OrderProducerTest.java => main/java/dk/erst/cm/api/order/OrderProducerService.java} (61%) create mode 100644 cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java create mode 100644 cm-api/src/main/java/dk/erst/cm/api/order/data/CustomerOrderData.java create mode 100644 cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java create mode 100644 cm-webapi/.gitignore create mode 100644 cm-webapi/src/main/java/dk/erst/cm/AppProperties.java delete mode 100644 cm-webapi/src/main/java/dk/erst/cm/webapi/BasketController.java create mode 100644 cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java diff --git a/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/BasketRepository.java b/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/BasketRepository.java new file mode 100644 index 0000000..4822521 --- /dev/null +++ b/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/BasketRepository.java @@ -0,0 +1,8 @@ +package dk.erst.cm.api.dao.mongo; + +import dk.erst.cm.api.data.Basket; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface BasketRepository extends MongoRepository { + +} diff --git a/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/OrderRepository.java b/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/OrderRepository.java new file mode 100644 index 0000000..d2c5ccb --- /dev/null +++ b/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/OrderRepository.java @@ -0,0 +1,8 @@ +package dk.erst.cm.api.dao.mongo; + +import dk.erst.cm.api.data.Order; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface OrderRepository extends MongoRepository { + +} diff --git a/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/ProductCatalogUpdateRepository.java b/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/ProductCatalogUpdateRepository.java index 9d836f5..b1d9ba0 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/ProductCatalogUpdateRepository.java +++ b/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/ProductCatalogUpdateRepository.java @@ -6,4 +6,6 @@ public interface ProductCatalogUpdateRepository extends MongoRepository { + ProductCatalogUpdate findTop1ByProductCatalogIdOrderByCreateTimeDesc(String productCatalogId); + } diff --git a/cm-api/src/main/java/dk/erst/cm/api/data/Basket.java b/cm-api/src/main/java/dk/erst/cm/api/data/Basket.java new file mode 100644 index 0000000..a712a58 --- /dev/null +++ b/cm-api/src/main/java/dk/erst/cm/api/data/Basket.java @@ -0,0 +1,23 @@ +package dk.erst.cm.api.data; + +import java.time.Instant; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Document +@Data +@NoArgsConstructor +public class Basket { + + @Id + private String id; + private Instant createTime; + private int version; + private int orderCount; + private int lineCount; + +} diff --git a/cm-api/src/main/java/dk/erst/cm/api/data/Order.java b/cm-api/src/main/java/dk/erst/cm/api/data/Order.java new file mode 100644 index 0000000..9dd6b6f --- /dev/null +++ b/cm-api/src/main/java/dk/erst/cm/api/data/Order.java @@ -0,0 +1,37 @@ +package dk.erst.cm.api.data; + +import java.time.Instant; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Document +@Data +@NoArgsConstructor +public class Order { + + @Id + private String id; + private Instant createTime; + private int version; + + private OrderStatus status; + private int orderIndex; + private String supplierName; + + @Indexed + private String orderNumber; + private int lineCount; + + private Object document; + + private String resultFileName; + + private Instant downloadDate; + private Instant deliveredDate; + +} diff --git a/cm-api/src/main/java/dk/erst/cm/api/data/OrderStatus.java b/cm-api/src/main/java/dk/erst/cm/api/data/OrderStatus.java new file mode 100644 index 0000000..3efe19b --- /dev/null +++ b/cm-api/src/main/java/dk/erst/cm/api/data/OrderStatus.java @@ -0,0 +1,7 @@ +package dk.erst.cm.api.data; + +public enum OrderStatus { + + GENERATED, DELIVERED, DELIVERY_FAILED, ORDER_CONFIRMED, ORDER_REJECTED + +} diff --git a/cm-api/src/main/java/dk/erst/cm/api/item/CatalogService.java b/cm-api/src/main/java/dk/erst/cm/api/item/CatalogService.java index 74c2bdc..861a73c 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/item/CatalogService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/item/CatalogService.java @@ -12,6 +12,7 @@ import dk.erst.cm.api.data.ProductCatalog; import dk.erst.cm.api.data.ProductCatalogUpdate; import dk.erst.cm.xml.ubl21.model.Catalogue; +import dk.erst.cm.xml.ubl21.model.NestedParty; import dk.erst.cm.xml.ubl21.model.Party; import dk.erst.cm.xml.ubl21.model.SchemeID; import lombok.extern.slf4j.Slf4j; @@ -72,6 +73,43 @@ public ProductCatalogUpdate saveCatalogue(Catalogue catalogue) { return c; } + public Party loadLastSellerParty(String productCatalogId) { + + log.debug("Requested to load last seller party for productCatalog " + productCatalogId); + + long start = System.currentTimeMillis(); + ProductCatalogUpdate catalogUpdate = productCatalogUpdateRepository.findTop1ByProductCatalogIdOrderByCreateTimeDesc(productCatalogId); + long duration = System.currentTimeMillis() - start; + if (duration > 50) { + log.warn("LastSellerParty Mongo lookup by " + productCatalogId + " took more than 50ms: " + 50); + } + + if (catalogUpdate != null && catalogUpdate.getDocument() != null) { + if (catalogUpdate.getDocument() instanceof Catalogue) { + Catalogue document = (Catalogue) catalogUpdate.getDocument(); + + if (document.getSellerSupplierParty() != null) { + NestedParty sellerSupplierParty = document.getSellerSupplierParty(); + if (sellerSupplierParty != null && sellerSupplierParty.getParty() != null) { + log.debug("LastSellerParty found by document.sellerSupplierParty.party for productCatalog " + productCatalogId); + return sellerSupplierParty.getParty(); + } + } + if (document.getProviderParty() != null) { + log.debug("LastSellerParty found by document.providerParty for productCatalog " + productCatalogId); + return document.getProviderParty(); + } + log.warn("Neither sellerSupplierParty, nor providerParty are defined by last catalogUpdate by id " + productCatalogId + ": " + document); + } else { + log.warn("Found catalogUpdate by lastCatalog with id" + productCatalogId + " has unexpected type: " + catalogUpdate.getDocument().getClass()); + } + } else { + log.warn("No catalogUpdate found by lastCatalog with id" + productCatalogId); + } + + return null; + } + private String buildSellerLocalId(Catalogue catalogue) { String sellerLogicalId = null; if (catalogue.getSellerSupplierParty() != null) { diff --git a/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerTest.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java similarity index 61% rename from cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerTest.java rename to cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java index ae55e77..945f579 100644 --- a/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerTest.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java @@ -1,28 +1,17 @@ package dk.erst.cm.api.order; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; import java.time.LocalDate; -import java.time.LocalTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; import java.util.UUID; -import org.junit.jupiter.api.Test; - -import com.helger.commons.error.IError; -import com.helger.commons.error.list.IErrorList; -import com.helger.ubl21.UBL21Reader; -import com.helger.ubl21.UBL21Validator; -import com.helger.ubl21.UBL21Writer; +import org.springframework.stereotype.Service; +import dk.erst.cm.api.data.Product; +import dk.erst.cm.api.order.data.CustomerOrderData; +import dk.erst.cm.xml.ubl21.model.CatalogueLine; import lombok.Data; import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.AddressType; import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.CountryType; @@ -39,84 +28,90 @@ import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.SupplierPartyType; import oasis.names.specification.ubl.schema.xsd.order_21.OrderType; -@SuppressWarnings("ConstantConditions") -class OrderProducerTest { - - @Test - void read() throws IOException { - try (InputStream is = new FileInputStream("../cm-resources/examples/order/OrderOnly.xml")) { - OrderType res = UBL21Reader.order().read(is); - assertEquals("1005", res.getIDValue()); - assertEquals("Contract0101", res.getContract().get(0).getIDValue()); - List orderLine = res.getOrderLine(); - for (int i = 0; i < orderLine.size(); i++) { - OrderLineType orderLineType = orderLine.get(i); - assertEquals(String.valueOf(i + 1), orderLineType.getLineItem().getIDValue()); - } - } - } +@Service +public class OrderProducerService { + + @Data + public static class PartyInfo { + + private String endpointID; + private String endpointIdSchemeID; + private String partyIdentificationID; + private String partyIdentificationIDSchemeID; + private String partyName; + private String legalEntityRegistrationName; + private String legalEntityCompanyID; + private String legalEntityCompanyIDSchemeID; - @Test - void produce() { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - OrderType order = buildOrder(); - IErrorList errorList = UBL21Validator.order().validate(order); - if (errorList.isNotEmpty()) { - System.out.println("Found " + errorList.size() + " errors:"); - for (int i = 0; i < errorList.size(); i++) { - IError error = errorList.get(i); - System.out.println((i + 1) + "\t" + error.toString()); - } + public PartyInfo(String defaultId, String defaultSchemeID, String defaultName) { + this.endpointID = defaultId; + this.endpointIdSchemeID = defaultSchemeID; + this.partyIdentificationID = defaultId; + this.partyIdentificationIDSchemeID = defaultSchemeID; + this.legalEntityCompanyID = defaultId; + this.legalEntityCompanyIDSchemeID = defaultSchemeID; + this.partyName = defaultName; + this.legalEntityRegistrationName = defaultName; } - assertTrue(errorList.isEmpty()); - UBL21Writer.order().write(order, out); - String xml = new String(out.toByteArray(), StandardCharsets.UTF_8); - System.out.println(xml); - assertTrue(xml.indexOf(order.getSellerSupplierParty().getParty().getPostalAddress().getCountry().getIdentificationCodeValue()) > 0); } - @SuppressWarnings("SpellCheckingInspection") - private OrderType buildOrder() { + public OrderType generateOrder(dk.erst.cm.api.data.Order dataOrder, CustomerOrderData customerOrderData, List productList) { OrderType order = new OrderType(); + order.setCustomizationID("urn:fdc:peppol.eu:poacc:trns:order:3"); order.setProfileID("urn:fdc:peppol.eu:poacc:bis:order_only:3"); order.setID(UUID.randomUUID().toString()); - order.setIssueDate(LocalDate.now()); - order.setIssueTime(LocalTime.now()); + order.setIssueDate(dataOrder.getCreateTime().atZone(ZoneOffset.UTC).toLocalDate()); + order.setIssueTime(dataOrder.getCreateTime().atZone(ZoneOffset.UTC).toLocalTime()); order.setDocumentCurrencyCode("DKK"); + CustomerPartyType buyerCustomerParty = new CustomerPartyType(); - buyerCustomerParty.setParty(buildParty(new PartyInfo("5798009882806", "0088", "Swedish Company"))); + buyerCustomerParty.setParty(buildParty(new PartyInfo("5798009882806", "0088", customerOrderData.getBuyerCompany().getRegistrationName()))); order.setBuyerCustomerParty(buyerCustomerParty); + SupplierPartyType supplierPartyType = new SupplierPartyType(); supplierPartyType.setParty(buildParty(new PartyInfo("5798009882783", "0088", "Danish Company"))); order.setSellerSupplierParty(supplierPartyType); - AddressType addressType = new AddressType(); - addressType.setCityName("Stockholm"); - addressType.setPostalZone("2100"); + + AddressType supplierAddress = new AddressType(); + supplierAddress.setCityName("Stockholm"); + supplierAddress.setPostalZone("2100"); CountryType countryType = new CountryType(); countryType.setIdentificationCode("SE"); - addressType.setCountry(countryType); - supplierPartyType.getParty().setPostalAddress(addressType); + supplierAddress.setCountry(countryType); + supplierPartyType.getParty().setPostalAddress(supplierAddress); + ArrayList validityPeriodList = new ArrayList<>(); PeriodType periodType = new PeriodType(); periodType.setEndDate(LocalDate.now().plusDays(1)); validityPeriodList.add(periodType); order.setValidityPeriod(validityPeriodList); + ArrayList orderLineList = new ArrayList<>(); order.setOrderLine(orderLineList); - OrderLineType line = new OrderLineType(); - LineItemType lineItem = new LineItemType(); - lineItem.setID("1"); - lineItem.setQuantity(BigDecimal.valueOf(1)); - lineItem.getQuantity().setUnitCode("EA"); - ItemType item = new ItemType(); - item.setName("Test"); - ItemIdentificationType itemIdentificationType = new ItemIdentificationType(); - itemIdentificationType.setID("1234"); - item.setSellersItemIdentification(itemIdentificationType); - lineItem.setItem(item); - line.setLineItem(lineItem); - orderLineList.add(line); + + for (int i = 0; i < productList.size(); i++) { + Product product = productList.get(i); + + CatalogueLine catalogueLine = (CatalogueLine) product.getDocument(); + + OrderLineType line = new OrderLineType(); + LineItemType lineItem = new LineItemType(); + lineItem.setID(String.valueOf(i + 1)); + lineItem.setQuantity(BigDecimal.valueOf(1)); + lineItem.getQuantity().setUnitCode("EA"); + ItemType item = new ItemType(); + item.setName(catalogueLine.getItem().getName()); + + ItemIdentificationType itemIdentificationType = new ItemIdentificationType(); + itemIdentificationType.setID(catalogueLine.getItem().getSellersItemIdentification().getId()); + item.setSellersItemIdentification(itemIdentificationType); + + lineItem.setItem(item); + line.setLineItem(lineItem); + orderLineList.add(line); + } + return order; } @@ -145,27 +140,4 @@ public PartyType buildParty(PartyInfo partyInfo) { return buyerParty; } - @Data - private static class PartyInfo { - - private String endpointID; - private String endpointIdSchemeID; - private String partyIdentificationID; - private String partyIdentificationIDSchemeID; - private String partyName; - private String legalEntityRegistrationName; - private String legalEntityCompanyID; - private String legalEntityCompanyIDSchemeID; - - public PartyInfo(String defaultId, String defaultSchemeID, String defaultName) { - this.endpointID = defaultId; - this.endpointIdSchemeID = defaultSchemeID; - this.partyIdentificationID = defaultId; - this.partyIdentificationIDSchemeID = defaultSchemeID; - this.legalEntityCompanyID = defaultId; - this.legalEntityCompanyIDSchemeID = defaultSchemeID; - this.partyName = defaultName; - this.legalEntityRegistrationName = defaultName; - } - } -} \ No newline at end of file +} diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java new file mode 100644 index 0000000..bc22adc --- /dev/null +++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java @@ -0,0 +1,63 @@ +package dk.erst.cm.api.order; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.time.Instant; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.helger.ubl21.UBL21Writer; + +import dk.erst.cm.api.dao.mongo.BasketRepository; +import dk.erst.cm.api.dao.mongo.OrderRepository; +import dk.erst.cm.api.data.Basket; +import dk.erst.cm.api.data.Order; +import dk.erst.cm.api.data.OrderStatus; +import oasis.names.specification.ubl.schema.xsd.order_21.OrderType; + +@Service +public class OrderService { + + private final BasketRepository basketRepository; + private final OrderRepository orderRepository; + + @Autowired + public OrderService(BasketRepository basketRepository, OrderRepository orderRepository) { + this.basketRepository = basketRepository; + this.orderRepository = orderRepository; + } + + public void saveBasket(Basket basket) { + this.basketRepository.save(basket); + } + + public void saveOrder(Order order) { + this.orderRepository.save(order); + } + + public void updateOrderStatus(String orderId, OrderStatus status) { + Optional optionalOrder = this.orderRepository.findById(orderId); + if (optionalOrder.isPresent()) { + Order order = optionalOrder.get(); + if (status == OrderStatus.DELIVERED) { + order.setDeliveredDate(Instant.now()); + } + order.setStatus(status); + this.orderRepository.save(order); + } + } + + public File saveOrderXML(File directory, OrderType sendOrder) throws IOException { + File tempFile = new File(directory, "delis-cm-" + sendOrder.getIDValue() + ".xml"); + try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile))) { + UBL21Writer.order().write(sendOrder, out); + } + return tempFile; + } + +} diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/data/CustomerOrderData.java b/cm-api/src/main/java/dk/erst/cm/api/order/data/CustomerOrderData.java new file mode 100644 index 0000000..4494545 --- /dev/null +++ b/cm-api/src/main/java/dk/erst/cm/api/order/data/CustomerOrderData.java @@ -0,0 +1,25 @@ +package dk.erst.cm.api.order.data; + +import lombok.Data; + +@Data +public class CustomerOrderData { + + private Company buyerCompany; + private Contact buyerContact; + + @Data + public static class Contact { + private String personName; + private String email; + private String telephone; + } + + @Data + public static class Company { + private String registrationName; + private String legalIdentifier; + private String partyIdentifier; + } + +} diff --git a/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java new file mode 100644 index 0000000..db67c92 --- /dev/null +++ b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java @@ -0,0 +1,89 @@ +package dk.erst.cm.api.order; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.helger.commons.error.IError; +import com.helger.commons.error.list.IErrorList; +import com.helger.ubl21.UBL21Reader; +import com.helger.ubl21.UBL21Validator; +import com.helger.ubl21.UBL21Writer; + +import dk.erst.cm.api.data.Order; +import dk.erst.cm.api.data.Product; +import dk.erst.cm.api.order.data.CustomerOrderData; +import dk.erst.cm.api.order.data.CustomerOrderData.Company; +import dk.erst.cm.xml.ubl21.model.CatalogueLine; +import dk.erst.cm.xml.ubl21.model.Item; +import dk.erst.cm.xml.ubl21.model.NestedID; +import lombok.extern.slf4j.Slf4j; +import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.OrderLineType; +import oasis.names.specification.ubl.schema.xsd.order_21.OrderType; + +@Slf4j +class OrderProducerServiceTest { + + @Test + void read() throws IOException { + try (InputStream is = new FileInputStream("../cm-resources/examples/order/OrderOnly.xml")) { + OrderType res = UBL21Reader.order().read(is); + assertEquals("1005", res.getIDValue()); + assertEquals("Contract0101", res.getContract().get(0).getIDValue()); + List orderLine = res.getOrderLine(); + for (int i = 0; i < orderLine.size(); i++) { + OrderLineType orderLineType = orderLine.get(i); + assertEquals(String.valueOf(i + 1), orderLineType.getLineItem().getIDValue()); + } + } + } + + @Test + void produce() { + OrderProducerService service = new OrderProducerService(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + List productList = new ArrayList(); + Product product = new Product(); + CatalogueLine catalogueLine = new CatalogueLine(); + Item item = new Item(); + item.setName("Test line"); + item.setSellersItemIdentification(new NestedID()); + item.getSellersItemIdentification().setId("TESTID"); + catalogueLine.setItem(item); + product.setDocument(catalogueLine); + + productList.add(product); + CustomerOrderData customerOrderData = new CustomerOrderData(); + Company buyerCompany = new Company(); + buyerCompany.setRegistrationName("Danish Customer Company"); + customerOrderData.setBuyerCompany(buyerCompany); + Order dataOrder = new Order(); + dataOrder.setCreateTime(Instant.now()); + OrderType order = service.generateOrder(dataOrder, customerOrderData, productList); + IErrorList errorList = UBL21Validator.order().validate(order); + if (errorList.isNotEmpty()) { + System.out.println("Found " + errorList.size() + " errors:"); + for (int i = 0; i < errorList.size(); i++) { + IError error = errorList.get(i); + System.out.println((i + 1) + "\t" + error.toString()); + } + } + assertTrue(errorList.isEmpty()); + UBL21Writer.order().write(order, out); + String xml = new String(out.toByteArray(), StandardCharsets.UTF_8); + System.out.println(xml); + assertTrue(xml.indexOf(order.getSellerSupplierParty().getParty().getPostalAddress().getCountry().getIdentificationCodeValue()) > 0); + } + +} \ No newline at end of file diff --git a/cm-webapi/.gitignore b/cm-webapi/.gitignore new file mode 100644 index 0000000..9814b0c --- /dev/null +++ b/cm-webapi/.gitignore @@ -0,0 +1 @@ +.integration/ \ No newline at end of file diff --git a/cm-webapi/src/main/java/dk/erst/cm/AppProperties.java b/cm-webapi/src/main/java/dk/erst/cm/AppProperties.java new file mode 100644 index 0000000..0c33b9c --- /dev/null +++ b/cm-webapi/src/main/java/dk/erst/cm/AppProperties.java @@ -0,0 +1,25 @@ +package dk.erst.cm; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "app") +@Data +public class AppProperties { + + private IntegrationProperties integration; + + @Getter + @Setter + @ToString + public static class IntegrationProperties { + private String inboxCatalogue; + private String inboxOrderResponse; + private String outboxOrder; + } +} diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/BasketController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/BasketController.java deleted file mode 100644 index e871b34..0000000 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/BasketController.java +++ /dev/null @@ -1,70 +0,0 @@ -package dk.erst.cm.webapi; - -import dk.erst.cm.api.item.ProductService; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -@CrossOrigin(maxAge = 3600) -@RestController -@Slf4j -public class BasketController { - - private final ProductService productService; - - @Autowired - public BasketController(ProductService productService) { - this.productService = productService; - } - - @Data - public static class Contact { - private String personName; - private String email; - private String telephone; - } - - @Data - public static class Company { - private String registrationName; - private String legalIdentifier; - private String partyIdentifier; - } - - @Data - public static class OrderData { - private Company buyerCompany; - private Contact buyerContact; - } - - @Data - public static class BasketData { - private Map orderLines; - } - - @Data - public static class SendBasketData { - private BasketData basketData; - private OrderData orderData; - } - - @Data - public static class SendBasketResponse { - private boolean success; - } - - @RequestMapping(value = "/api/basket/send") - public SendBasketResponse basketSend(@RequestBody SendBasketData query) { - log.info("Send basket with data " + query); - SendBasketResponse r = new SendBasketResponse(); - r.success = true; - return r; - } - -} diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/IndexController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/IndexController.java index e6de856..8785e21 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/IndexController.java +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/IndexController.java @@ -5,8 +5,10 @@ import org.springframework.web.bind.annotation.RestController; import dk.erst.cm.api.item.ProductService; +import lombok.extern.slf4j.Slf4j; @RestController +@Slf4j public class IndexController { @Autowired @@ -14,6 +16,7 @@ public class IndexController { @GetMapping("/api/status") public String index() { + log.info(null); return "OK: " + productService.countItems() + " items"; } diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java new file mode 100644 index 0000000..c122714 --- /dev/null +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java @@ -0,0 +1,320 @@ +package dk.erst.cm.webapi; + +import java.io.File; +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.apache.commons.io.FileUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import dk.erst.cm.AppProperties; +import dk.erst.cm.api.data.Basket; +import dk.erst.cm.api.data.Order; +import dk.erst.cm.api.data.OrderStatus; +import dk.erst.cm.api.data.Product; +import dk.erst.cm.api.item.CatalogService; +import dk.erst.cm.api.item.ProductService; +import dk.erst.cm.api.order.OrderProducerService; +import dk.erst.cm.api.order.OrderService; +import dk.erst.cm.api.order.data.CustomerOrderData; +import dk.erst.cm.xml.ubl21.model.Party; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import oasis.names.specification.ubl.schema.xsd.order_21.OrderType; + +@CrossOrigin(maxAge = 3600) +@RestController +@Slf4j +public class OrderController { + + private final ProductService productService; + private final OrderService orderService; + private final OrderProducerService orderProducerService; + private final CatalogService catalogService; + private AppProperties appProperties; + + @Autowired + public OrderController(ProductService productService, OrderService orderService, CatalogService catalogService, OrderProducerService orderProducerService, AppProperties appProperties) { + this.productService = productService; + this.orderService = orderService; + this.catalogService = catalogService; + this.orderProducerService = orderProducerService; + this.appProperties = appProperties; + } + + @Data + public static class BasketData { + private Map orderLines; + } + + @Data + public static class SendBasketData { + private BasketData basketData; + private CustomerOrderData orderData; + } + + @Data + public static class SendBasketResponse { + private boolean success; + private String basketId; + private String errorMessage; + private List errorProductIdList; + + public static SendBasketResponse error(String message) { + SendBasketResponse r = new SendBasketResponse(); + r.setSuccess(false); + r.setErrorMessage(message); + return r; + } + + public SendBasketResponse withErrorProductIdList(List errorProductIdList) { + this.setErrorProductIdList(errorProductIdList); + return this; + } + + public static SendBasketResponse success(String basketId) { + SendBasketResponse r = new SendBasketResponse(); + r.setSuccess(true); + r.setBasketId(basketId); + return r; + } + } + + @RequestMapping(value = "/api/basket/send") + public SendBasketResponse basketSend(@RequestBody SendBasketData query) { + log.info("basketSend: query=" + query); + SendBasketResponse res = basketSendInternal(query); + if (res.isSuccess()) { + log.info("basketSend OK: " + res); + } else { + log.info("basketSend Error: " + res); + } + return res; + } + + private SendBasketResponse basketSendInternal(SendBasketData query) { + Set queryProductIdSet = query.basketData.orderLines.keySet(); + Iterable products = productService.findAllByIds(queryProductIdSet); + + Set resolvedProductIdSet = new HashSet(); + + log.debug("1. Load necessary data for XML generation"); + + Map> byCatalogMap = new HashMap<>(); + for (Product p : products) { + String productCatalogId = p.getProductCatalogId(); + List productList = byCatalogMap.get(productCatalogId); + if (productList == null) { + productList = new ArrayList<>(); + byCatalogMap.put(productCatalogId, productList); + } + productList.add(p); + resolvedProductIdSet.add(p.getId()); + } + + if (resolvedProductIdSet.size() < queryProductIdSet.size()) { + int countNotFoundProducts = queryProductIdSet.size() - resolvedProductIdSet.size(); + String errorMessage = countNotFoundProducts + " product" + (countNotFoundProducts > 1 ? "s are" : "is") + " not found, please delete highlighted products to send the basket."; + Set notResolvedProductIdSet = new HashSet(queryProductIdSet); + notResolvedProductIdSet.removeAll(resolvedProductIdSet); + return SendBasketResponse.error(errorMessage).withErrorProductIdList(new ArrayList(notResolvedProductIdSet)); + } + + Set noSellerCatalogSet = new HashSet(); + Map sellerPartyByCatalog = new HashMap(); + for (String catalogId : byCatalogMap.keySet()) { + Party sellerParty = catalogService.loadLastSellerParty(catalogId); + if (sellerParty != null) { + sellerPartyByCatalog.put(catalogId, sellerParty); + } else { + noSellerCatalogSet.add(catalogId); + } + } + + if (!noSellerCatalogSet.isEmpty()) { + int countProductIncompleteSeller = 0; + + List errorProductIdList = new ArrayList(); + for (String catalogId : noSellerCatalogSet) { + List list = byCatalogMap.get(catalogId); + errorProductIdList.addAll(errorProductIdList); + countProductIncompleteSeller += list.size(); + } + String errorMessage = buildErrorMessageNoSellerInfo(byCatalogMap, noSellerCatalogSet, countProductIncompleteSeller); + return SendBasketResponse.error(errorMessage).withErrorProductIdList(errorProductIdList); + } + + log.debug("2. Generate internal models for XML"); + + List orderList = new ArrayList(); + int orderIndex = 0; + for (String catalogId : byCatalogMap.keySet()) { + List productList = byCatalogMap.get(catalogId); + + Order order = new Order(); + order.setOrderIndex(orderIndex); + order.setCreateTime(Instant.now()); + order.setId(UUID.randomUUID().toString()); + order.setLineCount(productList.size()); + order.setStatus(OrderStatus.GENERATED); + order.setOrderNumber(generateOrderNumber(order)); + order.setSupplierName(extractSupplierName(sellerPartyByCatalog.get(catalogId))); + order.setVersion(1); + OrderType sendOrder = orderProducerService.generateOrder(order, query.getOrderData(), productList); + order.setDocument(sendOrder); + + orderList.add(order); + + orderIndex++; + } + + Basket basket = new Basket(); + basket.setCreateTime(Instant.now()); + basket.setId(UUID.randomUUID().toString()); + basket.setLineCount(resolvedProductIdSet.size()); + basket.setOrderCount(orderList.size()); + basket.setVersion(1); + + File tempDirectory = createTempDirectory("delis-cm-" + basket.getId()); + + log.debug("3. Save XML files into temporary folder " + tempDirectory); + + Map fileNameToOrderMap = new HashMap(); + for (Order order : orderList) { + try { + File orderFile = orderService.saveOrderXML(tempDirectory, (OrderType) order.getDocument()); + log.debug(" - Saved order XML to "+orderFile.getAbsolutePath()); + order.setResultFileName(orderFile.getName()); + fileNameToOrderMap.put(orderFile.getName(), order); + } catch (IOException e) { + log.error(" - Failed to generate xml by order " + order, e); + return SendBasketResponse.error("Failed to generate XML for order to " + order.getSupplierName() + ": " + e.getMessage()); + } + } + + log.debug("4. Save basket and orders to database"); + + orderService.saveBasket(basket); + for (Order order : orderList) { + orderService.saveOrder(order); + } + + log.debug("5. Move XML files from temporary folder to destination folder at " + appProperties.getIntegration().getOutboxOrder()); + + File[] tempFiles = tempDirectory.listFiles(); + Set notMovedFiles = new HashSet(); + Set movedFiles = new HashSet(); + for (File file : tempFiles) { + File outFile = new File(appProperties.getIntegration().getOutboxOrder(), file.getName()); + try { + Order order = fileNameToOrderMap.get(file.getName()); + String supplierName = order.getSupplierName(); + FileUtils.moveFile(file, outFile); + movedFiles.add(file); + log.debug(" - Order " + order.getId() + " to " + supplierName + " successfully moved to " + outFile); + } catch (IOException e) { + log.error(" - Failed to move file " + file + " to " + outFile, e); + notMovedFiles.add(file); + } + } + + if (!notMovedFiles.isEmpty()) { + if (movedFiles.isEmpty()) { + return SendBasketResponse.error("Failed to move XML files to destination folder, please contact system administrator."); + } else { + String notSentOrdersToSuppliers = getSupplierNamesByFileSet(notMovedFiles, fileNameToOrderMap); + String sentOrdersToSuppliers = getSupplierNamesByFileSet(movedFiles, fileNameToOrderMap); + String errorMessage = notMovedFiles.size() + " file(s) to supplier(s) " + notSentOrdersToSuppliers + " failed to move to destination folder, please contact system administrator. " + movedFiles.size() + " orders to " + sentOrdersToSuppliers + " were sent."; + return SendBasketResponse.error(errorMessage); + } + } + + log.debug("6. Cleanup temporary folder " + tempDirectory); + + // Delete temporary folder anyway + if (!FileUtils.deleteQuietly(tempDirectory)) { + log.error("Failed to delete temporary directory, check that streams are closed: " + tempDirectory); + } + + log.debug("7. Basket #" + basket.getId() + " is sent succesfully"); + + return SendBasketResponse.success(basket.getId()); + } + + private String getSupplierNamesByFileSet(Set files, Map fileNameToOrderMap) { + StringBuilder sb = new StringBuilder(); + for (File file : files) { + Order order = fileNameToOrderMap.get(file.getName()); + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(order.getSupplierName()); + } + return sb.toString(); + } + + private String buildErrorMessageNoSellerInfo(Map> byCatalog, Set noSellerCatalog, int countProductWithCatalogWithoutSeller) { + StringBuilder sb = new StringBuilder(); + sb.append("Cannot send basket, as "); + sb.append(countProductWithCatalogWithoutSeller); + sb.append(" product"); + if (countProductWithCatalogWithoutSeller > 1) { + sb.append("s"); + } + int countNoSellerCatalog = noSellerCatalog.size(); + sb.append(" relate to "); + if (countNoSellerCatalog == 1) { + sb.append("a"); + } else { + sb.append(countNoSellerCatalog); + } + sb.append(" catalog"); + if (countNoSellerCatalog > 1) { + sb.append("s"); + } + sb.append(" for which there is not enough information about seller to send an order."); + if (byCatalog.size() > countNoSellerCatalog) { + sb.append(" Remove highlighted products from the basket to send the rest."); + } + return sb.toString(); + } + + private File createTempDirectory(String dirName) { + File tempDirectory = new File(FileUtils.getTempDirectory(), dirName); + tempDirectory.mkdirs(); + return tempDirectory; + } + + private String extractSupplierName(Party party) { + if (party.getPartyName() != null) { + return party.getPartyName().getName(); + } + if (party.getPartyLegalEntity() != null) { + if (party.getPartyLegalEntity().getRegistrationName() != null) { + return party.getPartyLegalEntity().getRegistrationName(); + } + } + return null; + } + + private final static DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(ZoneOffset.UTC); + + private String generateOrderNumber(Order order) { + String creationTimeFormatted = DATE_TIME_FORMATTER.format(order.getCreateTime()); + return creationTimeFormatted + "-" + (order.getOrderIndex() + 1); + } + +} diff --git a/cm-webapi/src/main/resources/application.properties b/cm-webapi/src/main/resources/application.properties index ef8b851..6a40bc7 100644 --- a/cm-webapi/src/main/resources/application.properties +++ b/cm-webapi/src/main/resources/application.properties @@ -5,9 +5,16 @@ logging.level.dk.erst.catalog = INFO spring.data.mongodb.uri=mongodb://localhost:27017/dc?ssl=false spring.data.mongodb.auto-index-creation = true +app.integration.inbox-catalogue=./.integration/inbox/catalogue +app.integration.inbox-order-response=./.integration/inbox/orderresponse +app.integration.outbox-order=./.integration/outbox/order + # Set to DEBUG to see all Mongo queries logging.level.org.springframework.data.mongodb.core.MongoTemplate=INFO +# Set to DEBUG to see OrderController details +logging.level.dk.erst.cm.webapi.OrderController=DEBUG + spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB From 0a6b7faf4b44418e5d3e5d3a101c764f5d837f55 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 11 Dec 2021 15:13:23 +0200 Subject: [PATCH 43/71] chore: test logging --- .../java/dk/erst/cm/api/order/OrderProducerServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java index db67c92..4e94ec2 100644 --- a/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java +++ b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java @@ -73,16 +73,16 @@ void produce() { OrderType order = service.generateOrder(dataOrder, customerOrderData, productList); IErrorList errorList = UBL21Validator.order().validate(order); if (errorList.isNotEmpty()) { - System.out.println("Found " + errorList.size() + " errors:"); + log.error("Found " + errorList.size() + " errors:"); for (int i = 0; i < errorList.size(); i++) { IError error = errorList.get(i); - System.out.println((i + 1) + "\t" + error.toString()); + log.error((i + 1) + "\t" + error.toString()); } } assertTrue(errorList.isEmpty()); UBL21Writer.order().write(order, out); String xml = new String(out.toByteArray(), StandardCharsets.UTF_8); - System.out.println(xml); + log.info(xml); assertTrue(xml.indexOf(order.getSellerSupplierParty().getParty().getPostalAddress().getCountry().getIdentificationCodeValue()) > 0); } From fdaf0da1df91b415c6f0bb26602554f5f45cda45 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 11 Dec 2021 15:25:20 +0200 Subject: [PATCH 44/71] chore: move logic from OrderController to BasketService --- .../dk/erst/cm/api/order/BasketService.java | 298 +++++++++++++++++ .../dk/erst/cm/webapi/OrderController.java | 300 +----------------- .../src/main/resources/application.properties | 2 +- 3 files changed, 310 insertions(+), 290 deletions(-) create mode 100644 cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java new file mode 100644 index 0000000..4f3cb58 --- /dev/null +++ b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java @@ -0,0 +1,298 @@ +package dk.erst.cm.api.order; + +import java.io.File; +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.apache.commons.io.FileUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import dk.erst.cm.api.data.Basket; +import dk.erst.cm.api.data.Order; +import dk.erst.cm.api.data.OrderStatus; +import dk.erst.cm.api.data.Product; +import dk.erst.cm.api.item.CatalogService; +import dk.erst.cm.api.item.ProductService; +import dk.erst.cm.api.order.data.CustomerOrderData; +import dk.erst.cm.xml.ubl21.model.Party; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import oasis.names.specification.ubl.schema.xsd.order_21.OrderType; + +@Service +@Slf4j +public class BasketService { + + private final ProductService productService; + private final OrderService orderService; + private final OrderProducerService orderProducerService; + private final CatalogService catalogService; + + @Autowired + public BasketService(ProductService productService, OrderService orderService, CatalogService catalogService, OrderProducerService orderProducerService) { + this.productService = productService; + this.orderService = orderService; + this.catalogService = catalogService; + this.orderProducerService = orderProducerService; + } + + @Data + public static class BasketData { + private Map orderLines; + } + + @Data + public static class SendBasketData { + private BasketData basketData; + private CustomerOrderData orderData; + } + + @Data + public static class SendBasketResponse { + private boolean success; + private String basketId; + private String errorMessage; + private List errorProductIdList; + + public static SendBasketResponse error(String message) { + SendBasketResponse r = new SendBasketResponse(); + r.setSuccess(false); + r.setErrorMessage(message); + return r; + } + + public SendBasketResponse withErrorProductIdList(List errorProductIdList) { + this.setErrorProductIdList(errorProductIdList); + return this; + } + + public static SendBasketResponse success(String basketId) { + SendBasketResponse r = new SendBasketResponse(); + r.setSuccess(true); + r.setBasketId(basketId); + return r; + } + } + + public SendBasketResponse basketSend(SendBasketData query, String outboxFolder) { + Set queryProductIdSet = query.basketData.orderLines.keySet(); + Iterable products = productService.findAllByIds(queryProductIdSet); + + Set resolvedProductIdSet = new HashSet(); + + log.debug("1. Load necessary data for XML generation"); + + Map> byCatalogMap = new HashMap<>(); + for (Product p : products) { + String productCatalogId = p.getProductCatalogId(); + List productList = byCatalogMap.get(productCatalogId); + if (productList == null) { + productList = new ArrayList<>(); + byCatalogMap.put(productCatalogId, productList); + } + productList.add(p); + resolvedProductIdSet.add(p.getId()); + } + + if (resolvedProductIdSet.size() < queryProductIdSet.size()) { + int countNotFoundProducts = queryProductIdSet.size() - resolvedProductIdSet.size(); + String errorMessage = countNotFoundProducts + " product" + (countNotFoundProducts > 1 ? "s are" : "is") + " not found, please delete highlighted products to send the basket."; + Set notResolvedProductIdSet = new HashSet(queryProductIdSet); + notResolvedProductIdSet.removeAll(resolvedProductIdSet); + return SendBasketResponse.error(errorMessage).withErrorProductIdList(new ArrayList(notResolvedProductIdSet)); + } + + Set noSellerCatalogSet = new HashSet(); + Map sellerPartyByCatalog = new HashMap(); + for (String catalogId : byCatalogMap.keySet()) { + Party sellerParty = catalogService.loadLastSellerParty(catalogId); + if (sellerParty != null) { + sellerPartyByCatalog.put(catalogId, sellerParty); + } else { + noSellerCatalogSet.add(catalogId); + } + } + + if (!noSellerCatalogSet.isEmpty()) { + int countProductIncompleteSeller = 0; + + List errorProductIdList = new ArrayList(); + for (String catalogId : noSellerCatalogSet) { + List list = byCatalogMap.get(catalogId); + errorProductIdList.addAll(errorProductIdList); + countProductIncompleteSeller += list.size(); + } + String errorMessage = buildErrorMessageNoSellerInfo(byCatalogMap, noSellerCatalogSet, countProductIncompleteSeller); + return SendBasketResponse.error(errorMessage).withErrorProductIdList(errorProductIdList); + } + + log.debug("2. Generate internal models for XML"); + + List orderList = new ArrayList(); + int orderIndex = 0; + for (String catalogId : byCatalogMap.keySet()) { + List productList = byCatalogMap.get(catalogId); + + Order order = new Order(); + order.setOrderIndex(orderIndex); + order.setCreateTime(Instant.now()); + order.setId(UUID.randomUUID().toString()); + order.setLineCount(productList.size()); + order.setStatus(OrderStatus.GENERATED); + order.setOrderNumber(generateOrderNumber(order)); + order.setSupplierName(extractSupplierName(sellerPartyByCatalog.get(catalogId))); + order.setVersion(1); + OrderType sendOrder = orderProducerService.generateOrder(order, query.getOrderData(), productList); + order.setDocument(sendOrder); + + orderList.add(order); + + orderIndex++; + } + + Basket basket = new Basket(); + basket.setCreateTime(Instant.now()); + basket.setId(UUID.randomUUID().toString()); + basket.setLineCount(resolvedProductIdSet.size()); + basket.setOrderCount(orderList.size()); + basket.setVersion(1); + + File tempDirectory = createTempDirectory("delis-cm-" + basket.getId()); + + log.debug("3. Save XML files into temporary folder " + tempDirectory); + + Map fileNameToOrderMap = new HashMap(); + for (Order order : orderList) { + try { + File orderFile = orderService.saveOrderXML(tempDirectory, (OrderType) order.getDocument()); + log.debug(" - Saved order XML to " + orderFile.getAbsolutePath()); + order.setResultFileName(orderFile.getName()); + fileNameToOrderMap.put(orderFile.getName(), order); + } catch (IOException e) { + log.error(" - Failed to generate xml by order " + order, e); + return SendBasketResponse.error("Failed to generate XML for order to " + order.getSupplierName() + ": " + e.getMessage()); + } + } + + log.debug("4. Save basket and orders to database"); + + orderService.saveBasket(basket); + for (Order order : orderList) { + orderService.saveOrder(order); + } + + log.debug("5. Move XML files from temporary folder to destination folder at " + outboxFolder); + + File[] tempFiles = tempDirectory.listFiles(); + Set notMovedFiles = new HashSet(); + Set movedFiles = new HashSet(); + for (File file : tempFiles) { + File outFile = new File(outboxFolder, file.getName()); + try { + Order order = fileNameToOrderMap.get(file.getName()); + String supplierName = order.getSupplierName(); + FileUtils.moveFile(file, outFile); + movedFiles.add(file); + log.debug(" - Order " + order.getId() + " to " + supplierName + " successfully moved to " + outFile); + } catch (IOException e) { + log.error(" - Failed to move file " + file + " to " + outFile, e); + notMovedFiles.add(file); + } + } + + if (!notMovedFiles.isEmpty()) { + if (movedFiles.isEmpty()) { + return SendBasketResponse.error("Failed to move XML files to destination folder, please contact system administrator."); + } else { + String notSentOrdersToSuppliers = getSupplierNamesByFileSet(notMovedFiles, fileNameToOrderMap); + String sentOrdersToSuppliers = getSupplierNamesByFileSet(movedFiles, fileNameToOrderMap); + String errorMessage = notMovedFiles.size() + " file(s) to supplier(s) " + notSentOrdersToSuppliers + " failed to move to destination folder, please contact system administrator. " + movedFiles.size() + " orders to " + sentOrdersToSuppliers + " were sent."; + return SendBasketResponse.error(errorMessage); + } + } + + log.debug("6. Cleanup temporary folder " + tempDirectory); + + // Delete temporary folder anyway + if (!FileUtils.deleteQuietly(tempDirectory)) { + log.error("Failed to delete temporary directory, check that streams are closed: " + tempDirectory); + } + + log.debug("7. Basket #" + basket.getId() + " is sent succesfully"); + + return SendBasketResponse.success(basket.getId()); + } + + private String getSupplierNamesByFileSet(Set files, Map fileNameToOrderMap) { + StringBuilder sb = new StringBuilder(); + for (File file : files) { + Order order = fileNameToOrderMap.get(file.getName()); + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(order.getSupplierName()); + } + return sb.toString(); + } + + private String buildErrorMessageNoSellerInfo(Map> byCatalog, Set noSellerCatalog, int countProductWithCatalogWithoutSeller) { + StringBuilder sb = new StringBuilder(); + sb.append("Cannot send basket, as "); + sb.append(countProductWithCatalogWithoutSeller); + sb.append(" product"); + if (countProductWithCatalogWithoutSeller > 1) { + sb.append("s"); + } + int countNoSellerCatalog = noSellerCatalog.size(); + sb.append(" relate to "); + if (countNoSellerCatalog == 1) { + sb.append("a"); + } else { + sb.append(countNoSellerCatalog); + } + sb.append(" catalog"); + if (countNoSellerCatalog > 1) { + sb.append("s"); + } + sb.append(" for which there is not enough information about seller to send an order."); + if (byCatalog.size() > countNoSellerCatalog) { + sb.append(" Remove highlighted products from the basket to send the rest."); + } + return sb.toString(); + } + + private File createTempDirectory(String dirName) { + File tempDirectory = new File(FileUtils.getTempDirectory(), dirName); + tempDirectory.mkdirs(); + return tempDirectory; + } + + private String extractSupplierName(Party party) { + if (party.getPartyName() != null) { + return party.getPartyName().getName(); + } + if (party.getPartyLegalEntity() != null) { + if (party.getPartyLegalEntity().getRegistrationName() != null) { + return party.getPartyLegalEntity().getRegistrationName(); + } + } + return null; + } + + private final static DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(ZoneOffset.UTC); + + private String generateOrderNumber(Order order) { + String creationTimeFormatted = DATE_TIME_FORMATTER.format(order.getCreateTime()); + return creationTimeFormatted + "-" + (order.getOrderIndex() + 1); + } +} diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java index c122714..a01ab13 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java @@ -1,19 +1,5 @@ package dk.erst.cm.webapi; -import java.io.File; -import java.io.IOException; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestBody; @@ -21,300 +7,36 @@ import org.springframework.web.bind.annotation.RestController; import dk.erst.cm.AppProperties; -import dk.erst.cm.api.data.Basket; -import dk.erst.cm.api.data.Order; -import dk.erst.cm.api.data.OrderStatus; -import dk.erst.cm.api.data.Product; -import dk.erst.cm.api.item.CatalogService; -import dk.erst.cm.api.item.ProductService; -import dk.erst.cm.api.order.OrderProducerService; -import dk.erst.cm.api.order.OrderService; -import dk.erst.cm.api.order.data.CustomerOrderData; -import dk.erst.cm.xml.ubl21.model.Party; -import lombok.Data; +import dk.erst.cm.api.order.BasketService; +import dk.erst.cm.api.order.BasketService.SendBasketData; +import dk.erst.cm.api.order.BasketService.SendBasketResponse; import lombok.extern.slf4j.Slf4j; -import oasis.names.specification.ubl.schema.xsd.order_21.OrderType; @CrossOrigin(maxAge = 3600) @RestController @Slf4j public class OrderController { - private final ProductService productService; - private final OrderService orderService; - private final OrderProducerService orderProducerService; - private final CatalogService catalogService; - private AppProperties appProperties; + private final BasketService basketService; + private final AppProperties appProperties; @Autowired - public OrderController(ProductService productService, OrderService orderService, CatalogService catalogService, OrderProducerService orderProducerService, AppProperties appProperties) { - this.productService = productService; - this.orderService = orderService; - this.catalogService = catalogService; - this.orderProducerService = orderProducerService; + public OrderController(BasketService basketService, AppProperties appProperties) { + this.basketService = basketService; this.appProperties = appProperties; } - @Data - public static class BasketData { - private Map orderLines; - } - - @Data - public static class SendBasketData { - private BasketData basketData; - private CustomerOrderData orderData; - } - - @Data - public static class SendBasketResponse { - private boolean success; - private String basketId; - private String errorMessage; - private List errorProductIdList; - - public static SendBasketResponse error(String message) { - SendBasketResponse r = new SendBasketResponse(); - r.setSuccess(false); - r.setErrorMessage(message); - return r; - } - - public SendBasketResponse withErrorProductIdList(List errorProductIdList) { - this.setErrorProductIdList(errorProductIdList); - return this; - } - - public static SendBasketResponse success(String basketId) { - SendBasketResponse r = new SendBasketResponse(); - r.setSuccess(true); - r.setBasketId(basketId); - return r; - } - } - @RequestMapping(value = "/api/basket/send") public SendBasketResponse basketSend(@RequestBody SendBasketData query) { - log.info("basketSend: query=" + query); - SendBasketResponse res = basketSendInternal(query); + log.info("START basketSend: query=" + query); + SendBasketResponse res = basketService.basketSend(query, appProperties.getIntegration().getOutboxOrder()); if (res.isSuccess()) { - log.info("basketSend OK: " + res); + log.info("END basketSend OK: " + res); } else { - log.info("basketSend Error: " + res); + log.info("END basketSend Error: " + res); } return res; } - private SendBasketResponse basketSendInternal(SendBasketData query) { - Set queryProductIdSet = query.basketData.orderLines.keySet(); - Iterable products = productService.findAllByIds(queryProductIdSet); - - Set resolvedProductIdSet = new HashSet(); - - log.debug("1. Load necessary data for XML generation"); - - Map> byCatalogMap = new HashMap<>(); - for (Product p : products) { - String productCatalogId = p.getProductCatalogId(); - List productList = byCatalogMap.get(productCatalogId); - if (productList == null) { - productList = new ArrayList<>(); - byCatalogMap.put(productCatalogId, productList); - } - productList.add(p); - resolvedProductIdSet.add(p.getId()); - } - - if (resolvedProductIdSet.size() < queryProductIdSet.size()) { - int countNotFoundProducts = queryProductIdSet.size() - resolvedProductIdSet.size(); - String errorMessage = countNotFoundProducts + " product" + (countNotFoundProducts > 1 ? "s are" : "is") + " not found, please delete highlighted products to send the basket."; - Set notResolvedProductIdSet = new HashSet(queryProductIdSet); - notResolvedProductIdSet.removeAll(resolvedProductIdSet); - return SendBasketResponse.error(errorMessage).withErrorProductIdList(new ArrayList(notResolvedProductIdSet)); - } - - Set noSellerCatalogSet = new HashSet(); - Map sellerPartyByCatalog = new HashMap(); - for (String catalogId : byCatalogMap.keySet()) { - Party sellerParty = catalogService.loadLastSellerParty(catalogId); - if (sellerParty != null) { - sellerPartyByCatalog.put(catalogId, sellerParty); - } else { - noSellerCatalogSet.add(catalogId); - } - } - - if (!noSellerCatalogSet.isEmpty()) { - int countProductIncompleteSeller = 0; - - List errorProductIdList = new ArrayList(); - for (String catalogId : noSellerCatalogSet) { - List list = byCatalogMap.get(catalogId); - errorProductIdList.addAll(errorProductIdList); - countProductIncompleteSeller += list.size(); - } - String errorMessage = buildErrorMessageNoSellerInfo(byCatalogMap, noSellerCatalogSet, countProductIncompleteSeller); - return SendBasketResponse.error(errorMessage).withErrorProductIdList(errorProductIdList); - } - - log.debug("2. Generate internal models for XML"); - - List orderList = new ArrayList(); - int orderIndex = 0; - for (String catalogId : byCatalogMap.keySet()) { - List productList = byCatalogMap.get(catalogId); - - Order order = new Order(); - order.setOrderIndex(orderIndex); - order.setCreateTime(Instant.now()); - order.setId(UUID.randomUUID().toString()); - order.setLineCount(productList.size()); - order.setStatus(OrderStatus.GENERATED); - order.setOrderNumber(generateOrderNumber(order)); - order.setSupplierName(extractSupplierName(sellerPartyByCatalog.get(catalogId))); - order.setVersion(1); - OrderType sendOrder = orderProducerService.generateOrder(order, query.getOrderData(), productList); - order.setDocument(sendOrder); - - orderList.add(order); - - orderIndex++; - } - - Basket basket = new Basket(); - basket.setCreateTime(Instant.now()); - basket.setId(UUID.randomUUID().toString()); - basket.setLineCount(resolvedProductIdSet.size()); - basket.setOrderCount(orderList.size()); - basket.setVersion(1); - - File tempDirectory = createTempDirectory("delis-cm-" + basket.getId()); - - log.debug("3. Save XML files into temporary folder " + tempDirectory); - - Map fileNameToOrderMap = new HashMap(); - for (Order order : orderList) { - try { - File orderFile = orderService.saveOrderXML(tempDirectory, (OrderType) order.getDocument()); - log.debug(" - Saved order XML to "+orderFile.getAbsolutePath()); - order.setResultFileName(orderFile.getName()); - fileNameToOrderMap.put(orderFile.getName(), order); - } catch (IOException e) { - log.error(" - Failed to generate xml by order " + order, e); - return SendBasketResponse.error("Failed to generate XML for order to " + order.getSupplierName() + ": " + e.getMessage()); - } - } - - log.debug("4. Save basket and orders to database"); - - orderService.saveBasket(basket); - for (Order order : orderList) { - orderService.saveOrder(order); - } - - log.debug("5. Move XML files from temporary folder to destination folder at " + appProperties.getIntegration().getOutboxOrder()); - - File[] tempFiles = tempDirectory.listFiles(); - Set notMovedFiles = new HashSet(); - Set movedFiles = new HashSet(); - for (File file : tempFiles) { - File outFile = new File(appProperties.getIntegration().getOutboxOrder(), file.getName()); - try { - Order order = fileNameToOrderMap.get(file.getName()); - String supplierName = order.getSupplierName(); - FileUtils.moveFile(file, outFile); - movedFiles.add(file); - log.debug(" - Order " + order.getId() + " to " + supplierName + " successfully moved to " + outFile); - } catch (IOException e) { - log.error(" - Failed to move file " + file + " to " + outFile, e); - notMovedFiles.add(file); - } - } - - if (!notMovedFiles.isEmpty()) { - if (movedFiles.isEmpty()) { - return SendBasketResponse.error("Failed to move XML files to destination folder, please contact system administrator."); - } else { - String notSentOrdersToSuppliers = getSupplierNamesByFileSet(notMovedFiles, fileNameToOrderMap); - String sentOrdersToSuppliers = getSupplierNamesByFileSet(movedFiles, fileNameToOrderMap); - String errorMessage = notMovedFiles.size() + " file(s) to supplier(s) " + notSentOrdersToSuppliers + " failed to move to destination folder, please contact system administrator. " + movedFiles.size() + " orders to " + sentOrdersToSuppliers + " were sent."; - return SendBasketResponse.error(errorMessage); - } - } - - log.debug("6. Cleanup temporary folder " + tempDirectory); - - // Delete temporary folder anyway - if (!FileUtils.deleteQuietly(tempDirectory)) { - log.error("Failed to delete temporary directory, check that streams are closed: " + tempDirectory); - } - - log.debug("7. Basket #" + basket.getId() + " is sent succesfully"); - - return SendBasketResponse.success(basket.getId()); - } - - private String getSupplierNamesByFileSet(Set files, Map fileNameToOrderMap) { - StringBuilder sb = new StringBuilder(); - for (File file : files) { - Order order = fileNameToOrderMap.get(file.getName()); - if (sb.length() > 0) { - sb.append(", "); - } - sb.append(order.getSupplierName()); - } - return sb.toString(); - } - - private String buildErrorMessageNoSellerInfo(Map> byCatalog, Set noSellerCatalog, int countProductWithCatalogWithoutSeller) { - StringBuilder sb = new StringBuilder(); - sb.append("Cannot send basket, as "); - sb.append(countProductWithCatalogWithoutSeller); - sb.append(" product"); - if (countProductWithCatalogWithoutSeller > 1) { - sb.append("s"); - } - int countNoSellerCatalog = noSellerCatalog.size(); - sb.append(" relate to "); - if (countNoSellerCatalog == 1) { - sb.append("a"); - } else { - sb.append(countNoSellerCatalog); - } - sb.append(" catalog"); - if (countNoSellerCatalog > 1) { - sb.append("s"); - } - sb.append(" for which there is not enough information about seller to send an order."); - if (byCatalog.size() > countNoSellerCatalog) { - sb.append(" Remove highlighted products from the basket to send the rest."); - } - return sb.toString(); - } - - private File createTempDirectory(String dirName) { - File tempDirectory = new File(FileUtils.getTempDirectory(), dirName); - tempDirectory.mkdirs(); - return tempDirectory; - } - - private String extractSupplierName(Party party) { - if (party.getPartyName() != null) { - return party.getPartyName().getName(); - } - if (party.getPartyLegalEntity() != null) { - if (party.getPartyLegalEntity().getRegistrationName() != null) { - return party.getPartyLegalEntity().getRegistrationName(); - } - } - return null; - } - - private final static DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(ZoneOffset.UTC); - - private String generateOrderNumber(Order order) { - String creationTimeFormatted = DATE_TIME_FORMATTER.format(order.getCreateTime()); - return creationTimeFormatted + "-" + (order.getOrderIndex() + 1); - } } diff --git a/cm-webapi/src/main/resources/application.properties b/cm-webapi/src/main/resources/application.properties index 6a40bc7..dada3ad 100644 --- a/cm-webapi/src/main/resources/application.properties +++ b/cm-webapi/src/main/resources/application.properties @@ -13,7 +13,7 @@ app.integration.outbox-order=./.integration/outbox/order logging.level.org.springframework.data.mongodb.core.MongoTemplate=INFO # Set to DEBUG to see OrderController details -logging.level.dk.erst.cm.webapi.OrderController=DEBUG +logging.level.dk.erst.cm.api.order.BasketService=DEBUG spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB From e618ecb4a10c39f52fd5d23e047d919242d6201e Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 11 Dec 2021 16:16:53 +0200 Subject: [PATCH 45/71] feature: add to basket also from top panel of product details for convenience, change top icon of basket to just ORDER text, remove artificial delay in basket adding --- cm-frontend/src/components/AddToBasket.js | 59 +++----- cm-frontend/src/components/BasketData.js | 1 - .../src/components/ProductDetailHeader.js | 17 ++- cm-frontend/src/components/TopNav.js | 132 +++++++++--------- cm-frontend/src/pages/ProductDetailPage.js | 126 ++++++++--------- 5 files changed, 167 insertions(+), 168 deletions(-) diff --git a/cm-frontend/src/components/AddToBasket.js b/cm-frontend/src/components/AddToBasket.js index 29ce636..8496840 100644 --- a/cm-frontend/src/components/AddToBasket.js +++ b/cm-frontend/src/components/AddToBasket.js @@ -1,56 +1,37 @@ +import React from "react"; import {Button} from "@material-ui/core"; -import React, {useEffect, useRef} from "react"; -import CircularProgress from "@material-ui/core/CircularProgress"; import {ProductBasketStatus} from "./BasketData"; +export const getProductBasketButtonTitle = (basketState) => { + switch (basketState) { + case ProductBasketStatus.Added: + return 'Remove from basket'; + default: + return 'Add to basket'; + } +} + +export const handleProductBasketIconClick = (basketData, product, changeBasket) => { + const state = basketData.getProductBasketStatus(product.id); + if (state === ProductBasketStatus.Empty) { + changeBasket(product.id, 1); + } else if (state === ProductBasketStatus.Added) { + changeBasket(product.id, 0); + } +} export default function AddToBasket(props) { const {changeBasket, basketData, product} = props; - const [state, setState] = React.useState(basketData.getProductBasketStatus(product.id)); - - const getButtonTitle = () => { - switch (state) { - case ProductBasketStatus.Adding: - return 'Adding to basket'; - case ProductBasketStatus.Added: - return 'Remove from basket'; - default: - return 'Add to basket'; - } - } - - const isProgress = () => { - return state === ProductBasketStatus.Adding; - } - - // TODO: Remove - temporary code to imitate slow adding - const timerRef = useRef(null); const handleClick = () => { - if (state === ProductBasketStatus.Empty) { - setState(ProductBasketStatus.Adding); - timerRef.current = setTimeout(() => { - changeBasket(product.id, 1); - setState(ProductBasketStatus.Added) - }, 300); - } else if (state === ProductBasketStatus.Added) { - changeBasket(product.id, 0); - setState(ProductBasketStatus.Empty); - } + handleProductBasketIconClick(basketData, product, changeBasket); } - useEffect(() => { - return () => clearTimeout(timerRef.current) - }, []); - return ( <> ) diff --git a/cm-frontend/src/components/BasketData.js b/cm-frontend/src/components/BasketData.js index 6ba9a45..8ecdd00 100644 --- a/cm-frontend/src/components/BasketData.js +++ b/cm-frontend/src/components/BasketData.js @@ -1,6 +1,5 @@ export const ProductBasketStatus = { Empty: 'empty', - Adding: 'adding', Added: 'added', } diff --git a/cm-frontend/src/components/ProductDetailHeader.js b/cm-frontend/src/components/ProductDetailHeader.js index 2f8d823..db246a2 100644 --- a/cm-frontend/src/components/ProductDetailHeader.js +++ b/cm-frontend/src/components/ProductDetailHeader.js @@ -1,7 +1,11 @@ import {Card, CardContent, Fab, makeStyles, Typography} from "@material-ui/core"; import ArrowIcon from '@material-ui/icons/KeyboardBackspaceOutlined'; import RefreshIcon from '@material-ui/icons/Refresh'; +import ShoppingBasketIcon from '@material-ui/icons/ShoppingBasketOutlined'; import {useHistory} from "react-router"; +import React from "react"; +import {getProductBasketButtonTitle, handleProductBasketIconClick} from "./AddToBasket"; +import {ProductBasketStatus} from "./BasketData"; const useStyles = makeStyles(theme => ({ @@ -49,7 +53,7 @@ const _emptyNavigator = { export default function ProductDetailHeader(prop) { - const {name, navigator = _emptyNavigator, id, refreshAction} = prop; + const {name, navigator = _emptyNavigator, id, refreshAction, basketData, product, changeBasket} = prop; const classes = useStyles(); @@ -63,6 +67,14 @@ export default function ProductDetailHeader(prop) { history.push(path); } + const handleIconClick = () => { + handleProductBasketIconClick(basketData, product, changeBasket); + } + + const getShoppingBasketColor = (product) => { + return product && basketData.getProductBasketStatus(product.id) === ProductBasketStatus.Empty ? "" : "primary"; + } + return ( @@ -71,6 +83,9 @@ export default function ProductDetailHeader(prop) { {name}
    + handleIconClick()}> + + navigateTo(navigator.getPrevious(id))}> diff --git a/cm-frontend/src/components/TopNav.js b/cm-frontend/src/components/TopNav.js index 02cd118..9287588 100644 --- a/cm-frontend/src/components/TopNav.js +++ b/cm-frontend/src/components/TopNav.js @@ -1,81 +1,85 @@ import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import {makeStyles} from '@material-ui/core/styles'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; -import { Link } from 'react-router-dom'; +import {Link} from 'react-router-dom'; import SearchBar from './SearchBar'; import './TopNav.css'; -import BasketBar from "./BasketBar"; +// import BasketBar from "./BasketBar"; const useStyles = makeStyles((theme) => ({ - root: { - flexGrow: 1, - }, -title: { - flexGrow: 1, - marginRight: theme.spacing(2), - }, -fullName: { - [theme.breakpoints.down('sm')]: { - display: 'none', - }, -}, -link: { - color: "inherit", - textDecoration: "none", - "&:hover": { - color: "#d3d3d3" - } - }, - basketBar: { - [theme.breakpoints.up('xs')]: { - margin: theme.spacing(2), + root: { + flexGrow: 1, }, - }, - searchBar: { - [theme.breakpoints.down('xs')]: { - margin: theme.spacing(2), - } - }, - flexBreak: { - display: 'none', - [theme.breakpoints.down('xs')]: { - display: 'flex', - flexBasis: '100%', - height: '0', + title: { + flexGrow: 1, + marginRight: theme.spacing(2), + }, + fullName: { + [theme.breakpoints.down('sm')]: { + display: 'none', + }, + }, + link: { + color: "inherit", + textDecoration: "none", + "&:hover": { + color: "#d3d3d3" + } + }, + basketBar: { + [theme.breakpoints.up('xs')]: { + margin: theme.spacing(2), + }, + }, + searchBar: { + [theme.breakpoints.down('xs')]: { + margin: theme.spacing(2), + } + }, + flexBreak: { + display: 'none', + [theme.breakpoints.down('xs')]: { + display: 'flex', + flexBasis: '100%', + height: '0', + } } - } })); export default function TopNav(props) { - const classes = useStyles(); + const classes = useStyles(); + + const {aboutAction, searchAction, showBasketBar} = props; - const { aboutAction, searchAction, showBasketBar } = props; + return ( +
    + + + + DELIS{' '}Catalogue + + + + + + {showBasketBar && ( - return ( -
    - - - - DELIS{' '}Catalogue - - - - - - { showBasketBar && ( - - - - )} -
    -
    - -
    - - -
    - ); + // + // + // + + + + )} +
    +
    + +
    + + +
    + ); } diff --git a/cm-frontend/src/pages/ProductDetailPage.js b/cm-frontend/src/pages/ProductDetailPage.js index 2138073..38e3328 100644 --- a/cm-frontend/src/pages/ProductDetailPage.js +++ b/cm-frontend/src/pages/ProductDetailPage.js @@ -1,89 +1,89 @@ import React from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import { useParams } from "react-router"; +import {makeStyles} from "@material-ui/core/styles"; +import {useParams} from "react-router"; import ProductDetail from "../components/ProductDetail"; import ProductDetailHeader from "../components/ProductDetailHeader"; import CircularProgress from "@material-ui/core/CircularProgress"; -import { Box, FormControl, FormControlLabel, Paper, Radio, RadioGroup } from "@material-ui/core"; +import {Box, FormControl, FormControlLabel, Paper, Radio, RadioGroup} from "@material-ui/core"; import DataService from "../services/DataService"; import MergeService from "../services/MergeService"; const useStyles = makeStyles(theme => ({ - paper: { - display: "flex", - flexDirection: "column", - justifyContent: "left", - alignItems: "left", - height: "100%", - padding: theme.spacing(2), - marginBottom: theme.spacing(3), - } + paper: { + display: "flex", + flexDirection: "column", + justifyContent: "left", + alignItems: "left", + height: "100%", + padding: theme.spacing(2), + marginBottom: theme.spacing(3), + } })); function ViewToggle(props) { - return ( - - - - } label="Table" /> - } label="JSON" /> - - - - ) + return ( + + + + } label="Table"/> + } label="JSON"/> + + + + ) } export default function ProductDetailPage(props) { - - const { navigator } = props; - let { id } = useParams(); + const {navigator} = props; + + let {id} = useParams(); - const classes = useStyles(); + const classes = useStyles(); - const [data, setData] = React.useState(null); - const [dataLoading, setDataLoading] = React.useState(true); - const [viewMode, setViewMode] = React.useState("table"); + const [data, setData] = React.useState(null); + const [dataLoading, setDataLoading] = React.useState(true); + const [viewMode, setViewMode] = React.useState("table"); - React.useEffect(() => { - loadProduct(id); - }, [id]); + React.useEffect(() => { + loadProduct(id); + }, [id]); - async function loadProduct(id) { - setDataLoading(true); - const response = await DataService.fetchProductDetails(id); - let res = await response.json(); - if (Array.isArray(res)) { - res = MergeService.mergeProducts(res); + async function loadProduct(id) { + setDataLoading(true); + const response = await DataService.fetchProductDetails(id); + let res = await response.json(); + if (Array.isArray(res)) { + res = MergeService.mergeProducts(res); + } + setData(res); + setDataLoading(false); } - setData(res); - setDataLoading(false); - } - const handleViewChange = (event) => { - setViewMode(event.target.value); - }; + const handleViewChange = (event) => { + setViewMode(event.target.value); + }; - return ( - <> - + return ( + <> + - + - - {dataLoading ? ( - - ) : ( - <> - {viewMode === "json" ? ( -
    {JSON.stringify(data, null, 2)}
    - ) : ( - - ) - } + + {dataLoading ? ( + + ) : ( + <> + {viewMode === "json" ? ( +
    {JSON.stringify(data, null, 2)}
    + ) : ( + + ) + } + + )} +
    - )} -
    - - ); + ); } From 9e0657fb951b222bf0535da5f093ac4df8dea781 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 11 Dec 2021 17:19:17 +0200 Subject: [PATCH 46/71] feature: show error messages after send, redirect to real basket id, highlight failed products --- cm-frontend/src/components/OrderLineList.js | 12 +++- cm-frontend/src/pages/SendPage.js | 69 ++++++++++++--------- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/cm-frontend/src/components/OrderLineList.js b/cm-frontend/src/components/OrderLineList.js index 07d0b07..5d928e6 100644 --- a/cm-frontend/src/components/OrderLineList.js +++ b/cm-frontend/src/components/OrderLineList.js @@ -60,7 +60,7 @@ function QuantityControl(props) { export default function OrderLineList(props) { - const {basketData, showRowDetails, changeBasket, productList, lockControls} = props; + const {basketData, showRowDetails, changeBasket, productList, lockControls, errorProductIdSet} = props; const useStyles = makeStyles((theme) => ({ paper: { @@ -71,6 +71,10 @@ export default function OrderLineList(props) { table: { minWidth: 600, }, + errorLine: { + textDecoration: "line-through", + color: theme.palette.error.main, + } })); const classes = useStyles(); @@ -82,6 +86,10 @@ export default function OrderLineList(props) { return null; } + const isErrorLine = (productId) => { + return productId && errorProductIdSet && errorProductIdSet.has(productId); + } + return @@ -96,7 +104,7 @@ export default function OrderLineList(props) { {!basketData.isEmpty() ? basketData.getOrderLineList().map((orderLine, index) => ( - showRowDetails(orderLine.productId)}> + showRowDetails(orderLine.productId)} className={isErrorLine(orderLine.productId) ? classes.errorLine : null}> {(index + 1)} {ItemDetailsService.itemName(productItem(orderLine.productId))} diff --git a/cm-frontend/src/pages/SendPage.js b/cm-frontend/src/pages/SendPage.js index 7547326..b5d6c5e 100644 --- a/cm-frontend/src/pages/SendPage.js +++ b/cm-frontend/src/pages/SendPage.js @@ -9,8 +9,7 @@ import SendIcon from "@material-ui/icons/Send"; import DataService from "../services/DataService"; import OrderLineList from "../components/OrderLineList"; import OrderHeader from "../components/OrderHeader"; -import delay from "../utils/delay"; -import BasketSendResult from "../components/BasketSendResult"; +import {Alert, AlertTitle} from "@material-ui/lab"; const useStyles = makeStyles(theme => ({ table: { @@ -31,10 +30,12 @@ export default function SendPage(props) { const [isLoading, setLoading] = React.useState(false); const [isSending, setSending] = React.useState(false); - const [sendResult, setSendResult] = React.useState(null); const [productList, setProductList] = React.useState({}); const [reloadCount, setReloadCount] = React.useState(0); + const [errorMessage, setErrorMessage] = React.useState(null); + const [errorProductIdSet, setErrorProductIdSet] = React.useState(null); + const classes = useStyles(); const {push} = useHistory(); @@ -48,19 +49,30 @@ export default function SendPage(props) { async function sendBasket() { if (!basketData.isEmpty() && !orderData.isEmpty()) { + setErrorMessage(null); setSending(true); - // TODO: Remove test delay - await delay(500); await DataService.sendBasket(basketData, orderData).then(response => { let responseData = response.data; console.log(responseData); - // setSendResult(responseData); - changeBasket(null, 0); - const basketId = "234f029dfj23d2"; - push("/basket/"+basketId); + + if (false) { + responseData.success = false; + responseData.errorMessage = "Some error message"; + responseData.errorProductIdList = basketData.getOrderLineList().slice(0, 1).map((ol) => ol.productId); + } + + if (responseData.success) { + changeBasket(null, 0); + const basketId = responseData.basketId; + push("/basket/"+basketId); + } else { + setErrorMessage(responseData.errorMessage); + setErrorProductIdSet(new Set(responseData.errorProductIdList)); + } } ).catch(error => { console.log('Error occurred: ' + error.message); + setErrorMessage(error.message); }); } } @@ -71,6 +83,7 @@ export default function SendPage(props) { async function loadProducts() { if (!basketData.isEmpty()) { + setErrorMessage(null); setLoading(true); const productIdList = basketData.getOrderLineList().map((orderLine) => orderLine.productId); await DataService.fetchProductsByIds(productIdList).then(response => { @@ -81,11 +94,11 @@ export default function SendPage(props) { const p = responseData[index]; productMapById[p.id] = p; } - console.log(productMapById); setProductList(productMapById); } ).catch(error => { console.log('Error occurred: ' + error.message); + setErrorMessage(error.message); }); } } @@ -98,16 +111,14 @@ export default function SendPage(props) { return ( <> - - {(sendResult === null) && ( - - {isSending ? ( - - ) : ( - sendAction()}/> - )} - - )} + + + {isSending ? ( + + ) : ( + sendAction()}/> + )} + {(isLoading) ? ( @@ -116,14 +127,16 @@ export default function SendPage(props) { ) : ( <> - {(sendResult === null) ? ( - <> - - - - ) : ( - - )} + <> + {errorMessage && ( + + Error +
    {errorMessage}
    +
    + )} + + + )} From 689861e4e11cc1d39ed464d87d5cb7cec2861bee Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 11 Dec 2021 19:46:16 +0200 Subject: [PATCH 47/71] fix: Warning: Failed prop type: Invalid prop `color` of value `XXX` supplied to `ForwardRef(Fab)`, expected one of ["default","inherit","primary","secondary"]. --- cm-frontend/src/components/ProductDetailHeader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cm-frontend/src/components/ProductDetailHeader.js b/cm-frontend/src/components/ProductDetailHeader.js index db246a2..70e8c5e 100644 --- a/cm-frontend/src/components/ProductDetailHeader.js +++ b/cm-frontend/src/components/ProductDetailHeader.js @@ -72,7 +72,7 @@ export default function ProductDetailHeader(prop) { } const getShoppingBasketColor = (product) => { - return product && basketData.getProductBasketStatus(product.id) === ProductBasketStatus.Empty ? "" : "primary"; + return product && basketData.getProductBasketStatus(product.id) === ProductBasketStatus.Empty ? "default" : "primary"; } return ( From d3e07305b1d2c29b175acce5f53cf53c69d94c85 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 11 Dec 2021 20:58:30 +0200 Subject: [PATCH 48/71] feature: struggled with storing XMLOffsetDate and XMLOffsetTime from PHAX UBL2.1 library into Mongo - done with intermediate solution where everything is converted to UTC; implement loading of basket details together with all order JSON model; add refresh on basket history --- .../cm/api/dao/mongo/OrderRepository.java | 7 +- .../main/java/dk/erst/cm/api/data/Order.java | 4 + .../dk/erst/cm/api/order/BasketService.java | 43 +++++++-- .../dk/erst/cm/api/order/OrderService.java | 13 +++ .../src/components/BasketSendResult.js | 21 ++--- cm-frontend/src/pages/BasketPage.js | 46 ++++++++- cm-frontend/src/pages/SendPage.js | 6 +- .../java/dk/erst/cm/MongoConverterConfig.java | 94 +++++++++++++++++++ .../dk/erst/cm/webapi/OrderController.java | 24 +++++ .../src/main/resources/application.properties | 4 +- .../dk/erst/cm/MongoConverterConfigTest.java | 66 +++++++++++++ 11 files changed, 301 insertions(+), 27 deletions(-) create mode 100644 cm-webapi/src/main/java/dk/erst/cm/MongoConverterConfig.java create mode 100644 cm-webapi/src/test/java/dk/erst/cm/MongoConverterConfigTest.java diff --git a/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/OrderRepository.java b/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/OrderRepository.java index d2c5ccb..1eb722e 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/OrderRepository.java +++ b/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/OrderRepository.java @@ -1,8 +1,13 @@ package dk.erst.cm.api.dao.mongo; -import dk.erst.cm.api.data.Order; +import java.util.List; + import org.springframework.data.mongodb.repository.MongoRepository; +import dk.erst.cm.api.data.Order; + public interface OrderRepository extends MongoRepository { + List findByBasketId(String basketId); + } diff --git a/cm-api/src/main/java/dk/erst/cm/api/data/Order.java b/cm-api/src/main/java/dk/erst/cm/api/data/Order.java index 9dd6b6f..8bc688c 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/data/Order.java +++ b/cm-api/src/main/java/dk/erst/cm/api/data/Order.java @@ -19,8 +19,12 @@ public class Order { private Instant createTime; private int version; + @Indexed + private String basketId; + private OrderStatus status; private int orderIndex; + private String supplierName; @Indexed diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java index 4f3cb58..952eaca 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java @@ -10,6 +10,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -46,6 +47,12 @@ public BasketService(ProductService productService, OrderService orderService, C this.orderProducerService = orderProducerService; } + @Data + public static class SentBasketData { + private Basket basket; + private List orderList; + } + @Data public static class BasketData { private Map orderLines; @@ -138,15 +145,23 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder) log.debug("2. Generate internal models for XML"); + Basket basket = new Basket(); + basket.setCreateTime(Instant.now()); + basket.setId(generateId()); + basket.setLineCount(resolvedProductIdSet.size()); + basket.setOrderCount(byCatalogMap.size()); + basket.setVersion(1); + List orderList = new ArrayList(); int orderIndex = 0; for (String catalogId : byCatalogMap.keySet()) { List productList = byCatalogMap.get(catalogId); Order order = new Order(); + order.setBasketId(basket.getId()); order.setOrderIndex(orderIndex); order.setCreateTime(Instant.now()); - order.setId(UUID.randomUUID().toString()); + order.setId(generateId()); order.setLineCount(productList.size()); order.setStatus(OrderStatus.GENERATED); order.setOrderNumber(generateOrderNumber(order)); @@ -160,13 +175,6 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder) orderIndex++; } - Basket basket = new Basket(); - basket.setCreateTime(Instant.now()); - basket.setId(UUID.randomUUID().toString()); - basket.setLineCount(resolvedProductIdSet.size()); - basket.setOrderCount(orderList.size()); - basket.setVersion(1); - File tempDirectory = createTempDirectory("delis-cm-" + basket.getId()); log.debug("3. Save XML files into temporary folder " + tempDirectory); @@ -233,6 +241,10 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder) return SendBasketResponse.success(basket.getId()); } + private String generateId() { + return UUID.randomUUID().toString(); + } + private String getSupplierNamesByFileSet(Set files, Map fileNameToOrderMap) { StringBuilder sb = new StringBuilder(); for (File file : files) { @@ -295,4 +307,19 @@ private String generateOrderNumber(Order order) { String creationTimeFormatted = DATE_TIME_FORMATTER.format(order.getCreateTime()); return creationTimeFormatted + "-" + (order.getOrderIndex() + 1); } + + public Optional loadSentBasketData(String basketId) { + Optional optional = this.orderService.findBasketById(basketId); + if (optional.isPresent()) { + SentBasketData sbd = new SentBasketData(); + sbd.setBasket(optional.get()); + sbd.setOrderList(this.orderService.findOrdersByBasketId(sbd.getBasket().getId())); + return Optional.of(sbd); + } + return Optional.empty(); + } + + public Optional loadSentOrder(String orderId) { + return this.orderService.findOrderById(orderId); + } } diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java index bc22adc..a4acd7f 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.OutputStream; import java.time.Instant; +import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; @@ -32,6 +33,18 @@ public OrderService(BasketRepository basketRepository, OrderRepository orderRepo this.orderRepository = orderRepository; } + public Optional findBasketById(String basketId) { + return basketRepository.findById(basketId); + } + + public List findOrdersByBasketId(String basketId) { + return orderRepository.findByBasketId(basketId); + } + + public Optional findOrderById(String orderId) { + return orderRepository.findById(orderId); + } + public void saveBasket(Basket basket) { this.basketRepository.save(basket); } diff --git a/cm-frontend/src/components/BasketSendResult.js b/cm-frontend/src/components/BasketSendResult.js index dc48184..9fbfbbb 100644 --- a/cm-frontend/src/components/BasketSendResult.js +++ b/cm-frontend/src/components/BasketSendResult.js @@ -14,7 +14,9 @@ import {DataRow, DataView} from "./ProductDetail"; export default function BasketSendResult(props) { - const {showSuccess = false} = props; + const {showSuccess = false, sentBasketData} = props; + + const {orderList, basket} = sentBasketData; const useStyles = makeStyles((theme) => ({ paper: { @@ -25,11 +27,6 @@ export default function BasketSendResult(props) { const classes = useStyles(); - const basketStatus = { - sentDate: '01.12.2021 12:54:43', - id: 'vs094fj34f309jv340', - } - const orderDataList = [ { id: "b42f4f4f3gfegsfdgadsfasdfasdf", @@ -66,9 +63,9 @@ export default function BasketSendResult(props) { - - - prev + cur.orderLines, 0)}/> + + + @@ -77,7 +74,7 @@ export default function BasketSendResult(props) { -
    +
    Order @@ -89,13 +86,13 @@ export default function BasketSendResult(props) { - {orderDataList?.map((row, index) => ( + {orderList?.map((row, index) => ( {(index + 1)} {row.status} {row.supplierName} {row.orderNumber} - {row.orderLines} + {row.lineCount} diff --git a/cm-frontend/src/pages/BasketPage.js b/cm-frontend/src/pages/BasketPage.js index 593453f..161061c 100644 --- a/cm-frontend/src/pages/BasketPage.js +++ b/cm-frontend/src/pages/BasketPage.js @@ -4,6 +4,10 @@ import Paper from "@material-ui/core/Paper"; import CircularProgress from "@material-ui/core/CircularProgress"; import PageHeader from '../components/PageHeader'; import BasketSendResult from "../components/BasketSendResult"; +import DataService from "../services/DataService"; +import {useParams} from "react-router"; +import {useLocation} from "react-router-dom"; +import {Alert, AlertTitle} from "@material-ui/lab"; const useStyles = makeStyles(theme => ({ table: { @@ -18,14 +22,42 @@ const useStyles = makeStyles(theme => ({ })); -export default function BasketPage(props) { +export default function BasketPage() { const [isLoading, setLoading] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(null); + const [sentBasketData, setSentBasketData] = React.useState(null); + const [reloadCount, setReloadCount] = React.useState(0); const classes = useStyles(); + const search = useLocation().search; + const ok = new URLSearchParams(search).get('ok'); + const {id} = useParams(); + + React.useEffect(() => { + loadBasket(id).finally(() => setLoading(false)); + }, [id, reloadCount]); + + async function loadBasket(id) { + setErrorMessage(null); + setLoading(true); + await DataService.fetchSentBasketData(id).then(response => { + let responseData = response.data; + console.log(responseData); + setSentBasketData(responseData); + }).catch((error) => { + console.log('Error occurred: ' + error.message); + setErrorMessage(error.message); + }); + } + + const refreshAction = () => { + setReloadCount(reloadCount + 1); + } + return ( <> - + {(isLoading) ? ( @@ -33,7 +65,15 @@ export default function BasketPage(props) { ) : ( <> - + {errorMessage && ( + + Error +
    {errorMessage}
    +
    + )} + {sentBasketData && ( + + )} )} diff --git a/cm-frontend/src/pages/SendPage.js b/cm-frontend/src/pages/SendPage.js index b5d6c5e..88243f4 100644 --- a/cm-frontend/src/pages/SendPage.js +++ b/cm-frontend/src/pages/SendPage.js @@ -47,6 +47,8 @@ export default function SendPage(props) { sendBasket().finally(() => setSending(false)); } + let imitateError = false; + async function sendBasket() { if (!basketData.isEmpty() && !orderData.isEmpty()) { setErrorMessage(null); @@ -55,7 +57,7 @@ export default function SendPage(props) { let responseData = response.data; console.log(responseData); - if (false) { + if (imitateError) { responseData.success = false; responseData.errorMessage = "Some error message"; responseData.errorProductIdList = basketData.getOrderLineList().slice(0, 1).map((ol) => ol.productId); @@ -64,7 +66,7 @@ export default function SendPage(props) { if (responseData.success) { changeBasket(null, 0); const basketId = responseData.basketId; - push("/basket/"+basketId); + push("/basket/"+basketId+"?ok=1"); } else { setErrorMessage(responseData.errorMessage); setErrorProductIdSet(new Set(responseData.errorProductIdList)); diff --git a/cm-webapi/src/main/java/dk/erst/cm/MongoConverterConfig.java b/cm-webapi/src/main/java/dk/erst/cm/MongoConverterConfig.java new file mode 100644 index 0000000..4d5457d --- /dev/null +++ b/cm-webapi/src/main/java/dk/erst/cm/MongoConverterConfig.java @@ -0,0 +1,94 @@ +package dk.erst.cm; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Date; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; + +import com.helger.commons.datetime.XMLOffsetDate; +import com.helger.commons.datetime.XMLOffsetTime; + +import lombok.extern.slf4j.Slf4j; + +@Configuration +public class MongoConverterConfig { + + @Bean + public MongoCustomConversions mongoCustomConversions() { + return new MongoCustomConversions(Arrays.asList( + + new DateToXMLOffsetDate(), + + new DateToXMLOffsetTimeConverter(), + + new XMLOffsetDateToDateConverter(), + + new XMLOffsetTimeToTimeConverter() + + )); + } + + @Slf4j + @ReadingConverter + public static class DateToXMLOffsetDate implements Converter { + @Override + public XMLOffsetDate convert(Date source) { + log.debug("DateToXMLOffsetDate " + source); + if (source == null) { + return null; + } + return XMLOffsetDate.ofInstant(source.toInstant(), ZoneOffset.UTC); + } + } + + @Slf4j + @WritingConverter + public static class XMLOffsetDateToDateConverter implements Converter { + @Override + public Date convert(XMLOffsetDate source) { + log.debug("XMLOffsetDateToDateConverter " + source); + if (source == null) { + return null; + } + LocalDate localDate = source.toLocalDate(); + ZonedDateTime zonedDateTime = localDate.atStartOfDay(source.getOffset() != null ? source.getOffset() : ZoneOffset.UTC); + return Date.from(Instant.from(zonedDateTime)); + } + } + + @Slf4j + @ReadingConverter + public static class DateToXMLOffsetTimeConverter implements Converter { + @Override + public XMLOffsetTime convert(Date source) { + log.debug("DateToXMLOffsetTimeConverter " + source); + if (source == null) { + return null; + } + return XMLOffsetTime.ofInstant(source.toInstant(), ZoneOffset.UTC); + } + } + + @Slf4j + @WritingConverter + public static class XMLOffsetTimeToTimeConverter implements Converter { + @Override + public Date convert(XMLOffsetTime source) { + log.debug("XMLOffsetTimeToTimeConverter " + source); + if (source == null) { + return null; + } + return Date.from(source.toLocalTime().atDate(LocalDate.now()).atZone(source.getOffset() != null ? source.getOffset() : ZoneOffset.UTC).withZoneSameLocal(ZoneOffset.UTC).toInstant()); + } + } + +} diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java index a01ab13..9df63d0 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java @@ -1,15 +1,22 @@ package dk.erst.cm.webapi; +import java.util.Optional; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import dk.erst.cm.AppProperties; +import dk.erst.cm.api.data.Order; import dk.erst.cm.api.order.BasketService; import dk.erst.cm.api.order.BasketService.SendBasketData; import dk.erst.cm.api.order.BasketService.SendBasketResponse; +import dk.erst.cm.api.order.BasketService.SentBasketData; import lombok.extern.slf4j.Slf4j; @CrossOrigin(maxAge = 3600) @@ -38,5 +45,22 @@ public SendBasketResponse basketSend(@RequestBody SendBasketData query) { return res; } + @RequestMapping(value = "/api/basket/{id}") + public ResponseEntity getBasketById(@PathVariable("id") String id) { + Optional findById = basketService.loadSentBasketData(id); + if (findById.isPresent()) { + return new ResponseEntity<>(findById.get(), HttpStatus.OK); + } + return ResponseEntity.notFound().build(); + } + + @RequestMapping(value = "/api/order/{id}") + public ResponseEntity getOrderById(@PathVariable("id") String id) { + Optional findById = basketService.loadSentOrder(id); + if (findById.isPresent()) { + return new ResponseEntity<>(findById.get(), HttpStatus.OK); + } + return ResponseEntity.notFound().build(); + } } diff --git a/cm-webapi/src/main/resources/application.properties b/cm-webapi/src/main/resources/application.properties index dada3ad..35aac04 100644 --- a/cm-webapi/src/main/resources/application.properties +++ b/cm-webapi/src/main/resources/application.properties @@ -13,7 +13,9 @@ app.integration.outbox-order=./.integration/outbox/order logging.level.org.springframework.data.mongodb.core.MongoTemplate=INFO # Set to DEBUG to see OrderController details -logging.level.dk.erst.cm.api.order.BasketService=DEBUG +logging.level.dk.erst.cm.api.order=DEBUG + +logging.level.dk.erst.cm.MongoConverterConfig=DEBUG spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB diff --git a/cm-webapi/src/test/java/dk/erst/cm/MongoConverterConfigTest.java b/cm-webapi/src/test/java/dk/erst/cm/MongoConverterConfigTest.java new file mode 100644 index 0000000..09101d4 --- /dev/null +++ b/cm-webapi/src/test/java/dk/erst/cm/MongoConverterConfigTest.java @@ -0,0 +1,66 @@ +package dk.erst.cm; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import com.helger.commons.datetime.XMLOffsetDate; +import com.helger.commons.datetime.XMLOffsetTime; + +import dk.erst.cm.MongoConverterConfig.DateToXMLOffsetDate; +import dk.erst.cm.MongoConverterConfig.DateToXMLOffsetTimeConverter; +import dk.erst.cm.MongoConverterConfig.XMLOffsetDateToDateConverter; +import dk.erst.cm.MongoConverterConfig.XMLOffsetTimeToTimeConverter; + +class MongoConverterConfigTest { + + @Test + void testDateConversions() { + DateToXMLOffsetDate dateToXml = new DateToXMLOffsetDate(); + XMLOffsetDateToDateConverter xmlToDate = new XMLOffsetDateToDateConverter(); + + ZoneOffset[] testZones = new ZoneOffset[] { ZoneOffset.UTC, null }; + + for (int i = 0; i < testZones.length; i++) { + ZoneOffset fromZone = testZones[i]; + + LocalDate fromLocalDate = LocalDate.now(); + Date convert = xmlToDate.convert(XMLOffsetDate.of(fromLocalDate, fromZone)); + + XMLOffsetDate res = dateToXml.convert(convert); + assertEquals(fromLocalDate, res.toLocalDate(), "Zone " + fromZone); + assertEquals(Optional.ofNullable(fromZone).orElse(ZoneOffset.UTC), res.getOffset(), "Zone " + fromZone); + } + } + + @Test + void testTimeConversions() { + DateToXMLOffsetTimeConverter timeToXml = new DateToXMLOffsetTimeConverter(); + XMLOffsetTimeToTimeConverter xmlToTime = new XMLOffsetTimeToTimeConverter(); + + // TODO: Implement Zones other than UTC... + ZoneOffset[] testZones = new ZoneOffset[] { null, + // ZoneOffset.ofHours(2), + // ZoneOffset.MAX, ZoneOffset.MIN, + ZoneOffset.UTC }; + + for (int i = 0; i < testZones.length; i++) { + ZoneOffset fromZone = testZones[i]; + LocalTime fromLocalTime = LocalTime.now(); + Date convert = xmlToTime.convert(XMLOffsetTime.of(fromLocalTime, fromZone)); + + System.out.println(convert); + + XMLOffsetTime res = timeToXml.convert(convert); + assertEquals(fromLocalTime, res.toLocalTime(), "Zone " + fromZone); + assertEquals(Optional.ofNullable(fromZone).orElse(ZoneOffset.UTC), res.getOffset(), "Zone " + fromZone); + } + } + +} From 6962581bf83a136e63b8b19d19cd36d173eee348 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 11:32:45 +0200 Subject: [PATCH 49/71] fix: show real count on success message after sending --- .../src/components/BasketSendResult.js | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/cm-frontend/src/components/BasketSendResult.js b/cm-frontend/src/components/BasketSendResult.js index 9fbfbbb..9921e71 100644 --- a/cm-frontend/src/components/BasketSendResult.js +++ b/cm-frontend/src/components/BasketSendResult.js @@ -27,29 +27,12 @@ export default function BasketSendResult(props) { const classes = useStyles(); - const orderDataList = [ - { - id: "b42f4f4f3gfegsfdgadsfasdfasdf", - orderNumber: "20211205-131812-01", - orderLines: 1, - supplierName: "Danish Supplier A/S", - status: "Generated", - }, - { - id: "b422434vfsdfdsfaf24ff", - orderNumber: "20211205-131812-02", - orderLines: 1, - supplierName: "Norwegian Supplier ApS", - status: "Downloaded", - }, - ] - return ( <> {showSuccess && ( Success -
    {orderDataList.length} order{orderDataList.length > 1 ? 's' : ''} in the basket are successfully generated and scheduled for sending.
    +
    {orderList.length} order{orderList.length > 1 ? 's' : ''} in the basket {orderList.length > 1 ? 'are' : 'is'} successfully generated and scheduled for sending.
    You can either:
    • copy and save link to the whole basket with all orders to track their status together;
    • @@ -58,6 +41,7 @@ export default function BasketSendResult(props) {
    • download each order in XML format separately.
    +
    )} From f1ecfaecf90b12a2a21341fa9f6e9febc007b3f6 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 12:28:31 +0200 Subject: [PATCH 50/71] fix: use Axios to load ProductDetails too, show errors, move API URL to a separate file --- cm-frontend/src/pages/ProductDetailPage.js | 36 ++++++++++++------- cm-frontend/src/services/DataService.js | 16 +++++++-- cm-frontend/src/services/DataServiceConfig.js | 2 ++ 3 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 cm-frontend/src/services/DataServiceConfig.js diff --git a/cm-frontend/src/pages/ProductDetailPage.js b/cm-frontend/src/pages/ProductDetailPage.js index 38e3328..0c1211f 100644 --- a/cm-frontend/src/pages/ProductDetailPage.js +++ b/cm-frontend/src/pages/ProductDetailPage.js @@ -46,18 +46,22 @@ export default function ProductDetailPage(props) { const [viewMode, setViewMode] = React.useState("table"); React.useEffect(() => { - loadProduct(id); + loadProduct(id).finally(() => setDataLoading(false)); }, [id]); async function loadProduct(id) { setDataLoading(true); - const response = await DataService.fetchProductDetails(id); - let res = await response.json(); - if (Array.isArray(res)) { - res = MergeService.mergeProducts(res); - } - setData(res); - setDataLoading(false); + await DataService.fetchProductDetails(id).then(response => { + let res = response.data; + if (Array.isArray(res)) { + res = MergeService.mergeProducts(res); + } + setData(res); + }).catch((error) => { + setData(null); + console.log('Error occurred: ' + error.message); + // setErrorMessage(error.message); + }); } const handleViewChange = (event) => { @@ -75,12 +79,18 @@ export default function ProductDetailPage(props) { ) : ( <> - {viewMode === "json" ? ( -
    {JSON.stringify(data, null, 2)}
    + {data !== null ? ( + <> + {viewMode === "json" ? ( +
    {JSON.stringify(data, null, 2)}
    + ) : ( + + ) + } + ) : ( - - ) - } +
    Product not found
    + )} )} diff --git a/cm-frontend/src/services/DataService.js b/cm-frontend/src/services/DataService.js index 9ba7e47..240bc15 100644 --- a/cm-frontend/src/services/DataService.js +++ b/cm-frontend/src/services/DataService.js @@ -1,10 +1,18 @@ import Axios from "axios"; +import {API_URL} from "./DataServiceConfig" -//const apiUrl = "http://localhost:8080/api"; -const apiUrl = "/dcm/api"; +Axios.defaults.timeout = 10000; + +const apiUrl = API_URL; const fetchProductDetails = (productId) => { - return fetch(apiUrl + "/products/" + productId); + return Axios.get(apiUrl + "/products/" + productId); +} +const fetchSentBasketData = (basketId) => { + return Axios.get(apiUrl + "/basket/" + basketId); +} +const fetchSentOrderData = (orderId) => { + return Axios.get(apiUrl + "/order/" + orderId); } const fetchProducts = (search, page = 0, size = 20) => { @@ -42,6 +50,8 @@ const DataService = { fetchProducts, fetchProductsByIds, sendBasket, + fetchSentBasketData, + fetchSentOrderData, uploadFiles, } diff --git a/cm-frontend/src/services/DataServiceConfig.js b/cm-frontend/src/services/DataServiceConfig.js new file mode 100644 index 0000000..9539523 --- /dev/null +++ b/cm-frontend/src/services/DataServiceConfig.js @@ -0,0 +1,2 @@ +const LOCAL_API = false; +export const API_URL = LOCAL_API ? "http://localhost:8080/dcm/api" : "/dcm/api"; From 5e331fa4712bbd50bd71b8806de6cdae2df09248 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 12:29:41 +0200 Subject: [PATCH 51/71] feature: show order details with link from basket and link back to basket, with links to products on order details (now points to orderLine.item.id - wrong) --- .../src/components/BasketSendResult.js | 15 +++- cm-frontend/src/components/OrderSendResult.js | 78 ++++++++++++++++++ .../src/components/ProductListContainer.js | 4 + cm-frontend/src/pages/OrderPage.js | 79 +++++++++++++++++++ 4 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 cm-frontend/src/components/OrderSendResult.js create mode 100644 cm-frontend/src/pages/OrderPage.js diff --git a/cm-frontend/src/components/BasketSendResult.js b/cm-frontend/src/components/BasketSendResult.js index 9921e71..337b787 100644 --- a/cm-frontend/src/components/BasketSendResult.js +++ b/cm-frontend/src/components/BasketSendResult.js @@ -11,6 +11,7 @@ import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import {StyledTableCell, StyledTableRow} from "../pages/ProductListPage"; import {DataRow, DataView} from "./ProductDetail"; +import {useHistory} from "react-router"; export default function BasketSendResult(props) { @@ -18,6 +19,8 @@ export default function BasketSendResult(props) { const {orderList, basket} = sentBasketData; + const [stateShowSuccess, setStateShowSuccess] = React.useState(showSuccess); + const useStyles = makeStyles((theme) => ({ paper: { padding: theme.spacing(2), @@ -25,11 +28,17 @@ export default function BasketSendResult(props) { }, })); + const {push} = useHistory(); + + const showRowDetails = (rowId) => { + push('/order/' + rowId); + } + const classes = useStyles(); return ( <> - {showSuccess && ( + {stateShowSuccess && ( Success
    {orderList.length} order{orderList.length > 1 ? 's' : ''} in the basket {orderList.length > 1 ? 'are' : 'is'} successfully generated and scheduled for sending.
    @@ -41,7 +50,7 @@ export default function BasketSendResult(props) {
  • download each order in XML format separately.
  • - +
    )} @@ -71,7 +80,7 @@ export default function BasketSendResult(props) { {orderList?.map((row, index) => ( - + showRowDetails(row.id)}> {(index + 1)} {row.status} {row.supplierName} diff --git a/cm-frontend/src/components/OrderSendResult.js b/cm-frontend/src/components/OrderSendResult.js new file mode 100644 index 0000000..39c0ba3 --- /dev/null +++ b/cm-frontend/src/components/OrderSendResult.js @@ -0,0 +1,78 @@ +import {makeStyles} from "@material-ui/core/styles"; +import Paper from "@material-ui/core/Paper"; +import {Button} from "@material-ui/core"; +import React from "react"; +import TableContainer from "@material-ui/core/TableContainer"; +import Table from "@material-ui/core/Table"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import {StyledTableCell, StyledTableRow} from "../pages/ProductListPage"; +import {DataRow, DataView} from "./ProductDetail"; +import {useHistory} from "react-router"; + +export default function OrderSendResult(props) { + + const {sentOrderData} = props; + + const useStyles = makeStyles((theme) => ({ + paper: { + padding: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + })); + + const classes = useStyles(); + + const {push} = useHistory(); + + const showBasketDetails = (basketId) => { + push('/basket/' + basketId); + } + const showProductDetails = (productId) => { + push('/product/view/' + productId); + } + + return ( + <> + + + + + + + + + + + + + + + + +
    + + + Line + Item name + Seller number + + + + {sentOrderData.document?.orderLine.map((row, index) => ( + showProductDetails(row.lineItem.idvalue)}> + {(index + 1)} + {row.lineItem.item.nameValue} + {row.lineItem.item.sellersItemIdentification.idvalue} + + ))} + +
    +
    +
    + + + ) +} diff --git a/cm-frontend/src/components/ProductListContainer.js b/cm-frontend/src/components/ProductListContainer.js index 891ae30..26cf3a1 100644 --- a/cm-frontend/src/components/ProductListContainer.js +++ b/cm-frontend/src/components/ProductListContainer.js @@ -11,6 +11,7 @@ import {createBasketData} from "./BasketData"; import {getOrderData} from "./OrderData"; import SendPage from "../pages/SendPage"; import BasketPage from "../pages/BasketPage"; +import OrderPage from "../pages/OrderPage"; const currentPosition = (list, id) => { if (list._cachedPos) { @@ -113,6 +114,9 @@ export function ProductListContainer() { + + + ); } diff --git a/cm-frontend/src/pages/OrderPage.js b/cm-frontend/src/pages/OrderPage.js new file mode 100644 index 0000000..2eedb18 --- /dev/null +++ b/cm-frontend/src/pages/OrderPage.js @@ -0,0 +1,79 @@ +import React from "react"; +import {makeStyles} from "@material-ui/core/styles"; +import Paper from "@material-ui/core/Paper"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import PageHeader from '../components/PageHeader'; +import DataService from "../services/DataService"; +import {useParams} from "react-router"; +import {Alert, AlertTitle} from "@material-ui/lab"; +import OrderSendResult from "../components/OrderSendResult"; + +const useStyles = makeStyles(theme => ({ + table: { + minWidth: 600, + }, + header: { + marginBottom: '1em' + }, + paper: { + padding: theme.spacing(2), + } +})); + + +export default function OrderPage() { + + const [isLoading, setLoading] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(null); + const [sentOrderData, setSentOrderData] = React.useState(null); + const [reloadCount, setReloadCount] = React.useState(0); + const classes = useStyles(); + + const {id} = useParams(); + + React.useEffect(() => { + loadOrder(id).finally(() => setLoading(false)); + }, [id, reloadCount]); + + async function loadOrder(id) { + setErrorMessage(null); + setLoading(true); + await DataService.fetchSentOrderData(id).then(response => { + let responseData = response.data; + console.log(responseData); + setSentOrderData(responseData); + }).catch((error) => { + console.log('Error occurred: ' + error.message); + setErrorMessage(error.message); + }); + } + + const refreshAction = () => { + setReloadCount(reloadCount + 1); + } + + return ( + <> + + + {(isLoading) ? ( + + + + ) : ( + <> + {errorMessage && ( + + Error +
    {errorMessage}
    +
    + )} + {sentOrderData && ( + + )} + + )} + + ); + +} \ No newline at end of file From 061cfa9663e6f9305468ad77d3b476501579f493 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 12:33:49 +0200 Subject: [PATCH 52/71] fix: set in Order XML /OrderLine/LineItem/ID to product.id so it is easy to navigate to a product in order details --- .../main/java/dk/erst/cm/api/order/OrderProducerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java index 945f579..ef4a08d 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java @@ -97,7 +97,7 @@ public OrderType generateOrder(dk.erst.cm.api.data.Order dataOrder, CustomerOrde OrderLineType line = new OrderLineType(); LineItemType lineItem = new LineItemType(); - lineItem.setID(String.valueOf(i + 1)); + lineItem.setID(product.getId()); lineItem.setQuantity(BigDecimal.valueOf(1)); lineItem.getQuantity().setUnitCode("EA"); ItemType item = new ItemType(); From 0146bb419fbb8a6612b26c3c38e29a5bf4937dee Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 12:34:07 +0200 Subject: [PATCH 53/71] chore: import Order --- .../main/java/dk/erst/cm/api/order/OrderProducerService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java index ef4a08d..c4637a5 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Service; +import dk.erst.cm.api.data.Order; import dk.erst.cm.api.data.Product; import dk.erst.cm.api.order.data.CustomerOrderData; import dk.erst.cm.xml.ubl21.model.CatalogueLine; @@ -55,7 +56,7 @@ public PartyInfo(String defaultId, String defaultSchemeID, String defaultName) { } } - public OrderType generateOrder(dk.erst.cm.api.data.Order dataOrder, CustomerOrderData customerOrderData, List productList) { + public OrderType generateOrder(Order dataOrder, CustomerOrderData customerOrderData, List productList) { OrderType order = new OrderType(); order.setCustomizationID("urn:fdc:peppol.eu:poacc:trns:order:3"); From f2e04ed7ad89f6b4bdd4faa0ac237b508cd56ed4 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 12:47:26 +0200 Subject: [PATCH 54/71] chore: add cross icon to Success alert on basket sent in addition to CLOSE button --- cm-frontend/src/components/BasketSendResult.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cm-frontend/src/components/BasketSendResult.js b/cm-frontend/src/components/BasketSendResult.js index 337b787..ae09f83 100644 --- a/cm-frontend/src/components/BasketSendResult.js +++ b/cm-frontend/src/components/BasketSendResult.js @@ -1,6 +1,6 @@ import {makeStyles} from "@material-ui/core/styles"; import Paper from "@material-ui/core/Paper"; -import {Button} from "@material-ui/core"; +import {Button, IconButton} from "@material-ui/core"; import React from "react"; import {Alert, AlertTitle} from "@material-ui/lab"; import TableContainer from "@material-ui/core/TableContainer"; @@ -12,6 +12,7 @@ import TableCell from "@material-ui/core/TableCell"; import {StyledTableCell, StyledTableRow} from "../pages/ProductListPage"; import {DataRow, DataView} from "./ProductDetail"; import {useHistory} from "react-router"; +import CloseIcon from "@material-ui/icons/Close"; export default function BasketSendResult(props) { @@ -34,12 +35,20 @@ export default function BasketSendResult(props) { push('/order/' + rowId); } + const closeAlertAction = () => setStateShowSuccess(false); + const classes = useStyles(); return ( <> {stateShowSuccess && ( - + { + closeAlertAction() + }}> + + + }> Success
    {orderList.length} order{orderList.length > 1 ? 's' : ''} in the basket {orderList.length > 1 ? 'are' : 'is'} successfully generated and scheduled for sending.
    You can either: From ac291e83e50e7bd8c88e1017efb8ce29fee90ca6 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 12:52:00 +0200 Subject: [PATCH 55/71] chore: order details --- cm-frontend/src/components/OrderSendResult.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cm-frontend/src/components/OrderSendResult.js b/cm-frontend/src/components/OrderSendResult.js index 39c0ba3..8fc41c4 100644 --- a/cm-frontend/src/components/OrderSendResult.js +++ b/cm-frontend/src/components/OrderSendResult.js @@ -50,13 +50,12 @@ export default function OrderSendResult(props) { - - + Line - Item name + Name Seller number From be669edeecc322b828778e443bda293f5c184b44 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 12:59:48 +0200 Subject: [PATCH 56/71] feature: show json model of Order --- cm-frontend/src/components/OrderSendResult.js | 56 ++++++++++++------- cm-frontend/src/pages/ProductDetailPage.js | 2 +- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/cm-frontend/src/components/OrderSendResult.js b/cm-frontend/src/components/OrderSendResult.js index 8fc41c4..cda57a3 100644 --- a/cm-frontend/src/components/OrderSendResult.js +++ b/cm-frontend/src/components/OrderSendResult.js @@ -11,11 +11,17 @@ import TableCell from "@material-ui/core/TableCell"; import {StyledTableCell, StyledTableRow} from "../pages/ProductListPage"; import {DataRow, DataView} from "./ProductDetail"; import {useHistory} from "react-router"; +import {ViewToggle} from "../pages/ProductDetailPage"; export default function OrderSendResult(props) { const {sentOrderData} = props; + const [viewMode, setViewMode] = React.useState("table"); + + const handleViewChange = (event) => { + setViewMode(event.target.value); + } const useStyles = makeStyles((theme) => ({ paper: { padding: theme.spacing(2), @@ -49,27 +55,37 @@ export default function OrderSendResult(props) { + + + - -
    - - - Line - Name - Seller number - - - - {sentOrderData.document?.orderLine.map((row, index) => ( - showProductDetails(row.lineItem.idvalue)}> - {(index + 1)} - {row.lineItem.item.nameValue} - {row.lineItem.item.sellersItemIdentification.idvalue} - - ))} - -
    -
    + + {viewMode === "json" ? ( +
    {JSON.stringify(sentOrderData, null, 2)}
    + ) : ( + + + + + + Line + Name + Seller number + + + + {sentOrderData.document?.orderLine.map((row, index) => ( + showProductDetails(row.lineItem.idvalue)}> + {(index + 1)} + {row.lineItem.item.nameValue} + {row.lineItem.item.sellersItemIdentification.idvalue} + + ))} + +
    +
    + )} +
    diff --git a/cm-frontend/src/pages/ProductDetailPage.js b/cm-frontend/src/pages/ProductDetailPage.js index 0c1211f..0265b85 100644 --- a/cm-frontend/src/pages/ProductDetailPage.js +++ b/cm-frontend/src/pages/ProductDetailPage.js @@ -20,7 +20,7 @@ const useStyles = makeStyles(theme => ({ } })); -function ViewToggle(props) { +export function ViewToggle(props) { return ( From e5d0bb803d2dbc433679b62fce0e100e868ef131 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 13:20:27 +0200 Subject: [PATCH 57/71] feature: hide empty fields when serializing to JSON by setting spring.jackson.default-property-inclusion=NON_EMPTY - e.g. for sent Order details --- cm-webapi/src/main/resources/application.properties | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cm-webapi/src/main/resources/application.properties b/cm-webapi/src/main/resources/application.properties index 35aac04..55803d6 100644 --- a/cm-webapi/src/main/resources/application.properties +++ b/cm-webapi/src/main/resources/application.properties @@ -15,10 +15,13 @@ logging.level.org.springframework.data.mongodb.core.MongoTemplate=INFO # Set to DEBUG to see OrderController details logging.level.dk.erst.cm.api.order=DEBUG -logging.level.dk.erst.cm.MongoConverterConfig=DEBUG +#logging.level.dk.erst.cm.MongoConverterConfig=DEBUG spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB server.tomcat.accesslog.enabled=true -server.tomcat.basedir=./.tomcat \ No newline at end of file +server.tomcat.basedir=./.tomcat + +# Needed to avoid serializing all many fields of Order to JSON for showing on GUI +spring.jackson.default-property-inclusion=NON_EMPTY \ No newline at end of file From 21065a4a563d519dbdd8decbf5bed3e262aa37a7 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 14:35:05 +0200 Subject: [PATCH 58/71] feature: implement copy link and copy basket link on basket and order details --- .../src/components/BasketSendResult.js | 82 ++++++++++--------- cm-frontend/src/components/OrderSendResult.js | 14 +++- cm-frontend/src/components/SmallSnackbar.js | 21 +++++ cm-frontend/src/services/ClipboardService.js | 24 ++++++ 4 files changed, 98 insertions(+), 43 deletions(-) create mode 100644 cm-frontend/src/components/SmallSnackbar.js create mode 100644 cm-frontend/src/services/ClipboardService.js diff --git a/cm-frontend/src/components/BasketSendResult.js b/cm-frontend/src/components/BasketSendResult.js index ae09f83..7ad1214 100644 --- a/cm-frontend/src/components/BasketSendResult.js +++ b/cm-frontend/src/components/BasketSendResult.js @@ -13,19 +13,21 @@ import {StyledTableCell, StyledTableRow} from "../pages/ProductListPage"; import {DataRow, DataView} from "./ProductDetail"; import {useHistory} from "react-router"; import CloseIcon from "@material-ui/icons/Close"; +import SmallSnackbar from "./SmallSnackbar"; +import {copyCurrentUrlToClipboard, copySubUrlToClipboard} from "../services/ClipboardService"; export default function BasketSendResult(props) { const {showSuccess = false, sentBasketData} = props; const {orderList, basket} = sentBasketData; + const [showSnackBar, setShowSnackBar] = React.useState(false); const [stateShowSuccess, setStateShowSuccess] = React.useState(showSuccess); const useStyles = makeStyles((theme) => ({ paper: { - padding: theme.spacing(2), - marginBottom: theme.spacing(2), + padding: theme.spacing(2), marginBottom: theme.spacing(2), }, })); @@ -37,31 +39,32 @@ export default function BasketSendResult(props) { const closeAlertAction = () => setStateShowSuccess(false); + const copyOrderUrl = (e, orderId) => { + e.stopPropagation(); + copySubUrlToClipboard('/order/' + orderId, () => setShowSnackBar(true)); + return false; + } + const classes = useStyles(); - return ( - <> - {stateShowSuccess && ( - { - closeAlertAction() - }}> - - - }> - Success -
    {orderList.length} order{orderList.length > 1 ? 's' : ''} in the basket {orderList.length > 1 ? 'are' : 'is'} successfully generated and scheduled for sending.
    -
    You can either: -
      -
    • copy and save link to the whole basket with all orders to track their status together;
    • -
    • download all orders in XML format;
    • -
    • copy and save links to each order separately to track their status;
    • -
    • download each order in XML format separately.
    • -
    -
    - -
    - )} + return (<> + {stateShowSuccess && ( { + closeAlertAction() + }}> + + }> + Success +
    {orderList.length} order{orderList.length > 1 ? 's' : ''} in the basket {orderList.length > 1 ? 'are' : 'is'} successfully generated and scheduled for sending.
    +
    You can either: +
      +
    • copy and save link to the whole basket with all orders to track their status together;
    • +
    • download all orders in XML format;
    • +
    • copy and save links to each order separately to track their status;
    • +
    • download each order in XML format separately.
    • +
    +
    + +
    )} @@ -70,7 +73,7 @@ export default function BasketSendResult(props) { - + @@ -88,23 +91,24 @@ export default function BasketSendResult(props) { - {orderList?.map((row, index) => ( - showRowDetails(row.id)}> - {(index + 1)} - {row.status} - {row.supplierName} - {row.orderNumber} - {row.lineCount} - - - - - - ))} + {orderList?.map((row, index) => ( showRowDetails(row.id)}> + {(index + 1)} + {row.status} + {row.supplierName} + {row.orderNumber} + {row.lineCount} + + + + + ))} + + setShowSnackBar(false)}/> + ) diff --git a/cm-frontend/src/components/OrderSendResult.js b/cm-frontend/src/components/OrderSendResult.js index cda57a3..392da58 100644 --- a/cm-frontend/src/components/OrderSendResult.js +++ b/cm-frontend/src/components/OrderSendResult.js @@ -12,12 +12,15 @@ import {StyledTableCell, StyledTableRow} from "../pages/ProductListPage"; import {DataRow, DataView} from "./ProductDetail"; import {useHistory} from "react-router"; import {ViewToggle} from "../pages/ProductDetailPage"; +import SmallSnackbar from "./SmallSnackbar"; +import {copyCurrentUrlToClipboard} from "../services/ClipboardService"; export default function OrderSendResult(props) { const {sentOrderData} = props; const [viewMode, setViewMode] = React.useState("table"); + const [showSnackBar, setShowSnackBar] = React.useState(false); const handleViewChange = (event) => { setViewMode(event.target.value); @@ -29,6 +32,7 @@ export default function OrderSendResult(props) { }, })); + const classes = useStyles(); const {push} = useHistory(); @@ -52,7 +56,7 @@ export default function OrderSendResult(props) { - + @@ -60,9 +64,9 @@ export default function OrderSendResult(props) { - {viewMode === "json" ? ( -
    {JSON.stringify(sentOrderData, null, 2)}
    - ) : ( + {viewMode === "json" ? ( +
    {JSON.stringify(sentOrderData, null, 2)}
    + ) : ( @@ -87,6 +91,8 @@ export default function OrderSendResult(props) { )} + + setShowSnackBar(false)}/> ) diff --git a/cm-frontend/src/components/SmallSnackbar.js b/cm-frontend/src/components/SmallSnackbar.js new file mode 100644 index 0000000..56c631e --- /dev/null +++ b/cm-frontend/src/components/SmallSnackbar.js @@ -0,0 +1,21 @@ +import {Alert} from "@material-ui/lab"; +import React from "react"; +import {Snackbar} from "@material-ui/core"; + +export default function SmallSnackbar(props) { + + const {message = "Copied", opened, hide} = props; + + const handleClose = (event, reason) => { + if (reason === 'clickaway') { + return; + } + hide(); + }; + + return ( + + {message} + + ) +} \ No newline at end of file diff --git a/cm-frontend/src/services/ClipboardService.js b/cm-frontend/src/services/ClipboardService.js new file mode 100644 index 0000000..ba02678 --- /dev/null +++ b/cm-frontend/src/services/ClipboardService.js @@ -0,0 +1,24 @@ +export const copyTextToClipboard = (value, postCopyFunction) => { + try { + navigator.clipboard.writeText(value).then(() => { + if (postCopyFunction) { + postCopyFunction(); + } + }); + } catch { + } +} + +export function copyCurrentUrlToClipboard(postCopyFunction) { + copyTextToClipboard(window.location.href, postCopyFunction); +} + +const ROOT_CONTEXT = '/dcm/'; + +export function copySubUrlToClipboard(subPath, postCopyFunction) { + const currentUrl = window.location.href; + const i = currentUrl.indexOf(ROOT_CONTEXT); + const copyPath = currentUrl.substring(0, i + ROOT_CONTEXT.length - 1) + subPath; + copyTextToClipboard(copyPath, postCopyFunction); +} + From a4c436ddfe4227b77343f83d08372841079c34bc Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 12 Dec 2021 15:12:22 +0200 Subject: [PATCH 59/71] feature: add config for endpointID and default note in order --- .../dk/erst/cm/api/order/BasketService.java | 11 ++++++-- .../cm/api/order/OrderProducerService.java | 26 ++++++++++++++----- .../api/order/OrderProducerServiceTest.java | 11 +++++++- .../main/java/dk/erst/cm/AppProperties.java | 9 +++++-- .../dk/erst/cm/CatalogApiApplication.java | 2 ++ .../dk/erst/cm/webapi/OrderController.java | 2 +- .../src/main/resources/application.properties | 2 ++ 7 files changed, 51 insertions(+), 12 deletions(-) diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java index 952eaca..d31f136 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java @@ -24,6 +24,8 @@ import dk.erst.cm.api.data.Product; import dk.erst.cm.api.item.CatalogService; import dk.erst.cm.api.item.ProductService; +import dk.erst.cm.api.order.OrderProducerService.OrderDefaultConfig; +import dk.erst.cm.api.order.OrderProducerService.PartyInfo; import dk.erst.cm.api.order.data.CustomerOrderData; import dk.erst.cm.xml.ubl21.model.Party; import lombok.Data; @@ -91,7 +93,7 @@ public static SendBasketResponse success(String basketId) { } } - public SendBasketResponse basketSend(SendBasketData query, String outboxFolder) { + public SendBasketResponse basketSend(SendBasketData query, String outboxFolder, OrderDefaultConfig orderConfig) { Set queryProductIdSet = query.basketData.orderLines.keySet(); Iterable products = productService.findAllByIds(queryProductIdSet); @@ -167,7 +169,12 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder) order.setOrderNumber(generateOrderNumber(order)); order.setSupplierName(extractSupplierName(sellerPartyByCatalog.get(catalogId))); order.setVersion(1); - OrderType sendOrder = orderProducerService.generateOrder(order, query.getOrderData(), productList); + + CustomerOrderData customerOrderData = query.getOrderData(); + PartyInfo buyer = new PartyInfo("5798009882806", "0088", customerOrderData.getBuyerCompany().getRegistrationName()); + PartyInfo seller = new PartyInfo("5798009882783", "0088", "Danish Company"); + + OrderType sendOrder = orderProducerService.generateOrder(order, orderConfig, buyer, seller, productList); order.setDocument(sendOrder); orderList.add(order); diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java index c4637a5..453d77f 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java @@ -5,15 +5,16 @@ import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; -import java.util.UUID; import org.springframework.stereotype.Service; import dk.erst.cm.api.data.Order; import dk.erst.cm.api.data.Product; -import dk.erst.cm.api.order.data.CustomerOrderData; import dk.erst.cm.xml.ubl21.model.CatalogueLine; import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.AddressType; import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.CountryType; import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.CustomerPartyType; @@ -27,11 +28,20 @@ import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.PartyType; import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.PeriodType; import oasis.names.specification.ubl.schema.xsd.commonaggregatecomponents_21.SupplierPartyType; +import oasis.names.specification.ubl.schema.xsd.commonbasiccomponents_21.NoteType; import oasis.names.specification.ubl.schema.xsd.order_21.OrderType; @Service public class OrderProducerService { + @Getter + @Setter + @ToString + public static class OrderDefaultConfig { + private String endpointGLN; + private String note; + } + @Data public static class PartyInfo { @@ -56,22 +66,26 @@ public PartyInfo(String defaultId, String defaultSchemeID, String defaultName) { } } - public OrderType generateOrder(Order dataOrder, CustomerOrderData customerOrderData, List productList) { + public OrderType generateOrder(Order dataOrder, OrderDefaultConfig defaultConfig, PartyInfo buyer, PartyInfo seller, List productList) { OrderType order = new OrderType(); order.setCustomizationID("urn:fdc:peppol.eu:poacc:trns:order:3"); order.setProfileID("urn:fdc:peppol.eu:poacc:bis:order_only:3"); - order.setID(UUID.randomUUID().toString()); + order.setUUID(dataOrder.getId()); + order.setID(dataOrder.getOrderNumber()); order.setIssueDate(dataOrder.getCreateTime().atZone(ZoneOffset.UTC).toLocalDate()); order.setIssueTime(dataOrder.getCreateTime().atZone(ZoneOffset.UTC).toLocalTime()); order.setDocumentCurrencyCode("DKK"); + if (defaultConfig.getNote() != null) { + order.getNote().add(new NoteType(defaultConfig.getNote())); + } CustomerPartyType buyerCustomerParty = new CustomerPartyType(); - buyerCustomerParty.setParty(buildParty(new PartyInfo("5798009882806", "0088", customerOrderData.getBuyerCompany().getRegistrationName()))); + buyerCustomerParty.setParty(buildParty(buyer)); order.setBuyerCustomerParty(buyerCustomerParty); SupplierPartyType supplierPartyType = new SupplierPartyType(); - supplierPartyType.setParty(buildParty(new PartyInfo("5798009882783", "0088", "Danish Company"))); + supplierPartyType.setParty(buildParty(seller)); order.setSellerSupplierParty(supplierPartyType); AddressType supplierAddress = new AddressType(); diff --git a/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java index 4e94ec2..3df63ce 100644 --- a/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java +++ b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java @@ -22,6 +22,8 @@ import dk.erst.cm.api.data.Order; import dk.erst.cm.api.data.Product; +import dk.erst.cm.api.order.OrderProducerService.OrderDefaultConfig; +import dk.erst.cm.api.order.OrderProducerService.PartyInfo; import dk.erst.cm.api.order.data.CustomerOrderData; import dk.erst.cm.api.order.data.CustomerOrderData.Company; import dk.erst.cm.xml.ubl21.model.CatalogueLine; @@ -70,7 +72,13 @@ void produce() { customerOrderData.setBuyerCompany(buyerCompany); Order dataOrder = new Order(); dataOrder.setCreateTime(Instant.now()); - OrderType order = service.generateOrder(dataOrder, customerOrderData, productList); + + PartyInfo buyer = new PartyInfo("5798009882806", "0088", "Buyer Company"); + PartyInfo seller = new PartyInfo("5798009882783", "0088", "Seller Company"); + + OrderDefaultConfig defaultConfig = new OrderDefaultConfig(); + defaultConfig.setNote("TEST NOTE"); + OrderType order = service.generateOrder(dataOrder, defaultConfig, buyer, seller, productList); IErrorList errorList = UBL21Validator.order().validate(order); if (errorList.isNotEmpty()) { log.error("Found " + errorList.size() + " errors:"); @@ -84,6 +92,7 @@ void produce() { String xml = new String(out.toByteArray(), StandardCharsets.UTF_8); log.info(xml); assertTrue(xml.indexOf(order.getSellerSupplierParty().getParty().getPostalAddress().getCountry().getIdentificationCodeValue()) > 0); + assertTrue(xml.indexOf(defaultConfig.getNote()) > 0); } } \ No newline at end of file diff --git a/cm-webapi/src/main/java/dk/erst/cm/AppProperties.java b/cm-webapi/src/main/java/dk/erst/cm/AppProperties.java index 0c33b9c..21c5096 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/AppProperties.java +++ b/cm-webapi/src/main/java/dk/erst/cm/AppProperties.java @@ -1,11 +1,13 @@ package dk.erst.cm; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import dk.erst.cm.api.order.OrderProducerService.OrderDefaultConfig; import lombok.Data; import lombok.Getter; import lombok.Setter; import lombok.ToString; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties(prefix = "app") @@ -13,6 +15,8 @@ public class AppProperties { private IntegrationProperties integration; + + private OrderDefaultConfig order; @Getter @Setter @@ -22,4 +26,5 @@ public static class IntegrationProperties { private String inboxOrderResponse; private String outboxOrder; } + } diff --git a/cm-webapi/src/main/java/dk/erst/cm/CatalogApiApplication.java b/cm-webapi/src/main/java/dk/erst/cm/CatalogApiApplication.java index 8661c26..aa5bf62 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/CatalogApiApplication.java +++ b/cm-webapi/src/main/java/dk/erst/cm/CatalogApiApplication.java @@ -3,8 +3,10 @@ import org.springframework.boot.Banner.Mode; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication +@EnableConfigurationProperties(AppProperties.class) public class CatalogApiApplication { public static void main(String[] args) { diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java index 9df63d0..0efd435 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java @@ -36,7 +36,7 @@ public OrderController(BasketService basketService, AppProperties appProperties) @RequestMapping(value = "/api/basket/send") public SendBasketResponse basketSend(@RequestBody SendBasketData query) { log.info("START basketSend: query=" + query); - SendBasketResponse res = basketService.basketSend(query, appProperties.getIntegration().getOutboxOrder()); + SendBasketResponse res = basketService.basketSend(query, appProperties.getIntegration().getOutboxOrder(), appProperties.getOrder()); if (res.isSuccess()) { log.info("END basketSend OK: " + res); } else { diff --git a/cm-webapi/src/main/resources/application.properties b/cm-webapi/src/main/resources/application.properties index 55803d6..c39675f 100644 --- a/cm-webapi/src/main/resources/application.properties +++ b/cm-webapi/src/main/resources/application.properties @@ -8,6 +8,8 @@ spring.data.mongodb.auto-index-creation = true app.integration.inbox-catalogue=./.integration/inbox/catalogue app.integration.inbox-order-response=./.integration/inbox/orderresponse app.integration.outbox-order=./.integration/outbox/order +app.order.endpointGLN=5798009882783 +app.order.note=This order is generated for education purposes via Delis Catalogue. # Set to DEBUG to see all Mongo queries logging.level.org.springframework.data.mongodb.core.MongoTemplate=INFO From 30727ef5df01062ce22a3720ac7bcdc44bb6b801 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Mon, 13 Dec 2021 10:07:09 +0200 Subject: [PATCH 60/71] chore: move xsd files for JAXB generation inside the project to solve the problem on Linux --- cm-resources/structure/README.md | 2 -- cm-xml-codelist/README.md | 4 +++- cm-xml-codelist/pom.xml | 3 --- .../structure => cm-xml-codelist/src/main/xsd}/codelist-1.xsd | 0 cm-xml-syntax/README.md | 4 +++- cm-xml-syntax/pom.xml | 3 --- .../structure => cm-xml-syntax/src/main/xsd}/structure-1.xsd | 0 7 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 cm-resources/structure/README.md rename {cm-resources/structure => cm-xml-codelist/src/main/xsd}/codelist-1.xsd (100%) rename {cm-resources/structure => cm-xml-syntax/src/main/xsd}/structure-1.xsd (100%) diff --git a/cm-resources/structure/README.md b/cm-resources/structure/README.md deleted file mode 100644 index 384c20b..0000000 --- a/cm-resources/structure/README.md +++ /dev/null @@ -1,2 +0,0 @@ -structure-1.xsd - copied from -codelist-1.xsd - generated by xml \ No newline at end of file diff --git a/cm-xml-codelist/README.md b/cm-xml-codelist/README.md index 9e9dd23..cd17943 100644 --- a/cm-xml-codelist/README.md +++ b/cm-xml-codelist/README.md @@ -6,4 +6,6 @@ It allows to have a model of Peppol document types with next information: - name - description - validation rules -- attributes, their code lists, cardinality \ No newline at end of file +- attributes, their code lists, cardinality + +codelist-xsd is generated by xml \ No newline at end of file diff --git a/cm-xml-codelist/pom.xml b/cm-xml-codelist/pom.xml index 55fc4a2..01fac10 100644 --- a/cm-xml-codelist/pom.xml +++ b/cm-xml-codelist/pom.xml @@ -61,9 +61,6 @@ UTF-8 dk.erst.cm.xml.syntax.codelist - - ../cm-resources/structure/codelist-1.xsd - -Xcommons-lang diff --git a/cm-resources/structure/codelist-1.xsd b/cm-xml-codelist/src/main/xsd/codelist-1.xsd similarity index 100% rename from cm-resources/structure/codelist-1.xsd rename to cm-xml-codelist/src/main/xsd/codelist-1.xsd diff --git a/cm-xml-syntax/README.md b/cm-xml-syntax/README.md index 9e9dd23..36067f9 100644 --- a/cm-xml-syntax/README.md +++ b/cm-xml-syntax/README.md @@ -6,4 +6,6 @@ It allows to have a model of Peppol document types with next information: - name - description - validation rules -- attributes, their code lists, cardinality \ No newline at end of file +- attributes, their code lists, cardinality + +structure-1.xsd - copied from diff --git a/cm-xml-syntax/pom.xml b/cm-xml-syntax/pom.xml index b1ef52b..e3a6628 100644 --- a/cm-xml-syntax/pom.xml +++ b/cm-xml-syntax/pom.xml @@ -53,9 +53,6 @@ UTF-8 dk.erst.cm.xml.syntax.structure - - ../cm-resources/structure/structure-1.xsd - -Xcommons-lang diff --git a/cm-resources/structure/structure-1.xsd b/cm-xml-syntax/src/main/xsd/structure-1.xsd similarity index 100% rename from cm-resources/structure/structure-1.xsd rename to cm-xml-syntax/src/main/xsd/structure-1.xsd From a7181bba93f43558c2a5051faf91f20af738c5fc Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Mon, 13 Dec 2021 10:11:49 +0200 Subject: [PATCH 61/71] chore: add more details about absolute path of code list --- .../main/java/dk/erst/cm/xml/syntax/CodeListLoadService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cm-xml-codelist/src/main/java/dk/erst/cm/xml/syntax/CodeListLoadService.java b/cm-xml-codelist/src/main/java/dk/erst/cm/xml/syntax/CodeListLoadService.java index 5c002b3..9b52779 100644 --- a/cm-xml-codelist/src/main/java/dk/erst/cm/xml/syntax/CodeListLoadService.java +++ b/cm-xml-codelist/src/main/java/dk/erst/cm/xml/syntax/CodeListLoadService.java @@ -22,10 +22,11 @@ public CodeList loadStructure(InputStream is, String description) throws JAXBExc public CodeList loadCodeList(CodeListStandard standard) { String pathname = "../cm-resources/structure/codelist/" + standard.getResourceName() + ".xml"; - try (InputStream is = new FileInputStream(new File(pathname))) { + File file = new File(pathname); + try (InputStream is = new FileInputStream(file)) { return this.loadStructure(is, pathname); } catch (Exception e) { - throw new IllegalStateException("Failed to load code list standard " + standard + " by path " + pathname, e); + throw new IllegalStateException("Failed to load code list standard " + standard + " by path " + pathname + " resovled to " + file.getAbsolutePath(), e); } } } From ac05fba89547e7a6710e1c414e01a89c3268404f Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Mon, 13 Dec 2021 10:20:42 +0200 Subject: [PATCH 62/71] fix: wrong case of parameters in code list definitions --- .../src/main/java/dk/erst/cm/xml/syntax/CodeListStandard.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cm-xml-codelist/src/main/java/dk/erst/cm/xml/syntax/CodeListStandard.java b/cm-xml-codelist/src/main/java/dk/erst/cm/xml/syntax/CodeListStandard.java index 9f47822..b2ad775 100644 --- a/cm-xml-codelist/src/main/java/dk/erst/cm/xml/syntax/CodeListStandard.java +++ b/cm-xml-codelist/src/main/java/dk/erst/cm/xml/syntax/CodeListStandard.java @@ -49,7 +49,7 @@ public enum CodeListStandard { UNECERec20("UNECERec20-11e"), - EAS("EAS", "eas"), + EAS("eas", "EAS"), EHF1_ActionCode_documentLevel("ehf-postaward-g2/actioncode-documentlevel", "Actioncodedocumentlevel"), From 80a20e414a9f373e949de2d4e82e2a35f2a63e1d Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 1 May 2022 01:14:31 +0300 Subject: [PATCH 63/71] Finally, solved the problem with JSON serialization of OrderType from com.helger.ubl:ph-ubl21 The issue was that IssueDate/IssueTime were serialized as below: "issueDate": { "value": { "dayOfYear": 120, "dayOfWeek": "SATURDAY", "year": 2022, "month": "APRIL", "monthValue": 4, "dayOfMonth": 30, "asString": "2022-04-30Z", "offset": "Z" }, "valueLocal": "2022-04-30" }, "issueTime": { "value": { "nano": 639000000, "hour": 21, "minute": 39, "second": 42, "asString": "21:39:42.639Z", "offset": "Z" }, "valueLocal": "21:39:42.639" } Adding custom serializers to MongoConverterConfig looked like did not help - but it was because serialization in Spring Controller response used Jackson serialization! Instead of writing own binding of Order (and potentially future other UBL 2.1 document types), instead of object response with Jackson serialization, MongoTemplate direct query is added to OrderService to load raw Mongo JSON in BSON format, which gives a benefit - same presentation in Mongo and on GUI. --- .gitignore | 3 +- .../dk/erst/cm/api/order/BasketService.java | 4 +- .../dk/erst/cm/api/order/OrderService.java | 131 ++++++++++------- cm-frontend/src/components/OrderSendResult.js | 10 +- .../java/dk/erst/cm/MongoConverterConfig.java | 139 ++++++++---------- .../dk/erst/cm/webapi/OrderController.java | 20 ++- 6 files changed, 157 insertions(+), 150 deletions(-) diff --git a/.gitignore b/.gitignore index 16fb120..16b5027 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,5 @@ log/ # Skip VisualStudio Code folders .vscode/ -.tomcat/ \ No newline at end of file +.tomcat/ +.integration/ diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java index d31f136..020a259 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java @@ -326,7 +326,7 @@ public Optional loadSentBasketData(String basketId) { return Optional.empty(); } - public Optional loadSentOrder(String orderId) { - return this.orderService.findOrderById(orderId); + public Optional loadSentOrderAsJSON(String orderId) { + return this.orderService.findOrderByIdAsJSON(orderId); } } diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java index a4acd7f..eb36657 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java @@ -1,76 +1,101 @@ package dk.erst.cm.api.order; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.time.Instant; -import java.util.List; -import java.util.Optional; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - import com.helger.ubl21.UBL21Writer; - import dk.erst.cm.api.dao.mongo.BasketRepository; import dk.erst.cm.api.dao.mongo.OrderRepository; import dk.erst.cm.api.data.Basket; import dk.erst.cm.api.data.Order; import dk.erst.cm.api.data.OrderStatus; import oasis.names.specification.ubl.schema.xsd.order_21.OrderType; +import org.bson.Document; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.stereotype.Service; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.time.Instant; +import java.util.List; +import java.util.Optional; @Service public class OrderService { - private final BasketRepository basketRepository; - private final OrderRepository orderRepository; + private final BasketRepository basketRepository; + private final OrderRepository orderRepository; + private final MongoTemplate mongoTemplate; + + @Autowired + public OrderService(MongoTemplate mongoTemplate, BasketRepository basketRepository, OrderRepository orderRepository) { + this.mongoTemplate = mongoTemplate; + this.basketRepository = basketRepository; + this.orderRepository = orderRepository; + } - @Autowired - public OrderService(BasketRepository basketRepository, OrderRepository orderRepository) { - this.basketRepository = basketRepository; - this.orderRepository = orderRepository; - } + public Optional findBasketById(String basketId) { + return basketRepository.findById(basketId); + } - public Optional findBasketById(String basketId) { - return basketRepository.findById(basketId); - } + public List findOrdersByBasketId(String basketId) { + return orderRepository.findByBasketId(basketId); + } - public List findOrdersByBasketId(String basketId) { - return orderRepository.findByBasketId(basketId); - } + @SuppressWarnings("unused") + public Optional findOrderById(String orderId) { + return orderRepository.findById(orderId); + } - public Optional findOrderById(String orderId) { - return orderRepository.findById(orderId); - } + /* + * If we return to controller an object of OrderType, it is serialized to JSON with Jackson. + * + * As a result, 2 different ways to serialize objects are used - MongoConverterConfig to save into MongoDB and Jackson to render on GUI. + * + * Moreover, it is quite difficult to configure them to serialize so complex structures as OrderType in the same way, + * so let's avoid double conversion and use the same approach for both. + * + * Finally, it gives us the opportunity to see exact values of MongoDB document fields, useful for querying. + */ + public Optional findOrderByIdAsJSON(String id) { + String result = mongoTemplate.execute("order", collection -> { + Document d = collection.find(new Document("_id", id)).first(); + if (d != null) { + return d.toJson(); + } + return null; + }); + return Optional.ofNullable(result); + } - public void saveBasket(Basket basket) { - this.basketRepository.save(basket); - } + public void saveBasket(Basket basket) { + this.basketRepository.save(basket); + } - public void saveOrder(Order order) { - this.orderRepository.save(order); - } + public void saveOrder(Order order) { + this.orderRepository.save(order); + } - public void updateOrderStatus(String orderId, OrderStatus status) { - Optional optionalOrder = this.orderRepository.findById(orderId); - if (optionalOrder.isPresent()) { - Order order = optionalOrder.get(); - if (status == OrderStatus.DELIVERED) { - order.setDeliveredDate(Instant.now()); - } - order.setStatus(status); - this.orderRepository.save(order); - } - } + @SuppressWarnings("unused") + public void updateOrderStatus(String orderId, OrderStatus status) { + Optional optionalOrder = this.orderRepository.findById(orderId); + if (optionalOrder.isPresent()) { + Order order = optionalOrder.get(); + if (status == OrderStatus.DELIVERED) { + order.setDeliveredDate(Instant.now()); + } + order.setStatus(status); + this.orderRepository.save(order); + } + } - public File saveOrderXML(File directory, OrderType sendOrder) throws IOException { - File tempFile = new File(directory, "delis-cm-" + sendOrder.getIDValue() + ".xml"); - try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile))) { - UBL21Writer.order().write(sendOrder, out); - } - return tempFile; - } + public File saveOrderXML(File directory, OrderType sendOrder) throws IOException { + File tempFile = new File(directory, "delis-cm-" + sendOrder.getIDValue() + ".xml"); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tempFile.toPath()))) { + UBL21Writer.order().write(sendOrder, out); + } + return tempFile; + } } diff --git a/cm-frontend/src/components/OrderSendResult.js b/cm-frontend/src/components/OrderSendResult.js index 392da58..9fb2e2d 100644 --- a/cm-frontend/src/components/OrderSendResult.js +++ b/cm-frontend/src/components/OrderSendResult.js @@ -47,7 +47,7 @@ export default function OrderSendResult(props) { return ( <> - + @@ -73,16 +73,18 @@ export default function OrderSendResult(props) { Line + Quantity Name Seller number {sentOrderData.document?.orderLine.map((row, index) => ( - showProductDetails(row.lineItem.idvalue)}> + showProductDetails(row.lineItem._id.value)}> {(index + 1)} - {row.lineItem.item.nameValue} - {row.lineItem.item.sellersItemIdentification.idvalue} + {row.lineItem.quantity.value} {row.lineItem.quantity.unitCode} + {row.lineItem.item.name.value} + {row.lineItem.item.sellersItemIdentification._id.value} ))} diff --git a/cm-webapi/src/main/java/dk/erst/cm/MongoConverterConfig.java b/cm-webapi/src/main/java/dk/erst/cm/MongoConverterConfig.java index 4d5457d..359239b 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/MongoConverterConfig.java +++ b/cm-webapi/src/main/java/dk/erst/cm/MongoConverterConfig.java @@ -1,12 +1,8 @@ package dk.erst.cm; -import java.time.Instant; -import java.time.LocalDate; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.Date; - +import com.helger.commons.datetime.XMLOffsetDate; +import com.helger.commons.datetime.XMLOffsetTime; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; @@ -14,81 +10,66 @@ import org.springframework.data.convert.WritingConverter; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; -import com.helger.commons.datetime.XMLOffsetDate; -import com.helger.commons.datetime.XMLOffsetTime; - -import lombok.extern.slf4j.Slf4j; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Date; @Configuration public class MongoConverterConfig { - @Bean - public MongoCustomConversions mongoCustomConversions() { - return new MongoCustomConversions(Arrays.asList( - - new DateToXMLOffsetDate(), - - new DateToXMLOffsetTimeConverter(), - - new XMLOffsetDateToDateConverter(), - - new XMLOffsetTimeToTimeConverter() - - )); - } - - @Slf4j - @ReadingConverter - public static class DateToXMLOffsetDate implements Converter { - @Override - public XMLOffsetDate convert(Date source) { - log.debug("DateToXMLOffsetDate " + source); - if (source == null) { - return null; - } - return XMLOffsetDate.ofInstant(source.toInstant(), ZoneOffset.UTC); - } - } - - @Slf4j - @WritingConverter - public static class XMLOffsetDateToDateConverter implements Converter { - @Override - public Date convert(XMLOffsetDate source) { - log.debug("XMLOffsetDateToDateConverter " + source); - if (source == null) { - return null; - } - LocalDate localDate = source.toLocalDate(); - ZonedDateTime zonedDateTime = localDate.atStartOfDay(source.getOffset() != null ? source.getOffset() : ZoneOffset.UTC); - return Date.from(Instant.from(zonedDateTime)); - } - } - - @Slf4j - @ReadingConverter - public static class DateToXMLOffsetTimeConverter implements Converter { - @Override - public XMLOffsetTime convert(Date source) { - log.debug("DateToXMLOffsetTimeConverter " + source); - if (source == null) { - return null; - } - return XMLOffsetTime.ofInstant(source.toInstant(), ZoneOffset.UTC); - } - } - - @Slf4j - @WritingConverter - public static class XMLOffsetTimeToTimeConverter implements Converter { - @Override - public Date convert(XMLOffsetTime source) { - log.debug("XMLOffsetTimeToTimeConverter " + source); - if (source == null) { - return null; - } - return Date.from(source.toLocalTime().atDate(LocalDate.now()).atZone(source.getOffset() != null ? source.getOffset() : ZoneOffset.UTC).withZoneSameLocal(ZoneOffset.UTC).toInstant()); - } - } + @Bean + public MongoCustomConversions mongoCustomConversions() { + return new MongoCustomConversions(Arrays.asList( + new DateToXMLOffsetDate(), + new DateToXMLOffsetTimeConverter(), + new XMLOffsetDateToDateConverter(), + new XMLOffsetTimeToTimeConverter() + )); + } + + @Slf4j + @ReadingConverter + public static class DateToXMLOffsetDate implements Converter { + @Override + public XMLOffsetDate convert(Date source) { + log.debug("DateToXMLOffsetDate {}", source); + return XMLOffsetDate.ofInstant(source.toInstant(), ZoneOffset.UTC); + } + } + + @Slf4j + @WritingConverter + public static class XMLOffsetDateToDateConverter implements Converter { + @Override + public Date convert(XMLOffsetDate source) { + log.debug("XMLOffsetDateToDateConverter {}", source); + LocalDate localDate = source.toLocalDate(); + ZonedDateTime zonedDateTime = localDate.atStartOfDay(source.getOffset() != null ? source.getOffset() : ZoneOffset.UTC); + return Date.from(Instant.from(zonedDateTime)); + } + } + + @Slf4j + @ReadingConverter + public static class DateToXMLOffsetTimeConverter implements Converter { + @Override + public XMLOffsetTime convert(Date source) { + log.debug("DateToXMLOffsetTimeConverter {}", source); + return XMLOffsetTime.ofInstant(source.toInstant(), ZoneOffset.UTC); + } + } + + @Slf4j + @WritingConverter + public static class XMLOffsetTimeToTimeConverter implements Converter { + @Override + public Date convert(XMLOffsetTime source) { + log.debug("XMLOffsetTimeToTimeConverter {}", source); + return Date.from(source.toLocalTime().atDate(LocalDate.now()).atZone(source.getOffset() != null ? source.getOffset() : ZoneOffset.UTC).withZoneSameLocal(ZoneOffset.UTC).toInstant()); + } + } } diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java index 0efd435..c114ed6 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java @@ -1,7 +1,11 @@ package dk.erst.cm.webapi; -import java.util.Optional; - +import dk.erst.cm.AppProperties; +import dk.erst.cm.api.order.BasketService; +import dk.erst.cm.api.order.BasketService.SendBasketData; +import dk.erst.cm.api.order.BasketService.SendBasketResponse; +import dk.erst.cm.api.order.BasketService.SentBasketData; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -11,13 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import dk.erst.cm.AppProperties; -import dk.erst.cm.api.data.Order; -import dk.erst.cm.api.order.BasketService; -import dk.erst.cm.api.order.BasketService.SendBasketData; -import dk.erst.cm.api.order.BasketService.SendBasketResponse; -import dk.erst.cm.api.order.BasketService.SentBasketData; -import lombok.extern.slf4j.Slf4j; +import java.util.Optional; @CrossOrigin(maxAge = 3600) @RestController @@ -55,8 +53,8 @@ public ResponseEntity getBasketById(@PathVariable("id") String i } @RequestMapping(value = "/api/order/{id}") - public ResponseEntity getOrderById(@PathVariable("id") String id) { - Optional findById = basketService.loadSentOrder(id); + public ResponseEntity getOrderById(@PathVariable("id") String id) { + Optional findById = basketService.loadSentOrderAsJSON(id); if (findById.isPresent()) { return new ResponseEntity<>(findById.get(), HttpStatus.OK); } From b4af9046a235de90f4f7740169509c5f77c72229 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 1 May 2022 01:26:29 +0300 Subject: [PATCH 64/71] Fix multiple warnings and one bug, found by Idea --- .../dk/erst/cm/api/order/BasketService.java | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java index 020a259..4ff4014 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java @@ -13,6 +13,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -97,18 +98,14 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder, Set queryProductIdSet = query.basketData.orderLines.keySet(); Iterable products = productService.findAllByIds(queryProductIdSet); - Set resolvedProductIdSet = new HashSet(); + Set resolvedProductIdSet = new HashSet<>(); log.debug("1. Load necessary data for XML generation"); Map> byCatalogMap = new HashMap<>(); for (Product p : products) { String productCatalogId = p.getProductCatalogId(); - List productList = byCatalogMap.get(productCatalogId); - if (productList == null) { - productList = new ArrayList<>(); - byCatalogMap.put(productCatalogId, productList); - } + List productList = byCatalogMap.computeIfAbsent(productCatalogId, k -> new ArrayList<>()); productList.add(p); resolvedProductIdSet.add(p.getId()); } @@ -116,13 +113,13 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder, if (resolvedProductIdSet.size() < queryProductIdSet.size()) { int countNotFoundProducts = queryProductIdSet.size() - resolvedProductIdSet.size(); String errorMessage = countNotFoundProducts + " product" + (countNotFoundProducts > 1 ? "s are" : "is") + " not found, please delete highlighted products to send the basket."; - Set notResolvedProductIdSet = new HashSet(queryProductIdSet); + Set notResolvedProductIdSet = new HashSet<>(queryProductIdSet); notResolvedProductIdSet.removeAll(resolvedProductIdSet); - return SendBasketResponse.error(errorMessage).withErrorProductIdList(new ArrayList(notResolvedProductIdSet)); + return SendBasketResponse.error(errorMessage).withErrorProductIdList(new ArrayList<>(notResolvedProductIdSet)); } - Set noSellerCatalogSet = new HashSet(); - Map sellerPartyByCatalog = new HashMap(); + Set noSellerCatalogSet = new HashSet<>(); + Map sellerPartyByCatalog = new HashMap<>(); for (String catalogId : byCatalogMap.keySet()) { Party sellerParty = catalogService.loadLastSellerParty(catalogId); if (sellerParty != null) { @@ -135,10 +132,10 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder, if (!noSellerCatalogSet.isEmpty()) { int countProductIncompleteSeller = 0; - List errorProductIdList = new ArrayList(); + List errorProductIdList = new ArrayList<>(); for (String catalogId : noSellerCatalogSet) { List list = byCatalogMap.get(catalogId); - errorProductIdList.addAll(errorProductIdList); + errorProductIdList.addAll(list.stream().map(Product::getId).collect(Collectors.toList())); countProductIncompleteSeller += list.size(); } String errorMessage = buildErrorMessageNoSellerInfo(byCatalogMap, noSellerCatalogSet, countProductIncompleteSeller); @@ -154,7 +151,7 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder, basket.setOrderCount(byCatalogMap.size()); basket.setVersion(1); - List orderList = new ArrayList(); + List orderList = new ArrayList<>(); int orderIndex = 0; for (String catalogId : byCatalogMap.keySet()) { List productList = byCatalogMap.get(catalogId); @@ -186,7 +183,7 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder, log.debug("3. Save XML files into temporary folder " + tempDirectory); - Map fileNameToOrderMap = new HashMap(); + Map fileNameToOrderMap = new HashMap<>(); for (Order order : orderList) { try { File orderFile = orderService.saveOrderXML(tempDirectory, (OrderType) order.getDocument()); @@ -209,8 +206,9 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder, log.debug("5. Move XML files from temporary folder to destination folder at " + outboxFolder); File[] tempFiles = tempDirectory.listFiles(); - Set notMovedFiles = new HashSet(); - Set movedFiles = new HashSet(); + Set notMovedFiles = new HashSet<>(); + Set movedFiles = new HashSet<>(); + assert tempFiles != null; for (File file : tempFiles) { File outFile = new File(outboxFolder, file.getName()); try { @@ -243,7 +241,7 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder, log.error("Failed to delete temporary directory, check that streams are closed: " + tempDirectory); } - log.debug("7. Basket #" + basket.getId() + " is sent succesfully"); + log.debug("7. Basket #" + basket.getId() + " is sent successfully"); return SendBasketResponse.success(basket.getId()); } @@ -292,6 +290,7 @@ private String buildErrorMessageNoSellerInfo(Map> byCatalo private File createTempDirectory(String dirName) { File tempDirectory = new File(FileUtils.getTempDirectory(), dirName); + //noinspection ResultOfMethodCallIgnored tempDirectory.mkdirs(); return tempDirectory; } From 2959fd4b92d752a4a78abfec1f9cceb4708a6c45 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 1 May 2022 01:33:23 +0300 Subject: [PATCH 65/71] Migrate to the latest Spring Boot version and opencsv --- cm-api/pom.xml | 6 +++--- cm-webapi/pom.xml | 9 ++------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/cm-api/pom.xml b/cm-api/pom.xml index 5af6067..45a0cf1 100644 --- a/cm-api/pom.xml +++ b/cm-api/pom.xml @@ -12,7 +12,7 @@ 1.8 1.0.0 - 2.4.0 + 2.6.7 UTF-8 ${java.version} ${java.version} @@ -40,7 +40,7 @@ com.opencsv opencsv - 4.4 + 5.6 @@ -91,4 +91,4 @@ - \ No newline at end of file + diff --git a/cm-webapi/pom.xml b/cm-webapi/pom.xml index 047ebea..fce6934 100644 --- a/cm-webapi/pom.xml +++ b/cm-webapi/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 2.4.0 + 2.6.7 @@ -36,11 +36,6 @@ org.springframework.boot spring-boot-starter-data-rest - - com.opencsv - opencsv - 4.4 - @@ -126,4 +121,4 @@ --> - \ No newline at end of file + From f3a0bd9b8be6d1ad51e57639d3a1425a0d48492d Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 1 May 2022 01:33:50 +0300 Subject: [PATCH 66/71] Increase default timeout for response from API to 30 secs --- cm-frontend/src/services/DataService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cm-frontend/src/services/DataService.js b/cm-frontend/src/services/DataService.js index 240bc15..619f98f 100644 --- a/cm-frontend/src/services/DataService.js +++ b/cm-frontend/src/services/DataService.js @@ -1,7 +1,7 @@ import Axios from "axios"; import {API_URL} from "./DataServiceConfig" -Axios.defaults.timeout = 10000; +Axios.defaults.timeout = 30000; const apiUrl = API_URL; @@ -55,4 +55,4 @@ const DataService = { uploadFiles, } -export default DataService; \ No newline at end of file +export default DataService; From 4a53e5a107202a7bcc8dd87b615f1d1a7185069d Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sun, 1 May 2022 15:15:24 +0300 Subject: [PATCH 67/71] Do not fill UUID of order - it is forbidden by schematron --- .../main/java/dk/erst/cm/api/order/OrderProducerService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java index 453d77f..3bcd6e6 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java @@ -71,7 +71,10 @@ public OrderType generateOrder(Order dataOrder, OrderDefaultConfig defaultConfig order.setCustomizationID("urn:fdc:peppol.eu:poacc:trns:order:3"); order.setProfileID("urn:fdc:peppol.eu:poacc:bis:order_only:3"); - order.setUUID(dataOrder.getId()); + /* + * PEPPOL-T01-B00110 Document MUST NOT contain elements not part of the data model. /Order[1] /UUID[1] + */ +// order.setUUID(dataOrder.getId()); order.setID(dataOrder.getOrderNumber()); order.setIssueDate(dataOrder.getCreateTime().atZone(ZoneOffset.UTC).toLocalDate()); order.setIssueTime(dataOrder.getCreateTime().atZone(ZoneOffset.UTC).toLocalTime()); From dcfaf1cbbddc656041bdbf1a0f351f7a3418c02b Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 7 May 2022 20:49:04 +0300 Subject: [PATCH 68/71] First implementation of jobs to load catalogue and future order response --- .../erst/cm/api/load/FolderLoadService.java | 60 +++++++ .../api/load/handler}/FileUploadConsumer.java | 113 +++++++------ .../java/dk/erst/cm/api/util/StatData.java | 97 +++++++++++ .../dk/erst/cm/CatalogApiApplication.java | 2 + .../java/dk/erst/cm/job/SchedulerConfig.java | 59 +++++++ .../java/dk/erst/cm/job/TaskScheduler.java | 64 ++++++++ .../dk/erst/cm/job/TaskSchedulerMonitor.java | 153 ++++++++++++++++++ .../dk/erst/cm/webapi/UploadController.java | 3 +- .../src/main/resources/application.properties | 8 +- cm-webapi/src/main/resources/application.yml | 13 ++ .../dk/erst/cm/api/item/ItemServiceTest.java | 2 +- 11 files changed, 513 insertions(+), 61 deletions(-) create mode 100644 cm-api/src/main/java/dk/erst/cm/api/load/FolderLoadService.java rename {cm-webapi/src/main/java/dk/erst/cm/webapi => cm-api/src/main/java/dk/erst/cm/api/load/handler}/FileUploadConsumer.java (78%) create mode 100644 cm-api/src/main/java/dk/erst/cm/api/util/StatData.java create mode 100644 cm-webapi/src/main/java/dk/erst/cm/job/SchedulerConfig.java create mode 100644 cm-webapi/src/main/java/dk/erst/cm/job/TaskScheduler.java create mode 100644 cm-webapi/src/main/java/dk/erst/cm/job/TaskSchedulerMonitor.java create mode 100644 cm-webapi/src/main/resources/application.yml diff --git a/cm-api/src/main/java/dk/erst/cm/api/load/FolderLoadService.java b/cm-api/src/main/java/dk/erst/cm/api/load/FolderLoadService.java new file mode 100644 index 0000000..9d3aae4 --- /dev/null +++ b/cm-api/src/main/java/dk/erst/cm/api/load/FolderLoadService.java @@ -0,0 +1,60 @@ +package dk.erst.cm.api.load; + +import dk.erst.cm.api.item.LoadCatalogService; +import dk.erst.cm.api.load.handler.FileUploadConsumer; +import dk.erst.cm.api.util.StatData; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; + +@Service +@Slf4j +public class FolderLoadService { + + private static final File[] EMPTY_FILE_LIST = new File[0]; + @Autowired + private LoadCatalogService loadCatalogService; + @Autowired + private PeppolLoadService loadService; + + public StatData loadCatalogues(File folder) { + StatData statData = new StatData(); + log.debug("Start load from folder {}", folder.getAbsolutePath()); + Optional optionalFiles = Optional.ofNullable(folder.listFiles((dir, fileName) -> fileName.toLowerCase().endsWith(".xml"))); + Arrays.stream(optionalFiles.orElse(EMPTY_FILE_LIST)).forEach(file -> { + FileUploadConsumer fileUploadConsumer = new FileUploadConsumer(loadCatalogService); + try { + log.info("Start reading file {}", file.getName()); + File tempFile = new File(file.getAbsolutePath() + ".tmp"); + if (!file.renameTo(tempFile)){ + log.debug("Could not rename file {}, skip it", file.getName()); + return; + } + try (InputStream inputStream = Files.newInputStream(tempFile.toPath())) { + loadService.loadXml(inputStream, file.getName(), fileUploadConsumer); + } + log.info("Loaded file " + file.getName() + " with " + fileUploadConsumer.getLineCount() + " lines"); + statData.increment("loaded-files"); + statData.increase("loaded-lines", fileUploadConsumer.getLineCount()); + if (tempFile.delete()) { + tempFile.deleteOnExit(); + } + } catch (Exception e) { + log.error("Error loading file " + file.getName(), e); + statData.increment("error-"+e.getClass().getName()); + } + }); + return statData; + } + public StatData loadOrderResponses(File folder) { + log.debug("Start load from folder {}", folder.getAbsolutePath()); + return StatData.error("Not implemented"); + } +} diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/FileUploadConsumer.java b/cm-api/src/main/java/dk/erst/cm/api/load/handler/FileUploadConsumer.java similarity index 78% rename from cm-webapi/src/main/java/dk/erst/cm/webapi/FileUploadConsumer.java rename to cm-api/src/main/java/dk/erst/cm/api/load/handler/FileUploadConsumer.java index dd3fca5..c67dedb 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/FileUploadConsumer.java +++ b/cm-api/src/main/java/dk/erst/cm/api/load/handler/FileUploadConsumer.java @@ -1,57 +1,56 @@ -package dk.erst.cm.webapi; - -import java.util.Arrays; -import java.util.Map; -import java.util.TreeMap; - -import dk.erst.cm.api.data.Product; -import dk.erst.cm.api.data.ProductCatalogUpdate; -import dk.erst.cm.api.item.LoadCatalogService; -import dk.erst.cm.api.load.handler.CatalogConsumer; -import dk.erst.cm.xml.ubl21.model.Catalogue; -import dk.erst.cm.xml.ubl21.model.CatalogueLine; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class FileUploadConsumer implements CatalogConsumer { - - private LoadCatalogService loadCatalogService; - - @Getter - private ProductCatalogUpdate productCatalogUpdate; - - @Getter - private int lineCount; - - @Getter - private Map lineActionStat; - - public static enum LineAction { - ADD, UPDATE, DELETE - } - - public FileUploadConsumer(LoadCatalogService loadCatalogService) { - this.loadCatalogService = loadCatalogService; - this.lineActionStat = new TreeMap(); - Arrays.stream(LineAction.values()).forEach(la -> this.lineActionStat.put(la, 0)); - } - - @Override - public void consumeHead(Catalogue catalog) { - this.productCatalogUpdate = loadCatalogService.saveCatalogue(catalog); - } - - @Override - public void consumeLine(CatalogueLine line) { - Product product = this.loadCatalogService.saveCatalogUpdateItem(productCatalogUpdate, line); - LineAction action = product == null ? LineAction.DELETE : product.getVersion() == 1 ? LineAction.ADD : LineAction.UPDATE; - - this.lineCount++; - this.lineActionStat.put(action, this.lineActionStat.get(action) + 1); - - if (this.lineCount % 100 == 0) { - log.info(String.format("Loaded %d lines, stat: %s", this.lineCount, this.lineActionStat.toString())); - } - } -} +package dk.erst.cm.api.load.handler; + +import java.util.Arrays; +import java.util.Map; +import java.util.TreeMap; + +import dk.erst.cm.api.data.Product; +import dk.erst.cm.api.data.ProductCatalogUpdate; +import dk.erst.cm.api.item.LoadCatalogService; +import dk.erst.cm.xml.ubl21.model.Catalogue; +import dk.erst.cm.xml.ubl21.model.CatalogueLine; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FileUploadConsumer implements CatalogConsumer { + + private final LoadCatalogService loadCatalogService; + + @Getter + private ProductCatalogUpdate productCatalogUpdate; + + @Getter + private int lineCount; + + @Getter + private final Map lineActionStat; + + public enum LineAction { + ADD, UPDATE, DELETE + } + + public FileUploadConsumer(LoadCatalogService loadCatalogService) { + this.loadCatalogService = loadCatalogService; + this.lineActionStat = new TreeMap<>(); + Arrays.stream(LineAction.values()).forEach(la -> this.lineActionStat.put(la, 0)); + } + + @Override + public void consumeHead(Catalogue catalog) { + this.productCatalogUpdate = loadCatalogService.saveCatalogue(catalog); + } + + @Override + public void consumeLine(CatalogueLine line) { + Product product = this.loadCatalogService.saveCatalogUpdateItem(productCatalogUpdate, line); + LineAction action = product == null ? LineAction.DELETE : product.getVersion() == 1 ? LineAction.ADD : LineAction.UPDATE; + + this.lineCount++; + this.lineActionStat.put(action, this.lineActionStat.get(action) + 1); + + if (this.lineCount % 100 == 0) { + log.info(String.format("Loaded %d lines, stat: %s", this.lineCount, this.lineActionStat)); + } + } +} diff --git a/cm-api/src/main/java/dk/erst/cm/api/util/StatData.java b/cm-api/src/main/java/dk/erst/cm/api/util/StatData.java new file mode 100644 index 0000000..839309b --- /dev/null +++ b/cm-api/src/main/java/dk/erst/cm/api/util/StatData.java @@ -0,0 +1,97 @@ +package dk.erst.cm.api.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class StatData { + + private final Map statMap; + + private final long startMs; + + private Object result; + + public StatData() { + this.startMs = System.currentTimeMillis(); + this.statMap = new HashMap<>(); + } + + public int getCount(String key) { + int[] c = this.statMap.get(key); + if (c != null) { + return c[0]; + } + return -1; + } + + public void incrementObject(Object key) { + increment(String.valueOf(key)); + } + + public void increment(String key) { + this.increase(key, 1); + } + + public void increase(String key, int count) { + String code = key == null ? "UNDEFINED" : key; + int[] c = statMap.get(code); + if (c == null) { + statMap.put(code, new int[]{count}); + } else { + c[0] += count; + } + } + + public static StatData error(String message) { + StatData sd = new StatData(); + sd.increment(message); + return sd; + } + + @Override + public String toString() { + return toStatString(); + } + + public String toStatString() { + if (isEmpty()) { + return "Nothing"; + } + StringBuilder sb = new StringBuilder(); + List keyList = new ArrayList<>(statMap.keySet()); + Collections.sort(keyList); + for (String key : keyList) { + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(key); + sb.append(": "); + sb.append(statMap.get(key)[0]); + } + return sb.toString(); + } + + public boolean isEmpty() { + return statMap.isEmpty(); + } + + public long getStartMs() { + return startMs; + } + + public String toDurationString() { + return (System.currentTimeMillis() - startMs) + " ms"; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } +} + diff --git a/cm-webapi/src/main/java/dk/erst/cm/CatalogApiApplication.java b/cm-webapi/src/main/java/dk/erst/cm/CatalogApiApplication.java index aa5bf62..f96acce 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/CatalogApiApplication.java +++ b/cm-webapi/src/main/java/dk/erst/cm/CatalogApiApplication.java @@ -4,9 +4,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableConfigurationProperties(AppProperties.class) +@EnableScheduling public class CatalogApiApplication { public static void main(String[] args) { diff --git a/cm-webapi/src/main/java/dk/erst/cm/job/SchedulerConfig.java b/cm-webapi/src/main/java/dk/erst/cm/job/SchedulerConfig.java new file mode 100644 index 0000000..805826e --- /dev/null +++ b/cm-webapi/src/main/java/dk/erst/cm/job/SchedulerConfig.java @@ -0,0 +1,59 @@ +package dk.erst.cm.job; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.FixedDelayTask; +import org.springframework.scheduling.config.IntervalTask; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +@Slf4j +public class SchedulerConfig implements SchedulingConfigurer { + private static final int POOL_SIZE = 2; + + @Value("${job.interval.load-catalogue:-1}") + private long loadCatalogue; + @Value("${job.interval.load-order-response:-1}") + private long loadOrderResponse; + + @Override + public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) { + List taskList = scheduledTaskRegistrar.getFixedDelayTaskList(); + + List newTaskList = new ArrayList<>(); + for (IntervalTask intervalTask : taskList) { + long interval = getExpectedInterval(intervalTask); + if (interval < 0) { + log.info("Skip interval task " + intervalTask); + continue; + } + FixedDelayTask newTask = new FixedDelayTask(intervalTask.getRunnable(), interval, interval); + newTaskList.add(newTask); + log.info("Set interval and delay to " + interval + " for " + newTask); + } + scheduledTaskRegistrar.setFixedDelayTasksList(newTaskList); + + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(POOL_SIZE); + threadPoolTaskScheduler.setThreadNamePrefix("tasks-"); + threadPoolTaskScheduler.initialize(); + } + + private long getExpectedInterval(IntervalTask intervalTask) { + String t = intervalTask.toString(); + if (t.endsWith("loadCatalogue")) { + return this.loadCatalogue * 1000; + } + if (t.endsWith("loadOrderResponse")) { + return this.loadOrderResponse * 1000; + } + return Long.MAX_VALUE; + } + +} diff --git a/cm-webapi/src/main/java/dk/erst/cm/job/TaskScheduler.java b/cm-webapi/src/main/java/dk/erst/cm/job/TaskScheduler.java new file mode 100644 index 0000000..3b1749a --- /dev/null +++ b/cm-webapi/src/main/java/dk/erst/cm/job/TaskScheduler.java @@ -0,0 +1,64 @@ +package dk.erst.cm.job; + +import dk.erst.cm.AppProperties; +import dk.erst.cm.api.load.FolderLoadService; +import dk.erst.cm.api.util.StatData; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.io.File; + +@Service +@Slf4j +public class TaskScheduler { + + @Autowired + private TaskSchedulerMonitor taskSchedulerMonitor; + @Autowired + private AppProperties appProperties; + @Autowired + private FolderLoadService folderLoadService; + + @Scheduled(fixedDelay = Long.MAX_VALUE) + public StatData loadCatalogue() { + String path = appProperties.getIntegration().getInboxCatalogue(); + return checkAndExecute("loadCatalogue", path, "inboxCatalogue", folder -> folderLoadService.loadCatalogues(folder)); + } + + @Scheduled(fixedDelay = Long.MAX_VALUE) + public StatData loadOrderResponse() { + String path = appProperties.getIntegration().getInboxOrderResponse(); + return checkAndExecute("loadOrderResponse", path, "inboxOrderResponse", folder -> folderLoadService.loadOrderResponses(folder)); + } + + private interface IExecuteTask { + StatData execute(File folder); + } + + protected StatData checkAndExecute(String taskName, String folder, String fieldName, IExecuteTask executeTask) { + TaskSchedulerMonitor.TaskResult task = taskSchedulerMonitor.build(taskName); + File folderFile = new File(folder); + if (!folderFile.exists() || !folderFile.isDirectory()) { + String error = String.format("Path %s = %s does not exist or is not a directory", fieldName, folderFile.getAbsolutePath()); + log.error("Task {}: {}", taskName, error); + return StatData.error(error); + } else { + try { + StatData sd = executeTask.execute(folderFile); + if (!sd.isEmpty()) { + String message = "Done loading from folder " + folderFile + " in " + sd.toDurationString() + " with next statistics of document status: " + sd.toStatString(); + log.info(message); + } + task.success(sd); + return sd; + } catch (Exception e) { + log.error("Task " + taskName + ": failed to process folder " + folder, e); + task.failure(e); + return StatData.error(e.getMessage()); + } + } + } + +} diff --git a/cm-webapi/src/main/java/dk/erst/cm/job/TaskSchedulerMonitor.java b/cm-webapi/src/main/java/dk/erst/cm/job/TaskSchedulerMonitor.java new file mode 100644 index 0000000..2c0c889 --- /dev/null +++ b/cm-webapi/src/main/java/dk/erst/cm/job/TaskSchedulerMonitor.java @@ -0,0 +1,153 @@ +package dk.erst.cm.job; + +import dk.erst.cm.api.util.StatData; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +public class TaskSchedulerMonitor { + + private final Map lastResultMap; + + public TaskSchedulerMonitor() { + this.lastResultMap = new HashMap<>(); + } + + public TaskResult build(String taskName) { + return new TaskResult(taskName, this); + } + + public synchronized String getLast(String taskName) { + TaskResult[] taskResults = this.lastResultMap.get(taskName); + return buildInfo(taskResults); + } + + private String buildInfo(TaskResult[] taskResults) { + if (taskResults == null) { + return "Not yet run"; + } + TaskResult prev = taskResults[1]; + TaskResult last = taskResults[0]; + StringBuilder sb = new StringBuilder(); + if (last != null) { + sb.append("Last run "); + sb.append(last); + } + if (prev != null) { + sb.append(", previous "); + sb.append(prev); + } + return sb.toString(); + } + + private synchronized void addResult(TaskResult result) { + TaskResult[] taskResults = this.lastResultMap.get(result.getTaskName()); + if (taskResults == null) { + taskResults = new TaskResult[2]; + taskResults[0] = result; + this.lastResultMap.put(result.getTaskName(), taskResults); + } else { + taskResults[1] = taskResults[0]; + taskResults[0] = result; + } + } + + public static class TaskResult { + + private final String taskName; + private final Date startTime; + private long duration; + private boolean success; + private Object result; + + private final TaskSchedulerMonitor monitor; + + public TaskResult(String taskName, TaskSchedulerMonitor monitor) { + this.taskName = taskName; + this.monitor = monitor; + this.startTime = Calendar.getInstance().getTime(); + this.duration = -1; + this.success = false; + this.result = null; + } + + public void success(Object result) { + this.success = true; + this.result = result; + this.duration = System.currentTimeMillis() - startTime.getTime(); + + this.monitor.addResult(this); + } + + public void failure(Exception e) { + this.success = false; + this.result = e.getMessage(); + this.duration = System.currentTimeMillis() - startTime.getTime(); + + this.monitor.addResult(this); + } + + public String getTaskName() { + return taskName; + } + + public Date getStartTime() { + return startTime; + } + + public long getDuration() { + return duration; + } + + public boolean isSuccess() { + return success; + } + + public Object getResult() { + return result; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + long lastRunMs = System.currentTimeMillis() - this.startTime.getTime(); + sb.append(Math.round(lastRunMs / 1000.0)); + sb.append(" sec ago"); + sb.append(" for "); + sb.append(this.duration); + sb.append(" ms"); + if (!success) { + sb.append(" FAILURE"); + } + if (result != null) { + String resultText = null; + if (result instanceof StatData) { + StatData s = (StatData)result; + if (!s.isEmpty()) { + resultText= String.valueOf(result); + } + } else if (result instanceof Exception) { + resultText = ((Exception)result).getMessage(); + } else if (result instanceof List) { + List list = (List) result; + if (!list.isEmpty()) { + resultText= list.size() + " elements"; + } + } else { + resultText = String.valueOf(result); + } + if (StringUtils.isNotEmpty(resultText)) { + sb.append(", "); + sb.append(resultText); + } + } + return sb.toString(); + } + } + +} diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/UploadController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/UploadController.java index 21a93e5..9833b5a 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/UploadController.java +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/UploadController.java @@ -3,7 +3,8 @@ import dk.erst.cm.api.data.ProductCatalogUpdate; import dk.erst.cm.api.item.LoadCatalogService; import dk.erst.cm.api.load.PeppolLoadService; -import dk.erst.cm.webapi.FileUploadConsumer.LineAction; +import dk.erst.cm.api.load.handler.FileUploadConsumer; +import dk.erst.cm.api.load.handler.FileUploadConsumer.LineAction; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; diff --git a/cm-webapi/src/main/resources/application.properties b/cm-webapi/src/main/resources/application.properties index c39675f..c69be58 100644 --- a/cm-webapi/src/main/resources/application.properties +++ b/cm-webapi/src/main/resources/application.properties @@ -1,16 +1,20 @@ server.servlet.context-path=/ logging.level.dk.erst.catalog = INFO +logging.level.dk.erst.cm.MongoConverterConfig = DEBUG spring.data.mongodb.uri=mongodb://localhost:27017/dc?ssl=false spring.data.mongodb.auto-index-creation = true app.integration.inbox-catalogue=./.integration/inbox/catalogue -app.integration.inbox-order-response=./.integration/inbox/orderresponse +app.integration.inbox-order-response=./.integration/inbox/order-response app.integration.outbox-order=./.integration/outbox/order app.order.endpointGLN=5798009882783 app.order.note=This order is generated for education purposes via Delis Catalogue. +job.interval.load-catalogue=10 +job.interval.load-order-response=-1 + # Set to DEBUG to see all Mongo queries logging.level.org.springframework.data.mongodb.core.MongoTemplate=INFO @@ -26,4 +30,4 @@ server.tomcat.accesslog.enabled=true server.tomcat.basedir=./.tomcat # Needed to avoid serializing all many fields of Order to JSON for showing on GUI -spring.jackson.default-property-inclusion=NON_EMPTY \ No newline at end of file +spring.jackson.default-property-inclusion=NON_EMPTY diff --git a/cm-webapi/src/main/resources/application.yml b/cm-webapi/src/main/resources/application.yml new file mode 100644 index 0000000..410a0a0 --- /dev/null +++ b/cm-webapi/src/main/resources/application.yml @@ -0,0 +1,13 @@ +app: + integration: + inbox-catalogue: ./.integration/inbox/catalogue + inbox-order-response: ./.integration/inbox/order-response + outbox-order: ./.integration/outbox/order + + order: + endpointGLN: '5798009882783' + note: 'This order is generated for education purposes via Delis Catalogue.' + +job.interval: + load-catalogue: 10 + load-order-response: -1 diff --git a/cm-webapi/src/test/java/dk/erst/cm/api/item/ItemServiceTest.java b/cm-webapi/src/test/java/dk/erst/cm/api/item/ItemServiceTest.java index 5a33da8..0af6ff5 100644 --- a/cm-webapi/src/test/java/dk/erst/cm/api/item/ItemServiceTest.java +++ b/cm-webapi/src/test/java/dk/erst/cm/api/item/ItemServiceTest.java @@ -8,7 +8,7 @@ import dk.erst.cm.api.load.PeppolLoadService; import dk.erst.cm.test.TestDocument; -import dk.erst.cm.webapi.FileUploadConsumer; +import dk.erst.cm.api.load.handler.FileUploadConsumer; import lombok.extern.slf4j.Slf4j; @SpringBootTest From b8a926f856dbaa039acdb8ded332479118dbbc45 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 7 May 2022 22:02:57 +0300 Subject: [PATCH 69/71] Implement download basket zip and order xml --- .../dk/erst/cm/api/order/BasketService.java | 12 ++ .../dk/erst/cm/api/order/OrderService.java | 6 +- .../src/components/BasketSendResult.js | 3 +- cm-frontend/src/components/OrderSendResult.js | 4 +- cm-frontend/src/services/DataService.js | 5 + .../dk/erst/cm/webapi/OrderController.java | 129 ++++++++++++------ 6 files changed, 115 insertions(+), 44 deletions(-) diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java index 4ff4014..36fd1dd 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java @@ -1,5 +1,6 @@ package dk.erst.cm.api.order; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.time.Instant; @@ -50,6 +51,7 @@ public BasketService(ProductService productService, OrderService orderService, C this.orderProducerService = orderProducerService; } + @Data public static class SentBasketData { private Basket basket; @@ -328,4 +330,14 @@ public Optional loadSentBasketData(String basketId) { public Optional loadSentOrderAsJSON(String orderId) { return this.orderService.findOrderByIdAsJSON(orderId); } + + public Optional loadSentOrderAsXML(String id) { + Optional orderById = this.orderService.findOrderById(id); + if (orderById.isPresent() && orderById.get().getDocument() != null) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.orderService.saveOrderXMLToStream((OrderType) orderById.get().getDocument(), out); + return Optional.of(out.toByteArray()); + } + return Optional.empty(); + } } diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java index eb36657..2abeb04 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java @@ -93,9 +93,13 @@ public void updateOrderStatus(String orderId, OrderStatus status) { public File saveOrderXML(File directory, OrderType sendOrder) throws IOException { File tempFile = new File(directory, "delis-cm-" + sendOrder.getIDValue() + ".xml"); try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tempFile.toPath()))) { - UBL21Writer.order().write(sendOrder, out); + saveOrderXMLToStream(sendOrder, out); } return tempFile; } + public void saveOrderXMLToStream(OrderType sendOrder, OutputStream out) { + UBL21Writer.order().write(sendOrder, out); + } + } diff --git a/cm-frontend/src/components/BasketSendResult.js b/cm-frontend/src/components/BasketSendResult.js index 7ad1214..0711fb6 100644 --- a/cm-frontend/src/components/BasketSendResult.js +++ b/cm-frontend/src/components/BasketSendResult.js @@ -15,6 +15,7 @@ import {useHistory} from "react-router"; import CloseIcon from "@material-ui/icons/Close"; import SmallSnackbar from "./SmallSnackbar"; import {copyCurrentUrlToClipboard, copySubUrlToClipboard} from "../services/ClipboardService"; +import DataService from "../services/DataService"; export default function BasketSendResult(props) { @@ -72,7 +73,7 @@ export default function BasketSendResult(props) { - + diff --git a/cm-frontend/src/components/OrderSendResult.js b/cm-frontend/src/components/OrderSendResult.js index 9fb2e2d..aaf146f 100644 --- a/cm-frontend/src/components/OrderSendResult.js +++ b/cm-frontend/src/components/OrderSendResult.js @@ -14,6 +14,8 @@ import {useHistory} from "react-router"; import {ViewToggle} from "../pages/ProductDetailPage"; import SmallSnackbar from "./SmallSnackbar"; import {copyCurrentUrlToClipboard} from "../services/ClipboardService"; +import DataService from "../services/DataService"; + export default function OrderSendResult(props) { @@ -55,7 +57,7 @@ export default function OrderSendResult(props) { - + diff --git a/cm-frontend/src/services/DataService.js b/cm-frontend/src/services/DataService.js index 619f98f..2ddde07 100644 --- a/cm-frontend/src/services/DataService.js +++ b/cm-frontend/src/services/DataService.js @@ -5,6 +5,9 @@ Axios.defaults.timeout = 30000; const apiUrl = API_URL; +const downloadOrderXmlLink = (id) => { return API_URL + "/order/" + id + "/xml"} +const downloadBasketZipLink = (id) => { return API_URL + "/basket/" + id + "/zip"} + const fetchProductDetails = (productId) => { return Axios.get(apiUrl + "/products/" + productId); } @@ -53,6 +56,8 @@ const DataService = { fetchSentBasketData, fetchSentOrderData, uploadFiles, + downloadOrderXmlLink, + downloadBasketZipLink, } export default DataService; diff --git a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java index c114ed6..af1b744 100644 --- a/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java +++ b/cm-webapi/src/main/java/dk/erst/cm/webapi/OrderController.java @@ -1,13 +1,14 @@ package dk.erst.cm.webapi; -import dk.erst.cm.AppProperties; -import dk.erst.cm.api.order.BasketService; -import dk.erst.cm.api.order.BasketService.SendBasketData; -import dk.erst.cm.api.order.BasketService.SendBasketResponse; -import dk.erst.cm.api.order.BasketService.SentBasketData; -import lombok.extern.slf4j.Slf4j; +import java.io.ByteArrayOutputStream; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PathVariable; @@ -15,50 +16,96 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Optional; +import dk.erst.cm.AppProperties; +import dk.erst.cm.api.data.Order; +import dk.erst.cm.api.order.BasketService; +import dk.erst.cm.api.order.BasketService.SendBasketData; +import dk.erst.cm.api.order.BasketService.SendBasketResponse; +import dk.erst.cm.api.order.BasketService.SentBasketData; +import dk.erst.cm.api.order.OrderService; +import lombok.extern.slf4j.Slf4j; +import oasis.names.specification.ubl.schema.xsd.order_21.OrderType; @CrossOrigin(maxAge = 3600) @RestController @Slf4j public class OrderController { - private final BasketService basketService; - private final AppProperties appProperties; + private final BasketService basketService; + private final OrderService orderService; + private final AppProperties appProperties; + + @Autowired + public OrderController(BasketService basketService, AppProperties appProperties, OrderService orderService) { + this.basketService = basketService; + this.appProperties = appProperties; + this.orderService = orderService; + } - @Autowired - public OrderController(BasketService basketService, AppProperties appProperties) { - this.basketService = basketService; - this.appProperties = appProperties; - } + @RequestMapping(value = "/api/basket/send") + public SendBasketResponse basketSend(@RequestBody SendBasketData query) { + log.info("START basketSend: query=" + query); + SendBasketResponse res = basketService.basketSend(query, appProperties.getIntegration().getOutboxOrder(), appProperties.getOrder()); + if (res.isSuccess()) { + log.info("END basketSend OK: " + res); + } else { + log.info("END basketSend Error: " + res); + } + return res; + } - @RequestMapping(value = "/api/basket/send") - public SendBasketResponse basketSend(@RequestBody SendBasketData query) { - log.info("START basketSend: query=" + query); - SendBasketResponse res = basketService.basketSend(query, appProperties.getIntegration().getOutboxOrder(), appProperties.getOrder()); - if (res.isSuccess()) { - log.info("END basketSend OK: " + res); - } else { - log.info("END basketSend Error: " + res); - } - return res; - } + @RequestMapping(value = "/api/basket/{id}") + public ResponseEntity getBasketById(@PathVariable("id") String id) { + Optional findById = basketService.loadSentBasketData(id); + if (findById.isPresent()) { + return new ResponseEntity<>(findById.get(), HttpStatus.OK); + } + return ResponseEntity.notFound().build(); + } - @RequestMapping(value = "/api/basket/{id}") - public ResponseEntity getBasketById(@PathVariable("id") String id) { - Optional findById = basketService.loadSentBasketData(id); - if (findById.isPresent()) { - return new ResponseEntity<>(findById.get(), HttpStatus.OK); - } - return ResponseEntity.notFound().build(); - } + @RequestMapping(value = "/api/basket/{id}/zip", produces = "application/zip") + public ResponseEntity getBasketByIdXML(@PathVariable("id") String id) { + Optional findById = basketService.loadSentBasketData(id); + if (findById.isPresent()) { + ResponseEntity.BodyBuilder resp = ResponseEntity.ok(); + resp.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"basket_" + id + ".zip\""); + resp.contentType(MediaType.parseMediaType("application/zip")); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(bos)) { + for (Order order : findById.get().getOrderList()) { + ZipEntry entry = new ZipEntry(order.getId() + ".xml"); + zos.putNextEntry(entry); + ByteArrayOutputStream orderStream = new ByteArrayOutputStream(); + orderService.saveOrderXMLToStream((OrderType) order.getDocument(), orderStream); + zos.write(orderStream.toByteArray()); + zos.closeEntry(); + } + } catch (Exception e) { + log.error("Error creating zip file", e); + } + return resp.body(bos.toByteArray()); + } + return ResponseEntity.notFound().build(); + } - @RequestMapping(value = "/api/order/{id}") - public ResponseEntity getOrderById(@PathVariable("id") String id) { - Optional findById = basketService.loadSentOrderAsJSON(id); - if (findById.isPresent()) { - return new ResponseEntity<>(findById.get(), HttpStatus.OK); - } - return ResponseEntity.notFound().build(); - } + @RequestMapping(value = "/api/order/{id}") + public ResponseEntity getOrderById(@PathVariable("id") String id) { + Optional findById = basketService.loadSentOrderAsJSON(id); + if (findById.isPresent()) { + return new ResponseEntity<>(findById.get(), HttpStatus.OK); + } + return ResponseEntity.notFound().build(); + } + @RequestMapping(value = "/api/order/{id}/xml", produces = "application/xml") + public ResponseEntity getOrderByIdXML(@PathVariable("id") String id) { + Optional findById = basketService.loadSentOrderAsXML(id); + if (findById.isPresent()) { + ResponseEntity.BodyBuilder resp = ResponseEntity.ok(); + resp.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"order_" + id + ".xml\""); + resp.contentType(MediaType.parseMediaType("application/xml")); + return resp.body(findById.get()); + } + return ResponseEntity.notFound().build(); + } } From 70f242f4acac5615b0ae3d27fbb71b4f4222d849 Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 7 May 2022 22:12:59 +0300 Subject: [PATCH 70/71] Use selected product quantity in XML --- .../main/java/dk/erst/cm/api/order/BasketService.java | 2 +- .../dk/erst/cm/api/order/OrderProducerService.java | 9 +++++++-- .../dk/erst/cm/api/order/OrderProducerServiceTest.java | 10 ++++++++-- cm-webapi/src/main/resources/application.properties | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java index 36fd1dd..2c2d627 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java @@ -173,7 +173,7 @@ public SendBasketResponse basketSend(SendBasketData query, String outboxFolder, PartyInfo buyer = new PartyInfo("5798009882806", "0088", customerOrderData.getBuyerCompany().getRegistrationName()); PartyInfo seller = new PartyInfo("5798009882783", "0088", "Danish Company"); - OrderType sendOrder = orderProducerService.generateOrder(order, orderConfig, buyer, seller, productList); + OrderType sendOrder = orderProducerService.generateOrder(order, orderConfig, buyer, seller, productList, query.basketData.orderLines); order.setDocument(sendOrder); orderList.add(order); diff --git a/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java index 3bcd6e6..7892bac 100644 --- a/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java +++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java @@ -5,6 +5,7 @@ import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.springframework.stereotype.Service; @@ -66,7 +67,7 @@ public PartyInfo(String defaultId, String defaultSchemeID, String defaultName) { } } - public OrderType generateOrder(Order dataOrder, OrderDefaultConfig defaultConfig, PartyInfo buyer, PartyInfo seller, List productList) { + public OrderType generateOrder(Order dataOrder, OrderDefaultConfig defaultConfig, PartyInfo buyer, PartyInfo seller, List productList, Map productQuantityMap) { OrderType order = new OrderType(); order.setCustomizationID("urn:fdc:peppol.eu:poacc:trns:order:3"); @@ -116,7 +117,11 @@ public OrderType generateOrder(Order dataOrder, OrderDefaultConfig defaultConfig OrderLineType line = new OrderLineType(); LineItemType lineItem = new LineItemType(); lineItem.setID(product.getId()); - lineItem.setQuantity(BigDecimal.valueOf(1)); + long quantity = 1; + if (productQuantityMap != null && productQuantityMap.containsKey(product.getId())) { + quantity = productQuantityMap.get(product.getId()); + } + lineItem.setQuantity(BigDecimal.valueOf(quantity)); lineItem.getQuantity().setUnitCode("EA"); ItemType item = new ItemType(); item.setName(catalogueLine.getItem().getName()); diff --git a/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java index 3df63ce..050ca9d 100644 --- a/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java +++ b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java @@ -10,7 +10,10 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.UUID; import org.junit.jupiter.api.Test; @@ -57,6 +60,7 @@ void produce() { ByteArrayOutputStream out = new ByteArrayOutputStream(); List productList = new ArrayList(); Product product = new Product(); + product.setId(UUID.randomUUID().toString()); CatalogueLine catalogueLine = new CatalogueLine(); Item item = new Item(); item.setName("Test line"); @@ -78,7 +82,9 @@ void produce() { OrderDefaultConfig defaultConfig = new OrderDefaultConfig(); defaultConfig.setNote("TEST NOTE"); - OrderType order = service.generateOrder(dataOrder, defaultConfig, buyer, seller, productList); + Map productQuantityMap = new HashMap(); + productList.forEach(p -> productQuantityMap.put(p.getId(), 2)); + OrderType order = service.generateOrder(dataOrder, defaultConfig, buyer, seller, productList, productQuantityMap); IErrorList errorList = UBL21Validator.order().validate(order); if (errorList.isNotEmpty()) { log.error("Found " + errorList.size() + " errors:"); @@ -95,4 +101,4 @@ void produce() { assertTrue(xml.indexOf(defaultConfig.getNote()) > 0); } -} \ No newline at end of file +} diff --git a/cm-webapi/src/main/resources/application.properties b/cm-webapi/src/main/resources/application.properties index c69be58..c36ed6f 100644 --- a/cm-webapi/src/main/resources/application.properties +++ b/cm-webapi/src/main/resources/application.properties @@ -1,7 +1,7 @@ server.servlet.context-path=/ logging.level.dk.erst.catalog = INFO -logging.level.dk.erst.cm.MongoConverterConfig = DEBUG +#logging.level.dk.erst.cm.MongoConverterConfig = DEBUG spring.data.mongodb.uri=mongodb://localhost:27017/dc?ssl=false spring.data.mongodb.auto-index-creation = true From 4b91fb7fef36c74393d9cee376aa9bbcb9b7684e Mon Sep 17 00:00:00 2001 From: Dmytro Lapko Date: Sat, 7 May 2022 22:14:49 +0300 Subject: [PATCH 71/71] Missed link to order xml on order list in basket --- cm-frontend/src/components/BasketSendResult.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cm-frontend/src/components/BasketSendResult.js b/cm-frontend/src/components/BasketSendResult.js index 0711fb6..8b994ac 100644 --- a/cm-frontend/src/components/BasketSendResult.js +++ b/cm-frontend/src/components/BasketSendResult.js @@ -99,7 +99,7 @@ export default function BasketSendResult(props) { {row.orderNumber} {row.lineCount} - + ))}