diff --git a/.gitignore b/.gitignore
index 7f0b1dd..16b5027 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,4 +50,7 @@ dist/
log/
# Skip VisualStudio Code folders
-.vscode/
\ No newline at end of file
+.vscode/
+
+.tomcat/
+.integration/
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-all1.0.0
- Catalog Manager :: Web API and Frontent together
+ Catalog Manager :: Web API and Frontend together1.81.0.0
@@ -79,7 +79,13 @@
+ org.apache.maven.pluginsmaven-resources-plugin
+ 2.6
+ false
+
+ UTF-8
+ position-react-build
@@ -101,7 +107,9 @@
+ org.apache.maven.pluginsmaven-clean-plugin
+ 3.0.0true
@@ -110,6 +118,7 @@
org.springframework.bootspring-boot-maven-plugin
+ 2.4.0dk.erst.cm.CatalogApiApplication
diff --git a/cm-api/pom.xml b/cm-api/pom.xml
index 66b7376..45a0cf1 100644
--- a/cm-api/pom.xml
+++ b/cm-api/pom.xml
@@ -12,7 +12,7 @@
1.81.0.0
- 2.4.0
+ 2.6.7UTF-8${java.version}${java.version}
@@ -24,6 +24,13 @@
cm-ubl${cm.version}
+
+
+ com.helger.ubl
+ ph-ubl21
+ 6.6.3
+
+
org.springframework.bootspring-boot-starter-data-mongodb
@@ -33,7 +40,7 @@
com.opencsvopencsv
- 4.4
+ 5.6
@@ -84,4 +91,4 @@
-
\ No newline at end of file
+
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..1eb722e
--- /dev/null
+++ b/cm-api/src/main/java/dk/erst/cm/api/dao/mongo/OrderRepository.java
@@ -0,0 +1,13 @@
+package dk.erst.cm.api.dao.mongo;
+
+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/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..8bc688c
--- /dev/null
+++ b/cm-api/src/main/java/dk/erst/cm/api/data/Order.java
@@ -0,0 +1,41 @@
+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;
+
+ @Indexed
+ private String basketId;
+
+ 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/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-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/order/BasketService.java b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java
new file mode 100644
index 0000000..2c2d627
--- /dev/null
+++ b/cm-api/src/main/java/dk/erst/cm/api/order/BasketService.java
@@ -0,0 +1,343 @@
+package dk.erst.cm.api.order;
+
+import java.io.ByteArrayOutputStream;
+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.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;
+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.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;
+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 SentBasketData {
+ private Basket basket;
+ private List orderList;
+ }
+
+ @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, OrderDefaultConfig orderConfig) {
+ 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.computeIfAbsent(productCatalogId, k -> new ArrayList<>());
+ 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(list.stream().map(Product::getId).collect(Collectors.toList()));
+ countProductIncompleteSeller += list.size();
+ }
+ String errorMessage = buildErrorMessageNoSellerInfo(byCatalogMap, noSellerCatalogSet, countProductIncompleteSeller);
+ return SendBasketResponse.error(errorMessage).withErrorProductIdList(errorProductIdList);
+ }
+
+ 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(generateId());
+ order.setLineCount(productList.size());
+ order.setStatus(OrderStatus.GENERATED);
+ order.setOrderNumber(generateOrderNumber(order));
+ order.setSupplierName(extractSupplierName(sellerPartyByCatalog.get(catalogId)));
+ order.setVersion(1);
+
+ 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, query.basketData.orderLines);
+ order.setDocument(sendOrder);
+
+ orderList.add(order);
+
+ orderIndex++;
+ }
+
+ 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<>();
+ assert tempFiles != null;
+ 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 successfully");
+
+ 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) {
+ 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);
+ //noinspection ResultOfMethodCallIgnored
+ 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);
+ }
+
+ 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 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/OrderProducerService.java b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java
new file mode 100644
index 0000000..7892bac
--- /dev/null
+++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderProducerService.java
@@ -0,0 +1,166 @@
+package dk.erst.cm.api.order;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.stereotype.Service;
+
+import dk.erst.cm.api.data.Order;
+import dk.erst.cm.api.data.Product;
+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;
+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.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 {
+
+ 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;
+ }
+ }
+
+ 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");
+ order.setProfileID("urn:fdc:peppol.eu:poacc:bis:order_only:3");
+ /*
+ * 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());
+ order.setDocumentCurrencyCode("DKK");
+ if (defaultConfig.getNote() != null) {
+ order.getNote().add(new NoteType(defaultConfig.getNote()));
+ }
+
+ CustomerPartyType buyerCustomerParty = new CustomerPartyType();
+ buyerCustomerParty.setParty(buildParty(buyer));
+ order.setBuyerCustomerParty(buyerCustomerParty);
+
+ SupplierPartyType supplierPartyType = new SupplierPartyType();
+ supplierPartyType.setParty(buildParty(seller));
+ order.setSellerSupplierParty(supplierPartyType);
+
+ AddressType supplierAddress = new AddressType();
+ supplierAddress.setCityName("Stockholm");
+ supplierAddress.setPostalZone("2100");
+ CountryType countryType = new CountryType();
+ countryType.setIdentificationCode("SE");
+ 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);
+
+ 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(product.getId());
+ 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());
+
+ ItemIdentificationType itemIdentificationType = new ItemIdentificationType();
+ itemIdentificationType.setID(catalogueLine.getItem().getSellersItemIdentification().getId());
+ 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;
+ }
+
+}
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..2abeb04
--- /dev/null
+++ b/cm-api/src/main/java/dk/erst/cm/api/order/OrderService.java
@@ -0,0 +1,105 @@
+package dk.erst.cm.api.order;
+
+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 MongoTemplate mongoTemplate;
+
+ @Autowired
+ public OrderService(MongoTemplate mongoTemplate, BasketRepository basketRepository, OrderRepository orderRepository) {
+ this.mongoTemplate = mongoTemplate;
+ this.basketRepository = basketRepository;
+ this.orderRepository = orderRepository;
+ }
+
+ public Optional findBasketById(String basketId) {
+ return basketRepository.findById(basketId);
+ }
+
+ public List findOrdersByBasketId(String basketId) {
+ return orderRepository.findByBasketId(basketId);
+ }
+
+ @SuppressWarnings("unused")
+ 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 saveOrder(Order order) {
+ 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(Files.newOutputStream(tempFile.toPath()))) {
+ saveOrderXMLToStream(sendOrder, out);
+ }
+ return tempFile;
+ }
+
+ public void saveOrderXMLToStream(OrderType sendOrder, OutputStream out) {
+ UBL21Writer.order().write(sendOrder, out);
+ }
+
+}
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/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-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..050ca9d
--- /dev/null
+++ b/cm-api/src/test/java/dk/erst/cm/api/order/OrderProducerServiceTest.java
@@ -0,0 +1,104 @@
+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.HashMap;
+import java.util.List;
+import java.util.Map;
+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 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;
+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();
+ product.setId(UUID.randomUUID().toString());
+ 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());
+
+ PartyInfo buyer = new PartyInfo("5798009882806", "0088", "Buyer Company");
+ PartyInfo seller = new PartyInfo("5798009882783", "0088", "Seller Company");
+
+ OrderDefaultConfig defaultConfig = new OrderDefaultConfig();
+ defaultConfig.setNote("TEST NOTE");
+ 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:");
+ for (int i = 0; i < errorList.size(); i++) {
+ IError error = errorList.get(i);
+ log.error((i + 1) + "\t" + error.toString());
+ }
+ }
+ assertTrue(errorList.isEmpty());
+ UBL21Writer.order().write(order, out);
+ 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);
+ }
+
+}
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
diff --git a/cm-frontend/src/components/AddToBasket.js b/cm-frontend/src/components/AddToBasket.js
new file mode 100644
index 0000000..8496840
--- /dev/null
+++ b/cm-frontend/src/components/AddToBasket.js
@@ -0,0 +1,38 @@
+import React from "react";
+import {Button} from "@material-ui/core";
+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 handleClick = () => {
+ handleProductBasketIconClick(basketData, product, changeBasket);
+ }
+
+ return (
+ <>
+
+ >
+ )
+}
\ No newline at end of file
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.
+