diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 86ea2c603..6a30e91e6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,31 +4,51 @@ Changelog This project adheres to `Semantic Versioning `_. +1.0.0-alpha.4 (2021-03-16) +-------------------------- + +**Added** + +* Introduce subtotals in Offer PDF ProductItem Table(`#349 `_) +* Add logging with throwable cause (`#371 `_) +* Introduce distinction of products in the offer PDF according to the associated service + data generation, data analysis and project management (`#364 `_) +* Introduce overheadRatio property to life.qbic.business.offers.Offer + used to show the applied overhead markup in the pricing footer of the Offer PDF(`#362 `_) +* Introduce first draft for OpenBis based project space and project creation (`#396 `_) +* Introduce first draft for product maintenance and creation (`#392 `_) + +**Fixed** + +* User cannot select other offers from the overview anymore, during the offer details are loaded + after a selection. Selection is enabled again after the resource has been loaded. This solves a + not yet reported issue that can be observed when dealing with a significant network delay. + +**Dependencies** + +**Deprecated** + 1.0.0-alpha.3 (2021-03-02) -------------------------- **Added** -* Authorization based on user roles (`#346 `_). Two new roles have been introduced that represent the organisational roles project manager ``Role.PROJECT_MANAGER`` and offer admin ``Role.OFFER_ADMIN``. The administrator will provide access to additional app features, such as the upcoming service product maintenance interface. -* Introduce Offer retrieval via Fetch Offer Use Case -* Replace the project description with project objective (`#339 `_) -* Added support to configure the chromium browser executable. An environment variable -* A `CHROMIUM_ALIAS` has been introduced that can be set to define the chromium executable in the deployment system of the application. Addresses `#336 `_ +* Authorization based on user roles. Two new roles have been introduced that represent + the organisational roles project manager `Role.PROJECT_MANAGER` and offer admin `Role + .OFFER_ADMIN`. The administrator will provide access to additional app features, such as the + upcoming service product maintenance interface. + +* Introduce Offer retrieval via Fetch Offer Use Case (`#344 `_) **Fixed** * Update the agreement section of the offer (`#329 `_) * Make the offer controls more intuitive (`#341 `_) * Update offers without changes is not possible anymore (`#222 `_) -* Rename CreateCustomer and UpdateCustomer classes and methods (`#315 `_) -* Fixed (`#324 `_) -* Create new affiliation does not show an error message anymore (`#320 `_) -* Updating a person entry now refreshes the search view (`#325 `_) +* Rename CreateCustomer and UpdateCustomer classes and methods (`#315 https://github.com/qbicsoftware/offer-manager-2-portlet/issues/315`_) **Dependencies** -* Upgraded log4j to log4j-core 2.14.0 - **Deprecated** @@ -69,8 +89,8 @@ This project adheres to `Semantic Versioning `_. * Addressed `#309 `_ * Replace the project description with project objective (`#339 `_) * Added support to configure the chromium browser executable. An environment variable -`CHROMIUM_ALIAS` has been introduced that can be set to define the chromium executable in the -deployment system of the application. Addresses `#336 `_ + `CHROMIUM_ALIAS` has been introduced that can be set to define the chromium executable in the + deployment system of the application. Addresses `#336 `_ **Fixed** @@ -79,6 +99,4 @@ deployment system of the application. Addresses `#336 `_ +For some Linux system the application is also provided by the name ``chromium-browser`` + +.. code-block:: bash + + sudo apt-get install chromium-browser -.. code-block: bash -brew install --cask chromium +After successful installation please provide the offer manager with your chromium installation by setting + +.. code-block:: bash + + export CHROMIUM_EXECUTABLE= Run the project with -.. code-block: bash -mvn clean jetty:run -Denvironment=testing +.. code-block:: bash + + mvn clean jetty:run -Denvironment=testing -And open the application through localhost:8080. The system property `-Denvironment=testing` will +And open the application through ``localhost:8080``. The system property ``-Denvironment=testing`` will enable to application to run in test mode and does not require a successful user role determination to access all the features. Authorization and roles ----------------------- -The offer manager app currently distinguishes between two roles: `Role.PROJECT_MANAGER` and -`Role.OFFER_ADMIN`. The admin role provides access to features such as the service +The offer manager app currently distinguishes between two roles: ``Role.PROJECT_MANAGER`` and +``Role.OFFER_ADMIN``. The admin role provides access to features such as the service product maintenance interface, and only dedicated users with the admin role will be able to access it. -The current production implementation of the `RoleService` interface is used for deployment in an -Liferay 6.2 GA6 environment and maps the Liferay **site-roles** "Project Manager" and "Offer -Administration" to the internal app role representation. +The current production implementation of the ``RoleService`` interface is used for deployment in an +``Liferay 6.2 GA6`` environment and maps the Liferay *site-roles* `"Project Manager"` and `"Offer +Administration"` to the internal app role representation. If an authenticated user has none of these roles, she will not be able to execute the application. @@ -65,10 +81,14 @@ System setup In order to enable the offer manager app to convert an offer as PDF, you need to define a environment variable in the system's environment accessible by the application. -The app will look for an environment variable `CHROMIUM_ALIAS`, so make sure to have set it. +The app will look for an environment variable ``CHROMIUM_EXECUTABLE``, so make sure to have set it. + +In the example of the local test environment, a simple +:: + + export CHROMIUM_EXECUTABLE=chromium -In the example of the local test environment, a simple `export CHROMIUM_EXECUTABLE=chromium` is -sufficient. +is sufficient. Credits diff --git a/offer-manager-app/pom.xml b/offer-manager-app/pom.xml index 8fe13437b..b02cd3871 100644 --- a/offer-manager-app/pom.xml +++ b/offer-manager-app/pom.xml @@ -5,7 +5,7 @@ offer-manager life.qbic - 1.0.0-alpha.3 + 1.0.0-alpha.4 4.0.0 war @@ -15,7 +15,7 @@ life.qbic offer-manager-domain - 1.0.0-alpha.3 + 1.0.0-alpha.4 compile diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy index a0c282d43..e516eac61 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy @@ -2,22 +2,35 @@ package life.qbic.portal.offermanager import groovy.util.logging.Log4j2 import life.qbic.business.offers.fetch.FetchOffer +import life.qbic.business.projects.create.CreateProject import life.qbic.datamodel.dtos.business.AcademicTitle import life.qbic.datamodel.dtos.business.AffiliationCategory -import life.qbic.business.customers.affiliation.create.CreateAffiliation -import life.qbic.business.customers.create.CreateCustomer +import life.qbic.business.persons.affiliation.create.CreateAffiliation +import life.qbic.business.persons.create.CreatePerson import life.qbic.business.offers.create.CreateOffer import life.qbic.datamodel.dtos.business.Offer import life.qbic.datamodel.dtos.general.Person import life.qbic.portal.offermanager.communication.EventEmitter import life.qbic.portal.offermanager.components.offer.overview.OfferOverviewController import life.qbic.portal.offermanager.components.offer.overview.OfferOverviewPresenter +import life.qbic.portal.offermanager.components.offer.overview.projectcreation.CreateProjectController +import life.qbic.portal.offermanager.components.offer.overview.projectcreation.CreateProjectPresenter +import life.qbic.portal.offermanager.components.offer.overview.projectcreation.CreateProjectView +import life.qbic.portal.offermanager.components.offer.overview.projectcreation.CreateProjectViewModel import life.qbic.portal.offermanager.components.person.search.SearchPersonView import life.qbic.portal.offermanager.components.person.search.SearchPersonViewModel import life.qbic.portal.offermanager.components.person.update.UpdatePersonViewModel +import life.qbic.portal.offermanager.components.product.MaintainProductsView +import life.qbic.portal.offermanager.components.product.MaintainProductsViewModel +import life.qbic.portal.offermanager.components.product.create.CreateProductView +import life.qbic.portal.offermanager.components.product.create.CreateProductViewModel import life.qbic.portal.offermanager.dataresources.persons.AffiliationResourcesService -import life.qbic.portal.offermanager.dataresources.persons.CustomerDbConnector +import life.qbic.portal.offermanager.dataresources.persons.PersonDbConnector import life.qbic.portal.offermanager.dataresources.persons.CustomerResourceService +import life.qbic.portal.offermanager.dataresources.projects.ProjectMainConnector +import life.qbic.portal.offermanager.dataresources.projects.ProjectDbConnector + +import life.qbic.openbis.openbisclient.OpenBisClient import life.qbic.portal.offermanager.dataresources.database.DatabaseSession import life.qbic.portal.offermanager.dataresources.offers.OfferDbConnector @@ -49,6 +62,8 @@ import life.qbic.portal.offermanager.components.person.create.CreatePersonView import life.qbic.portal.offermanager.components.offer.create.CreateOfferView import life.qbic.portal.offermanager.components.offer.overview.OfferOverviewView import life.qbic.portal.offermanager.components.AppView +import life.qbic.portal.offermanager.dataresources.projects.ProjectResourceService +import life.qbic.portal.offermanager.dataresources.projects.ProjectSpaceResourceService import life.qbic.portal.offermanager.security.Role import life.qbic.portal.utils.ConfigurationManager import life.qbic.portal.utils.ConfigurationManagerFactory @@ -77,6 +92,10 @@ class DependencyManager { private OfferOverviewModel offerOverviewModel private SearchPersonViewModel searchPersonViewModel private CreatePersonViewModel createCustomerViewModelNewOffer + private MaintainProductsViewModel maintainProductsViewModel + private CreateProductViewModel createProductViewModel + private CreateProductViewModel copyProductViewModel + private CreateProjectViewModel createProjectModel private AppPresenter presenter private CreatePersonPresenter createCustomerPresenter @@ -86,17 +105,22 @@ class DependencyManager { private CreateOfferPresenter createOfferPresenter private CreateOfferPresenter updateOfferPresenter private OfferOverviewPresenter offerOverviewPresenter + private CreateProjectPresenter createProjectPresenter - private CustomerDbConnector customerDbConnector + private PersonDbConnector customerDbConnector private OfferDbConnector offerDbConnector private ProductsDbConnector productsDbConnector + private ProjectMainConnector projectMainConnector + private ProjectDbConnector projectDbConnector + private OpenBisClient openbisClient - private CreateCustomer createCustomer - private CreateCustomer createCustomerNewOffer - private CreateCustomer updateCustomer + private CreatePerson createCustomer + private CreatePerson createCustomerNewOffer + private CreatePerson updateCustomer private CreateAffiliation createAffiliation private CreateOffer createOffer private CreateOffer updateOffer + private CreateProject createProject private FetchOffer fetchOfferOfferOverview private FetchOffer fetchOfferCreateOffer private FetchOffer fetchOfferUpdateOffer @@ -108,6 +132,7 @@ class DependencyManager { private CreateOfferController createOfferController private CreateOfferController updateOfferController private OfferOverviewController offerOverviewController + private CreateProjectController createProjectController private CreatePersonView createCustomerView private CreatePersonView updatePersonView @@ -124,6 +149,8 @@ class DependencyManager { private ProductsResourcesService productsResourcesService private ProjectManagerResourceService managerResourceService private PersonResourceService personResourceService + private ProjectSpaceResourceService projectSpaceResourceService + private ProjectResourceService projectResourceService private EventEmitter personUpdateEvent /** @@ -164,10 +191,18 @@ class DependencyManager { String sqlDatabase = Objects.requireNonNull(configurationManager.getMysqlDB(), "Mysql database name missing.") DatabaseSession.init(user, password, host, port, sqlDatabase) - customerDbConnector = new CustomerDbConnector(DatabaseSession.getInstance()) + customerDbConnector = new PersonDbConnector(DatabaseSession.getInstance()) productsDbConnector = new ProductsDbConnector(DatabaseSession.getInstance()) offerDbConnector = new OfferDbConnector(DatabaseSession.getInstance(), customerDbConnector, productsDbConnector) + projectDbConnector = new ProjectDbConnector(DatabaseSession.getInstance(), customerDbConnector) + + + final String openbisURL = configurationManager.getDataSourceUrl() + "/openbis/openbis" + openbisClient = new OpenBisClient(configurationManager.getDataSourceUser(), configurationManager.getDataSourcePassword(), openbisURL) + openbisClient.login() + + projectMainConnector = new ProjectMainConnector(projectDbConnector, openbisClient) } catch (Exception e) { log.error("Unexpected exception during customer database connection.", e) @@ -183,6 +218,8 @@ class DependencyManager { this.affiliationService = new AffiliationResourcesService(customerDbConnector) this.customerResourceService = new CustomerResourceService(customerDbConnector) this.personResourceService = new PersonResourceService(customerDbConnector) + this.projectSpaceResourceService = new ProjectSpaceResourceService(projectMainConnector) + this.projectResourceService = new ProjectResourceService(projectMainConnector) } private void setupEventEmitter(){ @@ -195,7 +232,7 @@ class DependencyManager { try { this.viewModel = new AppViewModel(affiliationService, this.userRole) } catch (Exception e) { - log.error("Unexpected excpetion during ${AppViewModel.getSimpleName()} view model setup.", e) + log.error("Unexpected exception during ${AppViewModel.getSimpleName()} view model setup.", e) throw e } @@ -207,7 +244,7 @@ class DependencyManager { createCustomerViewModel.academicTitles.addAll(AcademicTitle.values().collect {it.value}) } catch (Exception e) { - log.error("Unexpected excpetion during ${CreatePersonViewModel.getSimpleName()} view model setup.", e) + log.error("Unexpected exception during ${CreatePersonViewModel.getSimpleName()} view model setup.", e) throw e } @@ -220,7 +257,7 @@ class DependencyManager { createCustomerViewModelNewOffer.academicTitles.addAll(AcademicTitle.values().collect {it.value}) } catch (Exception e) { - log.error("Unexpected excpetion during ${CreatePersonViewModel.getSimpleName()} view model setup.", e) + log.error("Unexpected exception during ${CreatePersonViewModel.getSimpleName()} view model setup.", e) throw e } @@ -234,7 +271,7 @@ class DependencyManager { updatePersonViewModel.academicTitles.addAll(AcademicTitle.values().collect {it.value}) } catch (Exception e) { - log.error("Unexpected excpetion during ${UpdatePersonViewModel.getSimpleName()} view model setup.", e) + log.error("Unexpected exception during ${UpdatePersonViewModel.getSimpleName()} view model setup.", e) throw e } @@ -242,7 +279,7 @@ class DependencyManager { this.createAffiliationViewModel = new CreateAffiliationViewModel(affiliationService) createAffiliationViewModel.affiliationCategories.addAll(AffiliationCategory.values().collect{it.value}) } catch (Exception e) { - log.error("Unexpected excpetion during ${CreateAffiliationViewModel.getSimpleName()} view model setup.", e) + log.error("Unexpected exception during ${CreateAffiliationViewModel.getSimpleName()} view model setup.", e) throw e } @@ -264,20 +301,45 @@ class DependencyManager { productsResourcesService, offerUpdateEvent) } catch (Exception e) { - log.error("Unexpected excpetion during ${CreateOfferViewModel.getSimpleName()} view model setup.", e) + log.error("Unexpected exception during ${CreateOfferViewModel.getSimpleName()} view model setup.", e) throw e } try { this.offerOverviewModel = new OfferOverviewModel(overviewService, viewModel, offerUpdateEvent) } catch (Exception e) { - log.error("Unexpected excpetion during ${OfferOverviewModel.getSimpleName()} view model setup.", e) + log.error("Unexpected exception during ${OfferOverviewModel.getSimpleName()} view model setup.", e) } try { this.searchPersonViewModel = new SearchPersonViewModel(personResourceService, personUpdateEvent) }catch (Exception e) { - log.error("Unexpected excpetion during ${SearchPersonViewModel.getSimpleName()} view model setup.", e) + log.error("Unexpected exception during ${SearchPersonViewModel.getSimpleName()} view model setup.", e) + } + + try { + this.createProjectModel = new CreateProjectViewModel(projectSpaceResourceService, projectResourceService) + }catch (Exception e) { + log.error("Unexpected exception during ${CreateProjectViewModel.getSimpleName()} view model" + + " setup.", e) + } + + try { + this.maintainProductsViewModel = new MaintainProductsViewModel(productsResourcesService) + }catch (Exception e) { + log.error("Unexpected exception during ${MaintainProductsViewModel.getSimpleName()} view model setup.", e) + } + + try { + this.createProductViewModel = new CreateProductViewModel() + }catch (Exception e) { + log.error("Unexpected exception during ${CreateProductViewModel.getSimpleName()} view model setup.", e) + } + + try { + this.copyProductViewModel = new CreateProductViewModel() + }catch (Exception e) { + log.error("Unexpected exception during ${CreateProductViewModel.getSimpleName()} view model setup.", e) } } @@ -335,18 +397,28 @@ class DependencyManager { } catch (Exception e) { log.error("Unexpected exception during ${OfferOverviewPresenter.getSimpleName()} setup", e) } + try { + this.createProjectPresenter = new CreateProjectPresenter(createProjectModel, viewModel) + } catch (Exception e) { + log.error("Unexpected exception during ${OfferOverviewPresenter.getSimpleName()} setup", e) + } } private void setupUseCaseInteractors() { - this.createCustomer = new CreateCustomer(createCustomerPresenter, customerDbConnector) - this.createCustomerNewOffer = new CreateCustomer(createCustomerPresenterNewOffer, customerDbConnector) + this.createCustomer = new CreatePerson(createCustomerPresenter, customerDbConnector) + this.createCustomerNewOffer = new CreatePerson(createCustomerPresenterNewOffer, customerDbConnector) + this.createAffiliation = new CreateAffiliation(createAffiliationPresenter, customerDbConnector) + this.createOffer = new CreateOffer(offerDbConnector, createOfferPresenter) this.updateOffer = new CreateOffer(offerDbConnector, updateOfferPresenter) - this.updateCustomer = new CreateCustomer(updateCustomerPresenter, customerDbConnector) + this.updateCustomer = new CreatePerson(updateCustomerPresenter, customerDbConnector) + this.fetchOfferOfferOverview = new FetchOffer(offerDbConnector, offerOverviewPresenter) this.fetchOfferCreateOffer = new FetchOffer(offerDbConnector, createOfferPresenter) this.fetchOfferUpdateOffer = new FetchOffer(offerDbConnector, updateOfferPresenter) + + this.createProject = new CreateProject(createProjectPresenter, projectMainConnector, projectMainConnector) } private void setupControllers() { @@ -389,6 +461,11 @@ class DependencyManager { } catch (Exception e) { log.error("Unexpected exception during ${OfferOverviewController.getSimpleName()} setup", e) } + try { + this.createProjectController = new CreateProjectController(this.createProject) + } catch (Exception e) { + log.error("Unexpected exception during ${OfferOverviewController.getSimpleName()} setup", e) + } } private void setupViews() { @@ -449,9 +526,17 @@ class DependencyManager { throw e } + CreateProjectView createProjectView + try{ + createProjectView = new CreateProjectView(createProjectModel, createProjectController) + } catch (Exception e) { + log.error("Could not create ${CreateProjectView.getSimpleName()} view.", e) + throw e + } + OfferOverviewView overviewView try { - overviewView = new OfferOverviewView(offerOverviewModel, offerOverviewController) + overviewView = new OfferOverviewView(offerOverviewModel, offerOverviewController, createProjectView) } catch (Exception e) { log.error("Could not create ${OfferOverviewView.getSimpleName()} view.", e) throw e @@ -465,6 +550,30 @@ class DependencyManager { throw e } + CreateProductView createProductView + try{ + createProductView = new CreateProductView(createProductViewModel) + }catch(Exception e){ + log.error("Could not create ${CreateProductView.getSimpleName()} view.", e) + throw e + } + + CreateProductView copyProductView + try{ + copyProductView = new CreateProductView(copyProductViewModel) + }catch(Exception e){ + log.error("Could not create ${CreateProductView.getSimpleName()} view.", e) + throw e + } + + MaintainProductsView maintainProductsView + try{ + maintainProductsView = new MaintainProductsView(maintainProductsViewModel,createProductView,copyProductView) + }catch (Exception e) { + log.error("Could not create ${MaintainProductsView.getSimpleName()} view.", e) + throw e + } + AppView portletView try { CreatePersonView createCustomerView2 = new CreatePersonView(createCustomerController, this @@ -477,7 +586,9 @@ class DependencyManager { createOfferView, overviewView, updateOfferView, - searchPersonView + searchPersonView, + maintainProductsView, + createProjectView ) this.portletView = portletView } catch (Exception e) { diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferManagerApp.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferManagerApp.groovy index 32f3af725..17518952f 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferManagerApp.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferManagerApp.groovy @@ -59,6 +59,7 @@ class OfferManagerApp extends QBiCPortletUI { roleService = new LiferayRoleService() userId = VaadinService.getCurrentRequest().getRemoteUser() } + //fixme empty userId might lead to NullPointer if no ' environment' is set return loadAppRole(roleService, userId) } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferToPDFConverter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferToPDFConverter.groovy index 011d25c0c..d8f1d506a 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferToPDFConverter.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferToPDFConverter.groovy @@ -9,6 +9,11 @@ import life.qbic.datamodel.dtos.business.ProductItem import life.qbic.datamodel.dtos.business.ProjectManager import life.qbic.business.offers.Currency import life.qbic.business.offers.OfferExporter +import life.qbic.datamodel.dtos.business.services.DataStorage +import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis +import life.qbic.datamodel.dtos.business.services.ProjectManagement +import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis +import life.qbic.datamodel.dtos.business.services.Sequencing import org.jsoup.nodes.Document import org.jsoup.parser.Parser @@ -17,6 +22,7 @@ import java.nio.file.Path import java.nio.file.Paths import java.nio.file.StandardCopyOption import java.text.DateFormat +import java.text.DecimalFormat /** * Handles the conversion of offers to pdf files @@ -39,7 +45,17 @@ class OfferToPDFConverter implements OfferExporter { * alias that can be executed from the system's command * line. */ - final static CHROMIUM_EXECUTABLE = "CHROMIUM_EXECUTABLE" + static final CHROMIUM_EXECUTABLE = "CHROMIUM_EXECUTABLE" + + /** + * Variable used to count the number of productItems in a productTable + */ + private static int tableItemsCount + + /** + * Variable used to count the number of generated productTables in the Offer PDF + */ + private static int tableCount private final Offer offer @@ -68,6 +84,28 @@ class OfferToPDFConverter implements OfferExporter { .getResource("offer-template/stylesheet.css") .toURI()) + /** + * Possible product groups + * + * This enum describes the product groups into which the products of an offer are listed. + * + */ + enum ProductGroups { + DATA_GENERATION("Data generation"), + DATA_ANALYSIS("Data analysis"), + DATA_MANAGEMENT("Project management and data storage") + + private String name + + ProductGroups(String name) { + this.name = name; + } + + String getName() { + return this.name; + } + } + OfferToPDFConverter(Offer offer) { this.offer = Objects.requireNonNull(offer, "Offer object must not be a null reference") this.tempDir = Files.createTempDirectory("offer") @@ -104,7 +142,7 @@ class OfferToPDFConverter implements OfferExporter { setCustomerInformation() setManagerInformation() setSelectedItems() - setPrices() + setTotalPrices() setQuotationDetails() } @@ -132,6 +170,7 @@ class OfferToPDFConverter implements OfferExporter { htmlContent.getElementById("customer-postal-code").text(affiliation.postalCode) htmlContent.getElementById("customer-city").text(affiliation.city) htmlContent.getElementById("customer-country").text(affiliation.country) + } private void setManagerInformation() { @@ -152,42 +191,62 @@ class OfferToPDFConverter implements OfferExporter { // Let's clear the existing item template content first htmlContent.getElementById("product-items-1").empty() //and remove the footer on the first page - htmlContent.getElementById("grid-table-footer").remove() - // Set the start offer position - def itemPos = 1 - // max number of table items per page - def maxTableItems = 10 - // - def tableNum = 1 - def elementId = "product-items"+"-"+tableNum - // Create the items in html in the overview table - offer.items.each { item -> + //htmlContent.getElementById("grid-table-footer").remove() + + List productItems = offer.items + + //Initialize Number of table + tableCount = 1 + + //Initialize Count of ProductItems in table + tableItemsCount = 1 + int maxTableItems = 8 + + //Group ProductItems into Data Generation Data Analysis and Data & Project Management Categories + Map productItemsMap = groupItems(productItems) + + //Generate Product Table for each Category + generateProductTable(productItemsMap, maxTableItems) + + //Append total cost footer + if (tableItemsCount > maxTableItems) { + //If currentTable is filled with Items generate new one and add total pricing there + ++tableCount + String elementId = "product-items" + "-" + tableCount + htmlContent.getElementById("item-table-grid").append(ItemPrintout.tableHeader(elementId)) + htmlContent.getElementById("item-table-grid") + .append(ItemPrintout.tableFooter(offer.overheadRatio)) + } else { + //otherwise add total pricing to table + htmlContent.getElementById("item-table-grid") + .append(ItemPrintout.tableFooter(offer.overheadRatio)) + } + } - if (itemPos % maxTableItems == 0) //start (next) table - { - elementId = "product-items"+"-"+ ++tableNum - htmlContent.getElementById("item-table-grid").append(ItemPrintout.tableHeader(elementId)) - } - htmlContent.getElementById(elementId) - .append(ItemPrintout.itemInHTML(itemPos++, item)) + void setSubTotalPrices(ProductGroups productGroup, List productItems) { + double netSum = calculateNetSum(productItems) + double overheadSum = calculateOverheadSum(productItems) + double totalSum = netSum + overheadSum - } + final netPrice = Currency.getFormatterWithoutSymbol().format(netSum) + final overheadPrice = Currency.getFormatterWithoutSymbol().format(overheadSum) + final totalPrice = Currency.getFormatterWithoutSymbol().format(totalSum) - //create the footer only for the last page containing a table - htmlContent.getElementById("item-table-grid") - .append(ItemPrintout.tableFooter()) + htmlContent.getElementById("${productGroup}-net-costs-value").text(netPrice) + htmlContent.getElementById("${productGroup}-overhead-costs-value").text(overheadPrice) + htmlContent.getElementById("${productGroup}-total-costs-value").text(totalPrice) } - void setPrices() { + void setTotalPrices() { final totalPrice = Currency.getFormatterWithoutSymbol().format(offer.totalPrice) final taxes = Currency.getFormatterWithoutSymbol().format(offer.taxes) final netPrice = Currency.getFormatterWithoutSymbol().format(offer.netPrice) final netPrice_withSymbol = Currency.getFormatterWithSymbol().format(offer.netPrice) - + final overheadPrice = Currency.getFormatterWithoutSymbol().format(offer.overheads) htmlContent.getElementById("total-costs-net").text(netPrice_withSymbol) - + htmlContent.getElementById("overhead-cost-value").text(overheadPrice) htmlContent.getElementById("total-cost-value-net").text(netPrice) htmlContent.getElementById("vat-cost-value").text(taxes) htmlContent.getElementById("final-cost-value").text(totalPrice) @@ -201,6 +260,87 @@ class OfferToPDFConverter implements OfferExporter { htmlContent.getElementById("offer-date").text(dateFormat.format(offer.modificationDate)) } + double calculateNetSum(List productItems){ + double netSum = 0 + productItems.each { + netSum += it.quantity * it.product.unitPrice + } + return netSum + } + + double calculateOverheadSum(List productItems) { + double overheadSum = 0 + productItems.each { + if (it.product instanceof PrimaryAnalysis || it.product instanceof SecondaryAnalysis || it.product instanceof Sequencing) { + overheadSum += it.quantity * it.product.unitPrice * offer.overheadRatio + } + } + return overheadSum + } + + Map groupItems(List productItems) { + + Map> productItemsMap = [:] + + List dataGenerationItems = [] + List dataAnalysisItems = [] + //Project Management and Data Storage are grouped in the same category in the final Offer PDF + List dataManagementItems = [] + + // Sort ProductItems into "DataGeneration", "Data Analysis" and "Project & Data Management" + productItems.each { + if (it.product instanceof Sequencing) { + dataGenerationItems.add(it) + } + if (it.product instanceof PrimaryAnalysis || it.product instanceof SecondaryAnalysis) { + dataAnalysisItems.add(it) + } + if (it.product instanceof DataStorage || it.product instanceof ProjectManagement) { + dataManagementItems.add(it) + } + } + + //Map Lists to the "DataGeneration", "DataAnalysis" and "Project and Data Management" + productItemsMap[ProductGroups.DATA_GENERATION] = dataGenerationItems + productItemsMap[ProductGroups.DATA_ANALYSIS] = dataAnalysisItems + productItemsMap[ProductGroups.DATA_MANAGEMENT] = dataManagementItems + + return productItemsMap + } + + void generateProductTable(Map productItemsMap, int maxTableItems) { + // Create the items in html in the overview table + productItemsMap.each {ProductGroups productGroup, List items -> + //Check if there are ProductItems stored in map entry + if(items){ + def elementId = "product-items" + "-" + tableCount + //Append Table Title + htmlContent.getElementById(elementId).append(ItemPrintout.tableTitle(productGroup)) + items.eachWithIndex {ProductItem item, int itemPos -> + //start (next) table and add Product to it + if (tableItemsCount >= maxTableItems) { + ++tableCount + elementId = "product-items" + "-" + tableCount + htmlContent.getElementById("item-table-grid").append(ItemPrintout.tableHeader(elementId)) + tableItemsCount = 1 + } + //add product to current table + int productNumber = itemPos + 1 + htmlContent.getElementById(elementId).append(ItemPrintout.itemInHTML(productNumber, item)) + tableItemsCount++ + } + //add subtotal footer to table + htmlContent.getElementById(elementId).append(ItemPrintout.subTableFooter(productGroup)) + /* This variable indicates that the space which is normally reserved for 2 products, + should be accounted for the subtotal Footer */ + int productSpaceCount = 2 + + tableItemsCount = tableItemsCount + productSpaceCount + // Update Footer Prices + setSubTotalPrices(productGroup, items) + } + } + } /** * Small helper class to handle the HTML to PDF conversion. */ @@ -245,22 +385,25 @@ class OfferToPDFConverter implements OfferExporter { private static class ItemPrintout { static String itemInHTML(int offerPosition, ProductItem item) { + String totalCost = Currency.getFormatterWithoutSymbol().format(item.quantity * item.product.unitPrice) return """
${offerPosition}
${item.product.productName}
${item.quantity}
${item.product.unit}
${Currency.getFormatterWithoutSymbol().format(item.product.unitPrice)}
-
${Currency.getFormatterWithoutSymbol().format(item.quantity * item.product.unitPrice)}
+
${totalCost}
${item.product.description}
-
""" + + """ + } - static String tableHeader(String elementId){ + static String tableHeader(String elementId) { //1. add pagebreak //2. create empty table for elementId return """
@@ -276,12 +419,52 @@ class OfferToPDFConverter implements OfferExporter { """ } - static String tableFooter(){ + static String tableTitle(ProductGroups productGroup){ + + String tableTitle= productGroup.getName() + + + return """
+

${tableTitle}

+ """ + } + + static String subTableFooter(ProductGroups productGroup){ + + String footerTitle = productGroup.getName() + + return """ + """ + } + + static String tableFooter(double overheadRatio){ + + DecimalFormat decimalFormat = new DecimalFormat("#%") + String overheadPercentage = decimalFormat.format(overheadRatio) + return """""" +
+ """ } - } -} \ No newline at end of file + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/AppView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/AppView.groovy index 9e17ae078..492d5acd8 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/AppView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/AppView.groovy @@ -6,11 +6,12 @@ import com.vaadin.ui.* import com.vaadin.ui.themes.ValoTheme import life.qbic.portal.offermanager.components.affiliation.create.CreateAffiliationView import life.qbic.portal.offermanager.components.offer.create.CreateOfferView +import life.qbic.portal.offermanager.components.offer.overview.projectcreation.CreateProjectView import life.qbic.portal.offermanager.components.person.create.CreatePersonView import life.qbic.portal.offermanager.components.offer.overview.OfferOverviewView import life.qbic.portal.offermanager.components.person.search.SearchPersonView -import life.qbic.portal.offermanager.security.Role -import life.qbic.portal.offermanager.security.RoleService +import life.qbic.portal.offermanager.components.product.MaintainProductsView + /** * Class which connects the view elements with the ViewModel and the Controller @@ -18,8 +19,7 @@ import life.qbic.portal.offermanager.security.RoleService * This class provides the initial listeners * and layout upon which the views are presented * - * @since: 1.0.0 - * @author: Jennifer Bödker + * @since 1.0.0 * */ class AppView extends VerticalLayout { @@ -32,7 +32,8 @@ class AppView extends VerticalLayout { private final List featureViews private final OfferOverviewView overviewView private final SearchPersonView searchPersonView - + private final MaintainProductsView maintainProductsView + private final CreateProjectView createProjectView private final CreateOfferView updateOfferView AppView(AppViewModel portletViewModel, @@ -41,7 +42,9 @@ class AppView extends VerticalLayout { CreateOfferView createOfferView, OfferOverviewView overviewView, CreateOfferView updateOfferView, - SearchPersonView searchPersonView) { + SearchPersonView searchPersonView, + MaintainProductsView maintainProductsView, + CreateProjectView createProjectView) { super() this.portletViewModel = portletViewModel this.createCustomerView = createCustomerView @@ -51,6 +54,8 @@ class AppView extends VerticalLayout { this.overviewView = overviewView this.updateOfferView = updateOfferView this.searchPersonView = searchPersonView + this.maintainProductsView = maintainProductsView + this.createProjectView = createProjectView initLayout() registerListeners() @@ -71,7 +76,9 @@ class AppView extends VerticalLayout { createOfferView, overviewView, updateOfferView, - searchPersonView + searchPersonView, + maintainProductsView, + createProjectView ]) } @@ -98,6 +105,8 @@ class AppView extends VerticalLayout { verticalLayout.addComponent(this.overviewView) verticalLayout.addComponent(this.updateOfferView) verticalLayout.addComponent(this.searchPersonView) + verticalLayout.addComponent(this.maintainProductsView) + verticalLayout.addComponent(this.createProjectView) this.setSizeFull() this.addComponent(verticalLayout) @@ -153,19 +162,25 @@ class AppView extends VerticalLayout { Button searchPersonBtn + Button maintainProductBtn + + TomatoFeatures() { this.createOfferBtn = new Button("New Offer") this.createCustomerBtn = new Button("New Customer") this.createAffiliationBtn = new Button("New Affiliation") this.overviewBtn = new Button("Offer Overview") this.searchPersonBtn = new Button("Search Customer") + this.maintainProductBtn = new Button("Maintain Products") + this.addComponents( overviewBtn, createOfferBtn, createCustomerBtn, createAffiliationBtn, - searchPersonBtn + searchPersonBtn, + maintainProductBtn ) setStyles() setupListeners() @@ -178,6 +193,7 @@ class AppView extends VerticalLayout { createOfferBtn.setEnabled(portletViewModel.createOfferFeatureEnabled) createCustomerBtn.setEnabled(portletViewModel.createCustomerFeatureEnabled) searchPersonBtn.setEnabled(portletViewModel.searchCustomerFeatureEnabled) + maintainProductBtn.setEnabled(portletViewModel.maintainProductsFeatureEnabled) } private void setDefault() { @@ -190,6 +206,7 @@ class AppView extends VerticalLayout { createCustomerBtn.addStyleName(ValoTheme.BUTTON_BORDERLESS_COLORED) createAffiliationBtn.addStyleName(ValoTheme.BUTTON_BORDERLESS_COLORED) searchPersonBtn.addStyleName(ValoTheme.BUTTON_BORDERLESS_COLORED) + maintainProductBtn.addStyleName(ValoTheme.BUTTON_BORDERLESS_COLORED) } private void setIcons() { @@ -198,6 +215,7 @@ class AppView extends VerticalLayout { createCustomerBtn.setIcon(VaadinIcons.GRID_BIG_O) createAffiliationBtn.setIcon(VaadinIcons.GRID_BIG_O) searchPersonBtn.setIcon(VaadinIcons.GRID_BIG_O) + maintainProductBtn.setIcon(VaadinIcons.GRID_BIG_O) } private void setButtonActive(Button b) { @@ -235,6 +253,12 @@ class AppView extends VerticalLayout { searchPersonView.setVisible(true) setButtonActive(this.searchPersonBtn) }) + this.maintainProductBtn.addClickListener({ + hideAllFeatureViews() + setIcons() + maintainProductsView.setVisible(true) + setButtonActive(this.maintainProductBtn) + }) } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/AppViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/AppViewModel.groovy index 7b0df42bd..59d2353b5 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/AppViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/AppViewModel.groovy @@ -30,6 +30,8 @@ class AppViewModel { boolean searchCustomerFeatureEnabled + boolean maintainProductsFeatureEnabled + AppViewModel(AffiliationResourcesService service, Role role) { this(new ArrayList(), @@ -63,10 +65,11 @@ class AppViewModel { private void setBasicFeatures() { createCustomerFeatureEnabled = true createOfferFeatureEnabled = true - searchCustomerFeatureEnabled = false + searchCustomerFeatureEnabled = true + maintainProductsFeatureEnabled = false } private void setAdminFeatures() { - searchCustomerFeatureEnabled = true + maintainProductsFeatureEnabled = true } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationController.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationController.groovy index 15550784a..a864b22ee 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationController.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationController.groovy @@ -4,7 +4,7 @@ import groovy.util.logging.Log4j2 import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.datamodel.dtos.business.AffiliationCategory import life.qbic.datamodel.dtos.business.AffiliationCategoryFactory -import life.qbic.business.customers.affiliation.create.CreateAffiliationInput +import life.qbic.business.persons.affiliation.create.CreateAffiliationInput /** * Controller class adapter from view information into use case input interface diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationPresenter.groovy index 52b3dd20f..751b2498d 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationPresenter.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationPresenter.groovy @@ -2,7 +2,7 @@ package life.qbic.portal.offermanager.components.affiliation.create import groovy.util.logging.Log4j2 import life.qbic.datamodel.dtos.business.Affiliation -import life.qbic.business.customers.affiliation.create.CreateAffiliationOutput +import life.qbic.business.persons.affiliation.create.CreateAffiliationOutput import life.qbic.portal.offermanager.components.AppViewModel /** diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationView.groovy index 807aced5e..2f7acdfc6 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/affiliation/create/CreateAffiliationView.groovy @@ -1,6 +1,5 @@ package life.qbic.portal.offermanager.components.affiliation.create -import com.vaadin.data.Binder import com.vaadin.data.ValidationResult import com.vaadin.data.Validator import com.vaadin.data.ValueContext @@ -10,8 +9,7 @@ import com.vaadin.shared.ui.ContentMode import com.vaadin.ui.* import com.vaadin.ui.themes.ValoTheme import groovy.util.logging.Log4j2 -import life.qbic.business.Constants -import life.qbic.business.customers.affiliation.Country +import life.qbic.business.persons.affiliation.Country import life.qbic.datamodel.dtos.business.Affiliation import java.util.stream.Collectors; diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy index 085e13bf8..8c802d62d 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy @@ -18,7 +18,7 @@ import com.vaadin.ui.components.grid.HeaderRow import com.vaadin.ui.themes.ValoTheme import groovy.util.logging.Log4j2 import life.qbic.datamodel.dtos.business.Offer - +import life.qbic.portal.offermanager.components.offer.overview.projectcreation.CreateProjectView import life.qbic.portal.offermanager.dataresources.offers.OfferOverview import life.qbic.business.offers.Currency import life.qbic.portal.offermanager.components.GridUtils @@ -50,14 +50,21 @@ class OfferOverviewView extends FormLayout { private FileDownloader fileDownloader + private CreateProjectView createProjectView + + private Button createProjectButton - OfferOverviewView(OfferOverviewModel model, OfferOverviewController offerOverviewController) { + OfferOverviewView(OfferOverviewModel model, + OfferOverviewController offerOverviewController, + CreateProjectView createProjectView) { this.model = model this.offerOverviewController = offerOverviewController this.overviewGrid = new Grid<>() this.downloadBtn = new Button(VaadinIcons.DOWNLOAD) this.updateOfferBtn = new Button(VaadinIcons.EDIT) + this.createProjectButton = new Button("Create Project", VaadinIcons.PLUS_CIRCLE) this.downloadSpinner = new ProgressBar() + this.createProjectView = createProjectView initLayout() setupGrid() @@ -89,10 +96,17 @@ class OfferOverviewView extends FormLayout { updateOfferBtn.setStyleName(ValoTheme.BUTTON_LARGE) updateOfferBtn.setEnabled(false) updateOfferBtn.setDescription("Update offer") + createProjectButton.setEnabled(false) + createProjectButton.setStyleName(ValoTheme.BUTTON_LARGE) // Makes the progress bar a spinner downloadSpinner.setIndeterminate(true) downloadSpinner.setVisible(false) - activityContainer.addComponents(downloadBtn, updateOfferBtn, downloadSpinner) + // Add a button to create a project from an offer + activityContainer.addComponents( + downloadBtn, + updateOfferBtn, + createProjectButton, + downloadSpinner) activityContainer.setMargin(false) headerRow.addComponents(activityContainer,overviewGrid) @@ -143,6 +157,19 @@ class OfferOverviewView extends FormLayout { } private void setupListeners() { + setupGridListeners() + updateOfferBtn.addClickListener({ + model.offerEventEmitter.emit(model.getSelectedOffer()) + }) + createProjectButton.addClickListener({ + this.setVisible(false) + createProjectView.setVisible(true) + createProjectView.model.startedFromView = Optional.of(this) + createProjectView.model.selectedOffer = Optional.of(model.selectedOffer) + }) + } + + private void setupGridListeners() { overviewGrid.addSelectionListener( { selection -> selection.firstSelectedItem.ifPresent({overview -> @@ -151,9 +178,6 @@ class OfferOverviewView extends FormLayout { new LoadOfferInfoThread(UI.getCurrent(), overview).start() }) }) - updateOfferBtn.addClickListener({ - model.offerEventEmitter.emit(model.getSelectedOffer()) - }) } private void createResourceForDownload() { @@ -186,20 +210,33 @@ class OfferOverviewView extends FormLayout { @Override void run() { + + Optional selectedOffer = Optional.empty() ui.access(() -> { downloadSpinner.setVisible(true) overviewGrid.setEnabled(false) + selectedOffer = overviewGrid.getSelectionModel().getFirstSelectedItem() + overviewGrid.setSelectionMode(Grid.SelectionMode.NONE) downloadBtn.setEnabled(false) updateOfferBtn.setEnabled(false) + createProjectButton.setEnabled(false) }) offerOverviewController.fetchOffer(offerOverview.offerId) createResourceForDownload() ui.access(() -> { downloadSpinner.setVisible(false) + overviewGrid.setSelectionMode(Grid.SelectionMode.SINGLE) + // After we have set the single mode to NONE, the listeners seem to be gone + // So we set them again + // IMPORTANT: the selection must be set before we attach the listener, + // otherwise the selection listener gets triggered (LOOP!) + overviewGrid.select(selectedOffer.get()) + setupGridListeners() overviewGrid.setEnabled(true) downloadBtn.setEnabled(true) updateOfferBtn.setEnabled(true) + createProjectButton.setEnabled(true) ui.setPollInterval(-1) }) } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectController.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectController.groovy new file mode 100644 index 000000000..5106a5c94 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectController.groovy @@ -0,0 +1,68 @@ +package life.qbic.portal.offermanager.components.offer.overview.projectcreation + +import life.qbic.business.projects.create.CreateProjectInput +import life.qbic.datamodel.dtos.business.Offer +import life.qbic.datamodel.dtos.business.ProjectApplication +import life.qbic.datamodel.dtos.projectmanagement.ProjectCode +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace + +/** + *

Starts the use case 'Create Project'

+ * + *

Connects the create project component with the use case Create Projects.

+ * + * @since 1.0.0 + */ +class CreateProjectController { + + private final CreateProjectInput createProjectInput + + /** + * Constructor with an create project use case input instance. + * @param createProjectInput The create project use case input. + */ + CreateProjectController(CreateProjectInput createProjectInput) { + this.createProjectInput = createProjectInput + } + + /** + * Requests a project registration + * @param offer The offer associated with the project + * @param projectIdentifier The desired project identifier (space + code) + */ + void createProject(Offer offer, + ProjectIdentifier projectIdentifier) { + ProjectApplication application = createApplication(offer, projectIdentifier.projectSpace, + projectIdentifier.projectCode) + createProjectInput.createProject(application) + } + + /** + * Requests a project registration with a new project space creation + * @param offer The offer associated with the project + * @param projectSpace The desired project space + * @param projectCode The desired project code + */ + void createProjectAndSpace(Offer offer, + ProjectSpace projectSpace, + ProjectCode projectCode) { + ProjectApplication application = createApplication(offer, projectSpace, projectCode) + createProjectInput.createProjectWithSpace(application) + } + + private static ProjectApplication createApplication(Offer offer, + ProjectSpace projectSpace, + ProjectCode projectCode) { + return new ProjectApplication( + offer.identifier, + offer.projectTitle, + offer.projectObjective, + "", + offer.projectManager, + projectSpace, + offer.customer, + projectCode + ) + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectPresenter.groovy new file mode 100644 index 000000000..748adcab3 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectPresenter.groovy @@ -0,0 +1,59 @@ +package life.qbic.portal.offermanager.components.offer.overview.projectcreation + +import life.qbic.business.projects.create.CreateProjectOutput +import life.qbic.datamodel.dtos.business.OfferId +import life.qbic.datamodel.dtos.projectmanagement.Project +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier +import life.qbic.portal.offermanager.components.AppViewModel + +/** + *

Presenter that deals with the Create Project use case output

+ * + * The presenter currently deals with three different use case output + * scenarios: + *
    + *
  1. The project creation was successful
  2. + *
  3. A project with the given identifier already existed
  4. + *
  5. The project creation failed
  6. + *
+ * + * @since 1.0.0 + */ +class CreateProjectPresenter implements CreateProjectOutput{ + + private final CreateProjectViewModel createProjectViewModel + + private final AppViewModel appViewModel + + CreateProjectPresenter(CreateProjectViewModel createProjectViewModel, AppViewModel appViewModel) { + this.createProjectViewModel = createProjectViewModel + this.appViewModel = appViewModel + } + + /** + * {@inheritDocs} + */ + @Override + void failNotification(String notification) { + this.appViewModel.failureNotifications.add("This should not have happened. Please " + + "contact the QBiC helpdesk. \n${notification}") + } + + /** + * {@inheritDocs} + */ + @Override + void projectCreated(Project project) { + this.createProjectViewModel.setProjectCreated(true) + this.appViewModel.successNotifications.add("Project ${project.projectId} created.") + } + + /** + * {@inheritDocs} + */ + @Override + void projectAlreadyExists(ProjectIdentifier projectIdentifier, OfferId linkedOffer) { + this.appViewModel.failureNotifications.add("A project with the id ${projectIdentifier} " + + "already exists.") + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy new file mode 100644 index 000000000..82a65321a --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy @@ -0,0 +1,315 @@ +package life.qbic.portal.offermanager.components.offer.overview.projectcreation + +import com.vaadin.icons.VaadinIcons +import com.vaadin.shared.ui.ContentMode +import com.vaadin.ui.Button +import com.vaadin.ui.ComboBox +import com.vaadin.ui.GridLayout +import com.vaadin.ui.HorizontalLayout +import com.vaadin.ui.Label +import com.vaadin.ui.Panel +import com.vaadin.ui.RadioButtonGroup +import com.vaadin.ui.TextArea +import com.vaadin.ui.TextField +import com.vaadin.ui.VerticalLayout +import com.vaadin.ui.themes.ValoTheme +import life.qbic.datamodel.dtos.business.Offer +import life.qbic.datamodel.dtos.projectmanagement.ProjectCode +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace + + + +/** + *

Enables a user to create a project based on an existing offer

+ * + *

This view provides access the the use cases Create Project Space and + * Create Project.

+ *

Both use cases are part of the same scenario, when a user wants to create a project + * in QBiC's data management platform based on the information of an existing offer.

+ * + * @since 1.0.0 + */ +class CreateProjectView extends VerticalLayout{ + + private RadioButtonGroup projectSpaceSelection + + static final EnumMap spaceSelectionActionText = + new EnumMap(CreateProjectViewModel.SPACE_SELECTION.class) + + static { + spaceSelectionActionText.put( + CreateProjectViewModel.SPACE_SELECTION.EXISTING_SPACE, + "an existing project space") + spaceSelectionActionText.put( + CreateProjectViewModel.SPACE_SELECTION.NEW_SPACE, + "a new project space") + } + + final CreateProjectViewModel model + + private TextField desiredSpaceName + + private TextField resultingSpaceName + + private HorizontalLayout customSpaceLayout + + private HorizontalLayout existingSpaceLayout + + private ComboBox availableSpacesBox + + private HorizontalLayout projectCodeLayout + + private TextField desiredProjectCode + + private TextField resultingProjectCode + + private HorizontalLayout projectAvailability + + private Button createProjectButton + + private Panel selectedOfferInformation + + private GridLayout viewContainerGrid + + private VerticalLayout inputFields + + private CreateProjectController createProjectController + + /** + * This button enables the user to leave the create project view + * and navigate back to the previous view. + * This means that a click listener must be attached by the parent + * component that displayed this view in the first place. + */ + Button navigateBack + + CreateProjectView(CreateProjectViewModel createProjectModel, CreateProjectController createProjectController) { + this.model = createProjectModel + this.createProjectController = createProjectController + setupVaadinComponents() + configureListeners() + bindData() + } + + private void setupVaadinComponents() { + createGridAndContainers() + createSiteNavigation() + createTitle() + createOfferInfo() + createProjectSpaceElements() + createProjectCodeElements() + createProjectIdOverview() + setupVisibility() + setupActivity() + this.addComponent(viewContainerGrid) + } + + private void createGridAndContainers() { + viewContainerGrid = new GridLayout(2,3) + viewContainerGrid.setWidth("100%") + viewContainerGrid.setColumnExpandRatio(0, 0.6f) + viewContainerGrid.setColumnExpandRatio(1, 0.4f) + inputFields = new VerticalLayout() + inputFields.setMargin(false) + viewContainerGrid.addComponent(inputFields, 0,0) + } + + private void createOfferInfo() { + VerticalLayout container = new VerticalLayout() + selectedOfferInformation = new Panel("Selected Offer") + selectedOfferInformation.setContent(new Label("Offer Info Placeholder")) + container.addComponent(selectedOfferInformation) + viewContainerGrid.addComponent(container, 1, 0) + } + + private void createSiteNavigation() { + navigateBack = new Button("Go Back", VaadinIcons.ARROW_CIRCLE_LEFT) + navigateBack.setStyleName(ValoTheme.BUTTON_BORDERLESS_COLORED) + this.addComponent(navigateBack) + } + + private void createTitle() { + Label label = new Label("Project Creation") + label.setStyleName(ValoTheme.LABEL_HUGE) + this.addComponent(label) + } + + private void setupVisibility() { + customSpaceLayout.setVisible(false) + existingSpaceLayout.setVisible(false) + } + + private void setupActivity() { + resultingSpaceName.setEnabled(false) + resultingProjectCode.setEnabled(false) + createProjectButton.setEnabled(model.createProjectEnabled) + } + + private void createProjectSpaceElements() { + // Set a nice header + Label label = new Label("1. Please select/create a project space first") + label.setStyleName(ValoTheme.LABEL_H3) + this.inputFields.addComponent(label) + + /* The user needs to choose between creating a new project space + or select an existing one */ + // First we create a ratio group with the choices available + projectSpaceSelection = new RadioButtonGroup<>("Create project in", + model.spaceSelectionDataProvider) + projectSpaceSelection.setItemCaptionGenerator(item -> spaceSelectionActionText.get(item)) + this.inputFields.addComponent(projectSpaceSelection) + + // Case A: A new space needs to be created + customSpaceLayout = new HorizontalLayout() + desiredSpaceName = new TextField("New space name") + desiredSpaceName.setPlaceholder("Your space name") + desiredSpaceName.setWidth(300, Unit.PIXELS) + customSpaceLayout.addComponents(desiredSpaceName) + this.inputFields.addComponent(customSpaceLayout) + + // Case B: An existing space is selected + existingSpaceLayout = new HorizontalLayout() + availableSpacesBox = new ComboBox<>("Available project spaces") + existingSpaceLayout.addComponent(availableSpacesBox) + availableSpacesBox.setWidth(300, Unit.PIXELS) + this.inputFields.addComponent(existingSpaceLayout) + } + + private void createProjectCodeElements() { + // Set a nice header + Label label = new Label("2. Please set a project code") + label.setStyleName(ValoTheme.LABEL_H3) + this.inputFields.addComponent(label) + + // then a input field for the code + projectCodeLayout = new HorizontalLayout() + projectCodeLayout.setMargin(false) + def container = new HorizontalLayout() + desiredProjectCode = new TextField() + desiredProjectCode.setPlaceholder("Your desired code") + container.addComponents(desiredProjectCode) + // We also define some dynamic validation place holder + projectCodeLayout.addComponent(container) + projectAvailability = new HorizontalLayout() + projectCodeLayout.addComponent(projectAvailability) + this.inputFields.addComponent(projectCodeLayout) + } + + private void createProjectIdOverview() { + def projectIdContainer = new HorizontalLayout() + def caption = new Label("Resulting project identifier") + caption.setStyleName(ValoTheme.LABEL_H3) + resultingSpaceName = new TextField() + resultingSpaceName.setWidth(300, Unit.PIXELS) + resultingProjectCode = new TextField() + projectIdContainer.addComponents( + resultingSpaceName, + new Label("/"), + resultingProjectCode) + // Last but not least, the project creation button + createProjectButton = new Button("Create Project", VaadinIcons.CHECK_SQUARE) + projectIdContainer.addComponent(createProjectButton) + // Add the ui elements to the parent layout + this.inputFields.addComponent(caption) + this.inputFields.addComponent(projectIdContainer) + } + + private void configureListeners() { + // We update the model with the desired space name content + this.desiredSpaceName.addValueChangeListener({model.desiredSpaceName = it.value}) + // We update the model with the desired project code + this.desiredProjectCode.addValueChangeListener({model.desiredProjectCode = it.value}) + // Enable back navigation + this.navigateBack.addClickListener({ + this.setVisible(false) + if (model.startedFromView.isPresent()) { + model.startedFromView.get().setVisible(true) + } + }) + // We toggle between the two cases, weather a new space needs to be created + // or an existing space needs to be selected + this.projectSpaceSelection.addValueChangeListener({ + if (it.value == CreateProjectViewModel.SPACE_SELECTION.NEW_SPACE) { + existingSpaceLayout.setVisible(false) + customSpaceLayout.setVisible(true) + model.spaceSelection = CreateProjectViewModel.SPACE_SELECTION.NEW_SPACE + } else { + existingSpaceLayout.setVisible(true) + customSpaceLayout.setVisible(false) + model.spaceSelection = CreateProjectViewModel.SPACE_SELECTION.EXISTING_SPACE + } + }) + this.availableSpacesBox.addValueChangeListener({ + if (it.value) { + model.desiredSpaceName = it.value + } else { + model.desiredSpaceName = "" + } + }) + // Whenever the resulting space name is updated, we update the view + this.model.addPropertyChangeListener("resultingSpaceName", {this.resultingSpaceName + .setValue(model.resultingSpaceName)}) + // Whenever new project code validation messages are available, we update the view + this.model.addPropertyChangeListener("projectCodeValidationResult", { + this.projectAvailability.removeAllComponents() + this.resultingProjectCode.setValue(model.resultingProjectCode) + if (model.codeIsValid) { + // If the project code is valid, we display some nice success label + def label = new Label(model.projectCodeValidationResult) + label.setStyleName(ValoTheme.LABEL_SUCCESS) + this.projectAvailability.addComponent(label) + } else { + // otherwise we inform the user with a formatted failure label + def label = new Label(model.projectCodeValidationResult) + label.setStyleName(ValoTheme.LABEL_FAILURE) + this.projectAvailability.addComponent(label) + } + }) + // Whenever all validation is fine, we enable the button to create a project + this.model.addPropertyChangeListener("createProjectEnabled", { + this.createProjectButton.setEnabled(model.createProjectEnabled) + }) + this.model.addPropertyChangeListener("selectedOffer", { + displaySelectedOfferInfo() + }) + this.createProjectButton.addClickListener({ + if(model.spaceSelection == CreateProjectViewModel.SPACE_SELECTION.NEW_SPACE){ + createProjectController.createProjectAndSpace(model.selectedOffer.get(), new ProjectSpace(model.resultingSpaceName), + new ProjectCode(model.resultingProjectCode)) + }else{ + createProjectController.createProject(model.selectedOffer.get(), + new ProjectIdentifier( + new ProjectSpace(model.resultingSpaceName), + new ProjectCode(model.resultingProjectCode))) + } + }) + } + + private void bindData() { + availableSpacesBox.setDataProvider(model.availableSpaces) + } + + private void displaySelectedOfferInfo() { + if (model.selectedOffer.isPresent()) { + loadOfferData(model.selectedOffer.get()) + } + } + + private void loadOfferData(Offer offer) { + VerticalLayout content = new VerticalLayout() + content.addComponent(new Label("Offer ID", ContentMode.HTML)) + content.addComponent(new Label("${offer.identifier}")) + content.addComponent(new Label("Customer", ContentMode.HTML)) + content.addComponent(new Label("${offer.customer.firstName} ${offer.customer.lastName}")) + content.addComponent(new Label("Title", ContentMode.HTML)) + TextArea title = new TextArea() + title.setWidth("100%") + title.setRows(3) + title.setEnabled(false) + title.setValue(offer.projectTitle) + content.addComponent(title) + content.setSpacing(false) + selectedOfferInformation.setContent(content) + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectViewModel.groovy new file mode 100644 index 000000000..9f1f2c125 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectViewModel.groovy @@ -0,0 +1,140 @@ +package life.qbic.portal.offermanager.components.offer.overview.projectcreation + +import com.vaadin.data.provider.DataProvider +import com.vaadin.data.provider.ListDataProvider +import com.vaadin.ui.Layout +import groovy.beans.Bindable +import life.qbic.datamodel.dtos.business.Offer +import life.qbic.datamodel.dtos.projectmanagement.ProjectCode +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace +import life.qbic.portal.offermanager.dataresources.projects.ProjectResourceService +import life.qbic.portal.offermanager.dataresources.projects.ProjectSpaceResourceService + +/** + *

Holds the create project state information and logic

+ * + *

View model for the {@link CreateProjectView}.

+ * + * @since 1.0.0 + */ +class CreateProjectViewModel { + + /** + * Flag that indicates when a project has been created + */ + @Bindable Boolean projectCreated + + /** + * Saves the layout from which the create project component + * has been initiated. + * This view is set to visible again, if the user decides to navigate back. + */ + Optional startedFromView + + /** + * The selected offer that holds the information + * for the projected to be created by the user. + */ + @Bindable Optional selectedOffer + + @Bindable Boolean createProjectEnabled + + enum SPACE_SELECTION { + NEW_SPACE, EXISTING_SPACE + } + DataProvider spaceSelectionDataProvider + + @Bindable String resultingSpaceName + + @Bindable String desiredSpaceName + + @Bindable SPACE_SELECTION spaceSelection + + DataProvider availableSpaces + + @Bindable String desiredProjectCode + + @Bindable String resultingProjectCode + + List existingProjects + + @Bindable String projectCodeValidationResult + + @Bindable Boolean codeIsValid + + private final ProjectSpaceResourceService projectSpaceResourceService + + private final ProjectResourceService projectResourceService + + CreateProjectViewModel(ProjectSpaceResourceService projectSpaceResourceService, + ProjectResourceService projectResourceService) { + this.projectSpaceResourceService = projectSpaceResourceService + this.projectResourceService = projectResourceService + + spaceSelectionDataProvider = new ListDataProvider<>([SPACE_SELECTION.NEW_SPACE, + SPACE_SELECTION.EXISTING_SPACE]) + + availableSpaces = new ListDataProvider(projectSpaceResourceService.iterator().toList()) + existingProjects = projectResourceService.iterator().collect {it.projectCode} + initFields() + setupListeners() + } + + private void setupListeners() { + this.addPropertyChangeListener("desiredSpaceName", { + ProjectSpace space = new ProjectSpace(desiredSpaceName) + this.setResultingSpaceName(space.name) + }) + this.addPropertyChangeListener("desiredProjectCode", { + validateProjectCode() + evaluateProjectCreation() + }) + this.addPropertyChangeListener("resultingSpaceName", { + evaluateProjectCreation() + }) + this.addPropertyChangeListener("projectCreated", { + resetModel() + }) + } + + private void initFields() { + resultingSpaceName = "" + desiredSpaceName = "" + desiredProjectCode = "" + resultingProjectCode = "" + projectCodeValidationResult = "" + codeIsValid = false + startedFromView = Optional.empty() + createProjectEnabled = false + projectCreated = false + selectedOffer = Optional.empty() + + } + + private void resetModel() { + initFields() + } + + private void validateProjectCode() { + try { + ProjectCode code = new ProjectCode(desiredProjectCode.toUpperCase()) + this.setResultingProjectCode(code.code) + if (code in existingProjects) { + this.setCodeIsValid(false) + this.setProjectCodeValidationResult("Project with code $resultingProjectCode " + + "already exists.") + } else { + this.setCodeIsValid(true) + this.setProjectCodeValidationResult("Project code is valid.") + } + } catch (IllegalArgumentException e) { + this.setCodeIsValid(false) + this.setProjectCodeValidationResult("${desiredProjectCode} is not a valid QBiC " + + "project code.") + } + } + + private void evaluateProjectCreation() { + this.setCreateProjectEnabled(codeIsValid && resultingSpaceName) + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonController.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonController.groovy index 218dd12ad..b7966885c 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonController.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonController.groovy @@ -5,7 +5,8 @@ import life.qbic.datamodel.dtos.business.AcademicTitle import life.qbic.datamodel.dtos.business.AcademicTitleFactory import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.datamodel.dtos.business.Customer -import life.qbic.business.customers.create.CreateCustomerInput +import life.qbic.business.persons.create.CreatePersonInput +import life.qbic.datamodel.dtos.general.Person /** * Controller class adapter from view information into use case input interface @@ -18,25 +19,25 @@ import life.qbic.business.customers.create.CreateCustomerInput @Log4j2 class CreatePersonController { - private final CreateCustomerInput useCaseInput + private final CreatePersonInput useCaseInput - CreatePersonController(CreateCustomerInput useCaseInput) { + CreatePersonController(CreatePersonInput useCaseInput) { this.useCaseInput = useCaseInput } /** - * This method starts the create customer use case based on information that is provided from the view + * This method starts the create person use case based on information that is provided from the view * - * @param firstName the first name of the customer - * @param lastName the last name of the customer - * @param title the title if any of the customer. The title has to match the value of a known AcademicTitle. - * @param email the email address of the customer - * @param affiliations the affiliations of the customer + * @param firstName the first name of the person + * @param lastName the last name of the person + * @param title the title if any of the person. The title has to match the value of a known AcademicTitle. + * @param email the email address of the person + * @param affiliations the affiliations of the person * * @see AcademicTitle * @since 1.0.0 */ - void createNewCustomer(String firstName, String lastName, String title, String email, List affiliations) { + void createNewPerson(String firstName, String lastName, String title, String email, List affiliations) { AcademicTitleFactory academicTitleFactory = new AcademicTitleFactory() AcademicTitle academicTitle if (!title || title?.isEmpty()) { @@ -46,25 +47,25 @@ class CreatePersonController { } try { - Customer customer = new Customer.Builder(firstName, lastName, email).title(academicTitle).affiliations(affiliations).build() - this.useCaseInput.createCustomer(customer) + Person person = new Customer.Builder(firstName, lastName, email).title(academicTitle).affiliations(affiliations).build() + this.useCaseInput.createPerson(person) } catch(Exception ignored) { throw new IllegalArgumentException("Could not create customer from provided arguments.") } } /** - * This method creates a new customer and triggers the create customer use case to update the old customer entry + * This method creates a new person and triggers the create customer use case to update the old customer entry * - * @param oldEntry The customer that needs to be updated - * @param firstName the first name of the customer - * @param lastName the last name of the customer - * @param title the title if any of the customer. The title has to match the value of a known AcademicTitle. - * @param email the email address of the customer - * @param affiliations the affiliations of the customer + * @param oldEntry The person that needs to be updated + * @param firstName the first name of the person + * @param lastName the last name of the person + * @param title the title if any of the person. The title has to match the value of a known AcademicTitle. + * @param email the email address of the person + * @param affiliations the affiliations of the person * */ - void updateCustomer(Customer oldEntry, String firstName, String lastName, String title, String email, List affiliations){ + void updatePerson(Person oldEntry, String firstName, String lastName, String title, String email, List affiliations){ AcademicTitleFactory academicTitleFactory = new AcademicTitleFactory() AcademicTitle academicTitle if (!title || title?.isEmpty()) { @@ -74,8 +75,8 @@ class CreatePersonController { } try{ - Customer customer = new Customer.Builder(firstName, lastName, email).title(academicTitle).affiliations(affiliations).build() - this.useCaseInput.updateCustomer(oldEntry,customer) + Person person = new Customer.Builder(firstName, lastName, email).title(academicTitle).affiliations(affiliations).build() + this.useCaseInput.updatePerson(oldEntry,person) }catch(Exception ignored) { throw new IllegalArgumentException("Could not update customer from provided arguments.") } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonPresenter.groovy index 3d8f0a81e..d92e8d039 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonPresenter.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonPresenter.groovy @@ -2,7 +2,7 @@ package life.qbic.portal.offermanager.components.person.create import com.vaadin.event.ListenerMethod.MethodException import groovy.util.logging.Log4j2 -import life.qbic.business.customers.create.CreateCustomerOutput +import life.qbic.business.persons.create.CreatePersonOutput import life.qbic.datamodel.dtos.business.Customer import life.qbic.datamodel.dtos.business.ProjectManager import life.qbic.datamodel.dtos.general.Person @@ -11,33 +11,33 @@ import life.qbic.portal.offermanager.components.AppViewModel /** * AppPresenter for the CreatePersonView * - * This presenter handles the output of the CreateCustomer use case and prepares it for the + * This presenter handles the output of the createPerson use case and prepares it for the * CreatePersonView. * * @since: 1.0.0 */ @Log4j2 -class CreatePersonPresenter implements CreateCustomerOutput{ +class CreatePersonPresenter implements CreatePersonOutput{ private final AppViewModel viewModel - private final CreatePersonViewModel createCustomerViewModel + private final CreatePersonViewModel createPersonViewModel - CreatePersonPresenter(AppViewModel viewModel, CreatePersonViewModel createCustomerViewModel) { + CreatePersonPresenter(AppViewModel viewModel, CreatePersonViewModel createPersonViewModel) { this.viewModel = viewModel - this.createCustomerViewModel = createCustomerViewModel + this.createPersonViewModel = createPersonViewModel } - private void clearCustomerData() { - createCustomerViewModel.academicTitle = null - createCustomerViewModel.firstName = null - createCustomerViewModel.lastName = null - createCustomerViewModel.email = null - createCustomerViewModel.affiliation = null + private void clearPersonData() { + createPersonViewModel.academicTitle = null + createPersonViewModel.firstName = null + createPersonViewModel.lastName = null + createPersonViewModel.email = null + createPersonViewModel.affiliation = null - createCustomerViewModel.academicTitleValid = null - createCustomerViewModel.firstNameValid = null - createCustomerViewModel.lastNameValid = null - createCustomerViewModel.emailValid = null - createCustomerViewModel.affiliationValid = null + createPersonViewModel.academicTitleValid = null + createPersonViewModel.firstNameValid = null + createPersonViewModel.lastNameValid = null + createPersonViewModel.emailValid = null + createPersonViewModel.affiliationValid = null } @Override @@ -45,12 +45,12 @@ class CreatePersonPresenter implements CreateCustomerOutput{ viewModel.failureNotifications.add(notification) } - @Override + @Deprecated - void customerCreated(String message) { + void personCreated(String message) { try { viewModel.successNotifications.add(message) - clearCustomerData() + clearPersonData() } catch (MethodException listenerMethodException) { //fixme // Invocation of method selectionChange failed for `null` @@ -62,8 +62,8 @@ class CreatePersonPresenter implements CreateCustomerOutput{ } } - @Override - void customerCreated(Person person) { + + void personCreated(Person person) { Customer customer = new Customer.Builder(person.firstName, person.lastName, person.emailAddress) @@ -75,18 +75,18 @@ class CreatePersonPresenter implements CreateCustomerOutput{ .title(person.title) .affiliations(person.affiliations).build() try{ - if (createCustomerViewModel.outdatedCustomer) createCustomerViewModel.personResourceService.removeFromResource(createCustomerViewModel.outdatedCustomer) + if (createPersonViewModel.outdatedPerson) createPersonViewModel.personResourceService.removeFromResource(createPersonViewModel.outdatedPerson) }catch(Exception e){ log.error e.message log.error e.stackTrace.join("\n") } - createCustomerViewModel.customerService.addToResource(customer) - createCustomerViewModel.managerResourceService.addToResource(manager) - createCustomerViewModel.personResourceService.addToResource(person) + createPersonViewModel.customerService.addToResource(customer) + createPersonViewModel.managerResourceService.addToResource(manager) + createPersonViewModel.personResourceService.addToResource(person) //reset the view model - clearCustomerData() + clearPersonData() - viewModel.successNotifications.add("Successfully created/updated new person entry.") + viewModel.successNotifications.add("Successfully created new person entry.") } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonView.groovy index fc52e959e..8294c7002 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonView.groovy @@ -4,11 +4,9 @@ package life.qbic.portal.offermanager.components.person.create import com.vaadin.data.ValidationResult import com.vaadin.data.Validator import com.vaadin.data.ValueContext -import com.vaadin.data.provider.DataProvider import com.vaadin.data.provider.ListDataProvider import com.vaadin.data.validator.EmailValidator import com.vaadin.icons.VaadinIcons -import com.vaadin.server.SerializableBiFunction import com.vaadin.server.UserError import com.vaadin.shared.data.sort.SortDirection import com.vaadin.shared.ui.ContentMode @@ -18,14 +16,12 @@ import groovy.util.logging.Log4j2 import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.portal.offermanager.components.AppViewModel -import javax.swing.event.ListDataEvent - /** * This class generates a Form Layout in which the user - * can input the necessary information for the creation of a new customer + * can input the necessary information for the creation of a new person * * CreatePersonViewModel will be integrated into the qOffer 2.0 Portlet and provides an User Interface - * with the intention of enabling a user the creation of a new Customer in the QBiC Database + * with the intention of enabling a user the creation of a new person in the QBiC Database * * @since: 1.0.0 */ @@ -33,7 +29,7 @@ import javax.swing.event.ListDataEvent @Log4j2 class CreatePersonView extends VerticalLayout { private final AppViewModel sharedViewModel - private final CreatePersonViewModel createCustomerViewModel + private final CreatePersonViewModel createPersonViewModel final CreatePersonController controller ComboBox titleField @@ -46,11 +42,11 @@ class CreatePersonView extends VerticalLayout { Button abortButton Panel affiliationDetails - CreatePersonView(CreatePersonController controller, AppViewModel sharedViewModel, CreatePersonViewModel createCustomerViewModel) { + CreatePersonView(CreatePersonController controller, AppViewModel sharedViewModel, CreatePersonViewModel createPersonViewModel) { super() this.controller = controller this.sharedViewModel = sharedViewModel - this.createCustomerViewModel = createCustomerViewModel + this.createPersonViewModel = createPersonViewModel initLayout() bindViewModel() setupFieldValidators() @@ -59,39 +55,39 @@ class CreatePersonView extends VerticalLayout { /** * Generates a vaadin Form Layout as an UserInterface consisting of vaadin components - * to enable user input for Customer creation + * to enable user input for person creation */ private def initLayout() { - this.titleField = generateTitleSelector(createCustomerViewModel.academicTitles) + this.titleField = generateTitleSelector(createPersonViewModel.academicTitles) this.firstNameField = new TextField("First Name") - firstNameField.setPlaceholder("Customer first name") + firstNameField.setPlaceholder("First name") firstNameField.setRequiredIndicatorVisible(true) this.lastNameField = new TextField("Last Name") - lastNameField.setPlaceholder("Customer last name") + lastNameField.setPlaceholder("Last name") lastNameField.setRequiredIndicatorVisible(true) this.emailField = new TextField("Email Address") - emailField.setPlaceholder("Customer email address") + emailField.setPlaceholder("Email address") emailField.setRequiredIndicatorVisible(true) - this.affiliationComboBox = generateAffiliationSelector(createCustomerViewModel.availableAffiliations) + this.affiliationComboBox = generateAffiliationSelector(createPersonViewModel.availableAffiliations) affiliationComboBox.setRequiredIndicatorVisible(true) - this.addressAdditionComboBox = generateAffiliationSelector(createCustomerViewModel.availableAffiliations) + this.addressAdditionComboBox = generateAffiliationSelector(createPersonViewModel.availableAffiliations) addressAdditionComboBox.setRequiredIndicatorVisible(false) addressAdditionComboBox.setItemCaptionGenerator({it.addressAddition}) addressAdditionComboBox.setCaption("Address Addition") addressAdditionComboBox.enabled = false - this.submitButton = new Button("Create Customer") + this.submitButton = new Button("Create Person") submitButton.setIcon(VaadinIcons.USER_CHECK) submitButton.addStyleName(ValoTheme.BUTTON_FRIENDLY) submitButton.enabled = allValuesValid() - this.abortButton = new Button("Abort Customer Creation") + this.abortButton = new Button("Abort Person Creation") abortButton.setIcon(VaadinIcons.CLOSE_CIRCLE) abortButton.addStyleName(ValoTheme.BUTTON_DANGER) @@ -142,37 +138,37 @@ class CreatePersonView extends VerticalLayout { */ private void bindViewModel() { - this.titleField.addValueChangeListener({this.createCustomerViewModel.academicTitle = it.value }) - createCustomerViewModel.addPropertyChangeListener("academicTitle", { + this.titleField.addValueChangeListener({this.createPersonViewModel.academicTitle = it.value }) + createPersonViewModel.addPropertyChangeListener("academicTitle", { String newValue = it.newValue as String titleField.value = newValue ?: titleField.emptyValue }) - this.firstNameField.addValueChangeListener({this.createCustomerViewModel.firstName = it.value }) - createCustomerViewModel.addPropertyChangeListener("firstName", { + this.firstNameField.addValueChangeListener({this.createPersonViewModel.firstName = it.value }) + createPersonViewModel.addPropertyChangeListener("firstName", { String newValue = it.newValue as String firstNameField.value = newValue ?: firstNameField.emptyValue }) - this.lastNameField.addValueChangeListener({this.createCustomerViewModel.lastName = it.value }) - createCustomerViewModel.addPropertyChangeListener("lastName", { + this.lastNameField.addValueChangeListener({this.createPersonViewModel.lastName = it.value }) + createPersonViewModel.addPropertyChangeListener("lastName", { String newValue = it.newValue as String lastNameField.value = newValue ?: lastNameField.emptyValue }) - this.emailField.addValueChangeListener({this.createCustomerViewModel.email = it.value }) - createCustomerViewModel.addPropertyChangeListener("email", { + this.emailField.addValueChangeListener({this.createPersonViewModel.email = it.value }) + createPersonViewModel.addPropertyChangeListener("email", { String newValue = it.newValue as String emailField.value = newValue ?: emailField.emptyValue }) this.affiliationComboBox.addValueChangeListener({ - this.createCustomerViewModel.setAffiliation(it.value) + this.createPersonViewModel.setAffiliation(it.value) }) this.addressAdditionComboBox.addValueChangeListener({ - this.createCustomerViewModel.setAffiliation(it.value) + this.createPersonViewModel.setAffiliation(it.value) }) - createCustomerViewModel.addPropertyChangeListener("affiliation", { + createPersonViewModel.addPropertyChangeListener("affiliation", { Affiliation newValue = it.newValue as Affiliation if (newValue) { affiliationComboBox.value = newValue @@ -187,7 +183,7 @@ class CreatePersonView extends VerticalLayout { we listen to the valid properties. whenever the presenter resets values in the viewmodel and resets the valid properties the component error on the respective component is removed */ - createCustomerViewModel.addPropertyChangeListener({it -> + createPersonViewModel.addPropertyChangeListener({it -> switch (it.propertyName) { case "academicTitleValid": if (it.newValue || it.newValue == null) { @@ -219,12 +215,12 @@ class CreatePersonView extends VerticalLayout { break } submitButton.enabled = allValuesValid() - addressAdditionComboBox.enabled = !Objects.isNull(createCustomerViewModel.affiliation) + addressAdditionComboBox.enabled = !Objects.isNull(createPersonViewModel.affiliation) }) /* refresh affiliation list and set added item as selected item. This is needed to keep this field up to date and select an affiliation after it was created */ - createCustomerViewModel.availableAffiliations.addPropertyChangeListener({ + createPersonViewModel.availableAffiliations.addPropertyChangeListener({ affiliationComboBox.getDataProvider().refreshAll() refreshAddressAdditions() if (it instanceof ObservableList.ElementAddedEvent) { @@ -237,7 +233,7 @@ class CreatePersonView extends VerticalLayout { ListDataProvider dataProvider = this.addressAdditionComboBox.dataProvider as ListDataProvider dataProvider.clearFilters() dataProvider.addFilterByValue({it.organisation }, - createCustomerViewModel.affiliation?.organisation) + createPersonViewModel.affiliation?.organisation) dataProvider.setSortOrder({it.addressAddition}, SortDirection.ASCENDING) } /** @@ -253,41 +249,41 @@ class CreatePersonView extends VerticalLayout { this.firstNameField.addValueChangeListener({ event -> ValidationResult result = nameValidator.apply(event.getValue(), new ValueContext(this.firstNameField)) if (result.isError()) { - createCustomerViewModel.firstNameValid = false + createPersonViewModel.firstNameValid = false UserError error = new UserError(result.getErrorMessage()) firstNameField.setComponentError(error) } else { - createCustomerViewModel.firstNameValid = true + createPersonViewModel.firstNameValid = true } }) this.lastNameField.addValueChangeListener({ event -> ValidationResult result = nameValidator.apply(event.getValue(), new ValueContext(this.lastNameField)) if (result.isError()) { - createCustomerViewModel.lastNameValid = false + createPersonViewModel.lastNameValid = false UserError error = new UserError(result.getErrorMessage()) lastNameField.setComponentError(error) } else { - createCustomerViewModel.lastNameValid = true + createPersonViewModel.lastNameValid = true } }) this.emailField.addValueChangeListener({ event -> ValidationResult result = emailValidator.apply(event.getValue(), new ValueContext(this.emailField)) if (result.isError()) { - createCustomerViewModel.emailValid = false + createPersonViewModel.emailValid = false UserError error = new UserError(result.getErrorMessage()) emailField.setComponentError(error) } else { - createCustomerViewModel.emailValid = true + createPersonViewModel.emailValid = true } }) this.affiliationComboBox.addSelectionListener({selection -> ValidationResult result = selectionValidator.apply(selection.getValue(), new ValueContext(this.affiliationComboBox)) if (result.isError()) { - createCustomerViewModel.affiliationValid = false + createPersonViewModel.affiliationValid = false UserError error = new UserError(result.getErrorMessage()) affiliationComboBox.setComponentError(error) } else { - createCustomerViewModel.affiliationValid = true + createPersonViewModel.affiliationValid = true } }) } @@ -300,7 +296,7 @@ class CreatePersonView extends VerticalLayout { private static ComboBox generateAffiliationSelector(List affiliationList) { ComboBox affiliationComboBox = new ComboBox<>("Affiliation") - affiliationComboBox.setPlaceholder("Select customer affiliation") + affiliationComboBox.setPlaceholder("Select person affiliation") ListDataProvider dataProvider = new ListDataProvider<>(affiliationList) affiliationComboBox.setDataProvider(dataProvider) affiliationComboBox.setEmptySelectionAllowed(false) @@ -309,7 +305,7 @@ class CreatePersonView extends VerticalLayout { } /** - * Generates a Combobox, which can be used for AcademicTitle selection for a customer + * Generates a Combobox, which can be used for AcademicTitle selection for a person * @return Vaadin Combobox component */ private static ComboBox generateTitleSelector(List academicTitles) { @@ -327,36 +323,36 @@ class CreatePersonView extends VerticalLayout { * @return */ private boolean allValuesValid() { - return createCustomerViewModel.firstNameValid \ - && createCustomerViewModel.lastNameValid \ - && createCustomerViewModel.emailValid \ - && createCustomerViewModel.affiliationValid + return createPersonViewModel.firstNameValid \ + && createPersonViewModel.lastNameValid \ + && createPersonViewModel.emailValid \ + && createPersonViewModel.affiliationValid } private void registerListeners() { this.submitButton.addClickListener({ event -> try { // we assume that the view model and the view always contain the same information - String title = createCustomerViewModel.academicTitle - String firstName = createCustomerViewModel.firstName - String lastName = createCustomerViewModel.lastName - String email = createCustomerViewModel.email + String title = createPersonViewModel.academicTitle + String firstName = createPersonViewModel.firstName + String lastName = createPersonViewModel.lastName + String email = createPersonViewModel.email List affiliations = new ArrayList() - affiliations.add(createCustomerViewModel.affiliation) + affiliations.add(createPersonViewModel.affiliation) - if(createCustomerViewModel.outdatedCustomer){ - controller.updateCustomer(createCustomerViewModel.outdatedCustomer, firstName, lastName, title, email, affiliations) + if(createPersonViewModel.outdatedPerson){ + controller.updatePerson(createPersonViewModel.outdatedPerson, firstName, lastName, title, email, affiliations) } else{ - controller.createNewCustomer(firstName, lastName, title, email, affiliations) + controller.createNewPerson(firstName, lastName, title, email, affiliations) } } catch (IllegalArgumentException illegalArgumentException) { - log.error("Illegal arguments for customer creation. ${illegalArgumentException.getMessage()}") - log.debug("Illegal arguments for customer creation. ${illegalArgumentException.getMessage()}", illegalArgumentException) - sharedViewModel.failureNotifications.add("Could not create the customer. Please verify that your input is correct and try again.") + log.error("Illegal arguments for person creation. ${illegalArgumentException.getMessage()}") + log.debug("Illegal arguments for person creation. ${illegalArgumentException.getMessage()}", illegalArgumentException) + sharedViewModel.failureNotifications.add("Could not create the person. Please verify that your input is correct and try again.") } catch (Exception e) { - log.error("Unexpected error after customer creation form submission.", e) + log.error("Unexpected error after person creation form submission.", e) sharedViewModel.failureNotifications.add("An unexpected error occurred. We apologize for any inconveniences. Please inform us via email to support@qbic.zendesk.com.") } }) @@ -370,7 +366,7 @@ class CreatePersonView extends VerticalLayout { clearAllFields() } catch (Exception e) { - log.error("Unexpected error aborting the customer creation.", e) + log.error("Unexpected error aborting the person creation.", e) sharedViewModel.failureNotifications.add("An unexpected error occurred. We apologize for any inconveniences. Please inform us via email to support@qbic.zendesk.com.") } }) @@ -396,7 +392,7 @@ class CreatePersonView extends VerticalLayout { } /** - * Clears User Input from all fields in the Create Customer View and reset validation status of all Fields + * Clears User Input from all fields in the Create Person View and reset validation status of all Fields */ private void clearAllFields() { @@ -408,12 +404,12 @@ class CreatePersonView extends VerticalLayout { addressAdditionComboBox.selectedItem = addressAdditionComboBox.clear() affiliationDetails.setContent(null) - createCustomerViewModel.academicTitleValid = null - createCustomerViewModel.firstNameValid = null - createCustomerViewModel.lastNameValid = null - createCustomerViewModel.emailValid = null - createCustomerViewModel.affiliationValid = null - createCustomerViewModel.outdatedCustomer = null + createPersonViewModel.academicTitleValid = null + createPersonViewModel.firstNameValid = null + createPersonViewModel.lastNameValid = null + createPersonViewModel.emailValid = null + createPersonViewModel.affiliationValid = null + createPersonViewModel.outdatedPerson = null } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonViewModel.groovy index 026c2b74e..aa449f766 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonViewModel.groovy @@ -2,7 +2,6 @@ package life.qbic.portal.offermanager.components.person.create import groovy.beans.Bindable import life.qbic.datamodel.dtos.business.Affiliation -import life.qbic.datamodel.dtos.business.Customer import life.qbic.datamodel.dtos.general.Person import life.qbic.portal.offermanager.dataresources.persons.AffiliationResourcesService import life.qbic.portal.offermanager.dataresources.persons.CustomerResourceService @@ -23,7 +22,7 @@ import life.qbic.portal.offermanager.dataresources.persons.ProjectManagerResourc */ class CreatePersonViewModel { List academicTitles = new ArrayList<>() - Customer outdatedCustomer + Person outdatedPerson @Bindable String academicTitle @Bindable String firstName diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonView.groovy index f5776acda..46feab014 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonView.groovy @@ -13,7 +13,6 @@ import com.vaadin.ui.VerticalLayout import com.vaadin.ui.components.grid.HeaderRow import com.vaadin.ui.themes.ValoTheme import life.qbic.datamodel.dtos.business.AcademicTitle -import life.qbic.datamodel.dtos.business.Customer import life.qbic.datamodel.dtos.general.Person import life.qbic.portal.offermanager.components.GridUtils @@ -22,7 +21,7 @@ import life.qbic.portal.offermanager.components.person.create.CreatePersonView /** * Constructs the UI for the SearchPerson use case * - * This class provides the view elements so that a user can search for a customer through the UI + * This class provides the view elements so that a user can search for a person through the UI * * @since: 1.0.0 * @@ -32,9 +31,9 @@ class SearchPersonView extends FormLayout{ private final SearchPersonViewModel viewModel private final CreatePersonView updatePersonView - Grid customerGrid - Panel selectedCustomerInformation - Button updateCustomer + Grid personGrid + Panel selectedPersonInformation + Button updatePerson VerticalLayout detailsLayout VerticalLayout searchPersonLayout @@ -44,7 +43,7 @@ class SearchPersonView extends FormLayout{ initLayout() - generateCustomerGrid() + generatePersonGrid() addListeners() } @@ -53,26 +52,26 @@ class SearchPersonView extends FormLayout{ gridLabel.addStyleName(ValoTheme.LABEL_HUGE) - updateCustomer = new Button("Update Customer") - updateCustomer.setEnabled(false) + updatePerson = new Button("Update Person") + updatePerson.setEnabled(false) detailsLayout = new VerticalLayout() - detailsLayout.addComponent(updateCustomer) - detailsLayout.setComponentAlignment(updateCustomer, Alignment.MIDDLE_RIGHT) + detailsLayout.addComponent(updatePerson) + detailsLayout.setComponentAlignment(updatePerson, Alignment.MIDDLE_RIGHT) - customerGrid = new Grid<>() - selectedCustomerInformation = new Panel() + personGrid = new Grid<>() + selectedPersonInformation = new Panel() Label detailsLabel = new Label("Person Details: ") detailsLayout.addComponent(detailsLabel) detailsLabel.addStyleName(ValoTheme.LABEL_LARGE) - detailsLayout.addComponent(selectedCustomerInformation) + detailsLayout.addComponent(selectedPersonInformation) detailsLayout.setVisible(false) detailsLayout.setMargin(false) - searchPersonLayout = new VerticalLayout(gridLabel,customerGrid,detailsLayout) + searchPersonLayout = new VerticalLayout(gridLabel,personGrid,detailsLayout) searchPersonLayout.setMargin(false) this.addComponents(searchPersonLayout,updatePersonView) @@ -82,18 +81,18 @@ class SearchPersonView extends FormLayout{ private void addListeners(){ - customerGrid.addSelectionListener({ + personGrid.addSelectionListener({ if (it.firstSelectedItem.isPresent()) { - fillPanel(it.firstSelectedItem.get() as Customer) + fillPanel(it.firstSelectedItem.get()) detailsLayout.setVisible(true) - updateCustomer.setEnabled(true) + updatePerson.setEnabled(true) viewModel.selectedPerson = it.firstSelectedItem } else { detailsLayout.setVisible(false) } }) - updateCustomer.addClickListener({ + updatePerson.addClickListener({ viewModel.personEvent.emit(viewModel.selectedPerson) searchPersonLayout.setVisible(false) updatePersonView.setVisible(true) @@ -112,8 +111,8 @@ class SearchPersonView extends FormLayout{ } /** - * Fills the panel with the detailed customer information of the currently selected customer - * @param person The customer which + * Fills the panel with the detailed information of the currently selected person + * @param person The person which */ private void fillPanel(Person person){ VerticalLayout content = new VerticalLayout() @@ -134,66 +133,66 @@ class SearchPersonView extends FormLayout{ content.setMargin(true) content.setSpacing(false) - selectedCustomerInformation.setContent(content) - selectedCustomerInformation.setWidthUndefined() + selectedPersonInformation.setContent(content) + selectedPersonInformation.setWidthUndefined() } /** - * This method adds the retrieved Customer Information to the Customer grid + * This method adds the retrieved person Information to the person grid */ - private ListDataProvider setupCustomerDataProvider() { - def customerListDataProvider = new ListDataProvider<>(viewModel.getAvailablePersons()) - this.customerGrid.setDataProvider(customerListDataProvider) + private ListDataProvider setupPersonDataProvider() { + def personListDataProvider = new ListDataProvider<>(viewModel.getAvailablePersons()) + this.personGrid.setDataProvider(personListDataProvider) - return customerListDataProvider + return personListDataProvider } /** - * Method which generates the grid and populates the columns with the set Customer information from the setupDataProvider Method + * Method which generates the grid and populates the columns with the set person information from the setupDataProvider Method * - * This Method is responsible for setting up the grid and setting the customer information to the individual grid columns. + * This Method is responsible for setting up the grid and setting the person information to the individual grid columns. */ - private def generateCustomerGrid() { + private def generatePersonGrid() { try { - this.customerGrid.addColumn({ customer -> customer.firstName }) + this.personGrid.addColumn({ person -> person.firstName }) .setCaption("First Name").setId("FirstName") - this.customerGrid.addColumn({ customer -> customer.lastName }) + this.personGrid.addColumn({ person -> person.lastName }) .setCaption("Last Name").setId("LastName") - this.customerGrid.addColumn({ customer -> customer.emailAddress }) + this.personGrid.addColumn({ person -> person.emailAddress }) .setCaption("Email Address").setId("EmailAddress") - this.customerGrid.addColumn({ customer -> - customer.title == AcademicTitle.NONE ? "" : customer.title}) + this.personGrid.addColumn({ person -> + person.title == AcademicTitle.NONE ? "" : person.title}) .setCaption("Title").setId("Title") //specify size of grid and layout - customerGrid.setWidthFull() - customerGrid.setHeightMode(HeightMode.ROW) - customerGrid.setHeightByRows(5) + personGrid.setWidthFull() + personGrid.setHeightMode(HeightMode.ROW) + personGrid.setHeightByRows(5) } catch (Exception e) { - new Exception("Unexpected exception in building the customer grid", e) + new Exception("Unexpected exception in building the person grid", e) } /* Let's not forget to setup the grid's data provider */ - def customerDataProvider = setupCustomerDataProvider() + def personDataProvider = setupPersonDataProvider() /* Lastly, we add some content filters for the columns */ - addFilters(customerDataProvider) + addFilters(personDataProvider) } - private void addFilters(ListDataProvider customerListDataProvider) { - HeaderRow customerFilterRow = customerGrid.appendHeaderRow() - GridUtils.setupColumnFilter(customerListDataProvider, - customerGrid.getColumn("FirstName"), - customerFilterRow) - GridUtils.setupColumnFilter(customerListDataProvider, - customerGrid.getColumn("LastName"), - customerFilterRow) - GridUtils.setupColumnFilter(customerListDataProvider, - customerGrid.getColumn("EmailAddress"), - customerFilterRow) + private void addFilters(ListDataProvider personListDataProvider) { + HeaderRow personFilterRow = personGrid.appendHeaderRow() + GridUtils.setupColumnFilter(personListDataProvider, + personGrid.getColumn("FirstName"), + personFilterRow) + GridUtils.setupColumnFilter(personListDataProvider, + personGrid.getColumn("LastName"), + personFilterRow) + GridUtils.setupColumnFilter(personListDataProvider, + personGrid.getColumn("EmailAddress"), + personFilterRow) } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonViewModel.groovy index cc3601bee..a3cf89a13 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonViewModel.groovy @@ -37,7 +37,7 @@ class SearchPersonViewModel { } private void subscribeToResources() { - this.personService.subscribe((Person customer) -> { + this.personService.subscribe((Person person) -> { this.fetchPersonData() }) } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonViewModel.groovy index c89802d2f..1a1759ce6 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonViewModel.groovy @@ -39,7 +39,7 @@ class UpdatePersonViewModel extends CreatePersonViewModel{ this.customerUpdate.register((Person person) -> { loadData(person) - setOutdatedCustomer((Customer) person) + setOutdatedPerson(person) }) } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy new file mode 100644 index 000000000..8632f73d5 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy @@ -0,0 +1,170 @@ +package life.qbic.portal.offermanager.components.product + +import com.vaadin.data.provider.ListDataProvider +import com.vaadin.icons.VaadinIcons +import com.vaadin.ui.Alignment +import com.vaadin.ui.Button +import com.vaadin.ui.Grid +import com.vaadin.ui.HorizontalLayout +import com.vaadin.ui.Label +import com.vaadin.ui.Panel +import com.vaadin.ui.VerticalLayout +import com.vaadin.ui.components.grid.HeaderRow +import com.vaadin.ui.themes.ValoTheme +import life.qbic.business.offers.Currency +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.portal.offermanager.components.GridUtils +import life.qbic.portal.offermanager.components.product.create.CreateProductView +import life.qbic.portal.offermanager.dataresources.offers.OfferOverview + +/** + * + *

This class generates a VerticalLayout in which the user can maintain the service products

+ * + *

{@link MaintainProductsViewModel} will be integrated into the qOffer 2.0 Portlet and provides an User Interface + * with the intention of enabling an {@value life.qbic.portal.offermanager.security.Role#OFFER_ADMIN} to create, archive and copy products.

+ * + * @since 1.0.0 + * + */ +class MaintainProductsView extends VerticalLayout{ + + private final MaintainProductsViewModel viewModel + + Grid productGrid + HorizontalLayout buttonLayout + Button addProduct + Button copyProduct + Button archiveProduct + Panel productDescription + VerticalLayout maintenanceLayout + + CreateProductView createProductView + CreateProductView copyProductView + + MaintainProductsView(MaintainProductsViewModel viewModel, CreateProductView createProductView + , CreateProductView copyProductView){ + //todo add the controller + this.viewModel = viewModel + this.createProductView = createProductView + this.copyProductView = copyProductView + + setupPanel() + createButtons() + setupGrid() + setupDataProvider() + setupOverviewLayout() + setupTitle() + addSubViews() + setupListeners() + } + + private void setupTitle(){ + Label label = new Label("Service Product Maintenance") + label.setStyleName(ValoTheme.LABEL_HUGE) + maintenanceLayout.addComponent(label,0) + } + + private void createButtons(){ + addProduct = new Button("Add Product", VaadinIcons.PLUS) + copyProduct = new Button ("Copy Product", VaadinIcons.COPY) + archiveProduct = new Button("Archive Product", VaadinIcons.ARCHIVE) + + buttonLayout = new HorizontalLayout(productDescription, addProduct,copyProduct,archiveProduct) + buttonLayout.setMargin(false) + } + + private void setupGrid(){ + productGrid = new Grid<>() + + productGrid.addColumn({ product -> product.productId.toString() }) + .setCaption("Product Id").setId("ProductId") + productGrid.addColumn({ product -> product.productName }) + .setCaption("Name").setId("ProductName") + productGrid.addColumn({ product -> Currency.getFormatterWithSymbol().format(product.unitPrice) }) + .setCaption("Price").setId("UnitPrice") + productGrid.addColumn({ product -> product.unit.value}) + .setCaption("Unit").setId("ProductUnit") + + productGrid.setWidthFull() + + def productsDataProvider = setupDataProvider() + setupFilters(productsDataProvider) + } + + private ListDataProvider setupDataProvider(){ + def dataProvider = new ListDataProvider(viewModel.products) + productGrid.setDataProvider(dataProvider) + return dataProvider + } + + private void setupFilters(ListDataProvider dataProvider){ + HeaderRow productsFilterRow = productGrid.appendHeaderRow() + GridUtils.setupColumnFilter(dataProvider, + productGrid.getColumn("ProductId"), + productsFilterRow) + GridUtils.setupColumnFilter(dataProvider, + productGrid.getColumn("ProductName"), + productsFilterRow) + } + + private void setupPanel(){ + productDescription = new Panel("Product Description") + } + + private void setupOverviewLayout(){ + maintenanceLayout = new VerticalLayout(productGrid,buttonLayout) + maintenanceLayout.setSizeFull() + maintenanceLayout.setMargin(false) + maintenanceLayout.addComponents() + + maintenanceLayout.setComponentAlignment(buttonLayout,Alignment.TOP_RIGHT) + this.addComponents(maintenanceLayout) + } + + private void addSubViews(){ + this.addComponents(createProductView,copyProductView) + createProductView.setVisible(false) + copyProductView.setVisible(false) + } + + private void updateProductDescription(Product product){ + VerticalLayout content = new VerticalLayout() + //todo get product category + //content.addComponent(new Label("${product}", ContentMode.HTML)) + content.addComponent(new Label("${product.description}")) + content.setMargin(true) + content.setSpacing(false) + this.productDescription.setContent(content) + } + + private void setupListeners(){ + + productGrid.addSelectionListener({ + if(it.firstSelectedItem.isPresent()){ + updateProductDescription(it.firstSelectedItem.get()) + } + }) + + addProduct.addClickListener({ + maintenanceLayout.setVisible(false) + createProductView.setVisible(true) + }) + + createProductView.abortButton.addClickListener({ + maintenanceLayout.setVisible(true) + createProductView.setVisible(false) + }) + + copyProduct.addClickListener({ + maintenanceLayout.setVisible(false) + copyProduct.setVisible(true) + }) + + archiveProduct.addClickListener({ + //todo use the controller to trigger the use case + }) + + } + +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy new file mode 100644 index 000000000..a74ea420e --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy @@ -0,0 +1,43 @@ +package life.qbic.portal.offermanager.components.product + +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.portal.offermanager.dataresources.products.ProductsResourcesService + + +/** + *

A ViewModel holding data that is presented in a + * {@link life.qbic.portal.offermanager.components.product.MaintainProductsView}

+ * + *

This class holds all specific fields that are mutable in the view + * Whenever values change it should be reflected in the corresponding view. This class can be used + * for UI unit testing purposes.

+ * + *

This class can contain JavaBean objects to enable views to listen to changes in the values.

+ * + * @since 1.0.0 + * + */ +class MaintainProductsViewModel { + + ObservableList products = new ObservableList(new ArrayList()) + + Product selectedProduct + + private final ProductsResourcesService productsResourcesService + + MaintainProductsViewModel(ProductsResourcesService productsResourcesService) { + this.productsResourcesService = productsResourcesService + fetchProducts() + subscribe() + } + + private void fetchProducts(){ + products.addAll(productsResourcesService.iterator()) + } + + private void subscribe(){ + productsResourcesService.subscribe({ product -> + products << product + }) + } +} \ No newline at end of file diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy new file mode 100644 index 000000000..96ed1c404 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy @@ -0,0 +1,296 @@ +package life.qbic.portal.offermanager.components.product.create + +import com.vaadin.data.ValidationResult +import com.vaadin.data.Validator +import com.vaadin.data.ValueContext +import com.vaadin.data.validator.RegexpValidator +import com.vaadin.icons.VaadinIcons +import com.vaadin.server.UserError +import com.vaadin.ui.Alignment +import com.vaadin.ui.Button +import com.vaadin.ui.ComboBox +import com.vaadin.ui.HorizontalLayout +import com.vaadin.ui.Label +import com.vaadin.ui.TextField +import com.vaadin.ui.VerticalLayout +import com.vaadin.ui.themes.ValoTheme +import life.qbic.datamodel.dtos.business.ProductCategory +import life.qbic.datamodel.dtos.business.services.ProductUnit + +/** + *

This view serves the user to create a new service product

+ *
+ *

The view contains several text input fields and combo boxes in order to fully describe the new service products

+ * + * @since 1.0.0 + * +*/ +class CreateProductView extends HorizontalLayout{ + + private final CreateProductViewModel createProductViewModel + + TextField productNameField + TextField productDescriptionField + TextField productUnitPriceField + + ComboBox productUnitComboBox + ComboBox productCategoryComboBox + + Button createProductButton + Button abortButton + + CreateProductView(CreateProductViewModel createProductViewModel){ + + this.createProductViewModel = createProductViewModel + initTextFields() + initComboBoxes() + initButtons() + initLayout() + bindViewModel() + setupFieldValidators() + setupListeners() + } + + private void initLayout(){ + Label label = new Label("Create Service Product") + label.setStyleName(ValoTheme.LABEL_HUGE) + this.addComponent(label) + + //add textfields and boxes + HorizontalLayout sharedLayout = new HorizontalLayout(productUnitPriceField,productUnitComboBox) + sharedLayout.setWidthFull() + HorizontalLayout buttons = new HorizontalLayout(abortButton,createProductButton) + + VerticalLayout sideLayout = new VerticalLayout(label,productNameField,productDescriptionField,sharedLayout,productCategoryComboBox,buttons) + sideLayout.setSizeFull() + sideLayout.setComponentAlignment(buttons, Alignment.BOTTOM_RIGHT) + + this.setMargin(false) + this.setSpacing(false) + this.setSizeFull() + + this.addComponents(sideLayout) + } + + private void initTextFields(){ + productNameField = new TextField("Product Name") + productNameField.setPlaceholder("Product Name") + productNameField.setRequiredIndicatorVisible(true) + productNameField.setWidthFull() + + productDescriptionField = new TextField("Product Description") + productDescriptionField.setPlaceholder("Product Description") + productDescriptionField.setRequiredIndicatorVisible(true) + productDescriptionField.setWidthFull() + + productUnitPriceField = new TextField("Product Unit Price") + productUnitPriceField.setPlaceholder("00.00") + productUnitPriceField.setRequiredIndicatorVisible(true) + productUnitPriceField.setWidthFull() + } + + private void initComboBoxes(){ + productUnitComboBox = new ComboBox<>("Product Unit") + productUnitComboBox.setRequiredIndicatorVisible(true) + productUnitComboBox.setPlaceholder("Select Product Unit") + productUnitComboBox.setEmptySelectionAllowed(false) + productUnitComboBox.setItems(Arrays.asList(ProductUnit.values()) as List) + productUnitComboBox.setWidthFull() + + productCategoryComboBox = new ComboBox<>("Product Category") + productCategoryComboBox.setRequiredIndicatorVisible(true) + productCategoryComboBox.setPlaceholder("Select Product Category") + productCategoryComboBox.setEmptySelectionAllowed(false) + productCategoryComboBox.setItems(Arrays.asList(ProductCategory.values()) as List) + productCategoryComboBox.setWidthFull() + } + + private void initButtons(){ + abortButton = new Button("Abort", VaadinIcons.CLOSE) + createProductButton = new Button("Create", VaadinIcons.CHECK) + createProductButton.setEnabled(allValuesValid()) + } + + private void bindViewModel(){ + //bind all textfields + this.productNameField.addValueChangeListener({this.createProductViewModel.productName = it.value }) + + createProductViewModel.addPropertyChangeListener("productName", { + String newValue = it.newValue as String + productNameField.value = newValue ?: productNameField.emptyValue + }) + + this.productDescriptionField.addValueChangeListener({this.createProductViewModel.productDescription = it.value }) + + createProductViewModel.addPropertyChangeListener("productDescription", { + String newValue = it.newValue as String + productDescriptionField.value = newValue ?: productDescriptionField.emptyValue + }) + + this.productUnitPriceField.addValueChangeListener({this.createProductViewModel.productUnitPrice = it.value}) + + createProductViewModel.addPropertyChangeListener("productUnitPrice", { + String newValue = it.newValue as String + productUnitPriceField.value = newValue ?: productUnitPriceField.emptyValue + }) + + //bind combo boxes + createProductViewModel.addPropertyChangeListener("productUnit", { + ProductUnit newValue = it.newValue as ProductUnit + if (newValue) { + productUnitComboBox.value = newValue + } else { + productUnitComboBox.value = productUnitComboBox.emptyValue + } + }) + productUnitComboBox.addSelectionListener({ + createProductViewModel.setProductUnit(it.value as ProductUnit) + }) + + createProductViewModel.addPropertyChangeListener("productCategory", { + ProductCategory newValue = it.newValue as ProductCategory + if (newValue) { + productCategoryComboBox.value = newValue + } else { + productCategoryComboBox.value = productCategoryComboBox.emptyValue + } + }) + productCategoryComboBox.addSelectionListener({ + createProductViewModel.setProductCategory(it.value as ProductCategory) + }) + + /* + We listen to the valid properties. whenever the presenter resets values in the viewmodel + and resets the valid properties the component error on the respective component is removed + */ + createProductViewModel.addPropertyChangeListener({ + switch (it.propertyName) { + case "productNameValid": + if (it.newValue || it.newValue == null) { + productNameField.componentError = null + } + break + case "productDescriptionValid": + if (it.newValue || it.newValue == null) { + productDescriptionField.componentError = null + } + break + case "productUnitPriceValid": + if (it.newValue || it.newValue == null) { + productUnitPriceField.componentError = null + } + break + case "productUnitValid": + if (it.newValue || it.newValue == null) { + productUnitComboBox.componentError = null + } + break + case "productCategoryValid": + if (it.newValue || it.newValue == null) { + productCategoryComboBox.componentError = null + } + break + default: + break + } + createProductButton.enabled = allValuesValid() + }) + } + + /** + * This method adds validation to the fields of this view + */ + private void setupFieldValidators() { + + Validator nameValidator = Validator.from({String value -> (value && !value.trim().empty)}, "Please provide a valid name.") + Validator numberValidator = new RegexpValidator("This is not a number!", "[-]?[0-9]*\\.?[0-9]+") + Validator selectionValidator = Validator.from({o -> o != null}, "Please make a selection.") + + //Add Listeners to all Fields in the Form layout + this.productNameField.addValueChangeListener({ event -> + ValidationResult result = nameValidator.apply(event.getValue(), new ValueContext(this.productNameField)) + if (result.isError()) { + createProductViewModel.productNameValid = false + UserError error = new UserError(result.getErrorMessage()) + productNameField.setComponentError(error) + } else { + createProductViewModel.productNameValid = true + } + }) + this.productDescriptionField.addValueChangeListener({ event -> + ValidationResult result = nameValidator.apply(event.getValue(), new ValueContext(this.productDescriptionField)) + if (result.isError()) { + createProductViewModel.productDescriptionValid = false + UserError error = new UserError(result.getErrorMessage()) + productDescriptionField.setComponentError(error) + } else { + createProductViewModel.productDescriptionValid = true + } + }) + this.productUnitPriceField.addValueChangeListener({ event -> + ValidationResult result = numberValidator.apply(event.getValue(), new ValueContext(this.productUnitPriceField)) + if (result.isError()) { + createProductViewModel.productUnitPriceValid = false + UserError error = new UserError(result.getErrorMessage()) + productUnitPriceField.setComponentError(error) + } else { + createProductViewModel.productUnitPriceValid = true + } + }) + this.productUnitComboBox.addSelectionListener({selection -> + ValidationResult result = selectionValidator.apply(selection.getValue(), new ValueContext(this.productUnitComboBox)) + if (result.isError()) { + createProductViewModel.productUnitValid = false + UserError error = new UserError(result.getErrorMessage()) + productUnitComboBox.setComponentError(error) + } else { + createProductViewModel.productUnitValid = true + } + }) + this.productCategoryComboBox.addSelectionListener({ selection -> + ValidationResult result = selectionValidator.apply(selection.getValue(), new ValueContext(this.productCategoryComboBox)) + if (result.isError()) { + createProductViewModel.productCategoryValid = false + UserError error = new UserError(result.getErrorMessage()) + productCategoryComboBox.setComponentError(error) + } else { + createProductViewModel.productCategoryValid = true + } + }) + } + /** + * This is used to indicate whether all fields of this view are filled correctly. + * It relies on the separate fields for validation. + * @return + */ + private boolean allValuesValid() { + return createProductViewModel.productNameValid \ + && createProductViewModel.productDescriptionValid \ + && createProductViewModel.productUnitValid \ + && createProductViewModel.productUnitPriceValid \ + && createProductViewModel.productCategoryValid + } + + private void setupListeners(){ + abortButton.addClickListener({ clearAllFields() }) + } + + /** + * Clears User Input from all fields in the Create Products View and reset validation status of all Fields + */ + private void clearAllFields() { + + productNameField.clear() + productDescriptionField.clear() + productUnitPriceField.clear() + productCategoryComboBox.selectedItem = productCategoryComboBox.clear() + productUnitComboBox.selectedItem = productUnitComboBox.clear() + + createProductViewModel.productNameValid = null + createProductViewModel.productDescriptionValid = null + createProductViewModel.productUnitPriceValid = null + createProductViewModel.productCategoryValid = null + createProductViewModel.productUnitValid = null + } + +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductViewModel.groovy new file mode 100644 index 000000000..ce0846328 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductViewModel.groovy @@ -0,0 +1,27 @@ +package life.qbic.portal.offermanager.components.product.create + +import groovy.beans.Bindable +import life.qbic.datamodel.dtos.business.ProductCategory +import life.qbic.datamodel.dtos.business.services.ProductUnit + + +/** + *

Holds all values that the user specifies in the CreateProductView

+ * + * @since 1.0.0 + * +*/ +class CreateProductViewModel { + + @Bindable String productName + @Bindable Boolean productNameValid + @Bindable String productDescription + @Bindable Boolean productDescriptionValid + @Bindable String productUnitPrice //todo make sure to cast the string to a double in the use case + @Bindable Boolean productUnitPriceValid + @Bindable ProductUnit productUnit + @Bindable Boolean productUnitValid + @Bindable ProductCategory productCategory + @Bindable Boolean productCategoryValid + +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsController.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsController.groovy new file mode 100644 index 000000000..b63a1d5e4 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsController.groovy @@ -0,0 +1,109 @@ +package life.qbic.portal.offermanager.components.products + +import life.qbic.business.logging.Logger +import life.qbic.business.logging.Logging +import life.qbic.business.products.archive.ArchiveProductInput +import life.qbic.business.products.create.CreateProductInput + +import life.qbic.datamodel.dtos.business.ProductCategory +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.DataStorage +import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.datamodel.dtos.business.services.ProductUnit +import life.qbic.datamodel.dtos.business.services.ProjectManagement +import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis +import life.qbic.datamodel.dtos.business.services.Sequencing + +/** + *

Controls how the information flows into the use cases {@link life.qbic.business.products.create.CreateProduct} and {@link life.qbic.business.products.archive.ArchiveProduct}

+ * + *

This class allows to trigger the use cases and respectively create new products, copy or archive them.

+ * + * @since 1.0.0 + * + */ +class MaintainProductsController { + + private final CreateProductInput createProductInput + private final ArchiveProductInput archiveProductInput + private static final Logging log = Logger.getLogger(this.class) + + MaintainProductsController(CreateProductInput createProductInput, + ArchiveProductInput archiveProductInput){ + this.createProductInput = createProductInput + this.archiveProductInput = archiveProductInput + } + + /** + * Triggers the creation of a product in the database + * + * @param category The products category which determines what kind of product is created + * @param description The description of the product + * @param name The name of the product + * @param unitPrice The unit price of the product + * @param unit The unit in which the product is measured + */ + void createNewProduct(ProductCategory category, String description, String name, double unitPrice, ProductUnit unit){ + try { + Product product = ProductConverter.createProduct(category, description, name, unitPrice, unit) + createProductInput.create(product) + } catch (Exception unexpected) { + log.error("unexpected exception during create product call", unexpected) + throw new IllegalArgumentException("Could not create products from provided arguments.") + } + } + + /** + * Triggers the archiving of a product + * @param productId The ProductId of the product that should be archived + */ + void archiveProduct(ProductId productId){ + try{ + archiveProductInput.archive(productId) + }catch(Exception unexpected){ + log.error("unexpected exception at archive product call", unexpected) + throw new IllegalArgumentException("Could not create products from provided arguments.") + } + } + + private static class ProductConverter{ + + /** + * Creates a product DTO based on its products category + * + * @param category The products category which determines what kind of products is created + * @param description The description of the product + * @param name The name of the product + * @param unitPrice The unit price of the product + * @param unit The unit in which the product is measured + * @return + */ + static Product createProduct(ProductCategory category, String description, String name, double unitPrice, ProductUnit unit){ + Product product + switch (category) { + case "Data Storage": + //todo do we want to set the id manually to null or update the DTO constructor? + product = new DataStorage(name, description, unitPrice,unit, null) + break + case "Primary Bioinformatics": + product = new PrimaryAnalysis(name, description, unitPrice,unit, null) + break + case "Project Management": + product = new ProjectManagement(name, description, unitPrice,unit, null) + break + case "Secondary Bioinformatics": + product = new SecondaryAnalysis(name, description, unitPrice,unit, null) + break + case "Sequencing": + product = new Sequencing(name, description, unitPrice,unit, null) + break + } + if(!product) throw new IllegalArgumentException("Cannot parse products") + + return product + } + + } + +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsPresenter.groovy new file mode 100644 index 000000000..3aeb11aa8 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsPresenter.groovy @@ -0,0 +1,46 @@ +package life.qbic.portal.offermanager.components.products + +import life.qbic.business.products.archive.ArchiveProductOutput +import life.qbic.business.products.create.CreateProductOutput +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.portal.offermanager.components.AppViewModel + +/** + *

AppPresenter for the {@link MaintainProductsView}

+ *
+ *

This presenter handles the output of the {@link life.qbic.business.products.create.CreateProduct} and {@link life.qbic.business.products.archive.ArchiveProduct} use cases and prepares it for the {@link MaintainProductsView}.

+ * + * @since 1.0.0 + * + */ +class MaintainProductsPresenter implements CreateProductOutput, ArchiveProductOutput{ + + private final MaintainProductsViewModel productsViewModel + private final AppViewModel mainViewModel + + MaintainProductsPresenter(MaintainProductsViewModel productsViewModel, AppViewModel mainViewModel){ + this.productsViewModel = productsViewModel + this.mainViewModel = mainViewModel + } + + @Override + void archived(Product product) { + mainViewModel.successNotifications << "Successfully archived product $product.productId - $product.productName." + } + + @Override + void created(Product product) { + mainViewModel.successNotifications << "Successfully added new product $product.productId - $product.productName." + } + + @Override + void foundDuplicate(Product product) { + mainViewModel.failureNotifications << "Found duplicate product for $product.productId - $product.productName." + //todo triggers sth in the view-model to ask the user if he still wants to create the duplicate + } + + @Override + void failNotification(String notification) { + mainViewModel.failureNotifications << notification + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsView.groovy new file mode 100644 index 000000000..00dd3000b --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsView.groovy @@ -0,0 +1,24 @@ +package life.qbic.portal.offermanager.components.products + +import com.vaadin.ui.FormLayout + +/** + * + *

This class generates a Form Layout in which the user can maintain the service products

+ * + *

{@link MaintainProductsViewModel} will be integrated into the qOffer 2.0 Portlet and provides an User Interface + * with the intention of enabling an {@value life.qbic.portal.offermanager.security.Role#OFFER_ADMIN} to create, archive and copy products.

+ * + * @since 1.0.0 + * + */ +class MaintainProductsView extends FormLayout{ + + private final MaintainProductsController controller + private final MaintainProductsViewModel viewModel + + MaintainProductsView(MaintainProductsController controller, MaintainProductsViewModel viewModel){ + this.controller = controller + this.viewModel = viewModel + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsViewModel.groovy new file mode 100644 index 000000000..fd77fce31 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsViewModel.groovy @@ -0,0 +1,41 @@ +package life.qbic.portal.offermanager.components.products + +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.portal.offermanager.dataresources.products.ProductsResourcesService + + +/** + *

A ViewModel holding data that is presented in a + * {@link life.qbic.portal.offermanager.components.products.MaintainProductsView}

+ * + *

This class holds all specific fields that are mutable in the view + * Whenever values change it should be reflected in the corresponding view. This class can be used + * for UI unit testing purposes.

+ * + *

This class can contain JavaBean objects to enable views to listen to changes in the values.

+ * + * @since 1.0.0 + * + */ +class MaintainProductsViewModel { + + ObservableList products = new ObservableList(new ArrayList()) + + private final ProductsResourcesService productsResourcesService + + MaintainProductsViewModel(ProductsResourcesService productsResourcesService) { + this.productsResourcesService = productsResourcesService + fetchProducts() + subscribe() + } + + private void fetchProducts(){ + products.addAll(productsResourcesService.iterator()) + } + + private void subscribe(){ + productsResourcesService.subscribe({ product -> + products << product + }) + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferDbConnector.groovy index eb6f8fd58..3862f478a 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferDbConnector.groovy @@ -7,7 +7,7 @@ import life.qbic.datamodel.dtos.business.Customer import life.qbic.datamodel.dtos.business.Offer import life.qbic.business.exceptions.DatabaseQueryException import life.qbic.business.offers.create.CreateOfferDataSource -import life.qbic.portal.offermanager.dataresources.persons.CustomerDbConnector +import life.qbic.portal.offermanager.dataresources.persons.PersonDbConnector import life.qbic.portal.offermanager.dataresources.database.ConnectionProvider import life.qbic.portal.offermanager.dataresources.products.ProductsDbConnector @@ -28,7 +28,7 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ ConnectionProvider connectionProvider - CustomerDbConnector customerGateway + PersonDbConnector customerGateway ProductsDbConnector productGateway @@ -41,9 +41,9 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ "projectObjective, totalPrice, customerAffiliationId, vat, netPrice, overheads FROM offer" - OfferDbConnector(ConnectionProvider connectionProvider, CustomerDbConnector customerDbConnector, ProductsDbConnector productsDbConnector){ + OfferDbConnector(ConnectionProvider connectionProvider, PersonDbConnector personDbConnector, ProductsDbConnector productsDbConnector){ this.connectionProvider = connectionProvider - this.customerGateway = customerDbConnector + this.customerGateway = personDbConnector this.productGateway = productsDbConnector } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/AffiliationResourcesService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/AffiliationResourcesService.groovy index 1d0904870..b5448208a 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/AffiliationResourcesService.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/AffiliationResourcesService.groovy @@ -16,13 +16,13 @@ import life.qbic.portal.offermanager.dataresources.ResourcesService */ class AffiliationResourcesService implements ResourcesService { - private final CustomerDbConnector dbConnector + private final PersonDbConnector dbConnector private final List availableAffiliations private final EventEmitter eventEmitter - AffiliationResourcesService(CustomerDbConnector dbConnector) { + AffiliationResourcesService(PersonDbConnector dbConnector) { this.dbConnector = dbConnector this.availableAffiliations = dbConnector.listAllAffiliations() this.eventEmitter = new EventEmitter<>() diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/CustomerResourceService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/CustomerResourceService.groovy index 5d0943963..e0492f972 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/CustomerResourceService.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/CustomerResourceService.groovy @@ -15,15 +15,15 @@ import life.qbic.portal.offermanager.dataresources.ResourcesService */ class CustomerResourceService implements ResourcesService{ - private final CustomerDbConnector dbConnector + private final PersonDbConnector dbConnector private final List customerList private final EventEmitter eventEmitter - CustomerResourceService(CustomerDbConnector dbConnector) { + CustomerResourceService(PersonDbConnector dbConnector) { this.dbConnector = dbConnector - this.customerList = dbConnector.fetchAllActiveCustomers() + this.customerList = dbConnector.fetchAllCustomers() this.eventEmitter = new EventEmitter<>() } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/CustomerDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/PersonDbConnector.groovy similarity index 82% rename from offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/CustomerDbConnector.groovy rename to offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/PersonDbConnector.groovy index 5cde24a5e..a51fc3992 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/CustomerDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/PersonDbConnector.groovy @@ -9,11 +9,12 @@ import life.qbic.datamodel.dtos.business.AffiliationCategory import life.qbic.datamodel.dtos.business.AffiliationCategoryFactory import life.qbic.datamodel.dtos.business.Customer import life.qbic.datamodel.dtos.business.ProjectManager +import life.qbic.datamodel.dtos.general.CommonPerson import life.qbic.datamodel.dtos.general.Person -import life.qbic.business.customers.affiliation.create.CreateAffiliationDataSource -import life.qbic.business.customers.affiliation.list.ListAffiliationsDataSource -import life.qbic.business.customers.create.CreateCustomerDataSource -import life.qbic.business.customers.search.SearchCustomerDataSource +import life.qbic.business.persons.affiliation.create.CreateAffiliationDataSource +import life.qbic.business.persons.affiliation.list.ListAffiliationsDataSource +import life.qbic.business.persons.create.CreatePersonDataSource +import life.qbic.business.persons.search.SearchPersonDataSource import life.qbic.business.exceptions.DatabaseQueryException import life.qbic.portal.offermanager.dataresources.database.ConnectionProvider @@ -34,7 +35,7 @@ import java.sql.Statement * */ @Log4j2 -class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDataSource, CreateAffiliationDataSource, ListAffiliationsDataSource { +class PersonDbConnector implements CreatePersonDataSource, SearchPersonDataSource, CreateAffiliationDataSource, ListAffiliationsDataSource { /** * A connection to the customer database used to create queries. @@ -43,7 +44,7 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat private static final AffiliationCategoryFactory CATEGORY_FACTORY = new AffiliationCategoryFactory() private static final AcademicTitleFactory TITLE_FACTORY = new AcademicTitleFactory() - private static final String CUSTOMER_SELECT_QUERY = "SELECT id, first_name, last_name, title, email FROM person" + private static final String PERSON_SELECT_QUERY = "SELECT id, first_name, last_name, title, email FROM person" private static final String PM_SELECT_QUERY = "SELECT * FROM person" private static final String AFFILIATION_SELECT_QUERY = "SELECT id, organization AS organisation, address_addition AS addressAddition, street, postal_code AS postalCode, city, country, category FROM affiliation" @@ -53,17 +54,17 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat * @param connection a connection to the customer db * @see Connection */ - CustomerDbConnector(ConnectionProvider connectionProvider) { + PersonDbConnector(ConnectionProvider connectionProvider) { this.connectionProvider = connectionProvider } @Override @Deprecated - List findCustomer(String firstName, String lastName) throws DatabaseQueryException { + List findPerson(String firstName, String lastName) throws DatabaseQueryException { String sqlCondition = "WHERE first_name = ? AND last_name = ?" - String queryTemplate = CUSTOMER_SELECT_QUERY + " " + sqlCondition + String queryTemplate = PERSON_SELECT_QUERY + " " + sqlCondition Connection connection = connectionProvider.connect() - List customerList = new ArrayList<>() + List customerList = new ArrayList<>() connection.withCloseable { PreparedStatement preparedStatement = it.prepareStatement(queryTemplate) @@ -71,18 +72,18 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat preparedStatement.setString(2, lastName) ResultSet resultSet = preparedStatement.executeQuery() while (resultSet.next()) { - customerList.add(parseCustomerFromResultSet(resultSet)) + customerList.add(parseCommonPersonFromResultSet(resultSet)) } } return customerList } @Override - List findActiveCustomer(String firstName, String lastName) throws DatabaseQueryException { + List findActivePerson(String firstName, String lastName) throws DatabaseQueryException { String sqlCondition = "WHERE first_name = ? AND last_name = ? AND active = 1" - String queryTemplate = CUSTOMER_SELECT_QUERY + " " + sqlCondition + String queryTemplate = PERSON_SELECT_QUERY + " " + sqlCondition Connection connection = connectionProvider.connect() - List customerList = new ArrayList<>() + List personList = new ArrayList<>() connection.withCloseable { PreparedStatement preparedStatement = it.prepareStatement(queryTemplate) @@ -90,10 +91,10 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat preparedStatement.setString(2, lastName) ResultSet resultSet = preparedStatement.executeQuery() while (resultSet.next()) { - customerList.add(parseCustomerFromResultSet(resultSet)) + personList.add(parseCommonPersonFromResultSet(resultSet)) } } - return customerList + return personList } /* @@ -175,9 +176,9 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat * @param customer */ @Override - void addCustomer(Customer customer) throws DatabaseQueryException { + void addPerson(Person person) throws DatabaseQueryException { try { - if (customerExists(customer)) { + if (personExists(person)) { throw new DatabaseQueryException("Customer is already in the database.") } Connection connection = connectionProvider.connect() @@ -185,55 +186,55 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat connection.withCloseable {it -> try { - int customerId = createNewCustomer(it, customer) - storeAffiliation(it, customerId, customer.affiliations) + int personId = createNewPerson(it, person) + storeAffiliation(it, personId, person.affiliations) connection.commit() } catch (Exception e) { log.error(e.message) log.error(e.stackTrace.join("\n")) connection.rollback() connection.close() - throw new DatabaseQueryException("Could not create customer.") + throw new DatabaseQueryException("Could not create person.") } } } catch (DatabaseQueryException ignored) { - throw new DatabaseQueryException("The customer could not be created: ${customer.toString()}") + throw new DatabaseQueryException("The person could not be created: ${person.toString()}") } catch (Exception e) { log.error(e) log.error(e.stackTrace.join("\n")) - throw new DatabaseQueryException("The customer could not be created: ${customer.toString()}") + throw new DatabaseQueryException("The person could not be created: ${person.toString()}") } } - private boolean customerExists(Customer customer) { + private boolean personExists(Person person) { String query = "SELECT * FROM person WHERE first_name = ? AND last_name = ? AND email = ?" Connection connection = connectionProvider.connect() - def customerAlreadyInDb = false + def personAlreadyInDb = false connection.withCloseable { def statement = connection.prepareStatement(query) - statement.setString(1, customer.firstName) - statement.setString(2, customer.lastName) - statement.setString(3, customer.emailAddress) + statement.setString(1, person.firstName) + statement.setString(2, person.lastName) + statement.setString(3, person.emailAddress) statement.execute() def result = statement.getResultSet() - customerAlreadyInDb = result.next() + personAlreadyInDb = result.next() } - return customerAlreadyInDb + return personAlreadyInDb } - private static int createNewCustomer(Connection connection, Customer customer) { + private static int createNewPerson(Connection connection, Person person) { String query = "INSERT INTO person (first_name, last_name, title, email, active) " + "VALUES(?, ?, ?, ?, ?)" List generatedKeys = [] def statement = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS) - statement.setString(1, customer.firstName ) - statement.setString(2, customer.lastName) - statement.setString(3, customer.title.value) - statement.setString(4, customer.emailAddress ) + statement.setString(1, person.firstName ) + statement.setString(2, person.lastName) + statement.setString(3, person.title.value) + statement.setString(4, person.emailAddress ) //a new customer is always active statement.setBoolean(5, true) statement.execute() @@ -245,7 +246,7 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat return generatedKeys[0] } - private void storeAffiliation(Connection connection, int customerId, List + private void storeAffiliation(Connection connection, int personId, List affiliations) { String query = "INSERT INTO person_affiliation (person_id, affiliation_id) " + "VALUES(?, ?)" @@ -253,7 +254,7 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat affiliations.each {affiliation -> def affiliationId = getAffiliationId(affiliation) def statement = connection.prepareStatement(query) - statement.setInt(1, customerId) + statement.setInt(1, personId) statement.setInt(2, affiliationId) statement.execute() } @@ -293,11 +294,11 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat } @Override - void updateCustomerAffiliations(int customerId, List updatedAffiliations) { + void updatePersonAffiliations(int personId, List updatedAffiliations) { List existingAffiliations = null try { - existingAffiliations = getAffiliationForPersonId(customerId) + existingAffiliations = getAffiliationForPersonId(personId) } catch (Exception e) { log.error(e) @@ -322,8 +323,8 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat connection.withCloseable {it -> try { - storeAffiliation(connection, customerId, newAffiliations) - connection.commit() + storeAffiliation(connection, personId, newAffiliations) + connection.commit() } catch (Exception e) { log.error(e.message) @@ -335,7 +336,7 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat } } - private void changeCustomerActiveFlag(int customerId, boolean active) { + private void changePersonActiveFlag(int customerId, boolean active) { String query = "UPDATE person SET active = ? WHERE id = ?"; Connection connection = connectionProvider.connect() @@ -352,10 +353,10 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat * @inheritDoc */ @Override - void updateCustomer(int oldCustomerId, Customer updatedCustomer) { + void updatePerson(int oldPersonId, Person updatedPerson) { - if (!getCustomer(oldCustomerId)) { - throw new DatabaseQueryException("Customer is not in the database and can't be updated.") + if (!getPerson(oldPersonId)) { + throw new DatabaseQueryException("Person is not in the database and can't be updated.") } Connection connection = connectionProvider.connect() @@ -363,23 +364,23 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat connection.withCloseable {it -> try { - int newCustomerId = createNewCustomer(it, updatedCustomer) - List allAffiliations = fetchAffiliationsForPerson(oldCustomerId) + int newPersonId = createNewPerson(it, updatedPerson) + List allAffiliations = fetchAffiliationsForPerson(oldPersonId) - updatedCustomer.affiliations.each { + updatedPerson.affiliations.each { if(!allAffiliations.contains(it)) allAffiliations.add(it) } - storeAffiliation(it, newCustomerId, allAffiliations) + storeAffiliation(it, newPersonId, allAffiliations) connection.commit() // if our update is successful we set the old customer inactive - changeCustomerActiveFlag(oldCustomerId, false) + changePersonActiveFlag(oldPersonId, false) } catch (Exception e) { log.error(e.message) log.error(e.stackTrace.join("\n")) connection.rollback() connection.close() - throw new DatabaseQueryException("The customer could not be updated: ${updatedCustomer.toString()}.") + throw new DatabaseQueryException("The person could not be updated: ${updatedPerson.toString()}.") } } } @@ -593,7 +594,7 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat */ List fetchAllCustomers() { List customers = [] - String query = CUSTOMER_SELECT_QUERY + String query = PERSON_SELECT_QUERY + " WHERE active = 1" Connection connection = connectionProvider.connect() connection.withCloseable { def preparedStatement = it.prepareStatement(query) @@ -610,19 +611,19 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat * List all available persons that are set to active in the database. * @return A list of active persons */ - List fetchAllActiveCustomers() { - List customers = [] - String query = CUSTOMER_SELECT_QUERY + " WHERE active = 1" + List fetchAllActivePersons() { + List persons = [] + String query = PERSON_SELECT_QUERY + " WHERE active = 1" Connection connection = connectionProvider.connect() connection.withCloseable { def preparedStatement = it.prepareStatement(query) ResultSet resultSet = preparedStatement.executeQuery() while(resultSet.next()) { - Customer customer = parseCustomerFromResultSet(resultSet) - customers.add(customer) + Person person = parseCommonPersonFromResultSet(resultSet) + persons.add(person) } } - return customers + return persons } /** @@ -631,7 +632,7 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat */ List fetchAllProjectManagers() { List pms = [] - String query = PM_SELECT_QUERY + String query = PM_SELECT_QUERY + " WHERE active = 1" Connection connection = connectionProvider.connect() connection.withCloseable { def preparedStatement = it.prepareStatement(query) @@ -644,6 +645,23 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat return pms } + /** + * This method creates a customer for a result set obtained with the CUSTOMER_SELECT_QUERY + * @param resultSet + * @return a Customer DTO containing the information from the query result set + */ + private CommonPerson parseCommonPersonFromResultSet(ResultSet resultSet) { + int personId = resultSet.getInt(1) + String titleValue = resultSet.getString('title') + AcademicTitle title = TITLE_FACTORY.getForString(titleValue) + String firstName = resultSet.getString('first_name') + String lastName = resultSet.getString('last_name') + String email = resultSet.getString('email') + CommonPerson.Builder personBuilder = new CommonPerson.Builder(firstName, lastName, email).title(title) + List affiliations = getAffiliationForPersonId(personId) + return personBuilder.affiliations(affiliations).build() + } + /** * This method creates a customer for a result set obtained with the CUSTOMER_SELECT_QUERY * @param resultSet @@ -731,32 +749,48 @@ class CustomerDbConnector implements CreateCustomerDataSource, SearchCustomerDat } @Override - Customer getCustomer(int personPrimaryId) { - String query = CUSTOMER_SELECT_QUERY + " " +"WHERE id=?" + CommonPerson getPerson(int personPrimaryId) { + String query = PERSON_SELECT_QUERY + " " +"WHERE id=?" Connection connection = connectionProvider.connect() connection.withCloseable { PreparedStatement statement = it.prepareStatement(query) statement.setInt(1, personPrimaryId) ResultSet result = statement.executeQuery() - Customer person = null + CommonPerson person = null while (result.next()) { - person = parseCustomerFromResultSet(result) + person = parseCommonPersonFromResultSet(result) } return person } } + Customer getCustomer(int personPrimaryId) { + String query = PERSON_SELECT_QUERY + " " +"WHERE id=?" + Connection connection = connectionProvider.connect() + + connection.withCloseable { + PreparedStatement statement = it.prepareStatement(query) + statement.setInt(1, personPrimaryId) + ResultSet result = statement.executeQuery() + Customer person = null + while (result.next()) { + person = parseCustomerFromResultSet(result) + } + return person + } + } + @Override - Optional findCustomer(Customer customer) { - int customerID + Optional findPerson(Person person) { + int personID - findActiveCustomer(customer.firstName, customer.lastName).each {foundCustomer -> + findActivePerson(person.firstName, person.lastName).each { foundCustomer -> //todo is the email address sufficient to compare customers for identity? - if(foundCustomer.emailAddress == customer.emailAddress) customerID = getActivePersonId(foundCustomer) + if(foundCustomer.emailAddress == person.emailAddress) personID = getActivePersonId(foundCustomer) } - return Optional.of(customerID) + return Optional.of(personID) } ProjectManager getProjectManager(int personPrimaryId) { diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/PersonResourceService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/PersonResourceService.groovy index 0ba9880d6..c98478835 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/PersonResourceService.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/PersonResourceService.groovy @@ -15,16 +15,16 @@ import life.qbic.portal.offermanager.dataresources.ResourcesService */ class PersonResourceService implements ResourcesService{ - private final CustomerDbConnector personDbConnector + private final PersonDbConnector personDbConnector private final List availablePersonEntries private final EventEmitter eventEmitter - PersonResourceService(CustomerDbConnector personDbConnector) { + PersonResourceService(PersonDbConnector personDbConnector) { this.personDbConnector = Objects.requireNonNull(personDbConnector, "Database connector " + "must not be null.") - this.availablePersonEntries = personDbConnector.fetchAllActiveCustomers() + this.availablePersonEntries = personDbConnector.fetchAllActivePersons() this.eventEmitter = new EventEmitter<>() } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/ProjectManagerResourceService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/ProjectManagerResourceService.groovy index 501521a0f..4a937edd7 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/ProjectManagerResourceService.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/ProjectManagerResourceService.groovy @@ -19,7 +19,7 @@ class ProjectManagerResourceService implements ResourcesService{ private final EventEmitter resourceUpdateEvent - ProjectManagerResourceService(CustomerDbConnector dbConnector) { + ProjectManagerResourceService(PersonDbConnector dbConnector) { availableProjectManagers = dbConnector.fetchAllProjectManagers() resourceUpdateEvent = new EventEmitter<>() } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index 66d8854ee..8e0534e50 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy @@ -2,6 +2,10 @@ package life.qbic.portal.offermanager.dataresources.products import groovy.sql.GroovyRowResult import groovy.util.logging.Log4j2 +import life.qbic.business.products.archive.ArchiveProductDataSource +import life.qbic.business.products.create.CreateProductDataSource +import life.qbic.business.products.create.ProductExistsException +import life.qbic.datamodel.dtos.business.ProductId import life.qbic.datamodel.dtos.business.ProductItem import life.qbic.datamodel.dtos.business.services.* import life.qbic.business.exceptions.DatabaseQueryException @@ -20,7 +24,7 @@ import java.sql.SQLException * @since 1.0.0 */ @Log4j2 -class ProductsDbConnector { +class ProductsDbConnector implements ArchiveProductDataSource, CreateProductDataSource { private final ConnectionProvider provider @@ -220,11 +224,87 @@ class ProductsDbConnector { return productItems } + /** + * A product is archived by setting it inactive + * @param product The product that needs to be archived + * @since 1.0.0 + * @throws life.qbic.business.exceptions.DatabaseQueryException + */ + @Override + void archive(Product product) throws DatabaseQueryException { + Connection connection = provider.connect() + + connection.withCloseable { + def statement = connection.prepareStatement(Queries.ARCHIVE_PRODUCT) + statement.setString(1, product.productId.toString()) + statement.execute() + } + } + + /** + * Fetches a product from the database + * @param productId The product id of the product to be fetched + * @return returns an optional that contains the product if it has been found + * @since 1.0.0 + * @throws life.qbic.business.exceptions.DatabaseQueryException is thrown when any technical interaction with the data source fails + */ + @Override + Optional fetch(ProductId productId) throws DatabaseQueryException { + Connection connection = provider.connect() + String query = Queries.SELECT_ALL_PRODUCTS + " WHERE productId=?" + Optional product = Optional.empty() + + connection.withCloseable { + PreparedStatement preparedStatement = it.prepareStatement(query) + preparedStatement.setString(1, productId.identifier.toString()) + ResultSet result = preparedStatement.executeQuery() + + while (result.next()) { + product = Optional.of(rowResultToProduct(SqlExtensions.toRowResult(result))) + } + } + return product + } + + /** + * Stores a product in the database + * @param product The product that needs to be stored + * @since 1.0.0 + * @throws DatabaseQueryException if any technical interaction with the data source fails + * @throws ProductExistsException if the product already exists in the data source + */ + @Override + void store(Product product) throws DatabaseQueryException, ProductExistsException { + Connection connection = provider.connect() + + connection.withCloseable { + PreparedStatement preparedStatement = it.prepareStatement(Queries.INSERT_PRODUCT) + preparedStatement.setString(1, getProductType(product)) + preparedStatement.setString(2, product.description) + preparedStatement.setString(3, product.productName) + preparedStatement.setDouble(4, product.unitPrice) + preparedStatement.setString(5, product.unit.value) + preparedStatement.setString(6, product.productId.toString()) + + preparedStatement.execute() + } + } + /** * Class that encapsulates the available SQL queries. */ private static class Queries { + /** + * Query for inserting a product. + */ + final static String INSERT_PRODUCT = "INSERT INTO product (category, description, productName, unitPrice, unit, productId) VALUES (?, ?, ?, ?, ?, ?)" + + /** + * Inactivate a product + */ + final static String ARCHIVE_PRODUCT = "UPDATE product SET active = 0 WHERE productId = ?" + /** * Query for all available products. */ diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy new file mode 100644 index 000000000..ec4658ee6 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy @@ -0,0 +1,190 @@ +package life.qbic.portal.offermanager.dataresources.projects + +import groovy.util.logging.Log4j2 +import life.qbic.datamodel.dtos.general.Person +import life.qbic.portal.offermanager.dataresources.persons.PersonDbConnector + +import life.qbic.datamodel.dtos.business.* +import life.qbic.datamodel.dtos.projectmanagement.* +import life.qbic.business.projects.create.ProjectExistsException +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.portal.offermanager.dataresources.database.ConnectionProvider +import java.sql.Connection +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.Statement + +/** + * Provides operations on QBiC project data + * + * This class is responsible for transferring project data to the project/customer db + * + * @since 1.0.0 + * + */ +@Log4j2 +class ProjectDbConnector { + + /** + * A connection to the project/customer database used to create queries. + */ + private final ConnectionProvider connectionProvider + /** + * A connector to the customer database used to create queries. + */ + private final PersonDbConnector personDbConnector + + /** + * Constructor for a ProjectDbConnector + * @param connectionProvider a connection provider to the project/customer db + * @param personDbConnector db connector used to connect projects to customer and manager + * + */ + ProjectDbConnector(ConnectionProvider connectionProvider, PersonDbConnector personDbConnector) { + this.connectionProvider = connectionProvider + this.personDbConnector = personDbConnector + } + + /** + * parses existing projects from user database, might be needed later if more complex information is to be listed + */ + public List fetchProjects() { + List projects = [] + String query = "SELECT openbis_project_identifier from projects" + Connection connection = connectionProvider.connect() + connection.withCloseable { + def preparedStatement = it.prepareStatement(query) + ResultSet resultSet = preparedStatement.executeQuery() + while(resultSet.next()) { + try { + String[] tokens = resultSet.getString('openbis_project_identifier').split("/") + ProjectSpace space = tokens[1] + ProjectCode project = tokens[2] + } catch (Exception e) { + e.printStackTrace() + throw new DatabaseQueryException("Could not parse existing projects from database.") + } + projects.add(new ProjectIdentifier(space, project)) + } + } + return projects + } + + /** + * Add a project to the user database to connect additional metadata that is not stored in openBIS + * The project is uniquely recognizable by its openBIS project identifier, containing space and + * project code + * @param projectIdentifier a project identifier object denoting the openBIS identifier + * @param projectApplication a project application object used to add additional metadata + */ + public Project addProjectAndConnectPersonsInUserDB(projectIdentifier, projectApplication) { + //collect infos needed for database + String projectTitle = projectApplication.getProjectTitle() + Customer customer = projectApplication.getCustomer() + ProjectManager projectManager = projectApplication.getProjectManager() + + //fetch needed person ids from database + int customerID = personDBConnector.getPersonId(customer) + int managerID = personDBConnector.getPersonId(projectManager) + + Connection connection = connectionProvider.connect() + connection.setAutoCommit(false) + + connection.withCloseable {it -> + try { + int projectID = addProjectToDB(it, projectIdentifier, projectTitle) + addPersonToProject(it, projectID, managerID, "Manager") + addPersonToProject(it, projectID, customerID, "PI") + + it.commit() + + } catch (Exception e) { + log.error(e.message) + log.error(e.stackTrace.join("\n")) + it.rollback() + + throw new DatabaseQueryException("Could not add person and project data to user database.") + } + } + return new Project(projectIdentifier, projectTitle, projectApplication.getLinkedOffer()) + } + + private boolean isProjectInDB(String projectIdentifier) { + log.debug("Looking for project " + projectIdentifier + " in the DB"); + String sql = "SELECT * from projects WHERE openbis_project_identifier = ?"; + Connection connection = connectionProvider.connect() + connection.withCloseable { it -> + PreparedStatement statement = it.prepareStatement(sql); + statement.setString(1, projectIdentifier); + ResultSet rs = statement.executeQuery(); + if (rs.next()) { + return true + } + } + return false; + } + + private int addProjectToDB(Connection connection, String projectIdentifier, String projectName) { + if(isProjectInDB(projectIdentifier)) { + throw new ProjectExistsException("Project "+projectIdentifier+" is already in the user database") + } + log.debug("Trying to add project " + projectIdentifier + " to the person DB"); + String sql = "INSERT INTO projects (openbis_project_identifier, short_title) VALUES(?, ?)"; + try (PreparedStatement statement = + connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, projectIdentifier); + statement.setString(2, projectName); + statement.execute(); + ResultSet rs = statement.getGeneratedKeys(); + if (rs.next()) { + logout(conn); + log.debug("Successful."); + return rs.getInt(1); + } + } + return -1 + } + + private void addPersonToProject(Connection connection, int projectID, int personID, String role) { + if (!hasPersonRoleInProject(personID, projectID, role)) { + log.debug("Trying to add person with role " + role + " to a project."); + String sql = + "INSERT INTO projects_persons (project_id, person_id, project_role) VALUES(?, ?, ?)"; + try (PreparedStatement statement = + connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setInt(1, projectID); + statement.setInt(2, personID); + statement.setString(3, role); + statement.execute(); + log.debug("Successful."); + } catch (Exception e) { + log.error("SQL operation unsuccessful: " + e.getMessage()); + e.printStackTrace(); + } + } + } + + private boolean hasPersonRoleInProject(int personID, int projectID, String role) { + logger.info("Checking if person already has this role in the project."); + String sql = + "SELECT * from projects_persons WHERE person_id = ? AND project_id = ? and project_role = ?"; + boolean res = false; + Connection connection = connectionProvider.connect() + try { + PreparedStatement statement = connection.prepareStatement(sql); + statement.setInt(1, personID); + statement.setInt(2, projectID); + statement.setString(3, role); + ResultSet rs = statement.executeQuery(); + if (rs.next()) { + res = true; + logger.info("person already has this role!"); + } + } catch (Exception e) { + logger.error("SQL operation unsuccessful: " + e.getMessage()); + e.printStackTrace(); + } + logout(conn); + return res; + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy new file mode 100644 index 000000000..b5ba78f3c --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy @@ -0,0 +1,183 @@ +package life.qbic.portal.offermanager.dataresources.projects + +import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi +import groovy.transform.CompileStatic +import groovy.util.logging.Log4j2 + +import life.qbic.business.projects.spaces.CreateProjectSpaceDataSource +import life.qbic.business.projects.spaces.ProjectSpaceExistsException +import life.qbic.business.projects.create.CreateProjectDataSource +import life.qbic.business.projects.create.ProjectExistsException +import life.qbic.business.projects.create.SpaceNonExistingException + +import life.qbic.datamodel.dtos.business.* +import life.qbic.datamodel.dtos.projectmanagement.* +import life.qbic.business.exceptions.DatabaseQueryException + +import java.sql.Connection +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.Statement + +import life.qbic.openbis.openbisclient.OpenBisClient + +import ch.ethz.sis.openbis.generic.asapi.v3.dto.operation.SynchronousOperationExecutionOptions +import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.operation.IOperation +import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.create.CreateProjectsOperation +import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.create.ProjectCreation +import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.create.CreateSpacesOperation +import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.create.SpaceCreation +import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.SpacePermId + + +/** + * Provides operations on QBiC project data + * + * This class implements the data sources of the different use cases and is responsible for + * transferring data to the project/customer db and openBIS + * + * @since 1.0.0 + * + */ +@Log4j2 +@CompileStatic +class ProjectMainConnector implements CreateProjectDataSource, CreateProjectSpaceDataSource { + + /** + * A connection to the project (and customer) database used to create queries. + */ + private final ProjectDbConnector projectDbConnector + private final OpenBisClient openbisClient + private List openbisSpaces + private List openbisProjects + + /** + * Constructor for a ProjectMainConnector + * @param projectDbConnector a connector enabling interaction with the project database + * @param openbisClient an openBIS client API object + */ + ProjectMainConnector(ProjectDbConnector projectDbConnector, OpenBisClient openbisClient) { + this.projectDbConnector = projectDbConnector + this.openbisClient = openbisClient + fetchExistingSpaces() + fetchExistingProjects() + } + + private void fetchExistingSpaces() { + this.openbisSpaces = new ArrayList<>() + for(String spaceName : openbisClient.listSpaces()) { + this.openbisSpaces.add(new ProjectSpace(spaceName)) + } + } + + /** + * Returns a copy of the list of available project spaces that has been fetched from openBIS upon creation of this class instance + */ + public List listSpaces() { + return new ArrayList(openbisSpaces); + } + + private void fetchExistingProjects() { + //projectDbConnector.fetchProjects() might be used at some point to fetch more metadata + + openbisProjects = [] + for(ch.ethz.sis.openbis.generic.asapi.v3.dto.project.Project openbisProject : openbisClient.listProjects()) { + try { + ProjectSpace space = new ProjectSpace(openbisProject.getSpace().getCode()) + ProjectCode code = new ProjectCode(openbisProject.getCode()) + openbisProjects.add(new ProjectIdentifier(space, code)) + } catch (Exception e) { + log.error(e.message) + } + } + } + + private void createOpenbisSpace(String spaceName, String description) { + SpaceCreation space = new SpaceCreation() + space.setCode(spaceName) + + space.setDescription(description) + + IOperation operation = new CreateSpacesOperation(space) + handleOperations(operation) + } + + private void createOpenbisProject(ProjectSpace space, ProjectCode projectCode, String description) { + ProjectCreation project = new ProjectCreation(); + project.setCode(projectCode.toString()); + project.setSpaceId(new SpacePermId(space.toString())); + project.setDescription(description); + + IOperation operation = new CreateProjectsOperation(project); + handleOperations(operation); + } + + /** + * Returns a copied list of existing projects fetched upon creation of this class + */ + public List fetchProjects() { + return new ArrayList(openbisProjects); + } + + @Override + void createProjectSpace(ProjectSpace projectSpace) throws ProjectSpaceExistsException, DatabaseQueryException { + String spaceName = projectSpace.getName() + if(openbisClient.spaceExists(spaceName)) { + throw new ProjectSpaceExistsException("Project space "+spaceName+" could not be created, as it exists in openBIS already!") + } + try { + //we don't provide a description in our data model for now, but it's optional anyway + createOpenbisSpace(spaceName, "") + + } catch (Exception e) { + log.error(e.message) + log.error(e.stackTrace.join("\n")) + throw new DatabaseQueryException("Could not create project space.") + } + } + + @Override + Project createProject(ProjectApplication projectApplication) throws ProjectExistsException, DatabaseQueryException { + //collect infos needed for openBIS + ProjectSpace space = projectApplication.getProjectSpace() + ProjectCode projectCode = projectApplication.getProjectCode() + String description = projectApplication.getProjectObjective() + + ProjectIdentifier projectIdentifier = new ProjectIdentifier(space, projectCode) + + //collect infos needed for database + String projectTitle = projectApplication.getProjectTitle() + Customer customer = projectApplication.getCustomer() + ProjectManager projectManager = projectApplication.getProjectManager() + + //if the space does not exist, an error shall be thrown + if (!openbisClient.spaceExists(space.toString())) { + throw new SpaceNonExistingException("Could not create project because of non-existent space: "+space.toString()) + } + if (openbisClient.projectExists(space.toString(), projectCode.toString())) { + throw new ProjectExistsException("Project "+projectIdentifier.toString()+" could not be created, as it exists in openBIS already!") + } + try { + createOpenbisProject(space, projectCode, description) + } catch (Exception e) { + log.error(e.message) + log.error(e.stackTrace.join("\n")) + throw new DatabaseQueryException("Could not create project.") + } + + return projectDbConnector.addProjectAndConnectPersonsInUserDB(projectIdentifier, projectApplication) + } + + private void handleOperations(IOperation operation) { + IApplicationServerApi api = openbisClient.getV3() + + SynchronousOperationExecutionOptions executionOptions = new SynchronousOperationExecutionOptions() + List operationOptions = Arrays.asList(operation) + try { + api.executeOperations(openbisClient.getSessionToken(), operationOptions, executionOptions) + } catch (Exception e) { + log.error("Unexpected exception during openBIS operation.", e) + throw e + } + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectResourceService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectResourceService.groovy new file mode 100644 index 000000000..dd8dde5af --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectResourceService.groovy @@ -0,0 +1,62 @@ +package life.qbic.portal.offermanager.dataresources.projects + +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier +import life.qbic.portal.offermanager.communication.EventEmitter +import life.qbic.portal.offermanager.communication.Subscription +import life.qbic.portal.offermanager.dataresources.ResourcesService + +/** + * Service that holds resources about available projects + * + * This service holds resources about project identifiers and can be used to subscribe to any + * update event of the underlying resource data. + * + * @since 1.0.0 + */ +class ProjectResourceService implements ResourcesService{ + + private final ProjectMainConnector projectMainConnector + + private final List existingProjects + + private final EventEmitter eventEmitter + + ProjectResourceService(ProjectMainConnector projectMainConnector) { + this.projectMainConnector = Objects.requireNonNull(projectMainConnector, "Connector " + + "must not be null.") + this.existingProjects = projectMainConnector.fetchProjects() + this.eventEmitter = new EventEmitter<>() + } + + @Override + void reloadResources() { + + } + + @Override + void subscribe(Subscription subscription) { + this.eventEmitter.register(subscription) + } + + @Override + void unsubscribe(Subscription subscription) { + this.eventEmitter.unregister(subscription) + } + + @Override + void addToResource(ProjectIdentifier resourceItem) { + this.existingProjects.add(resourceItem) + this.eventEmitter.emit(resourceItem) + } + + @Override + void removeFromResource(ProjectIdentifier resourceItem) { + this.existingProjects.remove(resourceItem) + this.eventEmitter.emit(resourceItem) + } + + @Override + Iterator iterator() { + return new ArrayList<>(this.existingProjects).iterator() + } +} \ No newline at end of file diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectSpaceResourceService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectSpaceResourceService.groovy new file mode 100644 index 000000000..bc9ea5f20 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectSpaceResourceService.groovy @@ -0,0 +1,62 @@ +package life.qbic.portal.offermanager.dataresources.projects + +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace +import life.qbic.portal.offermanager.communication.EventEmitter +import life.qbic.portal.offermanager.communication.Subscription +import life.qbic.portal.offermanager.dataresources.ResourcesService + +/** + * Service that holds resources about available spaces + * + * This service holds resources about space information and can be used to subscribe to any + * update event of the underlying resource data. + * + * @since 1.0.0 + */ +class ProjectSpaceResourceService implements ResourcesService{ + + private final ProjectMainConnector projectMainConnector + + private final List availableSpaces + + private final EventEmitter eventEmitter + + ProjectSpaceResourceService(ProjectMainConnector projectMainConnector) { + this.projectMainConnector = Objects.requireNonNull(projectMainConnector, "Connector " + + "must not be null.") + this.availableSpaces = projectMainConnector.listSpaces() + this.eventEmitter = new EventEmitter<>() + } + + @Override + void reloadResources() { + + } + + @Override + void subscribe(Subscription subscription) { + this.eventEmitter.register(subscription) + } + + @Override + void unsubscribe(Subscription subscription) { + this.eventEmitter.unregister(subscription) + } + + @Override + void addToResource(ProjectSpace resourceItem) { + this.availableSpaces.add(resourceItem) + this.eventEmitter.emit(resourceItem) + } + + @Override + void removeFromResource(ProjectSpace resourceItem) { + this.availableSpaces.remove(resourceItem) + this.eventEmitter.emit(resourceItem) + } + + @Override + Iterator iterator() { + return new ArrayList<>(this.availableSpaces).iterator() + } +} diff --git a/offer-manager-app/src/main/resources/offer-template/offer.html b/offer-manager-app/src/main/resources/offer-template/offer.html index d9a2a296b..fc55e81e9 100644 --- a/offer-manager-app/src/main/resources/offer-template/offer.html +++ b/offer-manager-app/src/main/resources/offer-template/offer.html @@ -147,6 +147,18 @@

Quotation Details

A more detailed description
+
+
+
Total Cost:
+
4000
+
+
+
Net Cost
+
3000
+
+
+
Overhead Cost:
+
1000
-
+

This is some outro text. It has changed!!

- +
@@ -178,25 +190,24 @@

Agreement declaration

The invoice will be issued after completion of the project.

- +

- Quality control at all steps of the data processing will guarantee that the processed data is in accordance to DFG (German research foundation) guidance for good scientific practice. All project related data will be kept securely on our local infrastructure. - Any publication that contains information based on counselling with or data generated by QBiC and the involved technology platforms requires at least the acknowledgement of the scientists at QBiC and the - involved technology platforms, e.g. by stating “the xyz part of the work was supported by the Quantitative Biology Center (QBiC) and its technology platforms of the University of Tübingen”. + Quality control at all steps of the data processing will guarantee that the processed data is in accordance to DFG (German research foundation) guidance for good scientific practice. All project related data will be kept securely on our local infrastructure. + Any publication that contains information based on counselling with or data generated by QBiC and the involved technology platforms requires at least the acknowledgement of the scientists at QBiC and the + involved technology platforms, e.g. by stating “the xyz part of the work was supported by the Quantitative Biology Center (QBiC) and its technology platforms of the University of Tübingen”.

- This is required because QBiC is subsidized by the university and other external funding, and therefore needs recognition. -

+ This is required because QBiC is subsidized by the university and other external funding, and therefore needs recognition. +

Depending on the extent of involvement, either co-authorship (e.g. downstream analysis, analytical method development) or acknowledgement (e.g. direct pipeline output) will be required. Should the project deviate from the plan outlined above, the customer will be contacted by QBiC or one of the involved technology platforms again in order to discuss the continuation of the project. - -

+ +

By signing the quote, you confirm that all samples were collected according to governing law and the current scientific code of conduct, all necessary documentation has been performed and the respective permissions are available upon request (i.e. approval of the local Ethical Committee, informed consent, FELASA certificate, etc.).

If you agree with this offer, please return a signed copy.

-
+ - diff --git a/offer-manager-app/src/main/resources/offer-template/stylesheet.css b/offer-manager-app/src/main/resources/offer-template/stylesheet.css index d2759db1e..7f3eec55e 100644 --- a/offer-manager-app/src/main/resources/offer-template/stylesheet.css +++ b/offer-manager-app/src/main/resources/offer-template/stylesheet.css @@ -17,7 +17,7 @@ body { width: 100%; text-align: center; } - + /* Grid container */ .grid-container { /*max-height: 85vh;*/ @@ -45,7 +45,7 @@ body { position: relative; height: 100vh; } - + .pagebreak { clear: both; page-break-after: always; @@ -137,7 +137,7 @@ body { grid-column-start: 1; grid-column-end: 4; } - + .items-introduction-text p { font-size: 10pt; } @@ -145,7 +145,7 @@ body { .items-outroduction-text { grid-column-start: 1; grid-column-end: 4; - } + } /* Items table*/ .grid-container-table { @@ -156,24 +156,30 @@ body { margin-left: auto; font-size: 10pt; } - + #grid-table-header{ padding-left: 20px; padding-right: 20px; font-weight: bold; } - + .product-items{ padding-left: 20px; padding-right: 20px; } - + #grid-table-footer{ text-align: right; border-top: 1px solid #bbbbbb; padding: 10px 20px 2px 20px; } - + + #grid-sub-total-footer{ + text-align: right; + border-top: 1px solid #bbbbbb; + padding: 10px 0px 2px 20px; + } + .text-center{ text-align: center; } @@ -230,16 +236,20 @@ body { } .total-costs > td { - border-top: 1px solid #bbbbbb; + border-top: 2px solid #bbbbbb; } - /* Agreement Section */ + .single-underscore { + border-bottom: 2px solid #bbbbbb; + } - .agreement-section { - grid-column-start: 1; - grid-column-end: 4; + .double-underscore { + border-bottom: 3px double #bbbbbb; } + + /* Agreement Section */ + .signature { padding-top: 50px; grid-column-start: 1; @@ -277,7 +287,7 @@ body { padding-top: 15px; padding-bottom: 5px; } - + .col { -ms-flex-preferred-size: 0; flex-basis: 0; @@ -285,78 +295,78 @@ body { flex-grow: 1; max-width: 100%; } - + .col-1, .col-2, .col-3, .col-4, .col-5, .col-6{ position: relative; width: 100%; } - + .col-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } - + .col-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } - + .col-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } - + .col-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } - + .col-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } - + .col-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } - + .col-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } - + .col-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } - + .col-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } - + .col-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } - + .col-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } - + .col-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; diff --git a/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/CustomerDbConnectorSpec.groovy b/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/PersonDbConnectorSpec.groovy similarity index 92% rename from offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/CustomerDbConnectorSpec.groovy rename to offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/PersonDbConnectorSpec.groovy index 7b7b4a744..0de83211b 100644 --- a/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/CustomerDbConnectorSpec.groovy +++ b/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/PersonDbConnectorSpec.groovy @@ -2,10 +2,9 @@ package life.qbic.portal.qoffer2.database import groovy.sql.GroovyRowResult import life.qbic.datamodel.dtos.business.Affiliation -import life.qbic.datamodel.dtos.business.AcademicTitle import life.qbic.datamodel.dtos.business.AffiliationCategory import life.qbic.datamodel.dtos.business.Customer -import life.qbic.portal.offermanager.dataresources.persons.CustomerDbConnector +import life.qbic.portal.offermanager.dataresources.persons.PersonDbConnector import life.qbic.portal.offermanager.dataresources.database.ConnectionProvider import org.apache.groovy.sql.extensions.SqlExtensions import spock.lang.Specification @@ -15,12 +14,12 @@ import java.sql.PreparedStatement import java.sql.ResultSet /** - * Adds tests for {@link CustomerDbConnector} + * Adds tests for {@link PersonDbConnector} * * @since: 0.1.0 * */ -class CustomerDbConnectorSpec extends Specification{ +class PersonDbConnectorSpec extends Specification{ def "CustomerDbConnector shall return the id for a given person"(){ given: @@ -47,7 +46,7 @@ class CustomerDbConnectorSpec extends Specification{ ConnectionProvider connectionProvider = Stub (ConnectionProvider, {it.connect() >> connection}) //and: "an implementation of the SearchCustomerDataSource with this connection provider" - CustomerDbConnector dataSource = new CustomerDbConnector(connectionProvider) + PersonDbConnector dataSource = new PersonDbConnector(connectionProvider) when: int resultId = dataSource.getPersonId(new Customer.Builder(firstName,lastName,emailAddress).build()) @@ -89,10 +88,10 @@ class CustomerDbConnectorSpec extends Specification{ ConnectionProvider connectionProvider = Stub (ConnectionProvider, {it.connect() >> connection}) and: "an implementation of the SearchCustomerDataSource with this connection provider" - CustomerDbConnector dataSource = new CustomerDbConnector(connectionProvider) + PersonDbConnector dataSource = new PersonDbConnector(connectionProvider) when: "update customer affiliations is called" - dataSource.updateCustomerAffiliations(customerId, [new Affiliation.Builder(organization, + dataSource.updatePersonAffiliations(customerId, [new Affiliation.Builder(organization, street, postal_code, city).addressAddition(address_addition).country(country).category(category).build()]) then: "no affiliations are updated and a failNotification is thrown" @@ -137,7 +136,7 @@ class CustomerDbConnectorSpec extends Specification{ ConnectionProvider connectionProvider = Stub (ConnectionProvider, {it.connect() >> connection}) //and: "an implementation of the SearchCustomerDataSource with this connection provider" - CustomerDbConnector dataSource = new CustomerDbConnector(connectionProvider) + PersonDbConnector dataSource = new PersonDbConnector(connectionProvider) when: int resultId = dataSource.getAffiliationId(new Affiliation.Builder(organization,street,postal_code,city) diff --git a/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/SearchCustomerDataSourceSpec.groovy b/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/SearchPersonDataSourceSpec.groovy similarity index 89% rename from offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/SearchCustomerDataSourceSpec.groovy rename to offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/SearchPersonDataSourceSpec.groovy index 7d2460c6e..151fe8e04 100644 --- a/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/SearchCustomerDataSourceSpec.groovy +++ b/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/SearchPersonDataSourceSpec.groovy @@ -3,8 +3,8 @@ package life.qbic.portal.qoffer2.database import groovy.sql.GroovyRowResult import life.qbic.datamodel.dtos.business.AcademicTitleFactory import life.qbic.datamodel.dtos.business.Customer -import life.qbic.business.customers.search.SearchCustomerDataSource -import life.qbic.portal.offermanager.dataresources.persons.CustomerDbConnector +import life.qbic.business.persons.search.SearchPersonDataSource +import life.qbic.portal.offermanager.dataresources.persons.PersonDbConnector import life.qbic.portal.offermanager.dataresources.database.ConnectionProvider import org.apache.groovy.sql.extensions.SqlExtensions import spock.lang.Ignore @@ -21,7 +21,7 @@ import java.sql.ResultSet * * @since: 1.0.0 */ -class SearchCustomerDataSourceSpec extends Specification{ +class SearchPersonDataSourceSpec extends Specification{ AcademicTitleFactory factory = new AcademicTitleFactory() @Ignore @@ -50,10 +50,10 @@ class SearchCustomerDataSourceSpec extends Specification{ ConnectionProvider connectionProvider = Stub (ConnectionProvider, {it.connect() >> connection}) //and: "an implementation of the SearchCustomerDataSource with this connection provider" - SearchCustomerDataSource dataSource = new CustomerDbConnector(connectionProvider) + SearchPersonDataSource dataSource = new PersonDbConnector(connectionProvider) when: "the datasource is tasked with finding a customer with provided first and last name" - List foundCustomers = dataSource.findCustomer(firstName, lastName) + List foundCustomers = dataSource.findPerson(firstName, lastName) then: "the returned customer information matches information provided by the ResultSet" foundCustomers.size() == 1 diff --git a/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/web/controllers/CreateCustomerControllerSpec.groovy b/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/web/controllers/CreatePersonControllerSpec.groovy similarity index 82% rename from offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/web/controllers/CreateCustomerControllerSpec.groovy rename to offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/web/controllers/CreatePersonControllerSpec.groovy index 412b4a089..46162c00b 100644 --- a/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/web/controllers/CreateCustomerControllerSpec.groovy +++ b/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/web/controllers/CreatePersonControllerSpec.groovy @@ -4,7 +4,7 @@ import life.qbic.datamodel.dtos.business.AcademicTitle import life.qbic.datamodel.dtos.business.AcademicTitleFactory import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.datamodel.dtos.business.Customer -import life.qbic.business.customers.create.CreateCustomerInput +import life.qbic.business.persons.create.CreatePersonInput import life.qbic.portal.offermanager.components.person.create.CreatePersonController import spock.lang.Specification @@ -15,7 +15,7 @@ import spock.lang.Specification * * @since: 1.0.0 */ -class CreateCustomerControllerSpec extends Specification { +class CreatePersonControllerSpec extends Specification { def "CreateNewCustomer passes valid customer to use case"() { @@ -26,12 +26,12 @@ class CreateCustomerControllerSpec extends Specification { Affiliation affiliation = new Affiliation.Builder("Aperture", "Destructive way", "007", "Underground").build() List affiliations = [ affiliation ] - CreateCustomerInput createCustomerInput = Mock() + CreatePersonInput createCustomerInput = Mock() CreatePersonController controller = new CreatePersonController(createCustomerInput) when: - controller.createNewCustomer(firstName, lastName, title, email, affiliations) + controller.createNewPerson(firstName, lastName, title, email, affiliations) then: - 1 * createCustomerInput.createCustomer({Customer customer -> + 1 * createCustomerInput.createPerson({ Customer customer -> customer.firstName == firstName && \ customer.lastName == lastName && \ customer.title == academicTitle && \ diff --git a/offer-manager-domain/pom.xml b/offer-manager-domain/pom.xml index 2b82c5600..121a14f41 100644 --- a/offer-manager-domain/pom.xml +++ b/offer-manager-domain/pom.xml @@ -1,43 +1,47 @@ - offer-manager-domain - 4.0.0 - - offer-manager - life.qbic - 1.0.0-alpha.3 - - - - life.qbic - data-model-lib - - - org.apache.logging.log4j - log4j-core - 2.14.0 - - - - - - org.codehaus.gmavenplus - gmavenplus-plugin - - ${project.build.directory}/site/gapidocs - ${project.build.directory}/site/testgapidocs - - - - - - - - life.qbic - groovydoc-maven-plugin - - - + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + offer-manager-domain + 4.0.0 + + offer-manager + life.qbic + 1.0.0-alpha.4 + + + + life.qbic + data-model-lib + + + life.qbic + openbis-client-lib + + + log4j + log4j + 1.2.17 + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + ${project.build.directory}/site/gapidocs + ${project.build.directory}/site/testgapidocs + + + + + + + + life.qbic + groovydoc-maven-plugin + + + diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomer.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomer.groovy deleted file mode 100644 index c573eb8c3..000000000 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomer.groovy +++ /dev/null @@ -1,69 +0,0 @@ -package life.qbic.business.customers.create - -import life.qbic.business.customers.update.UpdateCustomer -import life.qbic.business.customers.update.UpdateCustomerOutput -import life.qbic.business.logging.Logger -import life.qbic.business.logging.Logging -import life.qbic.datamodel.dtos.business.Customer - -import life.qbic.business.exceptions.DatabaseQueryException -import life.qbic.datamodel.dtos.general.Person - -/** - * This use case creates a customer in the system - * - * Information on persons such as affiliation and names can be added to the user database. - * - * @since: 1.0.0 - */ -class CreateCustomer implements CreateCustomerInput, UpdateCustomerOutput { - - private CreateCustomerDataSource dataSource - private CreateCustomerOutput output - private UpdateCustomer updateCustomer - - private final Logging log = Logger.getLogger(CreateCustomer.class) - - - CreateCustomer(CreateCustomerOutput output, CreateCustomerDataSource dataSource){ - this.output = output - this.dataSource = dataSource - this.updateCustomer = new UpdateCustomer(this,dataSource) - } - - @Override - void createCustomer(Customer customer) { - try { - dataSource.addCustomer(customer) - try { - output.customerCreated(customer) - } catch (Exception ignored) { - log.error(ignored.stackTrace.toString()) - } - } catch(DatabaseQueryException databaseQueryException){ - output.failNotification(databaseQueryException.message) - } catch(Exception unexpected) { - println "-------------------------" - println "Unexpected Exception ...." - println unexpected.message - println unexpected.stackTrace.join("\n") - output.failNotification("Could not create new customer") - } - } - - @Override - void updateCustomer(Customer oldCustomer, Customer newCustomer) { - int customerId = dataSource.findCustomer(oldCustomer).get() - updateCustomer.updateCustomer(customerId,newCustomer) - } - - @Override - void customerUpdated(Person person) { - output.customerCreated(person) - } - - @Override - void failNotification(String notification) { - output.failNotification(notification) - } -} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerDataSource.groovy deleted file mode 100644 index ab1987de7..000000000 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerDataSource.groovy +++ /dev/null @@ -1,59 +0,0 @@ -package life.qbic.business.customers.create - -import life.qbic.datamodel.dtos.business.Affiliation -import life.qbic.datamodel.dtos.business.Customer -import life.qbic.business.exceptions.DatabaseQueryException - -/** - * Creates a customer in the database for the CreateCustomer use case - * - * This interface should be used to allow the use case to forward data to a data source that implements this use case - * and still follow the correct dependency flow from infrastructure to the domain logic - * - * @since: 1.0.0 - */ -interface CreateCustomerDataSource { - - /** - * Adds a customer to the user database - * - * @param customer a person to be added to known persons - * @throws DatabaseQueryException When a customer could not been added to the customer database - * @since 1.0.0 - */ - void addCustomer(Customer customer) throws DatabaseQueryException - - /** - * Updates the information of a given customer specified by a customer ID - * - * @param customerId to specify the customer to be updated - * @param updatedCustomer customer containing all information and the updates of a customer - * @throws DatabaseQueryException When a customer could not been updated in the customer - * database - * @since 1.0.0 - */ - void updateCustomer(int customerId, Customer updatedCustomer) throws DatabaseQueryException - - /** - * Returns a customer given a customer specified by a customer ID - * - * @param customerId to specify and existing customer - */ - Customer getCustomer(int customerId) - - /** - * Searches for a customer in a database and returns its id - * - * @param customer The customer that needs to be searched in the database - * @return an optional containing the customer if found - */ - Optional findCustomer(Customer customer) - - /** - * Updates affiliations of a customer specified by a customer ID. - * - * @param customerId to specify the customer whose affiliations should be updated - * @param affiliations that the customer should be associated to - */ - void updateCustomerAffiliations(int customerId, List affiliations) throws DatabaseQueryException -} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerInput.groovy deleted file mode 100644 index be155f299..000000000 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerInput.groovy +++ /dev/null @@ -1,29 +0,0 @@ -package life.qbic.business.customers.create - -import life.qbic.datamodel.dtos.business.Customer - - -/** - * Input interface for the {@link CreateCustomer} use case - * - * This interface describes the methods the use case exposes to its caller. - * - * @since: 1.0.0 - */ -interface CreateCustomerInput { - - /** - * Creates a new {@link Customer} for the customer database - * - * @param customer which should be added to the database - */ - void createCustomer(Customer customer) - - /** - * Updates the entry of an customer. Fundamental changes of the customer like their email address will lead to - * creating a new customer and deactivating the old entry - * @param oldCustomer The customer that needs to be updated - * @param newCustomer The customer with the updated information - */ - void updateCustomer(Customer oldCustomer, Customer newCustomer) -} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomer.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomer.groovy deleted file mode 100644 index 8cc098a41..000000000 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomer.groovy +++ /dev/null @@ -1,35 +0,0 @@ -package life.qbic.business.customers.search - -import life.qbic.datamodel.dtos.business.Customer - -/** - * A use case which describes how a customer is searched in the database - * - * A customer can be searched by its first and last name. The user gets a list with all persons matching the search. - * - * @since: 1.0.0 - * - */ -class SearchCustomer implements SearchCustomerInput{ - SearchCustomerDataSource dataSource - SearchCustomerOutput output - - SearchCustomer(SearchCustomerOutput output, SearchCustomerDataSource dataSource){ - this.output = output - this.dataSource = dataSource - } - - @Override - void searchCustomer(String firstName, String lastName) { - try { - List foundCustomer = dataSource.findCustomer(firstName, lastName) - if (foundCustomer.isEmpty()) { - output.failNotification("Could not find a customer for $firstName $lastName") - } else { - output.successNotification(foundCustomer) - } - } catch (Exception ignored) { - output.failNotification("Unexpected error when searching for the customer $firstName $lastName") - } - } -} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomerOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomerOutput.groovy deleted file mode 100644 index b1e418428..000000000 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomerOutput.groovy +++ /dev/null @@ -1,24 +0,0 @@ -package life.qbic.business.customers.search - -import life.qbic.datamodel.dtos.business.Customer -import life.qbic.business.UseCaseFailure - -/** - * Output interface for the {@link SearchCustomer} use - * case. - * - * @since: 1.0.0 - * - */ -interface SearchCustomerOutput extends UseCaseFailure { - - /** - * This method is called by the use case on success. - * - * It passes the search result for a given search query. - * - * @param foundCustomers A list of {@link Customer}. - * @since 1.0.0 - */ - void successNotification(List foundCustomers) -} \ No newline at end of file diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/update/UpdateCustomer.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/customers/update/UpdateCustomer.groovy deleted file mode 100644 index 9fd693f45..000000000 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/update/UpdateCustomer.groovy +++ /dev/null @@ -1,63 +0,0 @@ -package life.qbic.business.customers.update - -import life.qbic.business.customers.create.CreateCustomerDataSource -import life.qbic.business.customers.create.CreateCustomerOutput -import life.qbic.business.logging.Logger -import life.qbic.business.logging.Logging -import life.qbic.datamodel.dtos.business.Customer - -import life.qbic.business.exceptions.DatabaseQueryException - -/** - * This use case updates an existing customer in the system. New Affiliations of the customer are added to the respective table. - * If other changes are made to the customer, a new customer is created in the system and the old customer is set to inactive. - * - * @since: 1.0.0 - */ -class UpdateCustomer { - - private static final Logging log = Logger.getLogger(UpdateCustomer) - - private CreateCustomerDataSource dataSource - private UpdateCustomerOutput output - - UpdateCustomer(UpdateCustomerOutput output, CreateCustomerDataSource dataSource){ - this.output = output - this.dataSource = dataSource - } - - void updateCustomer(int customerId, Customer customer) { - Customer existingCustomer = dataSource.getCustomer(customerId) - boolean customerChanged = hasBasicCustomerDataChanged(existingCustomer, customer) - try { - if(customerChanged) { - dataSource.updateCustomer(customerId, customer) - } else { - dataSource.updateCustomerAffiliations(customerId, customer.affiliations) - } - //this exception catching is important to avoid displaying a wrong failure notification - try { - output.customerUpdated(customer) - } catch (Exception e) { - log.error(e.message) - log.error(e.stackTrace.join("\n")) - } - } catch(DatabaseQueryException databaseQueryException){ - output.failNotification(databaseQueryException.message) - } catch(Exception unexpected) { - log.error(unexpected.message) - log.error(unexpected.stackTrace.join("\n")) - output.failNotification("Could not update customer.") - } - } - - // determines if customer properties other than affiliations have changed - private static boolean hasBasicCustomerDataChanged(Customer existingCustomer, Customer newCustomer) { - boolean noFundamentalChange = existingCustomer.firstName.equals(newCustomer.firstName) - && existingCustomer.lastName.equals(newCustomer.lastName) - && existingCustomer.emailAddress.equals(newCustomer.emailAddress) - && existingCustomer.title.equals(newCustomer.title) - - return !noFundamentalChange - } -} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/logging/Logger.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/logging/Logger.groovy index d9d7f94b2..832392095 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/logging/Logger.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/logging/Logger.groovy @@ -37,20 +37,40 @@ class Logger { this.logger.info(message) } + @Override + void info(String message, Throwable cause) { + this.logger.info(message, cause) + } + @Override void warn(String message) { this.logger.warn(message) } + @Override + void warn(String message, Throwable cause) { + this.logger.warn(message, cause) + } + @Override void error(String message) { this.logger.error(message) } + @Override + void error(String message, Throwable cause) { + this.logger.error(message, cause) + } + @Override void debug(String message) { this.logger.debug(message) } + + @Override + void debug(String message, Throwable cause) { + this.logger.debug(message, cause) + } } } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/logging/Logging.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/logging/Logging.groovy index d809c9019..441e2fbbf 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/logging/Logging.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/logging/Logging.groovy @@ -12,26 +12,63 @@ interface Logging { /** * Logs a common information event. - * @param message + * @param message the message string to log + * @since 1.0.0 */ void info(String message) + /** + * Logs a message at the INFO level including the stack trace of the Throwable cause passed as parameter. + * @param message the message object to log. + * @cause the exception to log, including its stack trace + */ + void info(String message, Throwable cause) + /** * Logs a warn event, that does not indicate a runtime exception, but still might be * important to report. * @param message + * @since 1.0.0 */ void warn(String message) + /** + * Logs a warn event, that does not indicate a runtime exception, but still might be + * important to report. + * @param message the message object to log. + * @cause the exception to log, including its stack trace + * @since 1.0.0 + */ + void warn(String message, Throwable cause) + /** * Logs a error or exception during the application execution. * @param message + * @since 1.0.0 */ void error(String message) + /** + * Logs a error or exception during the application execution. + * @param message + * @param message the message object to log. + * @cause the exception to log, including its stack trace + * @since 1.0.0 + */ + void error(String message, Throwable cause) + /** * Logs runtime information that are useful for debugging. * @param message + * @since 1.0.0 */ void debug(String message) + + /** + * Logs runtime information that are useful for debugging. + * @param message the message object to log. + * @cause the exception to log, including its stack trace + * @since 1.0.0 + */ + void debug(String message, Throwable cause) } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Converter.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Converter.groovy index 10cbf3bf4..3e0a29793 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Converter.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Converter.groovy @@ -39,6 +39,11 @@ class Converter { .modificationDate(offer.modificationDate) .expirationDate(offer.expirationDate) .checksum(offer.checksum()) + .itemsWithOverhead(offer.overheadItems) + .itemsWithoutOverhead(offer.noOverheadItems) + .itemsWithOverheadNet(offer.overheadItemsNet) + .itemsWithoutOverheadNet(offer.noOverheadItemsNet) + .overheadRatio(offer.overheadRatio) .build() } static Offer buildOfferForCostCalculation(List items, diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy index e1aa37b3f..5686fc735 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy @@ -10,8 +10,11 @@ import life.qbic.datamodel.dtos.business.Customer import life.qbic.datamodel.dtos.business.ProductItem import life.qbic.datamodel.dtos.business.ProjectManager import life.qbic.datamodel.dtos.business.services.DataStorage +import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis import life.qbic.datamodel.dtos.business.services.ProjectManagement import life.qbic.business.offers.identifier.OfferId +import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis +import life.qbic.datamodel.dtos.business.services.Sequencing import java.nio.charset.StandardCharsets import java.security.MessageDigest @@ -67,14 +70,37 @@ class Offer { * The affiliation of the customer selected for this offer */ private Affiliation selectedCustomerAffiliation + /** + * A list of items for which an overhead cost is applicable + */ + private List itemsWithOverhead + /** + * A list of items for which an overhead cost is not applicable + */ + private List itemsWithoutOverhead + /** + * The net price of all items for which an overhead cost is applicable, without overhead and taxes + */ + private double itemsWithOverheadNetPrice + /** + * The net price of all items for which an overhead cost is not applicable, without overhead and taxes + */ + private double itemsWithoutOverheadNetPrice - /* - * Holds the determined overhead derived from the + /** + * Holds the determined overhead total derived from the * customer's affiliation. */ private double overhead - /* + /** + * The overhead ratio that is applied to calculate the total offer price. The ratio is chosen + * based on the customer's affiliation. + * e.g. 0.4 or a 40% markup for external customers + */ + private double overheadRatio + + /** * Holds the current VAT rate */ private static final double VAT = 0.19 @@ -96,6 +122,7 @@ class Offer { OfferId identifier Affiliation selectedCustomerAffiliation List availableVersions + double overheadRatio Builder(Customer customer, ProjectManager projectManager, String projectTitle, String projectObjective, List items, Affiliation selectedCustomerAffiliation) { this.customer = Objects.requireNonNull(customer, "Customer must not be null") @@ -126,6 +153,12 @@ class Offer { return this } + Builder overheadRatio(double overheadRatio){ + this.overheadRatio = overheadRatio + return this + } + + Offer build() { return new Offer(this) } @@ -147,6 +180,12 @@ class Offer { .stream() .map(id -> new OfferId(id)).collect() this.availableVersions.add(this.identifier) + this.itemsWithOverhead = getOverheadItems() + this.itemsWithoutOverhead = getNoOverheadItems() + this.itemsWithOverheadNetPrice = getOverheadItemsNet() + this.itemsWithoutOverheadNetPrice = getNoOverheadItemsNet() + this.overheadRatio = determineOverhead() + } /** @@ -179,16 +218,47 @@ class Offer { */ double getOverheadSum() { double overheadSum = 0 - for (ProductItem item : items) { - if (item.product instanceof DataStorage || item.product instanceof ProjectManagement) { - // No overheads are assigned for data storage and project management - } else { - overheadSum += item.quantity * item.product.unitPrice * this.overhead + items.each { + // No overheads are assigned for data storage and project management + if (it.product instanceof PrimaryAnalysis || it.product instanceof SecondaryAnalysis || it.product instanceof Sequencing) { + overheadSum += it.quantity * it.product.unitPrice * this.overhead } } return overheadSum } + + /** + * This method returns the net cost of all product items for which no overhead cost is calculated + * + * @return net cost of product items without overhead cost + */ + double getNoOverheadItemsNet() { + double costNoOverheadItemsNet = 0 + items.each { + // No overheads are assigned for data storage and project management + if (it.product instanceof DataStorage || it.product instanceof ProjectManagement) { + costNoOverheadItemsNet += it.quantity * it.product.unitPrice + } + } + return costNoOverheadItemsNet + } + + /** + * This method returns the net cost of product items for which an overhead cost is calculated + * + * @return net cost of product items with overhead cost + */ + double getOverheadItemsNet() { + double costOverheadItemsNet = 0 + items.each { + if (it.product instanceof PrimaryAnalysis || it.product instanceof SecondaryAnalysis || it.product instanceof Sequencing) { + costOverheadItemsNet += it.quantity * it.product.unitPrice + } + } + return costOverheadItemsNet + } + /** * The tax price on all items net price including overheads. * @@ -203,6 +273,36 @@ class Offer { return (calculateNetPrice() + getOverheadSum()) * VAT } + /** + * This method returns all ProductItems for which an Overhead cost is calculated + * + * @return ProductItem list containing all ProductItems with overhead cost + */ + List getOverheadItems() { + List listOverheadProductItem = [] + items.each { + if (it.product instanceof PrimaryAnalysis || it.product instanceof SecondaryAnalysis || it.product instanceof Sequencing) { + listOverheadProductItem.add(it) + } + } + return listOverheadProductItem + } + + /** + * This method returns all ProductItems for which no Overhead cost is calculated + * + * @return ProductItem list containing all ProductItems without overhead cost + */ + List getNoOverheadItems(){ + List listNoOverheadProductItem = [] + items.each { + if (it.product instanceof DataStorage || it.product instanceof ProjectManagement) { + listNoOverheadProductItem.add(it) + } + } + return listNoOverheadProductItem + } + Date getModificationDate() { return creationDate } @@ -239,6 +339,10 @@ class Offer { return selectedCustomerAffiliation } + double getOverheadRatio() { + return overheadRatio + } + /** * Returns a deep copy of all available offer versions. * diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/Country.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/Country.groovy similarity index 86% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/Country.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/Country.groovy index 007d9eea4..143880eba 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/Country.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/Country.groovy @@ -1,8 +1,4 @@ -package life.qbic.business.customers.affiliation - -import org.apache.tools.ant.taskdefs.Local - - +package life.qbic.business.persons.affiliation /** * Lists all countries based on the iso standard * diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliation.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliation.groovy similarity index 96% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliation.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliation.groovy index 72e1657fe..62c86e809 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliation.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliation.groovy @@ -1,4 +1,4 @@ -package life.qbic.business.customers.affiliation.create +package life.qbic.business.persons.affiliation.create import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.business.exceptions.DatabaseQueryException diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliationDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliationDataSource.groovy similarity index 91% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliationDataSource.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliationDataSource.groovy index ad8bc6652..d8c5efaca 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliationDataSource.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliationDataSource.groovy @@ -1,4 +1,4 @@ -package life.qbic.business.customers.affiliation.create +package life.qbic.business.persons.affiliation.create import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.business.exceptions.DatabaseQueryException diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliationInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliationInput.groovy similarity index 88% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliationInput.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliationInput.groovy index 8518e8bfb..91ab6638d 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliationInput.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliationInput.groovy @@ -1,4 +1,4 @@ -package life.qbic.business.customers.affiliation.create +package life.qbic.business.persons.affiliation.create import life.qbic.datamodel.dtos.business.Affiliation diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliationOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliationOutput.groovy similarity index 89% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliationOutput.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliationOutput.groovy index 70b744995..14bd39e56 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/create/CreateAffiliationOutput.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/create/CreateAffiliationOutput.groovy @@ -1,4 +1,4 @@ -package life.qbic.business.customers.affiliation.create +package life.qbic.business.persons.affiliation.create import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.business.UseCaseFailure diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliations.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliations.groovy similarity index 95% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliations.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliations.groovy index f15187074..47a1d27f3 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliations.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliations.groovy @@ -1,4 +1,4 @@ -package life.qbic.business.customers.affiliation.list +package life.qbic.business.persons.affiliation.list /** diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliationsDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliationsDataSource.groovy similarity index 87% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliationsDataSource.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliationsDataSource.groovy index 92d13d5da..8890c965c 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliationsDataSource.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliationsDataSource.groovy @@ -1,4 +1,4 @@ -package life.qbic.business.customers.affiliation.list +package life.qbic.business.persons.affiliation.list import life.qbic.datamodel.dtos.business.Affiliation diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliationsInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliationsInput.groovy similarity index 84% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliationsInput.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliationsInput.groovy index bfbbf8231..28910cdd7 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliationsInput.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliationsInput.groovy @@ -1,4 +1,4 @@ -package life.qbic.business.customers.affiliation.list +package life.qbic.business.persons.affiliation.list /** * The input interface for the List Affiliations use case. diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliationsOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliationsOutput.groovy similarity index 88% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliationsOutput.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliationsOutput.groovy index 80ec897b5..a926d4203 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/affiliation/list/ListAffiliationsOutput.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/affiliation/list/ListAffiliationsOutput.groovy @@ -1,4 +1,4 @@ -package life.qbic.business.customers.affiliation.list +package life.qbic.business.persons.affiliation.list import life.qbic.datamodel.dtos.business.Affiliation diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePerson.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePerson.groovy new file mode 100644 index 000000000..0651f0be6 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePerson.groovy @@ -0,0 +1,67 @@ +package life.qbic.business.persons.create + +import life.qbic.business.persons.update.UpdatePerson +import life.qbic.business.persons.update.UpdatePersonOutput +import life.qbic.business.logging.Logger +import life.qbic.business.logging.Logging +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.general.Person + +/** + * This use case creates a customer in the system + * + * Information on persons such as affiliation and names can be added to the user database. + * + * @since: 1.0.0 + */ +class CreatePerson implements CreatePersonInput, UpdatePersonOutput { + + private CreatePersonDataSource dataSource + private CreatePersonOutput output + private UpdatePerson updatePerson + + private final Logging log = Logger.getLogger(CreatePerson.class) + + + CreatePerson(CreatePersonOutput output, CreatePersonDataSource dataSource){ + this.output = output + this.dataSource = dataSource + this.updatePerson = new UpdatePerson(this,dataSource) + } + + @Override + void createPerson(Person person) { + try { + dataSource.addPerson(person) + try { + output.personCreated(person) + } catch (Exception ignored) { + log.error(ignored.stackTrace.toString()) + } + } catch(DatabaseQueryException databaseQueryException){ + output.failNotification(databaseQueryException.message) + } catch(Exception unexpected) { + println "-------------------------" + println "Unexpected Exception ...." + println unexpected.message + println unexpected.stackTrace.join("\n") + output.failNotification("Could not create new person") + } + } + + @Override + void updatePerson(Person oldPerson, Person newPerson) { + int personId = dataSource.findPerson(oldPerson).get() + updatePerson.updatePerson(personId,newPerson) + } + + @Override + void personUpdated(Person person) { + output.personCreated(person) + } + + @Override + void failNotification(String notification) { + output.failNotification(notification) + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonDataSource.groovy new file mode 100644 index 000000000..f482735d0 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonDataSource.groovy @@ -0,0 +1,61 @@ +package life.qbic.business.persons.create + +import life.qbic.datamodel.dtos.business.Affiliation +import life.qbic.datamodel.dtos.business.Customer +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.general.CommonPerson +import life.qbic.datamodel.dtos.general.Person + +/** + * Creates a person in the database for the CreatePerson use case + * + * This interface should be used to allow the use case to forward data to a data source that implements this use case + * and still follow the correct dependency flow from infrastructure to the domain logic + * + * @since: 1.0.0 + */ +interface CreatePersonDataSource { + + /** + * Adds a person to the user database + * + * @param person a person to be added to known persons + * @throws DatabaseQueryException When a person could not been added to the person database + * @since 1.0.0 + */ + void addPerson(Person person) throws DatabaseQueryException + + /** + * Updates the information of a given person specified by a person ID + * + * @param personId to specify the person to be updated + * @param updatedPerson person containing all information and the updates of a person + * @throws DatabaseQueryException When a person could not been updated in the person + * database + * @since 1.0.0 + */ + void updatePerson(int personId, Person updatedPerson) throws DatabaseQueryException + + /** + * Returns a person given a person specified by a person ID + * + * @param personId to specify and existing customer + */ + CommonPerson getPerson(int personId) + + /** + * Searches for a person in a database and returns its id + * + * @param person The person that needs to be searched in the database + * @return an optional containing the person if found + */ + Optional findPerson(Person person) + + /** + * Updates affiliations of a person specified by a customer ID. + * + * @param personId to specify the person whose affiliations should be updated + * @param affiliations that the person should be associated to + */ + void updatePersonAffiliations(int personId, List affiliations) throws DatabaseQueryException +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonInput.groovy new file mode 100644 index 000000000..20f0508c0 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonInput.groovy @@ -0,0 +1,29 @@ +package life.qbic.business.persons.create + +import life.qbic.datamodel.dtos.general.Person + + +/** + * Input interface for the {@link CreatePerson} use case + * + * This interface describes the methods the use case exposes to its caller. + * + * @since: 1.0.0 + */ +interface CreatePersonInput { + + /** + * Creates a new {@link Person} for the customer database + * + * @param customer which should be added to the database + */ + void createPerson(Person person) + + /** + * Updates the entry of a person. Fundamental changes of the person like their email address will lead to + * creating a new person and deactivating the old entry + * @param oldPerson The person that needs to be updated + * @param newPerson The person with the updated information + */ + void updatePerson(Person oldPerson, Person newPerson) +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonOutput.groovy similarity index 68% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerOutput.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonOutput.groovy index 5008eb8b6..3741735c8 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerOutput.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonOutput.groovy @@ -1,16 +1,16 @@ -package life.qbic.business.customers.create +package life.qbic.business.persons.create import life.qbic.business.UseCaseFailure import life.qbic.datamodel.dtos.general.Person /** - * Output interface for the {@link CreateCustomer} use + * Output interface for the {@link CreatePerson} use * case * * @since: 1.0.0 * @author: Tobias Koch */ -interface CreateCustomerOutput extends UseCaseFailure { +interface CreatePersonOutput extends UseCaseFailure { /** * Is called by the use case, when a new customer has been created @@ -18,12 +18,12 @@ interface CreateCustomerOutput extends UseCaseFailure { * @deprecated Use the more explicit #customerCreated(Person person) method */ @Deprecated - void customerCreated(String message) + void personCreated(String message) /** * Is called by the use case, when a new customer resource has been created * @param person The newly created person resource */ - void customerCreated(Person person) + void personCreated(Person person) } \ No newline at end of file diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPerson.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPerson.groovy new file mode 100644 index 000000000..e8acfd5b6 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPerson.groovy @@ -0,0 +1,35 @@ +package life.qbic.business.persons.search + +import life.qbic.datamodel.dtos.general.Person + +/** + * A use case which describes how a customer is searched in the database + * + * A customer can be searched by its first and last name. The user gets a list with all persons matching the search. + * + * @since: 1.0.0 + * + */ +class SearchPerson implements SearchPersonInput{ + SearchPersonDataSource dataSource + SearchPersonOutput output + + SearchPerson(SearchPersonOutput output, SearchPersonDataSource dataSource){ + this.output = output + this.dataSource = dataSource + } + + @Override + void searchPerson(String firstName, String lastName) { + try { + List foundCustomer = dataSource.findPerson(firstName, lastName) + if (foundCustomer.isEmpty()) { + output.failNotification("Could not find a person for $firstName $lastName") + } else { + output.successNotification(foundCustomer) + } + } catch (Exception ignored) { + output.failNotification("Unexpected error when searching for the person $firstName $lastName") + } + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomerDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPersonDataSource.groovy similarity index 54% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomerDataSource.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPersonDataSource.groovy index a20262101..58efae2fd 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomerDataSource.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPersonDataSource.groovy @@ -1,7 +1,7 @@ -package life.qbic.business.customers.search +package life.qbic.business.persons.search -import life.qbic.datamodel.dtos.business.Customer import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.general.Person /** * Retrieves data for the SearchCustomer use case @@ -12,32 +12,32 @@ import life.qbic.business.exceptions.DatabaseQueryException * @since: 1.0.0 * */ -interface SearchCustomerDataSource { +interface SearchPersonDataSource { /** * This method returns a customer matching the given search criteria * - * @param firstName The customer's first name - * @param lastName The customer's last name + * @param firstName The person's first name + * @param lastName The person's last name * @return A list of matching customer entries with the given first and last name. * @throws DatabaseQueryException If the data source query fails for technical reasons, this * exception is thrown. * * @since 1.0.0 */ - List findCustomer(String firstName, String lastName) throws DatabaseQueryException + List findPerson(String firstName, String lastName) throws DatabaseQueryException /** - * This method returns a customer matching the given search criteria only if it is set to active + * This method returns a person matching the given search criteria only if it is set to active * - * @param firstName The customer's first name - * @param lastName The customer's last name - * @return A list of matching customer entries with the given first and last name that are set to active + * @param firstName The person's first name + * @param lastName The person's last name + * @return A list of matching person entries with the given first and last name that are set to active * @throws DatabaseQueryException If the data source query fails for technical reasons, this * exception is thrown. * * @since 1.0.0 */ - List findActiveCustomer(String firstName, String lastName) throws DatabaseQueryException + List findActivePerson(String firstName, String lastName) throws DatabaseQueryException } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomerInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPersonInput.groovy similarity index 64% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomerInput.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPersonInput.groovy index c7d6ea0f0..6e1c44230 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomerInput.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPersonInput.groovy @@ -1,14 +1,14 @@ -package life.qbic.business.customers.search +package life.qbic.business.persons.search /** - * Input interface for the {@link SearchCustomer} use case + * Input interface for the {@link SearchPerson} use case * * This interface describes the methods the use case exposes to its caller. * * @since: 1.0.0 * */ -interface SearchCustomerInput { +interface SearchPersonInput { /** * This method triggers the search for a customer with matching firstname and lastname * @@ -16,5 +16,5 @@ interface SearchCustomerInput { * @param lastName: The last name of the customer * @since: 1.0.0 */ - void searchCustomer(String firstName, String lastName) + void searchPerson(String firstName, String lastName) } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPersonOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPersonOutput.groovy new file mode 100644 index 000000000..7ca9508cd --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPersonOutput.groovy @@ -0,0 +1,24 @@ +package life.qbic.business.persons.search + +import life.qbic.business.UseCaseFailure +import life.qbic.datamodel.dtos.general.Person + +/** + * Output interface for the {@link SearchPerson} use + * case. + * + * @since: 1.0.0 + * + */ +interface SearchPersonOutput extends UseCaseFailure { + + /** + * This method is called by the use case on success. + * + * It passes the search result for a given search query. + * + * @param foundPerson A list of {@link Person}. + * @since 1.0.0 + */ + void successNotification(List foundCustomers) +} \ No newline at end of file diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/persons/update/UpdatePerson.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/update/UpdatePerson.groovy new file mode 100644 index 000000000..3e75d521b --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/update/UpdatePerson.groovy @@ -0,0 +1,61 @@ +package life.qbic.business.persons.update + +import life.qbic.business.persons.create.CreatePersonDataSource +import life.qbic.business.logging.Logger +import life.qbic.business.logging.Logging +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.general.Person + +/** + * This use case updates an existing customer in the system. New Affiliations of the customer are added to the respective table. + * If other changes are made to the customer, a new customer is created in the system and the old customer is set to inactive. + * + * @since: 1.0.0 + */ +class UpdatePerson { + + private static final Logging log = Logger.getLogger(UpdatePerson) + + private CreatePersonDataSource dataSource + private UpdatePersonOutput output + + UpdatePerson(UpdatePersonOutput output, CreatePersonDataSource dataSource){ + this.output = output + this.dataSource = dataSource + } + + void updatePerson(int personId, Person person) { + Person existingCustomer = dataSource.getPerson(personId) + boolean customerChanged = hasBasicPersonDataChanged(existingCustomer, person) + try { + if(customerChanged) { + dataSource.updatePerson(personId, person) + } else { + dataSource.updatePersonAffiliations(personId, person.affiliations) + } + //this exception catching is important to avoid displaying a wrong failure notification + try { + output.personUpdated(person) + } catch (Exception e) { + log.error(e.message) + log.error(e.stackTrace.join("\n")) + } + } catch(DatabaseQueryException databaseQueryException){ + output.failNotification(databaseQueryException.message) + } catch(Exception unexpected) { + log.error(unexpected.message) + log.error(unexpected.stackTrace.join("\n")) + output.failNotification("Could not update person.") + } + } + + // determines if customer properties other than affiliations have changed + private static boolean hasBasicPersonDataChanged(Person existingPerson, Person newPerson) { + boolean noFundamentalChange = existingPerson.firstName.equals(newPerson.firstName) + && existingPerson.lastName.equals(newPerson.lastName) + && existingPerson.emailAddress.equals(newPerson.emailAddress) + && existingPerson.title.equals(newPerson.title) + + return !noFundamentalChange + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/update/UpdateCustomerOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/update/UpdatePersonOutput.groovy similarity index 54% rename from offer-manager-domain/src/main/groovy/life/qbic/business/customers/update/UpdateCustomerOutput.groovy rename to offer-manager-domain/src/main/groovy/life/qbic/business/persons/update/UpdatePersonOutput.groovy index 970505d82..bc22c0535 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/customers/update/UpdateCustomerOutput.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/persons/update/UpdatePersonOutput.groovy @@ -1,19 +1,19 @@ -package life.qbic.business.customers.update +package life.qbic.business.persons.update import life.qbic.business.UseCaseFailure import life.qbic.datamodel.dtos.general.Person /** - * Output interface for the {@link life.qbic.business.customers.update.UpdateCustomer} use + * Output interface for the {@link UpdatePerson} use * case * * @since: 1.0.0 */ -interface UpdateCustomerOutput extends UseCaseFailure { +interface UpdatePersonOutput extends UseCaseFailure { /** * Is called by the use case, when a customer resource has been updated * @param person The updated created person resource */ - void customerUpdated(Person person) + void personUpdated(Person person) } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/ProductDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/ProductDataSource.groovy new file mode 100644 index 000000000..b0d47ee73 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/ProductDataSource.groovy @@ -0,0 +1,39 @@ +package life.qbic.business.products + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.Product + +/** + * Defines the methods of the Datasource implementation + * + * @since: 1.0.0 + * + */ +interface ProductDataSource { + + /** + * Fetches a product from the database + * @param productId The product id of the product to be fetched + * @return returns an optional that contains the product if it has been found + * @since 1.0.0 + * @throws DatabaseQueryException + */ + Optional fetch(ProductId productId) throws DatabaseQueryException + + /** + * Stores a product in the database + * @param product The product that needs to be stored + * @since 1.0.0 + * @throws DatabaseQueryException + */ + void store(Product product) throws DatabaseQueryException + + /** + * A product is archived by setting it inactive + * @param product The product that needs to be archived + * @since 1.0.0 + * @throws DatabaseQueryException + */ + void archive(Product product) throws DatabaseQueryException +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProduct.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProduct.groovy new file mode 100644 index 000000000..1e5feb5d5 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProduct.groovy @@ -0,0 +1,42 @@ +package life.qbic.business.products.archive + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.Product + +/** + *

4.3.2 Archive Service Product

+ *
+ *

Offer Administrators are allowed to archive existing products. + *
The archived products should be still available in old offers but not selectable for new offers. + *

+ * + * @since: 1.0.0 + * + */ +class ArchiveProduct implements ArchiveProductInput { + + private final ArchiveProductDataSource dataSource + private final ArchiveProductOutput output + + ArchiveProduct(ArchiveProductDataSource dataSource, ArchiveProductOutput output) { + this.dataSource = dataSource + this.output = output + } + + @Override + void archive(ProductId productId) { + try { + Optional searchResult = this.dataSource.fetch(productId) + if (searchResult.isPresent()) { + dataSource.archive(searchResult.get()) + output.archived(searchResult.get()) + } else { + output.failNotification("Could not find a product with identifier ${productId.toString()}") + } + } catch (DatabaseQueryException ignored) { + output.failNotification("Could not archive product ${productId.toString()}") + } + + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductDataSource.groovy new file mode 100644 index 000000000..8e81e3e25 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductDataSource.groovy @@ -0,0 +1,31 @@ +package life.qbic.business.products.archive + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.Product + +/** + *

Data souce interface for {@link life.qbic.business.products.archive.ArchiveProduct}

+ * + * @since 1.0.0 + */ +interface ArchiveProductDataSource { + + /** + * A product is archived by setting it inactive + * @param product The product that needs to be archived + * @since 1.0.0 + * @throws life.qbic.business.exceptions.DatabaseQueryException + */ + void archive(Product product) throws DatabaseQueryException + + /** + * Fetches a product from the database + * @param productId The product id of the product to be fetched + * @return returns an optional that contains the product if it has been found + * @since 1.0.0 + * @throws life.qbic.business.exceptions.DatabaseQueryException is thrown when any technical interaction with the data source fails + */ + Optional fetch(ProductId productId) throws DatabaseQueryException + +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductInput.groovy new file mode 100644 index 000000000..b03fd3749 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductInput.groovy @@ -0,0 +1,20 @@ +package life.qbic.business.products.archive + +import life.qbic.datamodel.dtos.business.ProductId + +/** + * Input interface for the {@link ArchiveProduct} use case + * + * @since: 1.0.0 + * + */ +interface ArchiveProductInput { + + /** + * A product defined by its product id should be archived + * @param productId the product id for the product that will be archived + * @since 1.0.0 + */ + void archive(ProductId productId) + +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductOutput.groovy new file mode 100644 index 000000000..d1ce1730f --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductOutput.groovy @@ -0,0 +1,20 @@ +package life.qbic.business.products.archive + +import life.qbic.business.UseCaseFailure +import life.qbic.datamodel.dtos.business.services.Product + +/** + * Output interface for the {@link ArchiveProduct} use case + * + * @since: 1.0.0 + * + */ +interface ArchiveProductOutput extends UseCaseFailure { + + /** + * A product has been archived in the database + * @param product The product that has been archived + * @since 1.0.0 + */ + void archived(Product product) +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy new file mode 100644 index 000000000..f16f4682c --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy @@ -0,0 +1,41 @@ +package life.qbic.business.products.create + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.business.logging.Logger +import life.qbic.business.logging.Logging +import life.qbic.datamodel.dtos.business.services.Product + +/** + *

4.3.0 Create Service Product

+ *
+ *

When the service portfolio changed due to a business decision an Offer Administrator should be allowed to provide information on the new service offered and make it available to new offers upon creation. + *

+ * + * @since: 1.0.0 + + * + */ +class CreateProduct implements CreateProductInput { + private final CreateProductDataSource dataSource + private final CreateProductOutput output + private static final Logging log = Logger.getLogger(this.class) + + CreateProduct(CreateProductDataSource dataSource, CreateProductOutput output) { + this.dataSource = dataSource + this.output = output + } + + @Override + void create(Product product) { + try { + dataSource.store(product) + output.created(product) + } catch(DatabaseQueryException databaseQueryException) { + log.error("Product creation failed", databaseQueryException) + output.failNotification("Could not create product $product.productName with id $product.productId") + } catch(ProductExistsException productExistsException) { + log.warn("Product \"$product.productName\" already existed.", productExistsException) + output.foundDuplicate(product) + } + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy new file mode 100644 index 000000000..6ce0fa435 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy @@ -0,0 +1,22 @@ +package life.qbic.business.products.create + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.Product + +/** + *

Data source for {@link life.qbic.business.products.create.CreateProduct}

+ * + * @since 1.0.0 + */ +interface CreateProductDataSource { + + /** + * Stores a product in the database + * @param product The product that needs to be stored + * @since 1.0.0 + * @throws DatabaseQueryException if any technical interaction with the data source fails + * @throws ProductExistsException if the product already exists in the data source + */ + void store(Product product) throws DatabaseQueryException, ProductExistsException +} \ No newline at end of file diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductInput.groovy new file mode 100644 index 000000000..c28045753 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductInput.groovy @@ -0,0 +1,20 @@ +package life.qbic.business.products.create + +import life.qbic.datamodel.dtos.business.services.Product + +/** + * Input interface for the {@link CreateProduct} use case + * + * @since: 1.0.0 + * + */ +interface CreateProductInput { + + /** + * A product is created in the database + * @param product The product that is added to the database + * @since 1.0.0 + */ + void create(Product product) + +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductOutput.groovy new file mode 100644 index 000000000..df65a1940 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductOutput.groovy @@ -0,0 +1,29 @@ +package life.qbic.business.products.create + +import life.qbic.business.UseCaseFailure +import life.qbic.datamodel.dtos.business.services.Product + +/** + * Output interface for the {@link CreateProduct} use case + * + * @since: 1.0.0 + * + */ +interface CreateProductOutput extends UseCaseFailure{ + + /** + * A product has been created in the database + * @param product The product that has been created + * @since 1.0.0 + */ + void created(Product product) + + /** + * The product is already stored in the database + * @param product The product for which a duplicate has been found + * @since 1.0.0 + */ + void foundDuplicate(Product product) + + +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/ProductExistsException.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/ProductExistsException.groovy new file mode 100644 index 000000000..f8d3bb317 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/ProductExistsException.groovy @@ -0,0 +1,27 @@ +package life.qbic.business.products.create +import life.qbic.datamodel.dtos.business.ProductId + +/** + *

Signals that an attempt to store a product has failed

+ *

This exception will be thrown by the {@link CreateProductDataSource} when a product already exists.

+ * + * @since 1.0.0 + */ +class ProductExistsException extends RuntimeException { + + ProductExistsException(ProductId productId) { + super() + } + + ProductExistsException(ProductId productId, String message) { + super(message) + } + + ProductExistsException(ProductId productId, String message, Throwable cause) { + super(message, cause) + } + + ProductExistsException(ProductId productId, Throwable cause) { + super(cause) + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProject.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProject.groovy new file mode 100644 index 000000000..a32a54cd1 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProject.groovy @@ -0,0 +1,80 @@ +package life.qbic.business.projects.create + +import life.qbic.business.Constants +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.business.logging.Logger +import life.qbic.business.logging.Logging +import life.qbic.business.projects.spaces.CreateProjectSpace +import life.qbic.business.projects.spaces.CreateProjectSpaceDataSource +import life.qbic.business.projects.spaces.CreateProjectSpaceOutput +import life.qbic.datamodel.dtos.projectmanagement.Project +import life.qbic.datamodel.dtos.business.ProjectApplication +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace + +/** + *

A new project is created and linked to an offer

+ *
+ *

A new project is created and stored in the database. + * If a project with the respective project code already exists the database throws an {@link life.qbic.business.projects.create.ProjectExistsException}

+ * + * @since 1.0.0 + */ +class CreateProject implements CreateProjectInput, CreateProjectSpaceOutput { + + private final CreateProjectOutput output + private final CreateProjectDataSource dataSource + + private final CreateProjectSpace createProjectSpace + private ProjectApplication projectApplication + + private final Logging log = Logger.getLogger(CreateProject.class) + + CreateProject(CreateProjectOutput output, CreateProjectDataSource dataSource, CreateProjectSpaceDataSource createProjectSpaceDataSource) { + this.output = output + this.dataSource = dataSource + + this.createProjectSpace = new CreateProjectSpace(this, createProjectSpaceDataSource) + } + + + @Override + void createProject(ProjectApplication projectApplication) { + try { + Project createdProject = dataSource.createProject(projectApplication) + output.projectCreated(createdProject) + } catch (ProjectExistsException projectExistsException) { + log.error("The project ${projectApplication.projectCode} already exists in the database.",projectExistsException) + output.projectAlreadyExists(new ProjectIdentifier(projectApplication.projectSpace, projectApplication.projectCode), projectApplication.linkedOffer) + } catch (DatabaseQueryException e) { + log.error("An error occurred in the database while creating the project ${projectApplication.projectCode}.",e) + output.failNotification("The project application for ${projectApplication.projectCode} was not successful. The project can not be stored in the database.") + } catch (Exception exception) { + log.error("An unexpected error occurred during the project creation of project ${projectApplication.projectCode}",exception) + output.failNotification("An unexpected during the project creation occurred. " + + "Please contact ${Constants.QBIC_HELPDESK_EMAIL}.") + } + } + + @Override + void createProjectWithSpace(ProjectApplication projectApplication) { + this.projectApplication = projectApplication + createProjectSpace.createProjectSpace(projectApplication.projectSpace) + } + + @Override + void projectSpaceCreated(ProjectSpace projectSpace) { + log.info "successfully created the project space ${projectSpace}" + createProject(projectApplication) + } + + @Override + void projectSpaceAlreadyExists(ProjectSpace projectSpace) { + output.failNotification("Project space ${projectSpace} already exists.") + } + + @Override + void failNotification(String notification) { + output.failNotification(notification) + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectDataSource.groovy new file mode 100644 index 000000000..aa1b82121 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectDataSource.groovy @@ -0,0 +1,34 @@ +package life.qbic.business.projects.create + + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.projectmanagement.Project +import life.qbic.datamodel.dtos.business.ProjectApplication + +/** + *

Access to the project management datasource

+ * + *

This interface collects methods to interact with the project management + * datasource in the context of the Create Project use case.

+ * + * @since 1.0.0 + */ +interface CreateProjectDataSource { + + /** + * Creates a new QBiC project in the data management platform. + * + * @param projectApplication A project application with the information necessary for the + * project registration + * + * @return Information about the created project + * + * @since 1.0.0 + * @throws ProjectExistsException If the application was denied. Reasons for denial are + * currently: + * 1. A project with the same project title already exists + * @throws DatabaseQueryException If any technical interaction with the data source fails + */ + Project createProject(ProjectApplication projectApplication) + throws ProjectExistsException, DatabaseQueryException +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectInput.groovy new file mode 100644 index 000000000..902855807 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectInput.groovy @@ -0,0 +1,34 @@ +package life.qbic.business.projects.create + +import life.qbic.datamodel.dtos.business.ProjectApplication + +/** + *

Input interface for the Create Project use case.

+ * + * @since 1.0.0 + */ +interface CreateProjectInput { + + /** + *

Creates a new project based on a {@link life.qbic.datamodel.dtos.business.ProjectApplication}

+ *
+ *

Calling this method executes the Create Project use case. + * The output will be returned via the {@link CreateProjectOutput} interface. + *

+ * @param projectApplication The project application with information about the planned project. + * @since 1.0.0 + */ + void createProject(ProjectApplication projectApplication) + + /** + *

Creates a new project and a project space based on a {@link life.qbic.datamodel.dtos.business.ProjectApplication}

+ *
+ *

Calling this method executes the Create Project and Create Project Space use case. + * The output will be returned via the {@link CreateProjectOutput} interface. + *

+ * @param projectApplication The project application with information about the planned project. + * @since 1.0.0 + */ + void createProjectWithSpace(ProjectApplication projectApplication) + +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectOutput.groovy new file mode 100644 index 000000000..cf861c3ec --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectOutput.groovy @@ -0,0 +1,40 @@ +package life.qbic.business.projects.create + +import life.qbic.business.UseCaseFailure +import life.qbic.datamodel.dtos.business.OfferId +import life.qbic.datamodel.dtos.projectmanagement.Project +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier + +/** + *

Output interface for the Create Project use case

+ * + * Provides output methods that are called by the use case Create Project. + * + * @since 1.0.0 + */ +interface CreateProjectOutput extends UseCaseFailure { + + /** + *

Called when a project has been successfully created.

+ *
+ *

This output represents the ideal use case scenario.

+ * + * @param project The project that has been created iqn QBiC's data management system. + * @since 1.0.0 + */ + void projectCreated(Project project) + + /** + *

Called when a project with a given project identifier already exists.

+ *
+ *

This reflects the scenario, when a user provided a pre-defined project code + * which already exists in the underlying data source. In this case the project cannot be + * created.

+ * @param projectIdentifier The project identifier that already exists. + * @param linkedOffer The linked offer of the already existing project. + * @since 1.0.0 + */ + void projectAlreadyExists(ProjectIdentifier projectIdentifier, + OfferId linkedOffer) + +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/ProjectExistsException.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/ProjectExistsException.groovy new file mode 100644 index 000000000..da2a973a5 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/ProjectExistsException.groovy @@ -0,0 +1,30 @@ +package life.qbic.business.projects.create + +/** + *

Exception that indicates violations during an project application process

+ * + *

This exception is supposed to be thrown, if an application request to create + * a data resource in the data management system is not possible.

+ * + * Example: A project with a given title already exists in the data source. So the method should + * throw an ProjectExistsException and not a DatabaseQueryException. + *
+ * With this, the use case can be made aware of, that it is not a technical issue during the + * execution of an SQL query for example. + * + * @since 1.0.0 + */ +class ProjectExistsException extends RuntimeException{ + + ProjectExistsException(){ + super() + } + + ProjectExistsException(String message) { + super(message) + } + + ProjectExistsException(String message, Throwable throwable){ + super(message, throwable) + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/SpaceNonExistingException.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/SpaceNonExistingException.groovy new file mode 100644 index 000000000..3429db316 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/SpaceNonExistingException.groovy @@ -0,0 +1,31 @@ +package life.qbic.business.projects.create + +/** + *

Exception that indicates violations during an project application process

+ * + *

This exception is supposed to be thrown, if an application request to create + * a data resource in the data management system is not possible.

+ * + * Example: A project with a given space cannot be created, as the space does not exist in + * the data source. So the method should throw an SpaceNonExistingException and not a + * DatabaseQueryException. + *
+ * With this, the use case can be made aware of, that it is not a technical issue during the + * execution of an SQL query for example. + * + * @since 1.0.0 + */ +class SpaceNonExistingException extends RuntimeException{ + + SpaceNonExistingException(){ + super() + } + + SpaceNonExistingException(String message) { + super(message) + } + + SpaceNonExistingException(String message, Throwable throwable){ + super(message, throwable) + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpace.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpace.groovy new file mode 100644 index 000000000..20637ba02 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpace.groovy @@ -0,0 +1,49 @@ +package life.qbic.business.projects.spaces + +import life.qbic.business.Constants +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.business.logging.Logger +import life.qbic.business.logging.Logging +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace + +/** + *

Creates the project space to store new projects in.

+ *
+ *

A project needs to be assigned to a project space. If no such project space is available a new space needs to be created.

+ * + * @since 1.0.0 + */ +class CreateProjectSpace implements CreateProjectSpaceInput{ + + private final CreateProjectSpaceOutput output + private final CreateProjectSpaceDataSource dataSource + + private final Logging log = Logger.getLogger(CreateProjectSpace.class) + + + CreateProjectSpace(CreateProjectSpaceOutput output, CreateProjectSpaceDataSource dataSource){ + this.output = output + this.dataSource = dataSource + } + + /** + * {@inheritDoc} + */ + @Override + void createProjectSpace(ProjectSpace projectSpace) { + try{ + dataSource.createProjectSpace(projectSpace) + output.projectSpaceCreated(projectSpace) + }catch(ProjectSpaceExistsException existsException){ + log.error("The project space ${projectSpace.toString()} already exists in the database.",existsException) + output.projectSpaceAlreadyExists(projectSpace) + }catch(DatabaseQueryException databaseQueryException){ + log.error("An error occurred in the database while creating the project space${projectSpace.toString()}.",databaseQueryException) + output.failNotification("The project space ${projectSpace.toString()} creation was not successful. The project space cannot be stored in the database.") + }catch(Exception exception){ + log.error("An unexpected error occurred during the project space creation ${projectSpace.toString()}",exception) + output.failNotification("An unexpected during the project creation occurred. " + + "Please contact ${Constants.QBIC_HELPDESK_EMAIL}.") + } + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceDataSource.groovy new file mode 100644 index 000000000..b3fcb44d2 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceDataSource.groovy @@ -0,0 +1,27 @@ +package life.qbic.business.projects.spaces + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace + +/** + *

Access to the project management datasource

+ * + *

This interface collects methods to interact with the project management + * datasource in the context of the Create Project Space use case.

+ * + * @since 1.0.0 + */ +interface CreateProjectSpaceDataSource { + + /** + * Creates a new space with the given name in QBiC's data management system + * + * @param projectSpace The projectspace that should be created + * @since 1.0.0 + * @throws ProjectSpaceExistsException If the project space name already exists + * @throws DatabaseQueryException If a technical issue occurs during the data source interaction + */ + void createProjectSpace(ProjectSpace projectSpace) throws ProjectSpaceExistsException, + DatabaseQueryException + +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceInput.groovy new file mode 100644 index 000000000..158fe3273 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceInput.groovy @@ -0,0 +1,23 @@ +package life.qbic.business.projects.spaces + +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace + +/** + *

Input interface for the Create Project Space use case.

+ * + * @since 1.0.0 + */ +interface CreateProjectSpaceInput { + + /** + *

Creates a new project space in QBiCs data management platform.

+ *
+ *

A space is a logical grouping of projects that have the same context. The context is + * defined by the project manager and is not a rule set in stone.

+ * + * @param The desired project space to be created + * @since 1.0.0 + */ + void createProjectSpace(ProjectSpace projectSpace) + +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceOutput.groovy new file mode 100644 index 000000000..d3ba7db74 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceOutput.groovy @@ -0,0 +1,35 @@ +package life.qbic.business.projects.spaces + +import life.qbic.business.UseCaseFailure +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace + +/** + *

Describes the output interface of the Create Project Space use case.

+ * + * @since 1.0.0 + */ +interface CreateProjectSpaceOutput extends UseCaseFailure { + + /** + *

This method is called from the use case, after + * successful project space creation in QBiC's data management + * platform.

+ * + * @param projectSpace The created project space + * @since 1.0.0 + */ + void projectSpaceCreated(ProjectSpace projectSpace) + + /** + *

Called when a project space with a given identifier already exists.

+ *
+ *

This reflects the scenario, when a user provided a pre-defined space name + * which already exists in the underlying data source. In this case the space cannot be + * created.

+ * + * @param projectSpace The project space that already exists + * @since 1.0.0 + */ + void projectSpaceAlreadyExists(ProjectSpace projectSpace) + +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/ProjectSpaceExistsException.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/ProjectSpaceExistsException.groovy new file mode 100644 index 000000000..71de6e4ce --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/ProjectSpaceExistsException.groovy @@ -0,0 +1,30 @@ +package life.qbic.business.projects.spaces + +/** + *

Exception that indicates violations during the creation af a new project space

+ * + *

This exception is supposed to be thrown, if an application request to create + * a data resource in the data management system is not possible.

+ * + * Example: A project space with a given identifier already exists in the data source. So the + * method should throw an ProjectSpaceExistsException and not a DatabaseQueryException. + *
+ * With this, the use case can be made aware of, that it is not a technical issue during the + * execution of an SQL query for example. + * + * @since 1.0.0 + */ +class ProjectSpaceExistsException extends RuntimeException{ + + ProjectSpaceExistsException(){ + super() + } + + ProjectSpaceExistsException(String message) { + super(message) + } + + ProjectSpaceExistsException(String message, Throwable throwable){ + super(message, throwable) + } +} diff --git a/offer-manager-domain/src/test/groovy/life/qbic/business/products/archive/ArchiveProductSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/business/products/archive/ArchiveProductSpec.groovy new file mode 100644 index 000000000..ea520a533 --- /dev/null +++ b/offer-manager-domain/src/test/groovy/life/qbic/business/products/archive/ArchiveProductSpec.groovy @@ -0,0 +1,78 @@ +package life.qbic.business.products.archive + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.datamodel.dtos.business.services.ProductUnit +import spock.lang.Specification + +/** + *

Archive Product tests

+ * + *

This Specification contains tests for the use ArchiveProduct use case

+ * + * @since 1.0.0 + */ +class ArchiveProductSpec extends Specification { + + def "archive provides the product from the datasource to the output"() { + + given: "a product identifier and associated product" + ProductId productId = new ProductId("Test", "1234") + Product product = new Product("Test", "Test Description", 0, ProductUnit.PER_SAMPLE, productId){} + + and: "an output and datasource that returns the product for a given productId" + ArchiveProductDataSource dataSource = Stub() + ArchiveProductOutput output = Mock() + dataSource.fetch(productId) >> Optional.of(product) + + and: "an instance of the ArchiveProduct use case" + ArchiveProduct archiveProduct = new ArchiveProduct(dataSource, output) + + when: "the use case archives with the productId" + archiveProduct.archive(productId) + + then: "the output will receive the product" + 1 * output.archived(product) + } + + def "archive provides fail notification when no product was found "() { + + given: "a product identifier" + ProductId productId = new ProductId("Test", "1234") + + and: "an output and datasource that cannot find the id" + ArchiveProductDataSource dataSource = Stub() + ArchiveProductOutput output = Mock() + dataSource.fetch(productId) >> Optional.empty() + + and: "an instance of the ArchiveProduct use case" + ArchiveProduct archiveProduct = new ArchiveProduct(dataSource, output) + + when: "the use case archives with the productId" + archiveProduct.archive(productId) + + then: "the output will receive a failure notification" + 1 * output.failNotification(_ as String) + } + + def "archive product outputs a fail notification in case the database query fails for technical reasons"() { + given: "a product identifier and associated product" + ProductId productId = new ProductId("Test", "1234") + Product product = new Product("Test", "Test Description", 0, ProductUnit.PER_SAMPLE, productId){} + + and: "an output and datasource that fails for technical reasons" + ArchiveProductDataSource dataSource = Stub() + ArchiveProductOutput output = Mock() + dataSource.fetch(productId) >> { throw new DatabaseQueryException("Something went wrong") } + + and: "the use case under test" + ArchiveProduct archiveProduct = new ArchiveProduct(dataSource, output) + + when: "an instance of the ArchiveProduct use case" + archiveProduct.archive(productId) + + then: "the output will receive a failure notification" + 1 * output.failNotification(_ as String) + } +} diff --git a/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy new file mode 100644 index 000000000..10acb195e --- /dev/null +++ b/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy @@ -0,0 +1,90 @@ +package life.qbic.business.products.create + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.AtomicProduct +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.datamodel.dtos.business.services.ProductUnit +import spock.lang.Specification + +/** + *

Tests for the {@link CreateProduct} use case

+ * + *

This specification contains tests for all steps of the {@link CreateProduct} use case

+ * + * @since 1.0.0 + */ +class CreateProductSpec extends Specification { + CreateProductOutput output + ProductId productId + Product product + + def setup() { + output = Mock(CreateProductOutput) + productId = new ProductId("Test", "1234") + product = new AtomicProduct("test product", "this is a test product", 0.5, ProductUnit.PER_GIGABYTE, productId) + } + + def "Create stores the provided product in the data source"() { + given: "a data source that stores a product" + CreateProductDataSource dataSource = Stub(CreateProductDataSource) + String dataStatus = "" + dataSource.store(product) >> { dataStatus = "stored" } + and: "an instance of the use case" + CreateProduct createProduct = new CreateProduct(dataSource, output) + + when: "the create method is called" + createProduct.create(product) + + then: "the output is informed and no failure notification is send" + 1 * output.created(product) + 0 * output.foundDuplicate(_) + 0 * output.failNotification(_) + and: "the data was stored in the database" + dataStatus == "stored" + } + + def "Create informs the output that an entry matching the provided product already exists"() { + given: "a data source that detects a duplicate entry" + CreateProductDataSource dataSource = Stub(CreateProductDataSource) + String dataStatus = "" + dataSource.store(product) >> { + dataStatus = "not stored" + println(dataStatus) + throw new ProductExistsException(productId) + } + and: "an instance of the use case" + CreateProduct createProduct = new CreateProduct(dataSource, output) + + when: "the create method is called" + createProduct.create(product) + + then: "the output is informed and no failure notification is send" + 1 * output.foundDuplicate(product) + 0 * output.created(_) + 0 * output.failNotification(_) + and: "the data was not stored in the database" + dataStatus == "not stored" + } + + def "Create sends a failure notification in case technical errors occur at the data source"() { + given: "a data source that stores a product" + CreateProductDataSource dataSource = Stub(CreateProductDataSource) + String dataStatus = "" + dataSource.store(product) >> { + dataStatus = "not stored" + throw new DatabaseQueryException("This is a test") } + and: "an instance of the use case" + CreateProduct createProduct = new CreateProduct(dataSource, output) + + when: "the create method is called" + createProduct.create(product) + + then: "the output is send a failure notification" + 0 * output.created(_) + 0 * output.foundDuplicate(_) + 1 * output.failNotification(_ as String) + and: "the data was stored" + dataStatus == "not stored" + } +} diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/search/SearchCustomerSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/search/SearchCustomerSpec.groovy deleted file mode 100644 index 003fa2508..000000000 --- a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/search/SearchCustomerSpec.groovy +++ /dev/null @@ -1,62 +0,0 @@ -package life.qbic.portal.portlet.customers.search - -import life.qbic.business.customers.search.SearchCustomer -import life.qbic.business.customers.search.SearchCustomerDataSource -import life.qbic.business.customers.search.SearchCustomerOutput -import life.qbic.datamodel.dtos.business.Customer -import life.qbic.business.exceptions.DatabaseQueryException -import spock.lang.Specification - -/** - * Test the search of persons - * - * Stubs the database connection and verifies that if a customer is found the {@link life.qbic.business.customers.search.SearchCustomerOutput} is verified or if not - * an exception leads to a failure notification - * - * @since: 1.0.0 - * - */ -class SearchCustomerSpec extends Specification{ - - - def "find a searched customer"(){ - given: - SearchCustomerOutput output = Mock(SearchCustomerOutput.class) - SearchCustomerDataSource ds = Stub(SearchCustomerDataSource.class) - SearchCustomer searchCustomer = new SearchCustomer(output,ds) - - Customer luke = new Customer.Builder(firstName, lastName, "example@example.com").build() - - ds.findCustomer(firstName, lastName) >> [luke] - - when: - searchCustomer.searchCustomer(firstName, lastName) - - then: - 1* output.successNotification(_) - - where: - firstName | lastName - "Luke" | "Skywalker" - } - - def "notify of failure whenever the datasource throws an exception"(){ - given: - SearchCustomerOutput output = Mock(SearchCustomerOutput.class) - SearchCustomerDataSource ds = Stub(SearchCustomerDataSource.class) - SearchCustomer searchCustomer = new SearchCustomer(output,ds) - - ds.findCustomer(firstName, lastName) >> {throw new DatabaseQueryException("Customer not found")} - - when: - searchCustomer.searchCustomer(firstName, lastName) - - then: - 1* output.failNotification(_) - - where: - firstName | lastName - "Luke" | "Skywalker" - } - -} diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/update/UpdateCustomerInputSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/update/UpdateCustomerInputSpec.groovy deleted file mode 100644 index 80d9bb8e0..000000000 --- a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/update/UpdateCustomerInputSpec.groovy +++ /dev/null @@ -1,84 +0,0 @@ -package life.qbic.portal.portlet.customers.update - -import life.qbic.business.customers.create.CreateCustomerDataSource -import life.qbic.business.customers.create.CreateCustomerOutput -import life.qbic.business.customers.update.* -import life.qbic.datamodel.dtos.business.AcademicTitle -import life.qbic.datamodel.dtos.business.Customer -import life.qbic.datamodel.dtos.business.Affiliation - -import spock.lang.Specification - -/** - * - * - * - * - * @since: 1.0.0 - */ -class UpdateCustomerInputSpec extends Specification { - UpdateCustomerOutput output - CreateCustomerDataSource dataSource - - - def setup() { - output = Mock() - dataSource = Mock() - } - - def "given customer changes, update the customer using a mocked data source"(){ - given: "A new update customer use case instance" - UpdateCustomer useCase = new UpdateCustomer(output, dataSource) - dataSource.getCustomer(42) >> new Customer.Builder("Test", "user", "oldmail").title(AcademicTitle.NONE).build() - - when: "The use case method is called" - useCase.updateCustomer(customerId, customer) - - then: "The customer is updated using the data source" - 1 * dataSource.updateCustomer(customerId, customer) - 0 * dataSource.updateCustomerAffiliations(_ as String, _ as List) - - where: - customer = new Customer.Builder("Test", "user", "newmail").title(AcademicTitle.NONE).build() - customerId = 42 - } - - def "given no customer changes, update the affiliations using a mocked data source"(){ - given: "A new update customer use case instance" - UpdateCustomer useCase = new UpdateCustomer(output, dataSource) - dataSource.getCustomer(42) >> new Customer.Builder("Test", "user", "oldmail").title(AcademicTitle.NONE).affiliation(affiliation1).build() - - when: "The use case method is called" - useCase.updateCustomer(customerId, customer) - - then: "The customer affiliations are updated using the data source" - 0 * dataSource.updateCustomer(_ as String, _ as Customer) - 1 * dataSource.updateCustomerAffiliations(customerId, twoAffiliations) - - where: - affiliation1 = new Affiliation.Builder( - "org", "street", "zip", "city").build() - twoAffiliations = new ArrayList(Arrays.asList(new Affiliation.Builder( - "other org", "other street", "zip", "city").build(), affiliation1)) - customer = new Customer.Builder("Test", "user", "oldmail").title(AcademicTitle.NONE).affiliations(twoAffiliations).build() - customerId = 42 - } - - def "datasource throwing an exception leads to fail notification on output"() { - given: "a data source that throws an exception" - dataSource.getCustomer(_ as Integer) >> new Customer.Builder("Test", "user", "oldmail").title(AcademicTitle.NONE).build() - dataSource.updateCustomer(_ as Integer, _ as Customer) >> { throw new Exception("Something went wrong.") } - UpdateCustomer useCase = new UpdateCustomer(output, dataSource) - - when: "the use case is executed" - useCase.updateCustomer(customerId, customer) - - then: "the output receives a failure notification" - 1 * output.failNotification(_ as String) - 0 * output.customerUpdated(_ as Customer) - - where: - customer = new Customer.Builder("Test", "user", "newmail").title(AcademicTitle.NONE).build() - customerId = 420 - } -} diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/logging/LoggerSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/logging/LoggerSpec.groovy index ebea65365..8c89b1350 100644 --- a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/logging/LoggerSpec.groovy +++ b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/logging/LoggerSpec.groovy @@ -15,6 +15,8 @@ class LoggerSpec extends Specification { def "All four log levels should be callable without exception"() { given: "A log instance" Logging logger = Logger.getLogger(this.class) + and: "A throwable exception" + Throwable throwable = new NullPointerException("Test exception") when: logger.info("Just a test info message") @@ -22,6 +24,12 @@ class LoggerSpec extends Specification { logger.error("Just a test error message") logger.debug("Just a test debug message") + logger.info("Just a test info message", throwable) + logger.warn("Just a test warning message", throwable) + logger.error("Just a test error message", throwable) + logger.debug("Just a test debug message", throwable) + + then: noExceptionThrown() } diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/OfferSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/OfferSpec.groovy index 484a8dda4..fcda8a06d 100644 --- a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/OfferSpec.groovy +++ b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/OfferSpec.groovy @@ -14,6 +14,8 @@ import life.qbic.datamodel.dtos.business.services.DataStorage import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis import life.qbic.datamodel.dtos.business.services.ProductUnit import life.qbic.datamodel.dtos.business.services.ProjectManagement +import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis +import life.qbic.datamodel.dtos.business.services.Sequencing import spock.lang.Shared import spock.lang.Specification @@ -277,4 +279,41 @@ class OfferSpec extends Specification { then: !res } + + def "An Offer will provide methods to distinct between ProductItems associated with overhead costs and calculate their net sum"() { + given: + ProductItem primaryAnalysis = new ProductItem(2, new PrimaryAnalysis("Basic RNAsq", "Just an" + + " example primary analysis", 1.0, ProductUnit.PER_SAMPLE, "1")) + ProductItem secondaryAnalysis = new ProductItem(1, new SecondaryAnalysis("Basic RNAsq", "Just an" + + " example secondary analysis", 2.0, ProductUnit.PER_SAMPLE, "1")) + ProductItem sequencing = new ProductItem(3, new Sequencing("Basic Sequencing", "Just an" + + "example sequencing", 3.0, ProductUnit.PER_SAMPLE, "1")) + ProductItem projectManagement = new ProductItem(1, new ProjectManagement("Basic Management", + "Just an example", 10.0, ProductUnit.PER_DATASET, "1")) + ProductItem dataStorage = new ProductItem(2, new DataStorage("Data Storage", + "Just an example", 20.0, ProductUnit.PER_DATASET, "1")) + + List items = [primaryAnalysis, projectManagement, sequencing, dataStorage, secondaryAnalysis] + Offer offer = new Offer.Builder(customerWithAllAffiliations, projectManager, "Awesome Project", "An " + + "awesome project", items, externalAffiliation).build() + + + when: + List itemsWithoutOverhead = offer.getNoOverheadItems() + List itemsWithOverhead = offer.getOverheadItems() + double itemsWithoutOverheadNetPrice = offer.getNoOverheadItemsNet() + double itemsWithOverheadNetPrice = offer.getOverheadItemsNet() + + then: + + double expectedItemsWithOverheadNetPrice = 2 * 1.0 + 1 * 2.0 + 3 * 3.0 + double expectedItemsWithoutOverheadNetPrice = 1 * 10 + 2 * 20 + List expectedItemsWithOverhead = [primaryAnalysis, sequencing, secondaryAnalysis] + List expectedItemsWithoutOverhead = [projectManagement, dataStorage] + itemsWithOverhead == expectedItemsWithOverhead + itemsWithoutOverhead == expectedItemsWithoutOverhead + expectedItemsWithOverheadNetPrice == itemsWithOverheadNetPrice + expectedItemsWithoutOverheadNetPrice == itemsWithoutOverheadNetPrice + + } } diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/create/CreateOfferSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/create/CreateOfferSpec.groovy index 9bd709b84..874a6244d 100644 --- a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/create/CreateOfferSpec.groovy +++ b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/create/CreateOfferSpec.groovy @@ -1,8 +1,12 @@ package life.qbic.portal.portlet.offers.create +import life.qbic.business.offers.Converter import life.qbic.business.offers.create.CreateOffer import life.qbic.business.offers.create.CreateOfferDataSource import life.qbic.business.offers.create.CreateOfferOutput +import life.qbic.business.offers.identifier.ProjectPart +import life.qbic.business.offers.identifier.RandomPart +import life.qbic.business.offers.identifier.Version import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.datamodel.dtos.business.AffiliationCategory import life.qbic.datamodel.dtos.business.Customer @@ -10,9 +14,11 @@ import life.qbic.datamodel.dtos.business.Offer import life.qbic.datamodel.dtos.business.OfferId import life.qbic.datamodel.dtos.business.ProductItem import life.qbic.datamodel.dtos.business.ProjectManager +import life.qbic.datamodel.dtos.business.services.DataStorage import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis import life.qbic.datamodel.dtos.business.services.ProductUnit import life.qbic.datamodel.dtos.business.services.ProjectManagement +import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis import life.qbic.datamodel.dtos.business.services.Sequencing import spock.lang.Shared import spock.lang.Specification @@ -93,4 +99,42 @@ class CreateOfferSpec extends Specification { then: 1 * output.calculatedPrice(2.8, 0, 0, 2.8) } + + def "Creating an Offer DTO from the Offer Entity works correctly"() { + given: + ProductItem primaryAnalysis = new ProductItem(2, new PrimaryAnalysis("Basic RNAsq", "Just an" + + " example primary analysis", 1.0, ProductUnit.PER_SAMPLE, "1")) + ProductItem secondaryAnalysis = new ProductItem(1, new SecondaryAnalysis("Basic RNAsq", "Just an" + + " example secondary analysis", 2.0, ProductUnit.PER_SAMPLE, "1")) + ProductItem sequencing = new ProductItem(3, new Sequencing("Basic Sequencing", "Just an" + + "example sequencing", 3.0, ProductUnit.PER_SAMPLE, "1")) + ProductItem projectManagement = new ProductItem(1, new ProjectManagement("Basic Management", + "Just an example", 10.0, ProductUnit.PER_DATASET, "1")) + ProductItem dataStorage = new ProductItem(2, new DataStorage("Data Storage", + "Just an example", 20.0, ProductUnit.PER_DATASET, "1")) + List items = [primaryAnalysis, projectManagement, sequencing, dataStorage, secondaryAnalysis] + life.qbic.business.offers.identifier.OfferId offerId = new life.qbic.business.offers.identifier.OfferId (new RandomPart(), new ProjectPart("test"), new Version(0)) + and: + life.qbic.business.offers.Offer offerEntity = new life.qbic.business.offers.Offer.Builder(customer, projectManager, "Awesome Project", "An " + + "awesome project", items, selectedAffiliation).identifier(offerId).build() + + when: + final offerDto = Converter.convertOfferToDTO(offerEntity) + + then: + offerDto.projectManager == offerEntity.getProjectManager() + offerDto.customer == offerEntity.getCustomer() + offerDto.projectTitle == offerEntity.getProjectTitle() + offerDto.projectObjective == offerEntity.getProjectObjective() + offerDto.selectedCustomerAffiliation == offerEntity.getSelectedCustomerAffiliation() + offerDto.modificationDate == offerEntity.getModificationDate() + offerDto.expirationDate == offerEntity.getExpirationDate() + offerDto.items == offerEntity.getItems() + offerDto.itemsWithOverhead == offerEntity.getOverheadItems() + offerDto.itemsWithoutOverhead == offerEntity.getNoOverheadItems() + offerDto.totalPrice == offerEntity.getTotalCosts() + offerDto.overheads == offerEntity.getOverheadSum() + offerDto.itemsWithOverheadNetPrice == offerEntity.getOverheadItemsNet() + offerDto.itemsWithoutOverheadNetPrice == offerEntity.getNoOverheadItemsNet() + } } diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/create/CreateCustomerSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/create/CreatePersonSpec.groovy similarity index 63% rename from offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/create/CreateCustomerSpec.groovy rename to offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/create/CreatePersonSpec.groovy index 5febcddf3..56b3a7ea6 100644 --- a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/create/CreateCustomerSpec.groovy +++ b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/create/CreatePersonSpec.groovy @@ -1,8 +1,8 @@ -package life.qbic.portal.portlet.customers.create +package life.qbic.portal.portlet.persons.create -import life.qbic.business.customers.create.CreateCustomer -import life.qbic.business.customers.create.CreateCustomerDataSource -import life.qbic.business.customers.create.CreateCustomerOutput +import life.qbic.business.persons.create.CreatePerson +import life.qbic.business.persons.create.CreatePersonDataSource +import life.qbic.business.persons.create.CreatePersonOutput import life.qbic.datamodel.dtos.business.AcademicTitle import life.qbic.datamodel.dtos.business.Customer import life.qbic.datamodel.dtos.general.Person @@ -16,9 +16,9 @@ import spock.lang.Specification * @since: 1.0.0 * @author: Tobias Koch */ -class CreateCustomerSpec extends Specification { - CreateCustomerOutput output - CreateCustomerDataSource dataSource +class CreatePersonSpec extends Specification { + CreatePersonOutput output + CreatePersonDataSource dataSource def setup() { @@ -28,13 +28,13 @@ class CreateCustomerSpec extends Specification { def "given full information add the customer using a mocked data source"(){ given: "A new create customer use case instance" - CreateCustomer useCase = new CreateCustomer(output, dataSource) + CreatePerson useCase = new CreatePerson(output, dataSource) when: "The use case method is called" - useCase.createCustomer(customer) + useCase.createPerson(customer) then: "The customer is added using the data source" - 1 * dataSource.addCustomer(customer) + 1 * dataSource.addPerson(customer) where: customer = new Customer.Builder("Test", "user", "test").title(AcademicTitle.NONE).build() @@ -42,11 +42,11 @@ class CreateCustomerSpec extends Specification { def "datasource throwing an exception leads to fail notification on output"() { given: "a data source that throws an exception" - dataSource.addCustomer(_ as Customer) >> { throw new Exception("Something went wrong.") } - CreateCustomer useCase = new CreateCustomer(output, dataSource) + dataSource.addPerson(_ as Customer) >> { throw new Exception("Something went wrong.") } + CreatePerson useCase = new CreatePerson(output, dataSource) when: "the use case is executed" - useCase.createCustomer(customer) + useCase.createPerson(customer) then: "the output receives a failure notification" 1 * output.failNotification(_ as String) diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/search/SearchPersonSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/search/SearchPersonSpec.groovy new file mode 100644 index 000000000..c963a4f00 --- /dev/null +++ b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/search/SearchPersonSpec.groovy @@ -0,0 +1,62 @@ +package life.qbic.portal.portlet.persons.search + +import life.qbic.business.persons.search.SearchPerson +import life.qbic.business.persons.search.SearchPersonDataSource +import life.qbic.business.persons.search.SearchPersonOutput +import life.qbic.datamodel.dtos.business.Customer +import life.qbic.business.exceptions.DatabaseQueryException +import spock.lang.Specification + +/** + * Test the search of persons + * + * Stubs the database connection and verifies that if a customer is found the {@link SearchPersonOutput} is verified or if not + * an exception leads to a failure notification + * + * @since: 1.0.0 + * + */ +class SearchPersonSpec extends Specification{ + + + def "find a searched customer"(){ + given: + SearchPersonOutput output = Mock(SearchPersonOutput.class) + SearchPersonDataSource ds = Stub(SearchPersonDataSource.class) + SearchPerson searchCustomer = new SearchPerson(output,ds) + + Customer luke = new Customer.Builder(firstName, lastName, "example@example.com").build() + + ds.findPerson(firstName, lastName) >> [luke] + + when: + searchCustomer.searchPerson(firstName, lastName) + + then: + 1* output.successNotification(_) + + where: + firstName | lastName + "Luke" | "Skywalker" + } + + def "notify of failure whenever the datasource throws an exception"(){ + given: + SearchPersonOutput output = Mock(SearchPersonOutput.class) + SearchPersonDataSource ds = Stub(SearchPersonDataSource.class) + SearchPerson searchCustomer = new SearchPerson(output,ds) + + ds.findPerson(firstName, lastName) >> {throw new DatabaseQueryException("Customer not found")} + + when: + searchCustomer.searchPerson(firstName, lastName) + + then: + 1* output.failNotification(_) + + where: + firstName | lastName + "Luke" | "Skywalker" + } + +} diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/update/UpdatePersonInputSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/update/UpdatePersonInputSpec.groovy new file mode 100644 index 000000000..4d1388fe3 --- /dev/null +++ b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/update/UpdatePersonInputSpec.groovy @@ -0,0 +1,84 @@ +package life.qbic.portal.portlet.persons.update + +import life.qbic.business.persons.create.CreatePersonDataSource +import life.qbic.business.persons.update.* +import life.qbic.datamodel.dtos.business.AcademicTitle +import life.qbic.datamodel.dtos.business.Customer +import life.qbic.datamodel.dtos.business.Affiliation +import life.qbic.datamodel.dtos.general.CommonPerson +import life.qbic.datamodel.dtos.general.Person +import spock.lang.Specification + +/** + * + * + * + * + * @since: 1.0.0 + */ +class UpdatePersonInputSpec extends Specification { + UpdatePersonOutput output + CreatePersonDataSource dataSource + + + def setup() { + output = Mock() + dataSource = Mock() + } + + def "given customer changes, update the customer using a mocked data source"(){ + given: "A new update customer use case instance" + UpdatePerson useCase = new UpdatePerson(output, dataSource) + dataSource.getPerson(42) >> new CommonPerson.Builder("Test", "user", "oldmail").title(AcademicTitle.NONE).build() + + when: "The use case method is called" + useCase.updatePerson(customerId, customer) + + then: "The customer is updated using the data source" + 1 * dataSource.updatePerson(customerId, customer) + 0 * dataSource.updatePersonAffiliations(_ as String, _ as List) + + where: + customer = new CommonPerson.Builder("Test", "user", "newmail").title(AcademicTitle.NONE).build() + customerId = 42 + } + + def "given no customer changes, update the affiliations using a mocked data source"(){ + given: "A new update customer use case instance" + UpdatePerson useCase = new UpdatePerson(output, dataSource) + dataSource.getPerson(42) >> new CommonPerson.Builder("Test", "user", "oldmail").title(AcademicTitle.NONE).affiliation(affiliation1).build() + + when: "The use case method is called" + useCase.updatePerson(customerId, customer) + + then: "The customer affiliations are updated using the data source" + 0 * dataSource.updatePerson(_ as String, _ as Customer) + 1 * dataSource.updatePersonAffiliations(customerId, twoAffiliations) + + where: + affiliation1 = new Affiliation.Builder( + "org", "street", "zip", "city").build() + twoAffiliations = new ArrayList(Arrays.asList(new Affiliation.Builder( + "other org", "other street", "zip", "city").build(), affiliation1)) + customer = new CommonPerson.Builder("Test", "user", "oldmail").title(AcademicTitle.NONE).affiliations(twoAffiliations).build() + customerId = 42 + } + + def "datasource throwing an exception leads to fail notification on output"() { + given: "a data source that throws an exception" + dataSource.getPerson(_ as Integer) >> new CommonPerson.Builder("Test", "user", "oldmail").title(AcademicTitle.NONE).build() + dataSource.updatePerson(_ as Integer, _ as Person) >> { throw new Exception("Something went wrong.") } + UpdatePerson useCase = new UpdatePerson(output, dataSource) + + when: "the use case is executed" + useCase.updatePerson(customerId, customer) + + then: "the output receives a failure notification" + 1 * output.failNotification(_ as String) + 0 * output.personUpdated(_ as Person) + + where: + customer = new CommonPerson.Builder("Test", "user", "newmail").title(AcademicTitle.NONE).build() + customerId = 420 + } +} diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/projects/CreateProjectSpaceSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/projects/CreateProjectSpaceSpec.groovy new file mode 100644 index 000000000..51edfa234 --- /dev/null +++ b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/projects/CreateProjectSpaceSpec.groovy @@ -0,0 +1,73 @@ +package life.qbic.portal.portlet.projects + +import life.qbic.business.Constants +import life.qbic.business.projects.spaces.CreateProjectSpace +import life.qbic.business.projects.spaces.CreateProjectSpaceDataSource +import life.qbic.business.projects.spaces.CreateProjectSpaceOutput +import life.qbic.business.projects.spaces.ProjectSpaceExistsException +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace +import spock.lang.Shared +import spock.lang.Specification + +/** + *

Test the behaviour of {@link life.qbic.business.projects.spaces.CreateProjectSpace}

+ * + * @since 1.0.0 + */ +class CreateProjectSpaceSpec extends Specification{ + + @Shared CreateProjectSpace createProjectSpace + @Shared CreateProjectSpaceOutput output + @Shared CreateProjectSpaceDataSource dataSource + + @Shared ProjectSpace space + + def setup(){ + output = Mock(CreateProjectSpaceOutput) + dataSource = Stub(CreateProjectSpaceDataSource) + createProjectSpace = new CreateProjectSpace(output,dataSource) + + space = new ProjectSpace("my-new-space") + } + + def "A a successful project space creation throws no errors"(){ + given: "The database successfully creates the new space" + dataSource.createProjectSpace(space) >> {} + + when: "a new project space needs to be created" + createProjectSpace.createProjectSpace(space) + + then: "no error is thrown" + 1 * output.projectSpaceCreated(space) + 0 * output.failNotification(_) + 0 * output.projectSpaceAlreadyExists(_) + } + + def "No duplicate project spaces can be created"(){ + given: "The database successfully creates the new space" + dataSource.createProjectSpace(space) >> {throw new ProjectSpaceExistsException("Project space already exists")} + + when: "a new project space needs to be created" + createProjectSpace.createProjectSpace(space) + + then: "no error is thrown" + 0 * output.projectSpaceCreated(space) + 0 * output.failNotification(_) + 1 * output.projectSpaceAlreadyExists(space) + } + + def "Unexpected errors are caught"(){ + given: "The database successfully creates the new space" + dataSource.createProjectSpace(space) >> {throw new Exception("Project space already exists")} + + when: "a new project space needs to be created" + createProjectSpace.createProjectSpace(space) + + then: "no error is thrown" + 0 * output.projectSpaceCreated(space) + 1 * output.failNotification("An unexpected during the project creation occurred. " + + "Please contact ${Constants.QBIC_HELPDESK_EMAIL}.") + 0 * output.projectSpaceAlreadyExists(_) + } + +} diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/projects/CreateProjectSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/projects/CreateProjectSpec.groovy new file mode 100644 index 000000000..eb5fbe6b0 --- /dev/null +++ b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/projects/CreateProjectSpec.groovy @@ -0,0 +1,128 @@ +package life.qbic.portal.portlet.projects + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.business.projects.create.CreateProject +import life.qbic.business.projects.create.CreateProjectDataSource +import life.qbic.business.projects.create.CreateProjectOutput +import life.qbic.business.projects.create.ProjectExistsException +import life.qbic.business.projects.spaces.CreateProjectSpaceDataSource +import life.qbic.datamodel.dtos.business.Customer +import life.qbic.datamodel.dtos.business.OfferId +import life.qbic.datamodel.dtos.business.ProjectManager +import life.qbic.datamodel.dtos.projectmanagement.Project +import life.qbic.datamodel.dtos.business.ProjectApplication +import life.qbic.datamodel.dtos.projectmanagement.ProjectCode +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace +import spock.lang.Shared +import spock.lang.Specification + +/** + *

Adds tests for the {@link life.qbic.business.projects.create.CreateProject} use case

+ * + * @since 1.0.0 + * +*/ + +class CreateProjectSpec extends Specification{ + + @Shared CreateProject createProject + @Shared CreateProjectOutput output + @Shared CreateProjectDataSource dataSource + @Shared CreateProjectSpaceDataSource spaceDataSource + + @Shared ProjectManager projectManager + @Shared Customer customer + @Shared OfferId offerId + + @Shared ProjectApplication projectApplication + @Shared ProjectSpace space + @Shared ProjectIdentifier projectIdentifier + @Shared ProjectCode projectCode + + def setup(){ + dataSource = Stub(CreateProjectDataSource) + spaceDataSource = Stub(CreateProjectSpaceDataSource) + output = Mock(CreateProjectOutput) + createProject = new CreateProject(output,dataSource,spaceDataSource) + + offerId = new OfferId("my-project","abab","1") + projectManager = new ProjectManager.Builder("Max","Mustermann","a.b@c.d").build() + customer = new Customer.Builder("Maxine","Mustermann","e.b@c.d").build() + + space = new ProjectSpace("my-space-name") + projectCode = new ProjectCode("QABCD") + projectIdentifier = new ProjectIdentifier(space,projectCode) + } + + def "A valid project application creates a new project"(){ + given: "a project application is provided" + projectApplication = new ProjectApplication(offerId,"Title","objective","exp. design",projectManager,space, customer, projectCode) + Project.Builder projectBuilder = new Project.Builder(projectIdentifier, projectApplication.projectTitle) + projectBuilder.linkedOfferId(offerId) + Project project = projectBuilder.build() + + and: "the data source is able to create the project" + dataSource.createProject(projectApplication) >> project + + when: "the creation of the project is triggered" + createProject.createProject(projectApplication) + + then: "a project is successfully created" + 1 * output.projectCreated(project) + 0 * output.failNotification(_) + 0 * output.projectAlreadyExists(_,_) + } + + def "An a project with duplicate project codes cannot be created"(){ + given: "a project application is provided" + projectApplication = new ProjectApplication(offerId,"Title","objective","exp. design",projectManager,space, customer, projectCode) + + and: "the data source is able to create the project" + dataSource.createProject(projectApplication) >> {throw new ProjectExistsException("The project already exists in the database")} + + when: "the creation of the project is triggered" + createProject.createProject(projectApplication) + + then: "a project is successfully created" + 0 * output.projectCreated(_) + 0 * output.failNotification(_) + 1 * output.projectAlreadyExists(_,_) + } + + def "Create sends a fail notification to the output if the database fails"(){ + given: "a project application is provided" + projectApplication = new ProjectApplication(offerId,"Title","objective","exp. design",projectManager,space, customer, projectCode) + + and: "the data source is able to create the project" + dataSource.createProject(projectApplication) >> {throw new DatabaseQueryException("An exception occurred.")} + + when: "the creation of the project is triggered" + createProject.createProject(projectApplication) + + then: "a project is successfully created" + 0 * output.projectCreated(_) + 1 * output.failNotification("The project application for ${projectApplication.projectCode} was not successful. The project can not be stored in the database.") + 0 * output.projectAlreadyExists(_,_) + } + + def "If a new space is provided it shall be created before the project is created"(){ + given: "a project application is provided" + projectApplication = new ProjectApplication(offerId,"Title","objective","exp. design",projectManager,space, customer, projectCode) + Project.Builder projectBuilder = new Project.Builder(projectIdentifier, projectApplication.projectTitle) + projectBuilder.linkedOfferId(offerId) + Project project = projectBuilder.build() + + and: "the data source is able to create the project" + dataSource.createProject(projectApplication) >> project + spaceDataSource.createProjectSpace(space) >> {} + + when: "the creation of the project is triggered" + createProject.createProjectWithSpace(projectApplication) + + then: "a space and project are successfully created" + 1 * output.projectCreated(project) + 0 * output.failNotification(_) + 0 * output.projectAlreadyExists(_,_) + } +} diff --git a/pom.xml b/pom.xml index ee2c8432c..e2fa503a9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,5 @@ - 4.0.0 @@ -9,7 +8,7 @@ offer-manager-app offer-manager - 1.0.0-alpha.3 + 1.0.0-alpha.4 life.qbic The new offer manager http://github.com/qbicsoftware/qOffer_2.0 @@ -61,7 +60,8 @@ localhost:8080 - + @@ -111,10 +111,15 @@ + + life.qbic + openbis-client-lib + 1.5.0 + life.qbic data-model-lib - 2.2.0 + 2.3.0 com.vaadin @@ -243,20 +248,12 @@ jetty-maven-plugin 9.4.32.v20200930 - + 2 true 8005 @@ -286,8 +283,7 @@ - + jetty-cold-deploy 0