From c673a16f90c433f9b201ec6bf325e7bbd732f245 Mon Sep 17 00:00:00 2001 From: Sven F Date: Mon, 1 Mar 2021 15:20:44 +0100 Subject: [PATCH 01/40] Provide authorization architecture (#346) This PR introduces authorization of tomato features by user role. An RoleService interface represents the behaviour: the available tomato role for a given user identifier. Two draft implementations are provided. One for testing purposes to mimic the roles project manager and offer admin, and one for deploying the app in a Liferay environment. Co-authored-by: jnnfr --- AUTHORS.rst | 2 +- CHANGELOG.rst | 5 ++ README.rst | 23 ++++++- offer-manager-app/pom.xml | 11 +++- .../offermanager/DependencyManager.groovy | 8 ++- .../offermanager/OfferManagerApp.groovy | 31 +++++++++- .../offermanager/components/AppView.groovy | 9 +++ .../components/AppViewModel.groovy | 38 +++++++++++- .../portal/offermanager/security/Role.groovy | 19 ++++++ .../offermanager/security/RoleService.groovy | 25 ++++++++ .../liferay/LiferayRoleService.groovy | 60 +++++++++++++++++++ .../local/LocalAdminRoleService.groovy | 22 +++++++ .../local/LocalManagerRoleService.groovy | 22 +++++++ .../main/webapp/WEB-INF/liferay-display.xml | 2 +- offer-manager-domain/pom.xml | 2 +- pom.xml | 2 +- 16 files changed, 266 insertions(+), 15 deletions(-) create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/Role.groovy create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/RoleService.groovy create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/liferay/LiferayRoleService.groovy create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/local/LocalAdminRoleService.groovy create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/local/LocalManagerRoleService.groovy diff --git a/AUTHORS.rst b/AUTHORS.rst index ce6069a2d..f7c84155a 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -10,4 +10,4 @@ Development Lead Contributors ------------ -None yet. Why not be the first? +* Sven Fillinger, sven1103 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a5e4590dd..20d4e83b8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,11 @@ This project adheres to `Semantic Versioning `_. **Added** +* 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. + **Fixed** * Update the agreement section of the offer (`#329 `_) diff --git a/README.rst b/README.rst index a17b82715..3d7dd8364 100644 --- a/README.rst +++ b/README.rst @@ -55,12 +55,29 @@ You can do so via homebrew .. code-block: bash brew install --cask chromium -Create the project with +Run the project with .. code-block: bash -mvn clean jetty:run +mvn clean jetty:run -Denvironment=testing + +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 +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. + +If an authenticated user has none of these roles, she will not be able to execute the application. -open the application through localhost:8080 System setup ------------ diff --git a/offer-manager-app/pom.xml b/offer-manager-app/pom.xml index 20751b86e..7fed1d41f 100644 --- a/offer-manager-app/pom.xml +++ b/offer-manager-app/pom.xml @@ -5,7 +5,7 @@ offer-manager life.qbic - 1.0.0-SNAPSHOT + 1.0.0-alpha.3-SNAPSHOT 4.0.0 war @@ -15,13 +15,20 @@ life.qbic offer-manager-domain - 1.0.0-SNAPSHOT + 1.0.0-alpha.3-SNAPSHOT compile life.qbic data-model-lib + + + com.liferay.portal + portal-service + 6.2.5 + provided + life.qbic portal-utils-lib 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 7647cd0ae..ebf231eea 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 @@ -46,6 +46,7 @@ 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.security.Role import life.qbic.portal.utils.ConfigurationManager import life.qbic.portal.utils.ConfigurationManagerFactory @@ -62,6 +63,8 @@ import life.qbic.portal.utils.ConfigurationManagerFactory @Log4j2 class DependencyManager { + private final Role userRole + private AppViewModel viewModel private CreatePersonViewModel createCustomerViewModel private UpdatePersonViewModel updatePersonViewModel @@ -121,8 +124,9 @@ class DependencyManager { * This constructor creates a dependency manager with all the instances of required classes. * It ensures that the {@link #portletView} field is set. */ - DependencyManager() { + DependencyManager(Role userRole) { configurationManager = ConfigurationManagerFactory.getInstance() + this.userRole = userRole initializeDependencies() } @@ -181,7 +185,7 @@ class DependencyManager { private void setupViewModels() { // setup view models try { - this.viewModel = new AppViewModel(affiliationService) + this.viewModel = new AppViewModel(affiliationService, this.userRole) } catch (Exception e) { log.error("Unexpected excpetion during ${AppViewModel.getSimpleName()} view model setup.", e) throw 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 7541f0001..32f3af725 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 @@ -3,12 +3,18 @@ package life.qbic.portal.offermanager import com.vaadin.annotations.Theme import com.vaadin.server.Page import com.vaadin.server.VaadinRequest +import com.vaadin.server.VaadinService import com.vaadin.ui.Layout import com.vaadin.ui.Notification import com.vaadin.ui.VerticalLayout import groovy.transform.CompileStatic import groovy.util.logging.Log4j2 import life.qbic.portal.offermanager.components.StyledNotification +import life.qbic.portal.offermanager.security.Role +import life.qbic.portal.offermanager.security.RoleService +import life.qbic.portal.offermanager.security.liferay.LiferayRoleService +import life.qbic.portal.offermanager.security.local.LocalAdminRoleService +import life.qbic.portal.offermanager.security.local.LocalManagerRoleService /** * Entry point for the application. This class derives from {@link life.qbic.portal.portlet.QBiCPortletUI}. @@ -38,7 +44,30 @@ class OfferManagerApp extends QBiCPortletUI { } private void create() { - this.dependencyManager = new DependencyManager() + final Role userRole = determineUserRole() + this.dependencyManager = new DependencyManager(userRole) + } + + private static Role determineUserRole() { + RoleService roleService + String userId + if(System.getProperty("environment") == "testing") { + roleService = new LocalAdminRoleService() + userId = "test" + log.info("Running test environment configuration.") + } else { + roleService = new LiferayRoleService() + userId = VaadinService.getCurrentRequest().getRemoteUser() + } + return loadAppRole(roleService, userId) + } + + private static Role loadAppRole(RoleService roleService, String userId) { + Optional userRole = roleService.getRoleForUser(userId) + if (!userRole.isPresent()) { + throw new RuntimeException("Security issue: Can not determine user role.") + } + return userRole.get() } @Override 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 851335a49..9e17ae078 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 @@ -9,6 +9,8 @@ import life.qbic.portal.offermanager.components.offer.create.CreateOfferView 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 /** * Class which connects the view elements with the ViewModel and the Controller @@ -169,6 +171,13 @@ class AppView extends VerticalLayout { setupListeners() setIcons() setDefault() + enableFeatures() + } + + private void enableFeatures() { + createOfferBtn.setEnabled(portletViewModel.createOfferFeatureEnabled) + createCustomerBtn.setEnabled(portletViewModel.createCustomerFeatureEnabled) + searchPersonBtn.setEnabled(portletViewModel.searchCustomerFeatureEnabled) } private void setDefault() { 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 3100fa2b5..7b0df42bd 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 @@ -3,6 +3,8 @@ package life.qbic.portal.offermanager.components import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.portal.offermanager.dataresources.persons.AffiliationResourcesService +import life.qbic.portal.offermanager.security.Role +import life.qbic.portal.offermanager.security.RoleService /** @@ -20,21 +22,51 @@ class AppViewModel { private final AffiliationResourcesService service - AppViewModel(AffiliationResourcesService service) { + private final Role role + + boolean createOfferFeatureEnabled + + boolean createCustomerFeatureEnabled + + boolean searchCustomerFeatureEnabled + + AppViewModel(AffiliationResourcesService service, + Role role) { this(new ArrayList(), new ArrayList(), new ArrayList(), new ArrayList(), - service) + service, + role) } private AppViewModel(List affiliations, List academicTitles, List successNotifications, List failureNotifications, - AffiliationResourcesService service) { + AffiliationResourcesService service, + Role role) { this.successNotifications = new ObservableList(successNotifications) this.failureNotifications = new ObservableList(failureNotifications) this.service = service + this.role = role + activateFeatures() + } + + private void activateFeatures(){ + setBasicFeatures() + if (role.equals(Role.OFFER_ADMIN)) { + setAdminFeatures() + } + } + + private void setBasicFeatures() { + createCustomerFeatureEnabled = true + createOfferFeatureEnabled = true + searchCustomerFeatureEnabled = false + } + + private void setAdminFeatures() { + searchCustomerFeatureEnabled = true } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/Role.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/Role.groovy new file mode 100644 index 000000000..a20cded7b --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/Role.groovy @@ -0,0 +1,19 @@ +package life.qbic.portal.offermanager.security + +/** + * Describes available roles in the offer manager application. + * + * @since 1.0.0 + */ +enum Role { + /** + * A user with the role 'project manager' is able to access most offer + * manager features, except the service product maintenance. + */ + PROJECT_MANAGER, + /** + * A user with the role 'offer admin' is able to access all offer + * manager features, including the service product maintenance. + */ + OFFER_ADMIN +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/RoleService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/RoleService.groovy new file mode 100644 index 000000000..3d36e92c9 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/RoleService.groovy @@ -0,0 +1,25 @@ +package life.qbic.portal.offermanager.security + +/** + * A role service determines the role for a given user + * + * Implementations of this interface evaluate the userId + * against the associated role in the deployed system. + * + * In addition, the implementation must take care of + * the system's role mapping to the role provided in this + * application. + * + * @since 1.0.0 + */ +interface RoleService { + + /** + * Determines the role of a user with a given user identifier. + * @param userId The user identifier determined by the system environment after successful + * authentication. + * @return The role of the user + */ + Optional getRoleForUser(String userId) + +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/liferay/LiferayRoleService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/liferay/LiferayRoleService.groovy new file mode 100644 index 000000000..1f999d595 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/liferay/LiferayRoleService.groovy @@ -0,0 +1,60 @@ +package life.qbic.portal.offermanager.security.liferay + +import com.liferay.portal.kernel.exception.PortalException +import com.liferay.portal.model.User +import com.liferay.portal.model.UserGroupRole +import com.liferay.portal.service.UserGroupRoleLocalServiceUtil +import com.liferay.portal.service.UserLocalServiceUtil +import groovy.util.logging.Log4j2 +import life.qbic.portal.offermanager.security.Role +import life.qbic.portal.offermanager.security.RoleService + +/** + * Role service implementation for Liferay 6.2 GA-6 + * + * This role service can be used to determine the role of a user + * from a Liferay 6.2 portal environment. + * + * The current implementation matches the following Liferay roles + * to the roles of this application context: project manager and offer admin. + * + * The association is (Liferay role to app role): + * + * ProjectManager <-> PROJECT_MANAGER + * OfferAdmin <-> OFFER_ADMIN + * + * @since 1.0.0 + */ +@Log4j2 +class LiferayRoleService implements RoleService{ + + @Override + Optional getRoleForUser(String userId) { + Optional role = Optional.empty() + List userGroupRoles + try { + userGroupRoles = determineLiferayUser(userId) + } catch (PortalException e) { + log.error(String.format("Could not find user with id %s.", userId)) + log.error(e.message) + log.error(e.stackTrace.join("\n")) + return role + } + return determineLiferayRole(userGroupRoles) + } + + private static List determineLiferayUser(String userId) { + return UserGroupRoleLocalServiceUtil.getUserGroupRoles(Long.parseLong(userId)) + } + + private static Optional determineLiferayRole(List userGroupRoles) { + for (UserGroupRole role : userGroupRoles) { + if (role.getRole().getTitleCurrentValue().equals("Project Manager")) { + return Optional.of(Role.PROJECT_MANAGER) + } else if (role.getRole().getTitleCurrentValue().equals("Offer Administrator")) { + return Optional.of(Role.OFFER_ADMIN) + } + } + return Optional.empty() + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/local/LocalAdminRoleService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/local/LocalAdminRoleService.groovy new file mode 100644 index 000000000..e3823cdfc --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/local/LocalAdminRoleService.groovy @@ -0,0 +1,22 @@ +package life.qbic.portal.offermanager.security.local + +import life.qbic.portal.offermanager.security.Role +import life.qbic.portal.offermanager.security.RoleService + +/** + * Example role service for local testing. + * + * Will always return the role of an offer admin. + * + * @since 1.0.0 + */ +class LocalAdminRoleService implements RoleService { + + /** + * {@inheritDoc} + */ + @Override + Optional getRoleForUser(String userId) { + return Optional.of(Role.OFFER_ADMIN) + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/local/LocalManagerRoleService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/local/LocalManagerRoleService.groovy new file mode 100644 index 000000000..21fe0e4cc --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/security/local/LocalManagerRoleService.groovy @@ -0,0 +1,22 @@ +package life.qbic.portal.offermanager.security.local + +import life.qbic.portal.offermanager.security.Role +import life.qbic.portal.offermanager.security.RoleService + +/** + * Example role service for local testing. + * + * Will always return the role of an project manager. + * + * @since 1.0.0 + */ +class LocalManagerRoleService implements RoleService { + + /** + * {@inheritDoc} + */ + @Override + Optional getRoleForUser(String userId) { + return Optional.of(Role.PROJECT_MANAGER) + } +} diff --git a/offer-manager-app/src/main/webapp/WEB-INF/liferay-display.xml b/offer-manager-app/src/main/webapp/WEB-INF/liferay-display.xml index ec7db79c1..96b13e6c4 100644 --- a/offer-manager-app/src/main/webapp/WEB-INF/liferay-display.xml +++ b/offer-manager-app/src/main/webapp/WEB-INF/liferay-display.xml @@ -3,6 +3,6 @@ - + diff --git a/offer-manager-domain/pom.xml b/offer-manager-domain/pom.xml index a5fdcfb41..0289cbb4d 100644 --- a/offer-manager-domain/pom.xml +++ b/offer-manager-domain/pom.xml @@ -7,7 +7,7 @@ offer-manager life.qbic - 1.0.0-SNAPSHOT + 1.0.0-alpha.3-SNAPSHOT diff --git a/pom.xml b/pom.xml index 576a056c2..6857fec60 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ offer-manager-app offer-manager - 1.0.0-SNAPSHOT + 1.0.0-alpha.3-SNAPSHOT life.qbic The new offer manager http://github.com/qbicsoftware/qOffer_2.0 From 460567c806d308910b6da0b22e416b1c76d46ed0 Mon Sep 17 00:00:00 2001 From: Sven Fillinger Date: Tue, 2 Mar 2021 08:59:49 +0100 Subject: [PATCH 02/40] Remove broken linting --- .github/workflows/qube_lint.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/qube_lint.yml diff --git a/.github/workflows/qube_lint.yml b/.github/workflows/qube_lint.yml deleted file mode 100644 index 1ec1386bf..000000000 --- a/.github/workflows/qube_lint.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: qube lint - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - matrix: - python: [3.7, 3.8] - - steps: - - uses: actions/checkout@v2 - name: Check out source-code repository - - - name: Setup Python - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python }} - - - name: Install qube - run: | - pip install qube - - - name: Run qube lint - run: | - qube lint . From 7fb984ebc7ec96504db4bf4358174d371e622cf3 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Tue, 2 Mar 2021 09:01:55 +0100 Subject: [PATCH 03/40] Implement fetch offer application logic (#348) This PR connects the previously introduced Fetch Offer Use Case to the Create Offer and Offer Overview Components. This is necessary since both components allow the independent download of the generated offer which should include additional information not stored in the database. Co-authored-by: Sven F. --- CHANGELOG.rst | 2 ++ .../offermanager/DependencyManager.groovy | 30 +++++++++++++--- .../offer/create/CreateOfferController.groovy | 14 +++++++- .../offer/create/CreateOfferPresenter.groovy | 9 +++-- .../offer/create/CreateOfferView.groovy | 2 +- .../offer/create/CreateOfferViewModel.groovy | 6 ++-- .../offer/create/OfferOverviewView.groovy | 20 +++++++---- .../overview/OfferOverviewController.groovy | 32 +++++++++++++++++ .../offer/overview/OfferOverviewModel.groovy | 22 ++---------- .../overview/OfferOverviewPresenter.groovy | 35 +++++++++++++++++++ .../offer/overview/OfferOverviewView.groovy | 27 +++++++------- .../offer/update/UpdateOfferViewModel.groovy | 1 + .../business/offers/fetch/FetchOffer.groovy | 10 +++--- .../offers/fetch/FetchOfferSpec.groovy | 2 +- 14 files changed, 157 insertions(+), 55 deletions(-) create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewController.groovy create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewPresenter.groovy diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 20d4e83b8..3c39d1a5d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,8 @@ the organisational roles project manager `Role.PROJECT_MANAGER` and offer admin .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 + **Fixed** * Update the agreement section of the offer (`#329 `_) 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 ebf231eea..a0c282d43 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 @@ -1,6 +1,7 @@ package life.qbic.portal.offermanager import groovy.util.logging.Log4j2 +import life.qbic.business.offers.fetch.FetchOffer import life.qbic.datamodel.dtos.business.AcademicTitle import life.qbic.datamodel.dtos.business.AffiliationCategory import life.qbic.business.customers.affiliation.create.CreateAffiliation @@ -9,6 +10,8 @@ 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.person.search.SearchPersonView import life.qbic.portal.offermanager.components.person.search.SearchPersonViewModel import life.qbic.portal.offermanager.components.person.update.UpdatePersonViewModel @@ -82,6 +85,7 @@ class DependencyManager { private CreateAffiliationPresenter createAffiliationPresenter private CreateOfferPresenter createOfferPresenter private CreateOfferPresenter updateOfferPresenter + private OfferOverviewPresenter offerOverviewPresenter private CustomerDbConnector customerDbConnector private OfferDbConnector offerDbConnector @@ -93,6 +97,9 @@ class DependencyManager { private CreateAffiliation createAffiliation private CreateOffer createOffer private CreateOffer updateOffer + private FetchOffer fetchOfferOfferOverview + private FetchOffer fetchOfferCreateOffer + private FetchOffer fetchOfferUpdateOffer private CreatePersonController createCustomerController private CreatePersonController updateCustomerController @@ -100,6 +107,7 @@ class DependencyManager { private CreateAffiliationController createAffiliationController private CreateOfferController createOfferController private CreateOfferController updateOfferController + private OfferOverviewController offerOverviewController private CreatePersonView createCustomerView private CreatePersonView updatePersonView @@ -261,8 +269,7 @@ class DependencyManager { } try { - this.offerOverviewModel = new OfferOverviewModel(overviewService, offerDbConnector, - viewModel, offerUpdateEvent) + this.offerOverviewModel = new OfferOverviewModel(overviewService, viewModel, offerUpdateEvent) } catch (Exception e) { log.error("Unexpected excpetion during ${OfferOverviewModel.getSimpleName()} view model setup.", e) } @@ -323,6 +330,11 @@ class DependencyManager { } catch (Exception e) { log.error("Unexpected exception during ${CreateOfferViewModel.getSimpleName()} setup", e) } + try { + this.offerOverviewPresenter = new OfferOverviewPresenter(this.viewModel, this.offerOverviewModel) + } catch (Exception e) { + log.error("Unexpected exception during ${OfferOverviewPresenter.getSimpleName()} setup", e) + } } private void setupUseCaseInteractors() { @@ -332,6 +344,9 @@ class DependencyManager { this.createOffer = new CreateOffer(offerDbConnector, createOfferPresenter) this.updateOffer = new CreateOffer(offerDbConnector, updateOfferPresenter) this.updateCustomer = new CreateCustomer(updateCustomerPresenter, customerDbConnector) + this.fetchOfferOfferOverview = new FetchOffer(offerDbConnector, offerOverviewPresenter) + this.fetchOfferCreateOffer = new FetchOffer(offerDbConnector, createOfferPresenter) + this.fetchOfferUpdateOffer = new FetchOffer(offerDbConnector, updateOfferPresenter) } private void setupControllers() { @@ -360,15 +375,20 @@ class DependencyManager { throw e } try { - this.createOfferController = new CreateOfferController(this.createOffer,this.createOffer) + this.createOfferController = new CreateOfferController(this.createOffer, this.fetchOfferCreateOffer, this.createOffer) } catch (Exception e) { log.error("Unexpected exception during ${CreateOfferController.getSimpleName()} setup", e) } try { - this.updateOfferController = new CreateOfferController(this.updateOffer,this.updateOffer) + this.updateOfferController = new CreateOfferController(this.updateOffer, this.fetchOfferUpdateOffer, this.updateOffer) } catch (Exception e) { log.error("Unexpected exception during ${CreateOfferController.getSimpleName()} setup", e) } + try { + this.offerOverviewController = new OfferOverviewController(this.fetchOfferOfferOverview) + } catch (Exception e) { + log.error("Unexpected exception during ${OfferOverviewController.getSimpleName()} setup", e) + } } private void setupViews() { @@ -431,7 +451,7 @@ class DependencyManager { OfferOverviewView overviewView try { - overviewView = new OfferOverviewView(offerOverviewModel) + overviewView = new OfferOverviewView(offerOverviewModel, offerOverviewController) } catch (Exception e) { log.error("Could not create ${OfferOverviewView.getSimpleName()} view.", e) throw e diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferController.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferController.groovy index 188050956..77c15334c 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferController.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferController.groovy @@ -1,6 +1,7 @@ package life.qbic.portal.offermanager.components.offer.create import groovy.util.logging.Log4j2 +import life.qbic.business.offers.fetch.FetchOfferInput import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.datamodel.dtos.business.Customer import life.qbic.datamodel.dtos.business.Offer @@ -24,10 +25,12 @@ class CreateOfferController { private final CreateOfferInput input private final CalculatePrice calculatePrice + private final FetchOfferInput fetchOfferInput - CreateOfferController(CreateOfferInput input, CalculatePrice calculatePrice){ + CreateOfferController(CreateOfferInput input, FetchOfferInput fetchOfferInput ,CalculatePrice calculatePrice){ this.input = input this.calculatePrice = calculatePrice + this.fetchOfferInput = fetchOfferInput } /** @@ -77,4 +80,13 @@ class CreateOfferController { throw new IllegalArgumentException("Could not calculate price from provided arguments.") } } + /** + * Triggers the FetchOffer use case on execution + * + * @param offerId The identifier of the offer to be fetched + */ + void fetchOffer(OfferId offerId) { + this.fetchOfferInput.fetchOffer(offerId) + } + } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferPresenter.groovy index 5f5172f23..125796776 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferPresenter.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferPresenter.groovy @@ -4,7 +4,7 @@ import life.qbic.datamodel.dtos.business.Offer import life.qbic.business.offers.create.CreateOfferOutput import life.qbic.portal.offermanager.dataresources.offers.OfferResourcesService import life.qbic.portal.offermanager.components.AppViewModel - +import life.qbic.business.offers.fetch.FetchOfferOutput /** * AppPresenter for the CreateOffer * @@ -12,7 +12,7 @@ import life.qbic.portal.offermanager.components.AppViewModel * * @since: 1.0.0 */ -class CreateOfferPresenter implements CreateOfferOutput { +class CreateOfferPresenter implements CreateOfferOutput, FetchOfferOutput{ private final AppViewModel viewModel private final CreateOfferViewModel createOfferViewModel @@ -29,6 +29,7 @@ class CreateOfferPresenter implements CreateOfferOutput { void createdNewOffer(Offer createdOffer) { this.viewModel.successNotifications.add("Created offer with title " + "\'${createdOffer.projectTitle}\' successfully") + this.offerService.addToResource(createdOffer) } @@ -50,4 +51,8 @@ class CreateOfferPresenter implements CreateOfferOutput { this.viewModel.failureNotifications.add(notification) } + @Override + void fetchedOffer(Offer fetchedOffer) { + this.createOfferViewModel.savedOffer = Optional.of(fetchedOffer) + } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferView.groovy index 78c70e488..df49e6428 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferView.groovy @@ -56,7 +56,7 @@ class CreateOfferView extends FormLayout{ this.projectManagerSelectionView = new ProjectManagerSelectionView(viewModel) this.selectItemsView = new SelectItemsView(viewModel,sharedViewModel) - this.overviewView = new OfferOverviewView(viewModel, offerProviderService) + this.overviewView = new OfferOverviewView(viewModel, controller, offerProviderService) initLayout() registerListeners() diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy index c8283961a..65f16bfe7 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy @@ -3,6 +3,7 @@ package life.qbic.portal.offermanager.components.offer.create import groovy.beans.Bindable import life.qbic.datamodel.dtos.business.Affiliation import life.qbic.datamodel.dtos.business.Customer +import life.qbic.datamodel.dtos.business.Offer import life.qbic.datamodel.dtos.business.OfferId import life.qbic.datamodel.dtos.business.ProjectManager import life.qbic.datamodel.dtos.business.services.* @@ -47,8 +48,9 @@ class CreateOfferViewModel { @Bindable double netPrice = 0 @Bindable double taxes = 0 @Bindable double overheads = 0 - @Bindable - double totalPrice = 0 + @Bindable double totalPrice = 0 + + Optional savedOffer = Optional.empty() private final CustomerResourceService customerService private final ProductsResourcesService productsResourcesService diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/OfferOverviewView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/OfferOverviewView.groovy index e8dcedb4d..5ac2cc79a 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/OfferOverviewView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/OfferOverviewView.groovy @@ -46,14 +46,16 @@ class OfferOverviewView extends VerticalLayout{ Button previous Button save Button downloadOffer + CreateOfferController createOfferController - - OfferOverviewView(CreateOfferViewModel viewModel, OfferResourcesService service){ + OfferOverviewView(CreateOfferViewModel viewModel, CreateOfferController controller, OfferResourcesService service){ this.createOfferViewModel = viewModel + this.createOfferController = controller initLayout() setUpGrid() service.subscribe((Offer offer) -> { try { + createOfferController.fetchOffer(offer.identifier) addOfferResource(offer) } catch (Exception e) { log.error("Unable to create the offer PDF resource.") @@ -203,16 +205,22 @@ class OfferOverviewView extends VerticalLayout{ button. */ removeExistingResources() + //Check if an Offer has been saved. + if (!createOfferViewModel.savedOffer.isPresent()) { + downloadOffer.setEnabled(false) + return + } // Then we create a new PDF resource ... - OfferToPDFConverter converter = new OfferToPDFConverter(offer) + OfferToPDFConverter converter = new OfferToPDFConverter(createOfferViewModel.savedOffer.get()) StreamResource offerResource = new StreamResource((StreamResource.StreamSource res) -> { - return converter.getOfferAsPdf() - }, "${offer.identifier.toString()}.pdf") + return converter.getOfferAsPdf() + }, "${offer.identifier.toString()}.pdf") // ... and attach it to the download button currentFileDownloader = new FileDownloader(offerResource) currentFileDownloader.extend(downloadOffer) downloadOffer.setEnabled(true) - } + } + private void removeExistingResources() { if (currentFileDownloader) { diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewController.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewController.groovy new file mode 100644 index 000000000..0f2b814a1 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewController.groovy @@ -0,0 +1,32 @@ +package life.qbic.portal.offermanager.components.offer.overview + +import groovy.util.logging.Log4j2 +import life.qbic.business.offers.fetch.FetchOfferInput +import life.qbic.datamodel.dtos.business.OfferId + +/** + * Controller class adapter from view information into use case input interface + * + * This class translates the information that was received from the OfferOverView into method calls to the use case + * + * @since: 1.0.0 + * + */ +@Log4j2 +class OfferOverviewController { + + private final FetchOfferInput input + + OfferOverviewController(FetchOfferInput input){ + this.input = input + } + + /** + * This method calls the Fetch Offer use case with the provided OfferId + * + * @param offerId The OfferId of the Offer to be retrieved + */ + void fetchOffer(OfferId offerId){ + this.input.fetchOffer(offerId) + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewModel.groovy index d3ae354e7..ab0d7b0e7 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewModel.groovy @@ -4,7 +4,6 @@ import groovy.beans.Bindable import life.qbic.datamodel.dtos.business.Offer import life.qbic.portal.offermanager.communication.EventEmitter import life.qbic.portal.offermanager.components.AppViewModel -import life.qbic.portal.offermanager.dataresources.offers.OfferDbConnector import life.qbic.portal.offermanager.OfferToPDFConverter import life.qbic.portal.offermanager.dataresources.offers.OverviewService import life.qbic.portal.offermanager.dataresources.offers.OfferOverview @@ -32,12 +31,10 @@ class OfferOverviewModel { */ Optional selectedOffer - private Optional offer + Optional offer private final OverviewService service - private final OfferDbConnector connector - private final AppViewModel viewModel private boolean downloadButtonActive @@ -47,11 +44,9 @@ class OfferOverviewModel { EventEmitter offerEventEmitter OfferOverviewModel(OverviewService service, - OfferDbConnector connector, AppViewModel viewModel, EventEmitter offerEventEmitter) { this.service = service - this.connector = connector this.offerOverviewList = new ObservableList(new ArrayList(service.iterator().toList())) this.selectedOffer = Optional.empty() this.viewModel = viewModel @@ -69,15 +64,8 @@ class OfferOverviewModel { }) } - void setSelectedOffer(OfferOverview selectedOffer) { - this.selectedOffer = Optional.ofNullable(selectedOffer) - this.downloadButtonActive = false - if (this.selectedOffer.isPresent()) { - this.offer = loadOfferInfo() - } - } - Offer getSelectedOffer() { + this.downloadButtonActive = false if(offer.isPresent()) { return offer.get() } else { @@ -98,10 +86,4 @@ class OfferOverviewModel { "convert.")}) } - private Optional loadOfferInfo() { - Optional offer = selectedOffer - .map({ connector.getOffer(it.offerId) }) - .orElse(Optional.empty()) - return offer - } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewPresenter.groovy new file mode 100644 index 000000000..ddc49698a --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewPresenter.groovy @@ -0,0 +1,35 @@ +package life.qbic.portal.offermanager.components.offer.overview + +import life.qbic.business.offers.fetch.FetchOfferOutput +import life.qbic.datamodel.dtos.business.Offer +import life.qbic.portal.offermanager.components.AppViewModel + + +/** + * AppPresenter for the FetchOffer Use Case + * + * This presenter handles the output of the FetchOffer use case and prepares it for a view. + * + * @since: 1.0.0 + */ +class OfferOverviewPresenter implements FetchOfferOutput { + + private final AppViewModel viewModel + private final OfferOverviewModel offerOverviewModel + + OfferOverviewPresenter(AppViewModel viewModel, OfferOverviewModel offerOverviewModel){ + this.viewModel = viewModel + this.offerOverviewModel = offerOverviewModel + } + + @Override + void fetchedOffer(Offer fetchedOffer) { + this.offerOverviewModel.offer = Optional.ofNullable(fetchedOffer) + } + + @Override + void failNotification(String notification) { + this.viewModel.failureNotifications.add(notification) + } + +} 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 5e7c07236..085e13bf8 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 @@ -38,6 +38,8 @@ class OfferOverviewView extends FormLayout { final private OfferOverviewModel model + final private OfferOverviewController offerOverviewController + final private Grid overviewGrid final private Button downloadBtn @@ -49,8 +51,9 @@ class OfferOverviewView extends FormLayout { private FileDownloader fileDownloader - OfferOverviewView(OfferOverviewModel model) { + OfferOverviewView(OfferOverviewModel model, OfferOverviewController offerOverviewController) { this.model = model + this.offerOverviewController = offerOverviewController this.overviewGrid = new Grid<>() this.downloadBtn = new Button(VaadinIcons.DOWNLOAD) this.updateOfferBtn = new Button(VaadinIcons.EDIT) @@ -155,6 +158,7 @@ class OfferOverviewView extends FormLayout { private void createResourceForDownload() { removeExistingResources() + StreamResource offerResource = new StreamResource((StreamResource.StreamSource res) -> { return model.getOfferAsPdf() @@ -188,17 +192,16 @@ class OfferOverviewView extends FormLayout { downloadBtn.setEnabled(false) updateOfferBtn.setEnabled(false) }) - - model.setSelectedOffer(offerOverview) - createResourceForDownload() - - ui.access(() -> { - downloadSpinner.setVisible(false) - overviewGrid.setEnabled(true) - downloadBtn.setEnabled(true) - updateOfferBtn.setEnabled(true) - ui.setPollInterval(-1) - }) + offerOverviewController.fetchOffer(offerOverview.offerId) + createResourceForDownload() + + ui.access(() -> { + downloadSpinner.setVisible(false) + overviewGrid.setEnabled(true) + downloadBtn.setEnabled(true) + updateOfferBtn.setEnabled(true) + ui.setPollInterval(-1) + }) } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/update/UpdateOfferViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/update/UpdateOfferViewModel.groovy index 96e7b6189..a6834199f 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/update/UpdateOfferViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/update/UpdateOfferViewModel.groovy @@ -52,5 +52,6 @@ class UpdateOfferViewModel extends CreateOfferViewModel{ super.productItems.clear() super.productItems.addAll(offer.items.collect { new ProductItemViewModel(it.quantity, it.product)}) + super.savedOffer = Optional.of(offer) } } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/fetch/FetchOffer.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/fetch/FetchOffer.groovy index 65759849e..8295d32a2 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/fetch/FetchOffer.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/fetch/FetchOffer.groovy @@ -31,22 +31,22 @@ class FetchOffer implements FetchOfferInput { void fetchOffer(OfferId offerId) { try { Optional foundOffer = dataSource.getOffer(offerId) - if (foundOffer.isEmpty()) { - output.failNotification("Could not find an Offer for the given OfferId $offerId") + if (!foundOffer.isPresent()) { + output.failNotification("Could not find an Offer for the given OfferId ${offerId.toString()}") } else { Offer finalOffer = generateOfferFromSource(foundOffer.get()) - log.info("Successfully retrieved Offer with Id $offerId") + log.info("Successfully retrieved Offer with Id ${offerId.toString()} ") output.fetchedOffer(finalOffer) } } catch (DatabaseQueryException queryException) { log.error(queryException.message) - output.failNotification("Could not retrieve Offer with OfferId $offerId from the Database") + output.failNotification("Could not retrieve Offer with OfferId ${offerId.toString()} from the Database") } catch (Exception e) { log.error(e.message) log.error(e.stackTrace.join("\n")) - output.failNotification("Unexpected error when searching for the Offer associated with the Id $offerId") + output.failNotification("Unexpected error when searching for the Offer associated with the Id ${offerId.toString()}") } } private static Offer generateOfferFromSource(Offer offer){ diff --git a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/fetch/FetchOfferSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/fetch/FetchOfferSpec.groovy index 37bd76553..8dc59c583 100644 --- a/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/fetch/FetchOfferSpec.groovy +++ b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/offers/fetch/FetchOfferSpec.groovy @@ -87,7 +87,7 @@ class FetchOfferSpec extends Specification { FetchOfferDataSource ds = Stub(FetchOfferDataSource.class) FetchOffer fetchOffer = new FetchOffer(ds, output) - ds.getOffer(offerId) >> { throw new DatabaseQueryException("Could not retrieve Offer with OfferId $offerId from the Database") } + ds.getOffer(offerId) >> { throw new DatabaseQueryException("Could not retrieve Offer with OfferId ${offerId.toString()} from the Database") } when: fetchOffer.fetchOffer(offerId) From 972cddb3b780b1f68fa1a85e64b80dd449c303d5 Mon Sep 17 00:00:00 2001 From: jnnfr Date: Tue, 2 Mar 2021 12:32:54 +0100 Subject: [PATCH 04/40] Update *Customer use cases to *Person (#345) This PR updates the CreateCustomer, UpdateCustomer and SearchCustomer use cases to be *Person use cases Co-authored-by: wow-such-code --- CHANGELOG.rst | 1 + .../offermanager/DependencyManager.groovy | 25 +-- .../create/CreateAffiliationController.groovy | 2 +- .../create/CreateAffiliationPresenter.groovy | 2 +- .../create/CreateAffiliationView.groovy | 4 +- .../create/CreatePersonController.groovy | 45 ++--- .../create/CreatePersonPresenter.groovy | 56 +++--- .../person/create/CreatePersonView.groovy | 130 +++++++------- .../create/CreatePersonViewModel.groovy | 3 +- .../person/search/SearchPersonView.groovy | 103 ++++++----- .../search/SearchPersonViewModel.groovy | 2 +- .../update/UpdatePersonViewModel.groovy | 2 +- .../offers/OfferDbConnector.groovy | 8 +- .../AffiliationResourcesService.groovy | 4 +- .../persons/CustomerResourceService.groovy | 6 +- ...nector.groovy => PersonDbConnector.groovy} | 170 +++++++++++------- .../persons/PersonResourceService.groovy | 6 +- .../ProjectManagerResourceService.groovy | 2 +- ...ec.groovy => PersonDbConnectorSpec.groovy} | 15 +- ...oovy => SearchPersonDataSourceSpec.groovy} | 10 +- ...oovy => CreatePersonControllerSpec.groovy} | 10 +- .../customers/create/CreateCustomer.groovy | 69 ------- .../create/CreateCustomerDataSource.groovy | 59 ------ .../create/CreateCustomerInput.groovy | 29 --- .../customers/search/SearchCustomer.groovy | 35 ---- .../search/SearchCustomerOutput.groovy | 24 --- .../customers/update/UpdateCustomer.groovy | 63 ------- .../affiliation/Country.groovy | 6 +- .../create/CreateAffiliation.groovy | 2 +- .../create/CreateAffiliationDataSource.groovy | 2 +- .../create/CreateAffiliationInput.groovy | 2 +- .../create/CreateAffiliationOutput.groovy | 2 +- .../affiliation/list/ListAffiliations.groovy | 2 +- .../list/ListAffiliationsDataSource.groovy | 2 +- .../list/ListAffiliationsInput.groovy | 2 +- .../list/ListAffiliationsOutput.groovy | 2 +- .../persons/create/CreatePerson.groovy | 67 +++++++ .../create/CreatePersonDataSource.groovy | 61 +++++++ .../persons/create/CreatePersonInput.groovy | 29 +++ .../create/CreatePersonOutput.groovy} | 10 +- .../persons/search/SearchPerson.groovy | 35 ++++ .../search/SearchPersonDataSource.groovy} | 22 +-- .../search/SearchPersonInput.groovy} | 8 +- .../persons/search/SearchPersonOutput.groovy | 24 +++ .../persons/update/UpdatePerson.groovy | 61 +++++++ .../update/UpdatePersonOutput.groovy} | 8 +- .../search/SearchCustomerSpec.groovy | 62 ------- .../update/UpdateCustomerInputSpec.groovy | 84 --------- .../create/CreatePersonSpec.groovy} | 26 +-- .../persons/search/SearchPersonSpec.groovy | 62 +++++++ .../update/UpdatePersonInputSpec.groovy | 84 +++++++++ 51 files changed, 787 insertions(+), 763 deletions(-) rename offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/persons/{CustomerDbConnector.groovy => PersonDbConnector.groovy} (82%) rename offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/{CustomerDbConnectorSpec.groovy => PersonDbConnectorSpec.groovy} (92%) rename offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/database/{SearchCustomerDataSourceSpec.groovy => SearchPersonDataSourceSpec.groovy} (89%) rename offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/web/controllers/{CreateCustomerControllerSpec.groovy => CreatePersonControllerSpec.groovy} (82%) delete mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomer.groovy delete mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerDataSource.groovy delete mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/customers/create/CreateCustomerInput.groovy delete mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomer.groovy delete mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/customers/search/SearchCustomerOutput.groovy delete mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/customers/update/UpdateCustomer.groovy rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers => persons}/affiliation/Country.groovy (86%) rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers => persons}/affiliation/create/CreateAffiliation.groovy (96%) rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers => persons}/affiliation/create/CreateAffiliationDataSource.groovy (91%) rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers => persons}/affiliation/create/CreateAffiliationInput.groovy (88%) rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers => persons}/affiliation/create/CreateAffiliationOutput.groovy (89%) rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers => persons}/affiliation/list/ListAffiliations.groovy (95%) rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers => persons}/affiliation/list/ListAffiliationsDataSource.groovy (87%) rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers => persons}/affiliation/list/ListAffiliationsInput.groovy (84%) rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers => persons}/affiliation/list/ListAffiliationsOutput.groovy (88%) create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePerson.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonDataSource.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/persons/create/CreatePersonInput.groovy rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers/create/CreateCustomerOutput.groovy => persons/create/CreatePersonOutput.groovy} (68%) create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPerson.groovy rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers/search/SearchCustomerDataSource.groovy => persons/search/SearchPersonDataSource.groovy} (54%) rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers/search/SearchCustomerInput.groovy => persons/search/SearchPersonInput.groovy} (64%) create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/persons/search/SearchPersonOutput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/persons/update/UpdatePerson.groovy rename offer-manager-domain/src/main/groovy/life/qbic/business/{customers/update/UpdateCustomerOutput.groovy => persons/update/UpdatePersonOutput.groovy} (54%) delete mode 100644 offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/search/SearchCustomerSpec.groovy delete mode 100644 offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/customers/update/UpdateCustomerInputSpec.groovy rename offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/{customers/create/CreateCustomerSpec.groovy => persons/create/CreatePersonSpec.groovy} (63%) create mode 100644 offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/search/SearchPersonSpec.groovy create mode 100644 offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/persons/update/UpdatePersonInputSpec.groovy diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3c39d1a5d..1f86559d6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,7 @@ upcoming service product maintenance interface. * Update the agreement section of the offer (`#329 `_) * Make the offer controls more intuitive (`#341 `_) +* Rename CreateCustomer and UpdateCustomer classes and methods (`#315 https://github.com/qbicsoftware/offer-manager-2-portlet/issues/315`_) **Dependencies** 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..5ab2d170b 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 @@ -4,8 +4,8 @@ import groovy.util.logging.Log4j2 import life.qbic.business.offers.fetch.FetchOffer 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 @@ -16,7 +16,7 @@ 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.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.database.DatabaseSession @@ -87,13 +87,13 @@ class DependencyManager { private CreateOfferPresenter updateOfferPresenter private OfferOverviewPresenter offerOverviewPresenter - private CustomerDbConnector customerDbConnector + private PersonDbConnector customerDbConnector private OfferDbConnector offerDbConnector private ProductsDbConnector productsDbConnector - 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 @@ -164,7 +164,7 @@ 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) @@ -338,12 +338,15 @@ class DependencyManager { } 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) 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/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/dataresources/offers/OfferDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferDbConnector.groovy index a11e03eb3..818a61a7c 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 @@ -40,9 +40,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/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/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/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/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/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 + } +} From 4cbe12d12435957f456386826658dba5e9126067 Mon Sep 17 00:00:00 2001 From: Sven F Date: Tue, 2 Mar 2021 14:03:28 +0100 Subject: [PATCH 05/40] Fix checksum calculation for offer comparison (#350) This PR fixes the checksum calculation. --- CHANGELOG.rst | 1 + .../offers/OfferDbConnector.groovy | 46 +++++++++++++++++-- .../qbic/business/offers/Converter.groovy | 1 + .../life/qbic/business/offers/Offer.groovy | 15 +++--- pom.xml | 2 +- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1f86559d6..5e6605947 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,7 @@ upcoming service product maintenance interface. * 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 https://github.com/qbicsoftware/offer-manager-2-portlet/issues/315`_) **Dependencies** 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 818a61a7c..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 @@ -34,7 +34,8 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ private static final String OFFER_INSERT_QUERY = "INSERT INTO offer (offerId, " + "creationDate, expirationDate, customerId, projectManagerId, projectTitle, " + - "projectObjective, totalPrice, customerAffiliationId, vat, netPrice, overheads)" + "projectObjective, totalPrice, customerAffiliationId, vat, netPrice, overheads, " + + "checksum)" private static final String OFFER_SELECT_QUERY = "SELECT offerId, creationDate, expirationDate, customerId, projectManagerId, projectTitle," + "projectObjective, totalPrice, customerAffiliationId, vat, netPrice, overheads FROM offer" @@ -48,6 +49,11 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ @Override void store(Offer offer) throws DatabaseQueryException { + + if (offerAlreadyInDataSource(offer)) { + throw new DatabaseQueryException("Offer with equal content of ${offer.identifier.toString()} already exists.") + } + Connection connection = connectionProvider.connect() connection.setAutoCommit(false) @@ -93,8 +99,6 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ ids.add(offerId) } - ids.each { println it } - return ids } }catch(Exception e){ @@ -106,14 +110,43 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ } -/** + private boolean offerAlreadyInDataSource(Offer offer) { + String query = "SELECT checksum FROM offer WHERE checksum=?" + Connection connection = null + boolean isAlreadyInDataSource = false + + try{ + connection = connectionProvider.connect() + connection.withCloseable { + PreparedStatement preparedStatement = it.prepareStatement(query) + preparedStatement.setString(1, "${offer.checksum}") + ResultSet resultSet = preparedStatement.executeQuery() + int numberOfRows = 0 + while(resultSet.next()) { + numberOfRows++ + } + if (numberOfRows > 0) { + isAlreadyInDataSource = true + } + } + }catch(Exception e){ + log.error(e.message) + log.error(e.stackTrace.join("\n")) + connection.rollback() + throw new DatabaseQueryException("Could not check if offer ${offer.identifier} is " + + "already in the database.") + } + return isAlreadyInDataSource + } + + /** * The method stores the offer in the QBiC database * * @param offer with the information of the offer to be stored * @return the id of the stored offer in the database */ private int storeOffer(Offer offer, int projectManagerId, int customerId, int affiliationId){ - String sqlValues = "VALUE(?,?,?,?,?,?,?,?,?,?,?,?)" + String sqlValues = "VALUE(?,?,?,?,?,?,?,?,?,?,?,?,?)" String queryTemplate = OFFER_INSERT_QUERY + " " + sqlValues def identifier = offer.identifier List generatedKeys = [] @@ -133,6 +166,7 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ preparedStatement.setDouble(10, offer.taxes) preparedStatement.setDouble(11, offer.netPrice) preparedStatement.setDouble(12, offer.overheads) + preparedStatement.setString(13, offer.checksum) preparedStatement.execute() @@ -228,6 +262,7 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ def selectedAffiliationId = resultSet.getInt("customerAffiliationId") def selectedAffiliation = customerGateway.getAffiliation(selectedAffiliationId) def items = productGateway.getItemsForOffer(offerPrimaryId) + def checksum = resultSet.getString("checksum") offer = Optional.of(new Offer.Builder( customer, @@ -243,6 +278,7 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ .taxes(vat) .overheads(overheads) .netPrice(net) + .checksum(checksum) .build()) } } 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 070a31b83..10cbf3bf4 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 @@ -38,6 +38,7 @@ class Converter { .totalPrice(offer.getTotalCosts()) .modificationDate(offer.modificationDate) .expirationDate(offer.expirationDate) + .checksum(offer.checksum()) .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 fc80cd534..e1aa37b3f 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 @@ -319,10 +319,10 @@ class Offer { * Returns the checksum of the current Offer Object */ String checksum(){ - //Use SHA-1 algorithm + //Use SHA-2 algorithm MessageDigest shaDigest = MessageDigest.getInstance("SHA-256") - //SHA-1 checksum + //SHA-2 checksum return getOfferChecksum(shaDigest, this) } @@ -339,13 +339,16 @@ class Offer { digest.update(offer.projectTitle.getBytes(StandardCharsets.UTF_8)) offer.items.each {item -> - digest.update(item.product.toString().getBytes(StandardCharsets.UTF_8)) + digest.update(item.product.productName.getBytes(StandardCharsets.UTF_8)) digest.update(item.quantity.toString().getBytes(StandardCharsets.UTF_8)) } - digest.update(offer.customer.toString().getBytes(StandardCharsets.UTF_8)) - digest.update(offer.projectManager.toString().getBytes(StandardCharsets.UTF_8)) + digest.update(offer.customer.lastName.getBytes(StandardCharsets.UTF_8)) + digest.update(offer.projectManager.lastName.getBytes(StandardCharsets.UTF_8)) - digest.update(offer.selectedCustomerAffiliation.toString().getBytes(StandardCharsets.UTF_8)) + digest.update(offer.selectedCustomerAffiliation.getOrganisation().toString().getBytes(StandardCharsets + .UTF_8)) + digest.update(offer.selectedCustomerAffiliation.getStreet().toString().getBytes(StandardCharsets + .UTF_8)) //Get the hash's bytes byte[] bytes = digest.digest() diff --git a/pom.xml b/pom.xml index 6857fec60..fb5530285 100644 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,7 @@ life.qbic data-model-lib - 2.1.0 + 2.2.0 com.vaadin From add4fbe35c2f6f6ed1ab9e8cbff32a1f843b55a4 Mon Sep 17 00:00:00 2001 From: Sven Fillinger Date: Wed, 3 Mar 2021 09:17:35 +0100 Subject: [PATCH 06/40] Prepare development for new version 1.0.0-alpha.4 --- CHANGELOG.rst | 24 +++++++++++++++++++++++- offer-manager-app/pom.xml | 4 ++-- offer-manager-domain/pom.xml | 2 +- pom.xml | 2 +- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5e6605947..bb9ee0879 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,29 @@ Changelog This project adheres to `Semantic Versioning `_. -1.0.0-alpha3 +Unassigned +---------- + +**Added** + +**Fixed** + +**Dependencies** + +**Deprecated** + +1.0.0-alpha.4 +-------------- + +**Added** + +**Fixed** + +**Dependencies** + +**Deprecated** + +1.0.0-alpha.3 -------------- **Added** diff --git a/offer-manager-app/pom.xml b/offer-manager-app/pom.xml index 7fed1d41f..16b58f223 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-SNAPSHOT + 1.0.0-alpha.4-SNAPSHOT 4.0.0 war @@ -15,7 +15,7 @@ life.qbic offer-manager-domain - 1.0.0-alpha.3-SNAPSHOT + 1.0.0-alpha.4-SNAPSHOT compile diff --git a/offer-manager-domain/pom.xml b/offer-manager-domain/pom.xml index 0289cbb4d..155682757 100644 --- a/offer-manager-domain/pom.xml +++ b/offer-manager-domain/pom.xml @@ -7,7 +7,7 @@ offer-manager life.qbic - 1.0.0-alpha.3-SNAPSHOT + 1.0.0-alpha.4-SNAPSHOT diff --git a/pom.xml b/pom.xml index fb5530285..b76e3c30b 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ offer-manager-app offer-manager - 1.0.0-alpha.3-SNAPSHOT + 1.0.0-alpha.4-SNAPSHOT life.qbic The new offer manager http://github.com/qbicsoftware/qOffer_2.0 From 52ccfdc53a75ceac3aab298e778321063f892cc9 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Thu, 4 Mar 2021 16:34:55 +0100 Subject: [PATCH 07/40] Introduce Distinction between Products with and without overhead costs in Offer PDF * Introduce distinction between overhead and nonoverhead item tables in Offer PDF with associated subtotal costs * Introduce underscore css classes for subtotal styling * Update Template html with subtotal tables * Introduce method for subtotal calculation of ProductItems and for distinction between ProductItems in Offer Entity * Introduce Tests for Converter and new fields in Offer Entity * Connect PDf converter with fetchOffer Use case Co-authored-by: jnnfr --- CHANGELOG.rst | 5 +- .../offermanager/OfferToPDFConverter.groovy | 190 ++++++++++++++---- .../main/resources/offer-template/offer.html | 35 ++-- .../resources/offer-template/stylesheet.css | 58 +++--- .../qbic/business/offers/Converter.groovy | 4 + .../life/qbic/business/offers/Offer.groovy | 95 ++++++++- .../portal/portlet/offers/OfferSpec.groovy | 39 ++++ .../offers/create/CreateOfferSpec.groovy | 44 ++++ 8 files changed, 389 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bb9ee0879..8b536b4b8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,9 @@ Unassigned **Added** +* Introduce subtotals in Offer PDF ProductItem Table(`#349 `_) +* Introduce distinction between ProductItems with and without Overhead cost in Offer PDF(`#349 `_) + **Fixed** **Dependencies** @@ -36,7 +39,7 @@ the organisational roles project manager `Role.PROJECT_MANAGER` and offer admin .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 +* Introduce Offer retrieval via Fetch Offer Use Case (`#344 `_) **Fixed** 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..da942bc21 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 @@ -39,7 +39,29 @@ 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" + + /** + * Is the preceding part of the element id used in the html document for sections associated with overhead cost + * If the String value is changed here it also has to be adapted in the html document + */ + static final String OVERHEAD = "overhead" + + /** + * Is the preceding part of the element id used in the html document for sections associated without an overhead cost + * If the String value is changed here, it also has to be adapted in the html document + */ + static final String NO_OVERHEAD = "no-overhead" + + /** + * 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 @@ -104,7 +126,7 @@ class OfferToPDFConverter implements OfferExporter { setCustomerInformation() setManagerInformation() setSelectedItems() - setPrices() + setTotalPrices() setQuotationDetails() } @@ -132,6 +154,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 +175,44 @@ 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 -> - - 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)) - + //htmlContent.getElementById("grid-table-footer").remove() + + List listOverheadItems = offer.itemsWithOverhead + List listNoOverheadItems = offer.itemsWithoutOverhead + + //Initialize Number of table + tableCount = 1 + + //Initialize Count of ProductItems in table + tableItemsCount = 1 + int maxTableItems = 8 + //Generate ProductTable for Overhead and Non-Overhead Product Items + generateProductTable(OVERHEAD, listOverheadItems, maxTableItems) + generateProductTable(NO_OVERHEAD, listNoOverheadItems, 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()) + } else { + //otherwise add total pricing to table + htmlContent.getElementById("item-table-grid") + .append(ItemPrintout.tableFooter()) + } } - //create the footer only for the last page containing a table - htmlContent.getElementById("item-table-grid") - .append(ItemPrintout.tableFooter()) - - } - 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) - htmlContent.getElementById("total-costs-net").text(netPrice_withSymbol) - htmlContent.getElementById("total-cost-value-net").text(netPrice) htmlContent.getElementById("vat-cost-value").text(taxes) htmlContent.getElementById("final-cost-value").text(totalPrice) @@ -201,6 +226,63 @@ class OfferToPDFConverter implements OfferExporter { htmlContent.getElementById("offer-date").text(dateFormat.format(offer.modificationDate)) } + void setIntermediatePrices(String overheadStatus) { + + double itemsNetPrice + double overheadPrice + double totalPrice + + if (overheadStatus == "overhead") { + itemsNetPrice = offer.getItemsWithOverheadNetPrice() + overheadPrice = offer.getOverheads() + totalPrice = itemsNetPrice + overheadPrice + } + else { + //No Overhead Prices are calculated for these items + overheadPrice = 0.00 + itemsNetPrice = offer.getItemsWithOverheadNetPrice() + totalPrice = itemsNetPrice + } + + final String formattedTotalPrice = Currency.getFormatterWithoutSymbol().format(totalPrice) + final String formattedItemsWithOverheadNetPrice = Currency.getFormatterWithoutSymbol().format(itemsNetPrice) + final String formattedOverheadPrice = Currency.getFormatterWithoutSymbol().format(overheadPrice) + + htmlContent.getElementById("${overheadStatus}-value-total").text(formattedTotalPrice) + htmlContent.getElementById("${overheadStatus}-value-net").text(formattedItemsWithOverheadNetPrice) + htmlContent.getElementById("${overheadStatus}-value-overhead").text(formattedOverheadPrice) + } + + void generateProductTable(String overheadStatus, List productItems, int maxTableItems){ + // Create the items in html in the overview table + if (!productItems.isEmpty()) { + // set initial product item position in table + def itemPos = 1 + // max number of table items per page + def elementId = "product-items" + "-" + tableCount + //Append Table Title + htmlContent.getElementById(elementId).append(ItemPrintout.tableTitle(overheadStatus)) + productItems.each { productItem -> + //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 + htmlContent.getElementById(elementId).append(ItemPrintout.itemInHTML(itemPos++, productItem)) + tableItemsCount++ + } + // Add subtotal Footer to ProductItem + htmlContent.getElementById(elementId).append(ItemPrintout.subTotalFooter(overheadStatus)) + tableItemsCount++ + //Update Pricing of Footer + setIntermediatePrices(overheadStatus) + } + } + /** * Small helper class to handle the HTML to PDF conversion. */ @@ -245,22 +327,45 @@ 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 subTotalFooter(String overheadStatus) { + + return """ +
+
+
Total Cost:
+
4000
+
+
+
Net Cost
+
3000
+
+
+
Overhead Cost:
+
1000
+
+
+ """ + } + + static String tableHeader(String elementId) { //1. add pagebreak //2. create empty table for elementId return """
@@ -276,6 +381,19 @@ class OfferToPDFConverter implements OfferExporter { """ } + static String tableTitle(String overheadStatus){ + String tableTitle + if (overheadStatus == "overhead"){ + tableTitle = "Products with Overhead" + } + else { + tableTitle = "Products without Overheads" + } + return """
+

${tableTitle}

+ """ + } + static String tableFooter(){ return """""" +
+ """ } - - } -} \ No newline at end of file + } +} 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..527678a3e 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..78c4add58 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,24 @@ 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; } - + .text-center{ text-align: center; } @@ -230,16 +230,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 +281,7 @@ body { padding-top: 15px; padding-bottom: 5px; } - + .col { -ms-flex-preferred-size: 0; flex-basis: 0; @@ -285,78 +289,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-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..f8ef76fe9 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,10 @@ class Converter { .modificationDate(offer.modificationDate) .expirationDate(offer.expirationDate) .checksum(offer.checksum()) + .itemsWithOverhead(offer.overheadItems) + .itemsWithoutOverhead(offer.noOverheadItems) + .itemsWithOverheadNet(offer.overheadItemsNet) + .itemsWithoutOverheadNet(offer.noOverheadItemsNet) .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..8f33c4738 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,6 +70,22 @@ 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 @@ -126,6 +145,7 @@ class Offer { return this } + Offer build() { return new Offer(this) } @@ -147,6 +167,10 @@ 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() } /** @@ -179,16 +203,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 +258,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 } 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() + } } From e9340aec9a3f2efad7b9f2ff021bc4c9b60accb2 Mon Sep 17 00:00:00 2001 From: jnnfr Date: Fri, 5 Mar 2021 10:16:14 +0100 Subject: [PATCH 08/40] Add product maintenance interfaces (#354) This PR adds the interfaces for the use cases concerning the service product maintenance Co-authored-by: Tobias Koch --- .../products/ProductDataSource.groovy | 39 +++++++++++++++++++ .../products/archive/ArchiveProduct.groovy | 20 ++++++++++ .../archive/ArchiveProductInput.groovy | 20 ++++++++++ .../archive/ArchiveProductOutput.groovy | 20 ++++++++++ .../business/products/copy/CopyProduct.groovy | 20 ++++++++++ .../products/copy/CopyProductInput.groovy | 19 +++++++++ .../products/copy/CopyProductOutput.groovy | 20 ++++++++++ .../products/create/CreateProduct.groovy | 25 ++++++++++++ .../products/create/CreateProductInput.groovy | 27 +++++++++++++ .../create/CreateProductOutput.groovy | 29 ++++++++++++++ 10 files changed, 239 insertions(+) create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/ProductDataSource.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProduct.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductInput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductOutput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductInput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductOutput.groovy 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..fafa53fe5 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProduct.groovy @@ -0,0 +1,20 @@ +package life.qbic.business.products.archive + +import life.qbic.datamodel.dtos.business.ProductId + +/** + *

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 { + @Override + void archive(ProductId productId) { + + } +} 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/copy/CopyProduct.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy new file mode 100644 index 000000000..4e91c38ec --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy @@ -0,0 +1,20 @@ +package life.qbic.business.products.copy + +import life.qbic.datamodel.dtos.business.ProductId + +/** + *

4.3.2 Copy Service Product

+ *
+ *

Offer Administrators are allowed to create a new permutation of an existing product. + *
New permutations can include changes in unit price, sequencing technology and other attributes of service products. + *

+ * + * @since: 1.0.0 + * + */ +class CopyProduct implements CopyProductInput { + @Override + void copy(ProductId productId) { + + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy new file mode 100644 index 000000000..8eba2b76b --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy @@ -0,0 +1,19 @@ +package life.qbic.business.products.copy + +import life.qbic.datamodel.dtos.business.ProductId + +/** + * Input interface for the {@link CopyProduct} use case + * + * @since: 1.0.0 + * + */ +interface CopyProductInput { + + /** + * The content of a product is going to be copied + * @param productId The id of the product that should be copied + * @since 1.0.0 + */ + void copy(ProductId productId) +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy new file mode 100644 index 000000000..3428c9b62 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy @@ -0,0 +1,20 @@ +package life.qbic.business.products.copy + +import life.qbic.business.UseCaseFailure +import life.qbic.datamodel.dtos.business.services.Product + +/** + * Output interface for the {@link CopyProduct} use case + * + * @since: 1.0.0 + * + */ +interface CopyProductOutput extends UseCaseFailure { + + /** + * A copy of a product has been created. This method is called after the copied product has been stored in the database. + * @param product The product that has been copied + * @since 1.0.0 + */ + void copied(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..25933bab2 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy @@ -0,0 +1,25 @@ +package life.qbic.business.products.create + +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 { + @Override + void create(Product product) { + + } + + @Override + void createDuplicate(Product product) { + + } +} 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..463ea5099 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductInput.groovy @@ -0,0 +1,27 @@ +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) + + /** + * Even though a duplicate product in the database exist a new product should be added. + * The new product will receive a new id that allows to differentiate it from the old product. The old id will be ignored. + * @param product The product that should be added + * @since 1.0.0 + */ + void createDuplicate(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) + + +} From 5ac3bb0f8eed873283142ca7bc1c76aeecdede47 Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Fri, 5 Mar 2021 12:52:43 +0100 Subject: [PATCH 09/40] Rewrite README.rst to be ReStructure formatted (#357) --- README.rst | 52 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 3d7dd8364..4b3d58c5b 100644 --- a/README.rst +++ b/README.rst @@ -40,8 +40,9 @@ Usage information Build the project with -.. code-block: bash -mvn clean package +.. code-block:: bash + + mvn clean package Deploy the created portlet in a Liferay instance. Make sure that the chromium is installed on the server, it is required for the download of the offer. @@ -50,31 +51,46 @@ Local testing -------------- Make sure to have chromium installed on your laptop. -You can do so via homebrew +In case you use a Mac, can do so via homebrew + +.. code-block:: bash + + brew install --cask chromium + +If you want to build the chromium browser from source please see the instructions on `the chromium website `_ +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. @@ -85,10 +101,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 From 3f4301d00a916b78742f55ce68f7955f5e32b7d0 Mon Sep 17 00:00:00 2001 From: Sven F Date: Mon, 8 Mar 2021 10:21:10 +0100 Subject: [PATCH 10/40] Add interfaces for the Create Project and Project Space use case (#355) This PR provides the interfaces for the use cases Create Project and Create Project Space. Co-authored-by: jnnfr Co-authored-by: Tobias Koch --- .../create/CreateProjectDataSource.groovy | 34 ++++++++++++++++ .../projects/create/CreateProjectInput.groovy | 23 +++++++++++ .../create/CreateProjectOutput.groovy | 40 +++++++++++++++++++ .../create/ProjectExistsException.groovy | 30 ++++++++++++++ .../CreateProjectSpaceDataSource.groovy | 27 +++++++++++++ .../spaces/CreateProjectSpaceInput.groovy | 23 +++++++++++ .../spaces/CreateProjectSpaceOutput.groovy | 35 ++++++++++++++++ .../spaces/ProjectSpaceExistsException.groovy | 30 ++++++++++++++ pom.xml | 2 +- 9 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectDataSource.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectInput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectOutput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/ProjectExistsException.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceDataSource.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceInput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpaceOutput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/ProjectSpaceExistsException.groovy 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..00a66947d --- /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.projectmanagement.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..115d7e86b --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectInput.groovy @@ -0,0 +1,23 @@ +package life.qbic.business.projects.create + +import life.qbic.datamodel.dtos.business.Offer + +/** + *

Input interface for the Create Project use case.

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

Creates a new project based on the offer information

+ *
+ *

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

+ * @param offer The offer with information about the planned project. + * @since 1.0.0 + */ + void createProject(Offer offer) + +} 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/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/pom.xml b/pom.xml index b76e3c30b..8deaa5093 100644 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,7 @@ life.qbic data-model-lib - 2.2.0 + 2.3.0-SNAPSHOT com.vaadin From b4a0aca19bf5303f1d6cc155b554aa3ad6cc7bfe Mon Sep 17 00:00:00 2001 From: jnnfr Date: Tue, 9 Mar 2021 10:27:04 +0100 Subject: [PATCH 11/40] Exchange offer DTO with project application (#368) Updates the offer DTO as input in the CreateProjectInput interface with ProjectApplication --- .../qbic/business/projects/create/CreateProjectInput.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 115d7e86b..bb33b53ee 100644 --- 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 @@ -1,6 +1,6 @@ package life.qbic.business.projects.create -import life.qbic.datamodel.dtos.business.Offer +import life.qbic.datamodel.dtos.projectmanagement.ProjectApplication /** *

Input interface for the Create Project use case.

@@ -15,9 +15,9 @@ interface CreateProjectInput { *

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

- * @param offer The offer with information about the planned project. + * @param projectApplication The project application with information about the planned project. * @since 1.0.0 */ - void createProject(Offer offer) + void createProject(ProjectApplication projectApplication) } From 25cfaebc2062a6eee21df2a2fd4d6c908e28f5a0 Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Tue, 9 Mar 2021 10:37:37 +0100 Subject: [PATCH 12/40] Archive Product (#363) * Implement ArchiveProduct * remove template description Co-authored-by: Tobias Koch Co-authored-by: jnnfr --- .../offermanager/OfferManagerApp.groovy | 1 + .../products/archive/ArchiveProduct.groovy | 22 ++++++ .../archive/ArchiveProductDataSource.groovy | 31 ++++++++ .../archive/ArchiveProductSpec.groovy | 78 +++++++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/archive/ArchiveProductDataSource.groovy create mode 100644 offer-manager-domain/src/test/groovy/life/qbic/business/products/archive/ArchiveProductSpec.groovy 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-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 index fafa53fe5..1e5feb5d5 100644 --- 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 @@ -1,6 +1,8 @@ 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

@@ -13,8 +15,28 @@ import life.qbic.datamodel.dtos.business.ProductId * */ 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/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..702e148b1 --- /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", "Test1234") + 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", "Test1234") + + 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", "Test1234") + 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) + } +} From d4316af6b7535cf6ee0ab300b63a190135f05ffb Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Tue, 9 Mar 2021 13:22:00 +0100 Subject: [PATCH 13/40] Add log methods that accept a throwable (#371) * Add log methods that accept a throwable * Add since tag to every logging method --- CHANGELOG.rst | 2 +- .../life/qbic/business/logging/Logger.groovy | 20 ++++++++++ .../life/qbic/business/logging/Logging.groovy | 39 ++++++++++++++++++- .../portal/portlet/logging/LoggerSpec.groovy | 8 ++++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8b536b4b8..87bf4a3a7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,7 +22,7 @@ Unassigned * Introduce subtotals in Offer PDF ProductItem Table(`#349 `_) * Introduce distinction between ProductItems with and without Overhead cost in Offer PDF(`#349 `_) - +* Add logging with throwable cause (`#371 `_) **Fixed** **Dependencies** 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/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() } From 60589cd17938ee907a852848b7346d4f58d4bf24 Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Thu, 11 Mar 2021 12:25:26 +0100 Subject: [PATCH 14/40] Feature Create Product (#369) * Change interfaces * Implement tests for the create method * Implement simple optimal case * Add check for data storage call * Improve JavaDoc Co-authored-by: jnnfr * Improve JavaDoc Co-authored-by: jnnfr * add links to java doc Co-authored-by: Tobias Koch Co-authored-by: name --- .../business/products/copy/CopyProduct.groovy | 20 ----- .../products/copy/CopyProductInput.groovy | 19 ---- .../products/copy/CopyProductOutput.groovy | 20 ----- .../products/create/CreateProduct.groovy | 24 ++++- .../create/CreateProductDataSource.groovy | 22 +++++ .../products/create/CreateProductInput.groovy | 7 -- .../create/ProductExistsException.groovy | 27 ++++++ .../products/create/CreateProductSpec.groovy | 90 +++++++++++++++++++ 8 files changed, 159 insertions(+), 70 deletions(-) delete mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy delete mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy delete mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/create/ProductExistsException.groovy create mode 100644 offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy deleted file mode 100644 index 4e91c38ec..000000000 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package life.qbic.business.products.copy - -import life.qbic.datamodel.dtos.business.ProductId - -/** - *

4.3.2 Copy Service Product

- *
- *

Offer Administrators are allowed to create a new permutation of an existing product. - *
New permutations can include changes in unit price, sequencing technology and other attributes of service products. - *

- * - * @since: 1.0.0 - * - */ -class CopyProduct implements CopyProductInput { - @Override - void copy(ProductId productId) { - - } -} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy deleted file mode 100644 index 8eba2b76b..000000000 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package life.qbic.business.products.copy - -import life.qbic.datamodel.dtos.business.ProductId - -/** - * Input interface for the {@link CopyProduct} use case - * - * @since: 1.0.0 - * - */ -interface CopyProductInput { - - /** - * The content of a product is going to be copied - * @param productId The id of the product that should be copied - * @since 1.0.0 - */ - void copy(ProductId productId) -} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy deleted file mode 100644 index 3428c9b62..000000000 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package life.qbic.business.products.copy - -import life.qbic.business.UseCaseFailure -import life.qbic.datamodel.dtos.business.services.Product - -/** - * Output interface for the {@link CopyProduct} use case - * - * @since: 1.0.0 - * - */ -interface CopyProductOutput extends UseCaseFailure { - - /** - * A copy of a product has been created. This method is called after the copied product has been stored in the database. - * @param product The product that has been copied - * @since 1.0.0 - */ - void copied(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 index 25933bab2..f16f4682c 100644 --- 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 @@ -1,5 +1,8 @@ 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 /** @@ -13,13 +16,26 @@ import life.qbic.datamodel.dtos.business.services.Product * */ class CreateProduct implements CreateProductInput { - @Override - void create(Product product) { + 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 createDuplicate(Product product) { - + 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 index 463ea5099..c28045753 100644 --- 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 @@ -17,11 +17,4 @@ interface CreateProductInput { */ void create(Product product) - /** - * Even though a duplicate product in the database exist a new product should be added. - * The new product will receive a new id that allows to differentiate it from the old product. The old id will be ignored. - * @param product The product that should be added - * @since 1.0.0 - */ - void createDuplicate(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/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..42fdb2474 --- /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", "ABCD1234") + 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" + } +} From 51150db8b7955c6fbb23e636a196ef7aa21e4045 Mon Sep 17 00:00:00 2001 From: Sven F Date: Thu, 11 Mar 2021 12:55:36 +0100 Subject: [PATCH 15/40] Create project UI draft (#374) This PR provides the user interface and model for the create project component. Co-authored-by: jnnfr --- CHANGELOG.rst | 4 + .../offermanager/DependencyManager.groovy | 24 +- .../offermanager/components/AppView.groovy | 12 +- .../offer/overview/OfferOverviewView.groovy | 48 +++- .../projectcreation/CreateProjectView.groovy | 237 ++++++++++++++++++ .../CreateProjectViewModel.groovy | 108 ++++++++ 6 files changed, 422 insertions(+), 11 deletions(-) create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectViewModel.groovy diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 87bf4a3a7..023d0e525 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,10 @@ Unassigned * Add logging with throwable cause (`#371 `_) **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** 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 5ab2d170b..e112a71df 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 @@ -12,6 +12,8 @@ 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.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 @@ -77,6 +79,7 @@ class DependencyManager { private OfferOverviewModel offerOverviewModel private SearchPersonViewModel searchPersonViewModel private CreatePersonViewModel createCustomerViewModelNewOffer + private CreateProjectViewModel createProjectModel private AppPresenter presenter private CreatePersonPresenter createCustomerPresenter @@ -115,6 +118,7 @@ class DependencyManager { private CreateAffiliationView createAffiliationView private AppView portletView private ConfigurationManager configurationManager + private CreateProjectView createProjectView private OverviewService overviewService private EventEmitter offerUpdateEvent @@ -279,6 +283,13 @@ class DependencyManager { }catch (Exception e) { log.error("Unexpected excpetion during ${SearchPersonViewModel.getSimpleName()} view model setup.", e) } + + try { + this.createProjectModel = new CreateProjectViewModel() + }catch (Exception e) { + log.error("Unexpected excpetion during ${CreateProjectViewModel.getSimpleName()} view model" + + " setup.", e) + } } private void setupPresenters() { @@ -452,9 +463,17 @@ class DependencyManager { throw e } + CreateProjectView createProjectView + try{ + createProjectView = new CreateProjectView(createProjectModel) + } 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 @@ -480,7 +499,8 @@ class DependencyManager { createOfferView, overviewView, updateOfferView, - searchPersonView + searchPersonView, + createProjectView ) this.portletView = portletView } catch (Exception e) { 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..0527cacc7 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,9 +6,11 @@ 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 @@ -32,7 +34,7 @@ class AppView extends VerticalLayout { private final List featureViews private final OfferOverviewView overviewView private final SearchPersonView searchPersonView - + private final CreateProjectView createProjectView private final CreateOfferView updateOfferView AppView(AppViewModel portletViewModel, @@ -41,7 +43,8 @@ class AppView extends VerticalLayout { CreateOfferView createOfferView, OfferOverviewView overviewView, CreateOfferView updateOfferView, - SearchPersonView searchPersonView) { + SearchPersonView searchPersonView, + CreateProjectView createProjectView) { super() this.portletViewModel = portletViewModel this.createCustomerView = createCustomerView @@ -51,6 +54,7 @@ class AppView extends VerticalLayout { this.overviewView = overviewView this.updateOfferView = updateOfferView this.searchPersonView = searchPersonView + this.createProjectView = createProjectView initLayout() registerListeners() @@ -71,7 +75,8 @@ class AppView extends VerticalLayout { createOfferView, overviewView, updateOfferView, - searchPersonView + searchPersonView, + createProjectView ]) } @@ -98,6 +103,7 @@ class AppView extends VerticalLayout { verticalLayout.addComponent(this.overviewView) verticalLayout.addComponent(this.updateOfferView) verticalLayout.addComponent(this.searchPersonView) + verticalLayout.addComponent(this.createProjectView) this.setSizeFull() this.addComponent(verticalLayout) 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..1b520b1fe 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,18 @@ 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) + }) + } + + private void setupGridListeners() { overviewGrid.addSelectionListener( { selection -> selection.firstSelectedItem.ifPresent({overview -> @@ -151,9 +177,6 @@ class OfferOverviewView extends FormLayout { new LoadOfferInfoThread(UI.getCurrent(), overview).start() }) }) - updateOfferBtn.addClickListener({ - model.offerEventEmitter.emit(model.getSelectedOffer()) - }) } private void createResourceForDownload() { @@ -186,20 +209,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/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..5db470226 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy @@ -0,0 +1,237 @@ +package life.qbic.portal.offermanager.components.offer.overview.projectcreation + +import com.vaadin.icons.VaadinIcons +import com.vaadin.ui.Button +import com.vaadin.ui.ComboBox +import com.vaadin.ui.HorizontalLayout +import com.vaadin.ui.Label +import com.vaadin.ui.RadioButtonGroup +import com.vaadin.ui.TextField +import com.vaadin.ui.VerticalLayout +import com.vaadin.ui.themes.ValoTheme +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 + + /** + * 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) { + this.model = createProjectModel + setupVaadinComponents() + configureListeners() + bindData() + } + + private void setupVaadinComponents() { + createSiteNavigation() + createTitle() + createProjectSpaceElements() + createProjectCodeElements() + createProjectIdOverview() + setupVisibility() + setupActivity() + } + + 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.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.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.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.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.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.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.addComponent(caption) + this.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) + } else { + existingSpaceLayout.setVisible(true) + customSpaceLayout.setVisible(false) + } + }) + 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) + }) + } + + private void bindData() { + availableSpacesBox.setDataProvider(model.availableSpaces) + } +} 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..c6a98b4ae --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectViewModel.groovy @@ -0,0 +1,108 @@ +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.HorizontalLayout +import com.vaadin.ui.Layout +import groovy.beans.Bindable +import life.qbic.datamodel.dtos.projectmanagement.ProjectCode +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace + +/** + *

Holds the create project state information and logic

+ * + *

View model for the {@link CreateProjectView}.

+ * + * @since 1.0.0 + */ +class CreateProjectViewModel { + + /** + * 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 + + @Bindable Boolean createProjectEnabled + + enum SPACE_SELECTION { + NEW_SPACE, EXISTING_SPACE + } + DataProvider spaceSelectionDataProvider + + @Bindable String resultingSpaceName + + @Bindable String desiredSpaceName + + DataProvider availableSpaces + + @Bindable String desiredProjectCode + + @Bindable String resultingProjectCode + + List existingProjects + + @Bindable String projectCodeValidationResult + + @Bindable Boolean codeIsValid + + CreateProjectViewModel() { + spaceSelectionDataProvider = new ListDataProvider<>([SPACE_SELECTION.NEW_SPACE, + SPACE_SELECTION.EXISTING_SPACE]) + resultingSpaceName = "" + desiredSpaceName = "" + desiredProjectCode = "" + resultingProjectCode = "" + projectCodeValidationResult = "" + codeIsValid = false + startedFromView = Optional.empty() + // TODO use space resource service once available + availableSpaces = new ListDataProvider([new ProjectSpace("Example Space One"), + new ProjectSpace("Example Space Two")]) + // TODO use project resource service once available + existingProjects = [ + new ProjectCode("QABCD"), + new ProjectCode("QTEST") + ] + createProjectEnabled = false + 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() + }) + } + + 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) + } +} From 8b528a4dd237a5d57a19ee60fe9e1d25a4f183bd Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Thu, 11 Mar 2021 15:24:06 +0100 Subject: [PATCH 16/40] Sort products into data generation, data analysis and project management in offer PDF(#364) * Remove subtotal calculation and tables and sort products into data generation, data analysis and project management Co-authored-by: Tobias Koch Co-authored-by: Sven F. --- CHANGELOG.rst | 4 +- .../offermanager/OfferToPDFConverter.groovy | 157 ++++++++---------- 2 files changed, 71 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 023d0e525..af08d9ce4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,8 +21,10 @@ Unassigned **Added** * Introduce subtotals in Offer PDF ProductItem Table(`#349 `_) -* Introduce distinction between ProductItems with and without Overhead cost in Offer PDF(`#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 `_) + **Fixed** * User cannot select other offers from the overview anymore, during the offer details are loaded 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 da942bc21..55d9be5c8 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 @@ -41,18 +46,6 @@ class OfferToPDFConverter implements OfferExporter { */ static final CHROMIUM_EXECUTABLE = "CHROMIUM_EXECUTABLE" - /** - * Is the preceding part of the element id used in the html document for sections associated with overhead cost - * If the String value is changed here it also has to be adapted in the html document - */ - static final String OVERHEAD = "overhead" - - /** - * Is the preceding part of the element id used in the html document for sections associated without an overhead cost - * If the String value is changed here, it also has to be adapted in the html document - */ - static final String NO_OVERHEAD = "no-overhead" - /** * Variable used to count the number of productItems in a productTable */ @@ -177,8 +170,7 @@ class OfferToPDFConverter implements OfferExporter { //and remove the footer on the first page //htmlContent.getElementById("grid-table-footer").remove() - List listOverheadItems = offer.itemsWithOverhead - List listNoOverheadItems = offer.itemsWithoutOverhead + List productItems = offer.items //Initialize Number of table tableCount = 1 @@ -186,9 +178,12 @@ class OfferToPDFConverter implements OfferExporter { //Initialize Count of ProductItems in table tableItemsCount = 1 int maxTableItems = 8 - //Generate ProductTable for Overhead and Non-Overhead Product Items - generateProductTable(OVERHEAD, listOverheadItems, maxTableItems) - generateProductTable(NO_OVERHEAD, listNoOverheadItems, maxTableItems) + + //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) { @@ -226,63 +221,59 @@ class OfferToPDFConverter implements OfferExporter { htmlContent.getElementById("offer-date").text(dateFormat.format(offer.modificationDate)) } - void setIntermediatePrices(String overheadStatus) { + Map groupItems(List productItems) { - double itemsNetPrice - double overheadPrice - double totalPrice + def productItemsMap = [:] - if (overheadStatus == "overhead") { - itemsNetPrice = offer.getItemsWithOverheadNetPrice() - overheadPrice = offer.getOverheads() - totalPrice = itemsNetPrice + overheadPrice - } - else { - //No Overhead Prices are calculated for these items - overheadPrice = 0.00 - itemsNetPrice = offer.getItemsWithOverheadNetPrice() - totalPrice = itemsNetPrice + 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) + } } - final String formattedTotalPrice = Currency.getFormatterWithoutSymbol().format(totalPrice) - final String formattedItemsWithOverheadNetPrice = Currency.getFormatterWithoutSymbol().format(itemsNetPrice) - final String formattedOverheadPrice = Currency.getFormatterWithoutSymbol().format(overheadPrice) + //Map Lists to the "DataGeneration", "DataAnalysis" and "Project and Data Management" + productItemsMap.dataGeneration = dataGenerationItems + productItemsMap.dataAnalysis = dataAnalysisItems + productItemsMap.dataManagement= dataManagementItems - htmlContent.getElementById("${overheadStatus}-value-total").text(formattedTotalPrice) - htmlContent.getElementById("${overheadStatus}-value-net").text(formattedItemsWithOverheadNetPrice) - htmlContent.getElementById("${overheadStatus}-value-overhead").text(formattedOverheadPrice) + return productItemsMap } - void generateProductTable(String overheadStatus, List productItems, int maxTableItems){ + void generateProductTable(Map productItemsMap, int maxTableItems) { // Create the items in html in the overview table - if (!productItems.isEmpty()) { - // set initial product item position in table - def itemPos = 1 - // max number of table items per page - def elementId = "product-items" + "-" + tableCount - //Append Table Title - htmlContent.getElementById(elementId).append(ItemPrintout.tableTitle(overheadStatus)) - productItems.each { productItem -> - //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 + productItemsMap.each { productGroup, 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.toString())) + items.eachWithIndex { item, 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 + htmlContent.getElementById(elementId).append(ItemPrintout.itemInHTML(itemPos, item as ProductItem)) + tableItemsCount++ } - //add product to current table - htmlContent.getElementById(elementId).append(ItemPrintout.itemInHTML(itemPos++, productItem)) - tableItemsCount++ } - // Add subtotal Footer to ProductItem - htmlContent.getElementById(elementId).append(ItemPrintout.subTotalFooter(overheadStatus)) - tableItemsCount++ - //Update Pricing of Footer - setIntermediatePrices(overheadStatus) } } - /** * Small helper class to handle the HTML to PDF conversion. */ @@ -345,26 +336,6 @@ class OfferToPDFConverter implements OfferExporter { } - static String subTotalFooter(String overheadStatus) { - - return """ -
-
-
Total Cost:
-
4000
-
-
-
Net Cost
-
3000
-
-
-
Overhead Cost:
-
1000
-
-
- """ - } - static String tableHeader(String elementId) { //1. add pagebreak //2. create empty table for elementId @@ -381,26 +352,33 @@ class OfferToPDFConverter implements OfferExporter { """ } - static String tableTitle(String overheadStatus){ - String tableTitle - if (overheadStatus == "overhead"){ - tableTitle = "Products with Overhead" + static String tableTitle(String productGroup){ + + String tableTitle = "" + + if (productGroup == "dataGeneration"){ + tableTitle = "Data Generation" + } + if (productGroup == "dataAnalysis"){ + tableTitle = "Data Analysis" } - else { - tableTitle = "Products without Overheads" + if (productGroup == "dataManagement"){ + tableTitle = "Data Management" } + return """

${tableTitle}

""" } static String tableFooter(){ + return """
VAT (19%):
0.00
@@ -412,5 +390,6 @@ class OfferToPDFConverter implements OfferExporter {
""" } + } } From 6aded6f4516ecbe7b6cdbc29ec2f107b6fde347a Mon Sep 17 00:00:00 2001 From: jnnfr Date: Thu, 11 Mar 2021 15:26:52 +0100 Subject: [PATCH 17/40] Implements the domain logic of CreateProject and CreateProjectSpace(#370) This PR implements the CreateProject and the CreateProjectSpace use cases and links them. Co-authored-by: Sven F. --- .../projects/create/CreateProject.groovy | 80 +++++++++++ .../projects/create/CreateProjectInput.groovy | 13 +- .../projects/spaces/CreateProjectSpace.groovy | 49 +++++++ .../projects/CreateProjectSpaceSpec.groovy | 73 +++++++++++ .../portlet/projects/CreateProjectSpec.groovy | 124 ++++++++++++++++++ 5 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProject.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/projects/spaces/CreateProjectSpace.groovy create mode 100644 offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/projects/CreateProjectSpaceSpec.groovy create mode 100644 offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/projects/CreateProjectSpec.groovy 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..77382f407 --- /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.projectmanagement.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/CreateProjectInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/projects/create/CreateProjectInput.groovy index bb33b53ee..9ca2f8cc5 100644 --- 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 @@ -10,7 +10,7 @@ import life.qbic.datamodel.dtos.projectmanagement.ProjectApplication interface CreateProjectInput { /** - *

Creates a new project based on the offer information

+ *

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

*
*

Calling this method executes the Create Project use case. * The output will be returned via the {@link CreateProjectOutput} interface. @@ -20,4 +20,15 @@ interface CreateProjectInput { */ void createProject(ProjectApplication projectApplication) + /** + *

Creates a new project and a project space based on a {@link life.qbic.datamodel.dtos.projectmanagement.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/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/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..8226fd251 --- /dev/null +++ b/offer-manager-domain/src/test/groovy/life/qbic/portal/portlet/projects/CreateProjectSpec.groovy @@ -0,0 +1,124 @@ +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.projectmanagement.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 project = new Project(projectIdentifier,projectApplication.projectTitle,offerId) + + 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 project = new Project(projectIdentifier,projectApplication.projectTitle,offerId) + + 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(_,_) + } +} From db7801482fe92534a905dc2bb547957bd4aa59b7 Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Fri, 12 Mar 2021 12:52:52 +0100 Subject: [PATCH 18/40] Adjust import statements for new data-model-lib (#378) * Adjust import statements for new data-model-lib * Adjust tests to use the Project.Builder --- .../qbic/business/projects/create/CreateProject.groovy | 3 ++- .../projects/create/CreateProjectDataSource.groovy | 3 +-- .../business/projects/create/CreateProjectInput.groovy | 7 +++---- .../portal/portlet/projects/CreateProjectSpec.groovy | 10 +++++++--- 4 files changed, 13 insertions(+), 10 deletions(-) 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 index 77382f407..229decd4c 100644 --- 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 @@ -8,7 +8,8 @@ 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.projectmanagement.ProjectApplication +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 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 index 00a66947d..d88f008d2 100644 --- 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 @@ -3,8 +3,7 @@ package life.qbic.business.projects.create import life.qbic.business.exceptions.DatabaseQueryException import life.qbic.datamodel.dtos.projectmanagement.Project -import life.qbic.datamodel.dtos.projectmanagement.ProjectApplication - +import life.qbic.datamodel.dtos.business.ProjectApplication /** *

Access to the project management datasource

* 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 index 9ca2f8cc5..d227c24d4 100644 --- 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 @@ -1,7 +1,6 @@ package life.qbic.business.projects.create -import life.qbic.datamodel.dtos.projectmanagement.ProjectApplication - +import life.qbic.datamodel.dtos.business.ProjectApplication /** *

Input interface for the Create Project use case.

* @@ -10,7 +9,7 @@ import life.qbic.datamodel.dtos.projectmanagement.ProjectApplication interface CreateProjectInput { /** - *

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

+ *

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. @@ -21,7 +20,7 @@ interface CreateProjectInput { void createProject(ProjectApplication projectApplication) /** - *

Creates a new project and a project space based on a {@link life.qbic.datamodel.dtos.projectmanagement.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. 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 index 8226fd251..eb5fbe6b0 100644 --- 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 @@ -10,7 +10,7 @@ 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.projectmanagement.ProjectApplication +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 @@ -58,7 +58,9 @@ class CreateProjectSpec extends Specification{ 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 project = new Project(projectIdentifier,projectApplication.projectTitle,offerId) + 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 @@ -107,7 +109,9 @@ class CreateProjectSpec extends Specification{ 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 project = new Project(projectIdentifier,projectApplication.projectTitle,offerId) + 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 From 8a9f7a78d5ee73f12536ccb0b70cdf8e7018cc77 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Mon, 15 Mar 2021 10:07:58 +0100 Subject: [PATCH 19/40] Introduce subtotal footers to product groups in Offer PDF (#376) * Introduce subtotal Footers to each productgroup Co-authored-by: jnnfr --- .../offermanager/OfferToPDFConverter.groovy | 76 ++++++++++++++----- .../main/resources/offer-template/offer.html | 12 +-- .../resources/offer-template/stylesheet.css | 6 ++ 3 files changed, 69 insertions(+), 25 deletions(-) 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 55d9be5c8..871bfc0ac 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 @@ -83,6 +83,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") @@ -223,7 +245,7 @@ class OfferToPDFConverter implements OfferExporter { Map groupItems(List productItems) { - def productItemsMap = [:] + Map> productItemsMap = [:] List dataGenerationItems = [] List dataAnalysisItems = [] @@ -244,22 +266,22 @@ class OfferToPDFConverter implements OfferExporter { } //Map Lists to the "DataGeneration", "DataAnalysis" and "Project and Data Management" - productItemsMap.dataGeneration = dataGenerationItems - productItemsMap.dataAnalysis = dataAnalysisItems - productItemsMap.dataManagement= dataManagementItems + 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 { productGroup, items -> + 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.toString())) - items.eachWithIndex { item, itemPos -> + 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 @@ -268,9 +290,12 @@ class OfferToPDFConverter implements OfferExporter { tableItemsCount = 1 } //add product to current table - htmlContent.getElementById(elementId).append(ItemPrintout.itemInHTML(itemPos, item as ProductItem)) + htmlContent.getElementById(elementId).append(ItemPrintout.itemInHTML(itemPos, item)) tableItemsCount++ } + //add subtotal footer to table + htmlContent.getElementById(elementId).append(ItemPrintout.subTableFooter(productGroup)) + tableItemsCount++ } } } @@ -352,25 +377,38 @@ class OfferToPDFConverter implements OfferExporter { """ } - static String tableTitle(String productGroup){ + static String tableTitle(ProductGroups productGroup){ - String tableTitle = "" + String tableTitle= productGroup.getName() - if (productGroup == "dataGeneration"){ - tableTitle = "Data Generation" - } - if (productGroup == "dataAnalysis"){ - tableTitle = "Data Analysis" - } - if (productGroup == "dataManagement"){ - tableTitle = "Data Management" - } return """

${tableTitle}

""" } + static String subTableFooter(ProductGroups productGroup){ + + String footerTitle = productGroup.getName() + + return """ + """ + } + static String tableFooter(){ return """ -
+
Total Cost:
-
4000
+
4000
-
+
Net Cost
-
3000
+
3000
-
+
Overhead Cost:
-
1000
+
1000