diff --git a/flex-training-flexinale/.gitignore b/flex-training-flexinale/.gitignore
new file mode 100644
index 0000000..65e9f4e
--- /dev/null
+++ b/flex-training-flexinale/.gitignore
@@ -0,0 +1,56 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### IntelliJ
+.idea/
+out/
+*.iml
+*.ipr
+*.iws
+target/
+
+~*xlsx
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
diff --git a/flex-training-flexinale/LICENSE b/flex-training-flexinale/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/flex-training-flexinale/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/flex-training-flexinale/README.md b/flex-training-flexinale/README.md
new file mode 100644
index 0000000..64dc0c6
--- /dev/null
+++ b/flex-training-flexinale/README.md
@@ -0,0 +1,14 @@
+# flex-training-flexinale
+iSAQB Advanced Level FLEX training: This code contains our case study for the "flexinale" example.
+This is the main repo containing blueprint and solution.
+
+## Architecture as a monolithic system
+This part is in the folder flexinale-monolith
+
+## Architecture as a modulithic system
+Part 1 is in the folder flexinale-modulith-1-onion
+Part 2 is in the folder flexinale-modulith-1-components
+
+## Architecture as a distributed, service-oriented, event-based system
+This part is in the folder flexinale-distributed
+
diff --git a/flex-training-flexinale/flexinale-distributed/README.md b/flex-training-flexinale/flexinale-distributed/README.md
new file mode 100644
index 0000000..ae9ef11
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/README.md
@@ -0,0 +1,6 @@
+# flex-training-filmfestival
+iSAQB Advanced Level FLEX training: This code contains our case study code for the "filmfestival" example.
+This is the main repo containing blueprint and solution.
+
+## Architecture as a distributed, service-oriented, event-based system
+This part is in the folder flexinale-distributed
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/SpotbugsExcludeFilter.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/SpotbugsExcludeFilter.xml
new file mode 100644
index 0000000..ecf3aeb
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/SpotbugsExcludeFilter.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/Dockerfile b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/Dockerfile
new file mode 100644
index 0000000..39721bb
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/Dockerfile
@@ -0,0 +1,8 @@
+FROM amazoncorretto:21.0.1-alpine3.18
+
+WORKDIR /app
+COPY ../../target/flexinale-distributed-backoffice-2024.3.0-spring-boot-fat-jar.jar /app
+
+EXPOSE 8081
+
+CMD ["java", "-jar", "flexinale-distributed-backoffice-2024.3.0-spring-boot-fat-jar.jar"]
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/docker_build.sh b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/docker_build.sh
new file mode 100644
index 0000000..6ce54f5
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/docker_build.sh
@@ -0,0 +1 @@
+docker build -t de.accso/flexinale-distributed-backoffice:2024.3.0 -f Dockerfile ../../
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/Dockerfile b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/Dockerfile
new file mode 100644
index 0000000..39721bb
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/Dockerfile
@@ -0,0 +1,8 @@
+FROM amazoncorretto:21.0.1-alpine3.18
+
+WORKDIR /app
+COPY ../../target/flexinale-distributed-backoffice-2024.3.0-spring-boot-fat-jar.jar /app
+
+EXPOSE 8081
+
+CMD ["java", "-jar", "flexinale-distributed-backoffice-2024.3.0-spring-boot-fat-jar.jar"]
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/podman_build.bat b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/podman_build.bat
new file mode 100644
index 0000000..3466b89
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/podman_build.bat
@@ -0,0 +1 @@
+podman build -t de.accso/flexinale-distributed-backoffice:2024.3.0 -f Dockerfile ../../
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/pom.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/pom.xml
new file mode 100644
index 0000000..c522a9c
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/pom.xml
@@ -0,0 +1,167 @@
+
+
+ 4.0.0
+
+
+ de.accso
+ flexinale-distributed
+ 2024.3.0
+ ../pom.xml
+
+
+ flexinale-distributed-backoffice
+ 2024.3.0
+ Flexinale Distributed Backoffice
+ Flexinale - FLEX case-study "film festival", distributed services, backoffice
+
+ jar
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+ ${micrometer.version}
+
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang3.version}
+
+
+
+ org.postgresql
+ postgresql
+ ${org-postgres.version}
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
+ org.apache.poi
+ poi
+ ${apache-poi.version}
+
+
+ org.apache.poi
+ poi-ooxml
+ ${apache-poi.version}
+
+
+
+ de.accso
+ flexinale-distributed-common
+ 2024.3.0
+
+
+ de.accso
+ flexinale-distributed-common
+ 2024.3.0
+ test-jar
+ test
+
+
+ de.accso
+ flexinale-distributed-security
+ runtime
+ 2024.3.0
+
+
+ de.accso
+ flexinale-distributed-security_api_contract
+ 2024.3.0
+
+
+ de.accso
+ flexinale-distributed-backoffice_api_contract
+ 2024.3.0
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ spring-boot-fat-jar
+
+
+
+
+ build-info
+
+
+
+ Distributed
+ Backoffice
+
+
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs-maven-plugin.version}
+
+
+
+ com.h3xstream.findsecbugs
+ findsecbugs-plugin
+ ${findsecbugs-maven-plugin.version}
+
+
+ SpotbugsExcludeFilter.xml
+
+
+
+
+ com.github.spotbugs
+ spotbugs
+ ${spotbugs.version}
+
+
+
+
+ verify
+
+ check
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed - start Backoffice app (_FlexinaleDistributedApplicationBackoffice_).run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed - start Backoffice app (_FlexinaleDistributedApplicationBackoffice_).run.xml
new file mode 100644
index 0000000..b19d135
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed - start Backoffice app (_FlexinaleDistributedApplicationBackoffice_).run.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Events Published.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Events Published.run.xml
new file mode 100644
index 0000000..42efdf7
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Events Published.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Health.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Health.run.xml
new file mode 100644
index 0000000..b2f3088
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Health.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Info.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Info.run.xml
new file mode 100644
index 0000000..8198fa7
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Info.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Metrics.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Metrics.run.xml
new file mode 100644
index 0000000..8e365df
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Metrics.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Prometheus.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Prometheus.run.xml
new file mode 100644
index 0000000..05de5cd
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Prometheus.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBackoffice.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBackoffice.java
new file mode 100644
index 0000000..ad5a2ef
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBackoffice.java
@@ -0,0 +1,23 @@
+package de.accso.flexinale;
+
+import jakarta.persistence.EntityManagerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Profile;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.transaction.PlatformTransactionManager;
+
+@SpringBootApplication
+@Profile({"!test-integrated &!test-distributed & !testdata"})
+public class FlexinaleDistributedApplicationBackoffice {
+
+ public static void main(String[] args) {
+ SpringApplication.run(FlexinaleDistributedApplicationBackoffice.class, args);
+ }
+
+ @Bean
+ public PlatformTransactionManager transactionManager(final EntityManagerFactory entityManagerFactory) {
+ return new JpaTransactionManager(entityManagerFactory);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/ExcelHelper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/ExcelHelper.java
new file mode 100644
index 0000000..2b773d8
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/ExcelHelper.java
@@ -0,0 +1,15 @@
+package de.accso.flexinale.backoffice.api_in.rest;
+
+import org.apache.poi.poifs.filesystem.FileMagic;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+final class ExcelHelper {
+ private ExcelHelper() {
+ }
+
+ static boolean isExcelFile(final InputStream stream) throws IOException {
+ return (FileMagic.valueOf(stream) == FileMagic.OOXML);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/FilmRestController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/FilmRestController.java
new file mode 100644
index 0000000..ea9525f
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/FilmRestController.java
@@ -0,0 +1,130 @@
+package de.accso.flexinale.backoffice.api_in.rest;
+
+import de.accso.flexinale.backoffice.api_out.event.FilmPublisher;
+import de.accso.flexinale.backoffice.api_out.event.VorfuehrungPublisher;
+import de.accso.flexinale.backoffice.application.services.FilmUploadService;
+import de.accso.flexinale.backoffice.domain.model.Film;
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+import de.accso.flexinale.backoffice.application.services.FilmService;
+import de.accso.flexinale.backoffice.application.services.VorfuehrungService;
+import de.accso.flexinale.common.application.PersistMode;
+import de.accso.flexinale.common.application.PersistedEntityAndResult;
+import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.ADDED;
+import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.UPDATED;
+
+@RestController
+@Profile({ "!testdata-besucherportal & !testdata-ticketing" })
+public class FilmRestController {
+ private static final Logger LOGGER = LoggerFactory.getLogger(FilmRestController.class);
+
+ @Autowired
+ private FilmService filmService;
+
+ @Autowired
+ private FilmUploadService filmUploadService;
+
+ @Autowired
+ private FilmPublisher filmPublisher;
+
+ @Autowired
+ private VorfuehrungService vorfuehrungService;
+
+ @Autowired
+ private VorfuehrungPublisher vorfuehrungPublisher;
+
+ @GetMapping(value = "/rest/filme", produces = "application/json")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ public List filme() {
+ return filmService.filme();
+ }
+
+ @GetMapping(value = "/rest/film/{id}", produces = "application/json")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ public Film film(@PathVariable final String id) {
+ Identifiable.Id filmId = Identifiable.Id.of(id);
+ Film film = filmService.film(filmId);
+ if (film == null) {
+ throw new RestResourceNotFoundException("no Film %s found.".formatted(id));
+ }
+
+ return film;
+ }
+
+ // -----------------------------------------------------------------------------------------------------------
+
+ ResponseEntity loadAndPersistAndPublishNewFilme(
+ final BufferedInputStream stream, final String originalFilename) throws IOException
+ {
+ // load and persist
+ Collection> addedAndUpdatedFilms =
+ filmUploadService.loadDataFromExcelSheetAndPersistAndReturn(stream, PersistMode.UPDATE);
+
+ List addedFilms = addedAndUpdatedFilms.stream()
+ .filter(filmWithResult -> filmWithResult.result().equals(ADDED))
+ .map(PersistedEntityAndResult::entity)
+ .toList();
+ List updatedFilms = addedAndUpdatedFilms.stream()
+ .filter(filmWithResult -> filmWithResult.result().equals(UPDATED))
+ .map(PersistedEntityAndResult::entity)
+ .toList();
+
+ publishFilmsAndVorfuehrungen(addedFilms, updatedFilms);
+
+ String message = "Uploaded the Excel file %s adding %d new Film entities and updating %d existing Film entities."
+ .formatted(originalFilename, addedFilms.size(), updatedFilms.size());
+
+ RestUploadResult restUploadResult = new RestUploadResult(HttpStatus.OK, message);
+ return ResponseEntity.status(restUploadResult.status())
+ .body(new RestResponseMessage(restUploadResult.message()));
+ }
+
+ private void publishFilmsAndVorfuehrungen(final List addedFilms, final List updatedFilms) {
+ filmPublisher.publishNewFilms(addedFilms);
+ filmPublisher.publishUpdatedFilms(updatedFilms);
+
+ List vorfuehrungen = vorfuehrungService.vorfuehrungen();
+ for (Film updatedFilm: updatedFilms) {
+ List vorfuehrungenToPublishAsUpdated = vorfuehrungen.stream()
+ .filter(vorfuehrung -> vorfuehrung.film.id.equals(updatedFilm.id))
+ .toList();
+ vorfuehrungPublisher.publishUpdatedVorfuehrungen(vorfuehrungenToPublishAsUpdated);
+ }
+ }
+
+ @PostMapping("/rest/filme")
+ @PreAuthorize("hasRole('ROLE_ADMIN')")
+ public ResponseEntity uploadFilme(@RequestParam("file") final MultipartFile file) {
+ try (BufferedInputStream stream = new BufferedInputStream(file.getInputStream())) {
+ if (!ExcelHelper.isExcelFile(stream)) {
+ String message = "Please upload a valid Excel file!";
+ throw new FlexinaleIllegalStateException(message);
+ }
+
+ return loadAndPersistAndPublishNewFilme(stream, file.getOriginalFilename());
+ }
+ catch (Exception ex) {
+ String message = "Could not upload the Excel file: %s: %s".formatted(file.getOriginalFilename(), ex.getMessage());
+ RestBadRequestException exception = new RestBadRequestException(message, ex);
+ LOGGER.error(message, exception);
+
+ throw exception;
+ }
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/KinoKinoSaalRestController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/KinoKinoSaalRestController.java
new file mode 100644
index 0000000..f3c0ddd
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/KinoKinoSaalRestController.java
@@ -0,0 +1,167 @@
+package de.accso.flexinale.backoffice.api_in.rest;
+
+import de.accso.flexinale.backoffice.api_out.event.KinoPublisher;
+import de.accso.flexinale.backoffice.api_out.event.VorfuehrungPublisher;
+import de.accso.flexinale.backoffice.application.services.KinoUploadService;
+import de.accso.flexinale.backoffice.application.services.KinoSaalUploadService;
+import de.accso.flexinale.backoffice.domain.model.Kino;
+import de.accso.flexinale.backoffice.domain.model.KinoSaal;
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+import de.accso.flexinale.backoffice.application.services.KinoService;
+import de.accso.flexinale.backoffice.application.services.VorfuehrungService;
+import de.accso.flexinale.common.application.PersistMode;
+import de.accso.flexinale.common.application.PersistedEntityAndResult;
+import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.ADDED;
+import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.UPDATED;
+
+@RestController
+@Profile({ "!testdata-besucherportal & !testdata-ticketing" })
+public class KinoKinoSaalRestController {
+ private static final Logger LOGGER = LoggerFactory.getLogger(KinoKinoSaalRestController.class);
+
+ @Autowired
+ private KinoService kinoService;
+
+ @Autowired
+ private KinoUploadService kinoUploadService;
+
+ @Autowired
+ private KinoSaalUploadService kinoSaalUploadService;
+
+ @Autowired
+ private KinoPublisher kinoPublisher;
+
+ @Autowired
+ private VorfuehrungService vorfuehrungService;
+
+ @Autowired
+ private VorfuehrungPublisher vorfuehrungPublisher;
+
+ @GetMapping(value = "/rest/kinos", produces = "application/json")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ public List kinos() {
+ return kinoService.kinos();
+ }
+
+ @GetMapping(value = "/rest/kino/{id}", produces = "application/json")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ public Kino kino(@PathVariable final String id) {
+ Identifiable.Id kinoId = Identifiable.Id.of(id);
+ Kino kino = kinoService.kino(kinoId);
+ if (kino == null) {
+ throw new RestResourceNotFoundException("no Kino %s found.".formatted(id));
+ }
+
+ return kino;
+ }
+
+ // -----------------------------------------------------------------------------------------------------------
+
+ @PostMapping("/rest/kinos")
+ @PreAuthorize("hasRole('ROLE_ADMIN')")
+ public ResponseEntity uploadKinosKinoSaele(@RequestParam("file") final MultipartFile file) {
+ String originalFilename = file.getOriginalFilename();
+
+ try {
+ Collection alleKinoSaele;
+
+ // we need to open the stream twice, separately for KinoSaele ...
+ try (BufferedInputStream stream = new BufferedInputStream(file.getInputStream())) {
+ if (!ExcelHelper.isExcelFile(stream)) {
+ String message = "Please upload a valid Excel file!";
+ throw new FlexinaleIllegalStateException(message);
+ }
+ alleKinoSaele = loadKinoSaele(stream);
+ }
+
+ // ... and for Kino
+ try (BufferedInputStream stream = new BufferedInputStream(file.getInputStream())) {
+ return loadAndPersistAndPublishNewKinosKinoSaele(stream, originalFilename, alleKinoSaele);
+ }
+ }
+ catch (Exception ex) {
+ String message = "Could not upload the Excel file: %s: %s".formatted(file.getOriginalFilename(), ex.getMessage());
+ RestBadRequestException exception = new RestBadRequestException(message, ex);
+ LOGGER.error(message, exception);
+
+ throw exception;
+ }
+ }
+
+ Collection loadKinoSaele(final BufferedInputStream stream) throws IOException {
+ return kinoSaalUploadService.loadDataFromExcelSheet(stream);
+ }
+
+ ResponseEntity loadAndPersistAndPublishNewKinosKinoSaele(
+ final BufferedInputStream stream, final String originalFilename,
+ final Collection alleKinoSaele) throws IOException {
+ // and then load and persist all Kinos (including their KinoSaele)
+ Map> alleKinosUndIhreSaele =
+ kinoSaalUploadService.mapKinoSaeleToKino(alleKinoSaele);
+
+ List addedKinos;
+ List updatedKinos;
+ try {
+ kinoUploadService.beforeLoad(alleKinosUndIhreSaele); // fix state of Kino-KinoSaal in map
+ // load and persist
+ Collection> addedAndUpdatedKinos =
+ kinoUploadService.loadDataFromExcelSheetAndPersistAndReturn(stream, PersistMode.UPDATE);
+
+ addedKinos = addedAndUpdatedKinos.stream()
+ .filter(kinoWithResult -> kinoWithResult.result().equals(ADDED))
+ .map(PersistedEntityAndResult::entity)
+ .toList();
+ updatedKinos = addedAndUpdatedKinos.stream()
+ .filter(kinoWithResult -> kinoWithResult.result().equals(UPDATED))
+ .map(PersistedEntityAndResult::entity)
+ .toList();
+
+ publishKinosAndVorfuehrungen(addedKinos, updatedKinos);
+ }
+ finally {
+ kinoUploadService.afterLoad(); // clear map
+ }
+
+ String message = "Uploaded the Excel file %s adding %d new Kinos entities and updating %d existing Kinos entities."
+ .formatted(originalFilename, addedKinos.size(), updatedKinos.size());
+
+ RestUploadResult restUploadResult = new RestUploadResult(HttpStatus.OK, message);
+ return ResponseEntity.status(restUploadResult.status())
+ .body(new RestResponseMessage(restUploadResult.message()));
+ }
+
+ private void publishKinosAndVorfuehrungen(final List addedKinos, final List updatedKinos) {
+ kinoPublisher.publishNewKinos(addedKinos);
+ kinoPublisher.publishUpdatedKinos(updatedKinos);
+
+ List vorfuehrungen = vorfuehrungService.vorfuehrungen();
+ List vorfuehrungenToPublishAsUpdated = new ArrayList<>();
+ for (Kino updatedKino: updatedKinos) {
+ for (KinoSaal updatedKinoSaal: updatedKino.kinoSaele) {
+ vorfuehrungen.stream()
+ .filter(vorfuehrung -> vorfuehrung.kinoSaal.id.equals(updatedKinoSaal.id))
+ .forEach(vorfuehrungenToPublishAsUpdated::add);
+ }
+ }
+ vorfuehrungPublisher.publishUpdatedVorfuehrungen(vorfuehrungenToPublishAsUpdated);
+ }
+}
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestBadRequestException.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestBadRequestException.java
new file mode 100644
index 0000000..266d42f
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestBadRequestException.java
@@ -0,0 +1,19 @@
+package de.accso.flexinale.backoffice.api_in.rest;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.BAD_REQUEST)
+@SuppressWarnings("unused")
+public class RestBadRequestException extends RuntimeException {
+
+ // see https://www.springboottutorial.com/spring-boot-exception-handling-for-rest-services
+
+ public RestBadRequestException(final String message) {
+ super(message);
+ }
+
+ public RestBadRequestException(final String message, final Exception ex) {
+ super(message, ex);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResourceNotFoundException.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResourceNotFoundException.java
new file mode 100644
index 0000000..12efd83
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResourceNotFoundException.java
@@ -0,0 +1,14 @@
+package de.accso.flexinale.backoffice.api_in.rest;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.NOT_FOUND)
+public class RestResourceNotFoundException extends RuntimeException {
+
+ // see https://www.springboottutorial.com/spring-boot-exception-handling-for-rest-services
+
+ public RestResourceNotFoundException(final String message) {
+ super(message);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResponseMessage.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResponseMessage.java
new file mode 100644
index 0000000..63e271e
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResponseMessage.java
@@ -0,0 +1,4 @@
+package de.accso.flexinale.backoffice.api_in.rest;
+
+record RestResponseMessage(String message) {
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestUploadResult.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestUploadResult.java
new file mode 100644
index 0000000..d56cee6
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestUploadResult.java
@@ -0,0 +1,7 @@
+package de.accso.flexinale.backoffice.api_in.rest;
+
+import org.springframework.http.HttpStatus;
+
+@SuppressWarnings("SameParameterValue")
+record RestUploadResult(HttpStatus status, String message) {
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/VorfuehrungRestController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/VorfuehrungRestController.java
new file mode 100644
index 0000000..befcdd6
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/VorfuehrungRestController.java
@@ -0,0 +1,109 @@
+package de.accso.flexinale.backoffice.api_in.rest;
+
+import de.accso.flexinale.backoffice.api_out.event.VorfuehrungPublisher;
+import de.accso.flexinale.backoffice.application.services.VorfuehrungUploadService;
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+import de.accso.flexinale.backoffice.application.services.VorfuehrungService;
+import de.accso.flexinale.common.application.PersistMode;
+import de.accso.flexinale.common.application.PersistedEntityAndResult;
+import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.ADDED;
+import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.UPDATED;
+
+@RestController
+@Profile({ "!testdata-besucherportal & !testdata-ticketing" })
+public class VorfuehrungRestController {
+ private static final Logger LOGGER = LoggerFactory.getLogger(VorfuehrungRestController.class);
+
+ @Autowired
+ private VorfuehrungService vorfuehrungService;
+
+ @Autowired
+ private VorfuehrungUploadService vorfuehrungUploadService;
+
+ @Autowired
+ private VorfuehrungPublisher vorfuehrungPublisher;
+
+ @GetMapping(value = "/rest/vorfuehrungen", produces = "application/json")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ public List vorfuehrungen() {
+ return vorfuehrungService.vorfuehrungen();
+ }
+
+ @GetMapping(value = "/rest/vorfuehrungen/{id}", produces = "application/json")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ public Vorfuehrung vorfuehrung(@PathVariable final String id) {
+ Identifiable.Id vorfuehrungId = Identifiable.Id.of(id);
+ Vorfuehrung vorfuehrung = vorfuehrungService.vorfuehrung(vorfuehrungId);
+ if (vorfuehrung == null) {
+ throw new RestResourceNotFoundException("no Vorfuehrung %s found.".formatted(id));
+ }
+
+ return vorfuehrung;
+ }
+
+ // -----------------------------------------------------------------------------------------------------------
+
+ @PostMapping("/rest/vorfuehrungen")
+ @PreAuthorize("hasRole('ROLE_ADMIN')")
+ public ResponseEntity uploadVorfuehrungen(@RequestParam("file") final MultipartFile file) {
+ try (BufferedInputStream stream = new BufferedInputStream(file.getInputStream())) {
+ if (!ExcelHelper.isExcelFile(stream)) {
+ String message = "Please upload a valid Excel file!";
+ throw new FlexinaleIllegalStateException(message);
+ }
+
+ return loadAndPersistAndPublishNewVorfuehrungen(stream, file.getOriginalFilename());
+ }
+ catch (Exception ex) {
+ String message = "Could not upload the Excel file: %s: %s".formatted(file.getOriginalFilename(), ex.getMessage());
+ RestBadRequestException exception = new RestBadRequestException(message, ex);
+ LOGGER.error(message, exception);
+
+ throw exception;
+ }
+ }
+
+ ResponseEntity loadAndPersistAndPublishNewVorfuehrungen(
+ final BufferedInputStream stream, final String originalFilename) throws IOException
+ {
+ // load and persist
+ Collection> addedAndUpdatedVorfuehrungen =
+ vorfuehrungUploadService.loadDataFromExcelSheetAndPersistAndReturn(stream, PersistMode.UPDATE);
+
+ // publish as events
+ List addedVorfuehrungen = addedAndUpdatedVorfuehrungen.stream()
+ .filter(vorfuehrungWithResult -> vorfuehrungWithResult.result().equals(ADDED))
+ .map(PersistedEntityAndResult::entity)
+ .toList();
+ vorfuehrungPublisher.publishNewVorfuehrungen(addedVorfuehrungen);
+ List updatedVorfuehrungen = addedAndUpdatedVorfuehrungen.stream()
+ .filter(vorfuehrungWithResult -> vorfuehrungWithResult.result().equals(UPDATED))
+ .map(PersistedEntityAndResult::entity)
+ .toList();
+ vorfuehrungPublisher.publishUpdatedVorfuehrungen(updatedVorfuehrungen);
+
+ String message = "Uploaded the Excel file %s adding %d new Vorfuehrungen entities and updating %d existing Vorfuehrungen entities."
+ .formatted(originalFilename, addedVorfuehrungen.size(), updatedVorfuehrungen.size());
+
+ RestUploadResult restUploadResult = new RestUploadResult(HttpStatus.OK, message);
+ return ResponseEntity.status(restUploadResult.status())
+ .body(new RestResponseMessage(restUploadResult.message()));
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/BootstrappingPostConstructBackoffice.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/BootstrappingPostConstructBackoffice.java
new file mode 100644
index 0000000..961552e
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/BootstrappingPostConstructBackoffice.java
@@ -0,0 +1,59 @@
+package de.accso.flexinale.backoffice.api_out.event;
+
+import de.accso.flexinale.backoffice.application.services.FilmService;
+import de.accso.flexinale.backoffice.application.services.KinoService;
+import de.accso.flexinale.backoffice.application.services.VorfuehrungService;
+import jakarta.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+@Component
+@Profile({"!test-integrated & !testdata"})
+public class BootstrappingPostConstructBackoffice {
+ private static final Logger LOGGER = LoggerFactory.getLogger(BootstrappingPostConstructBackoffice.class);
+
+ @Autowired
+ FilmService filmService;
+
+ @Autowired
+ FilmPublisher filmPublisher;
+
+ @Autowired
+ KinoService kinoService;
+
+ @Autowired
+ KinoPublisher kinoPublisher;
+
+ @Autowired
+ VorfuehrungService vorfuehrungService;
+
+ @Autowired
+ VorfuehrungPublisher vorfuehrungPublisher;
+
+ // Alternatively annotate method with @EventListener(ApplicationReadyEvent.class)
+ @PostConstruct
+ public void postConstruct() {
+ LOGGER.info("Publishing existing data for FKKsV as *Created events");
+
+ loadAllFilmeFromDatabaseAndPublish();
+ loadAllKinosFromDatabaseAndPublish();
+ loadAllVorfuehrungenFromDatabaseAndPublish();
+
+ LOGGER.info("Publishing existing data for FKKsV as *Created events ... done");
+ }
+
+ private void loadAllVorfuehrungenFromDatabaseAndPublish() {
+ filmPublisher.publishNewFilms( filmService.filme() );
+ }
+
+ private void loadAllKinosFromDatabaseAndPublish() {
+ kinoPublisher.publishNewKinos( kinoService.kinos() );
+ }
+
+ private void loadAllFilmeFromDatabaseAndPublish() {
+ vorfuehrungPublisher.publishNewVorfuehrungen( vorfuehrungService.vorfuehrungen() );
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/FilmPublisher.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/FilmPublisher.java
new file mode 100644
index 0000000..cba0f7e
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/FilmPublisher.java
@@ -0,0 +1,80 @@
+package de.accso.flexinale.backoffice.api_out.event;
+
+import de.accso.flexinale.backoffice.api_contract.event.FilmCRUDEvent;
+import de.accso.flexinale.backoffice.api_contract.event.FilmCreatedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.FilmDeletedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.FilmUpdatedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.backoffice.api_out.event.mapper.Film2FilmTOMapper;
+import de.accso.flexinale.backoffice.domain.model.Film;
+import de.accso.flexinale.common.api.eventbus.EventBus;
+import de.accso.flexinale.common.api.eventbus.EventBusFactory;
+import de.accso.flexinale.common.api.eventbus.EventNotification;
+import de.accso.flexinale.common.api.eventbus.EventPublisher.EventPublisher3;
+
+import java.util.Collection;
+
+@SuppressWarnings("unused")
+public class FilmPublisher implements EventPublisher3 {
+ private final EventBus createBus;
+ private final EventBus updateBus;
+ private final EventBus deleteBus;
+
+ private final EventNotification notification;
+
+ public FilmPublisher(final EventBusFactory eventBusFactory,
+ final EventNotification notification) {
+ this.createBus = eventBusFactory.createOrGetEventBusFor(FilmCreatedEvent.class);
+ this.updateBus = eventBusFactory.createOrGetEventBusFor(FilmUpdatedEvent.class);
+ this.deleteBus = eventBusFactory.createOrGetEventBusFor(FilmDeletedEvent.class);
+
+ this.notification = notification;
+ }
+
+ public void publishNewFilms(final Collection films) {
+ publishCRUDEvents(films, FilmCRUDEvent.CRUD.CREATE);
+ }
+
+ public void publishUpdatedFilms(final Collection films) {
+ publishCRUDEvents(films, FilmCRUDEvent.CRUD.UPDATE);
+ }
+
+ // currently we do not publish Deletions events
+ public void publishDeletedFilms(final Collection films) {
+ publishCRUDEvents(films, FilmCRUDEvent.CRUD.DELETE);
+ }
+
+ private void publishCRUDEvents(final Collection changedFilms, final FilmCRUDEvent.CRUD mode) {
+ for (Film film: changedFilms) {
+ FilmTO filmTO = Film2FilmTOMapper.map(film);
+ switch (mode) {
+ case CREATE -> this.post (FilmCreatedEvent.class, new FilmCreatedEvent(filmTO));
+ case UPDATE -> this.post2(FilmUpdatedEvent.class, new FilmUpdatedEvent(filmTO));
+ case DELETE -> this.post3(FilmDeletedEvent.class, new FilmDeletedEvent(filmTO));
+ }
+ }
+ }
+
+ @Override
+ public String getName() {
+ return FilmPublisher.class.getName();
+ }
+
+ @Override
+ public void post(final Class eventType, final FilmCreatedEvent event) {
+ this.createBus.publish(eventType, event);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void post2(final Class eventType, final FilmUpdatedEvent event) {
+ this.updateBus.publish(eventType, event);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void post3(final Class eventType, final FilmDeletedEvent event) {
+ this.deleteBus.publish(eventType, event);
+ this.notification.notify(event);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/KinoPublisher.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/KinoPublisher.java
new file mode 100644
index 0000000..240ca48
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/KinoPublisher.java
@@ -0,0 +1,80 @@
+package de.accso.flexinale.backoffice.api_out.event;
+
+import de.accso.flexinale.backoffice.api_contract.event.KinoCRUDEvent;
+import de.accso.flexinale.backoffice.api_contract.event.KinoCreatedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.KinoDeletedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.KinoUpdatedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.backoffice.api_out.event.mapper.KinoKinoSaal2KinoKinoSaalTOMapper;
+import de.accso.flexinale.backoffice.domain.model.Kino;
+import de.accso.flexinale.common.api.eventbus.EventBus;
+import de.accso.flexinale.common.api.eventbus.EventBusFactory;
+import de.accso.flexinale.common.api.eventbus.EventNotification;
+import de.accso.flexinale.common.api.eventbus.EventPublisher.EventPublisher3;
+
+import java.util.Collection;
+
+@SuppressWarnings("unused")
+public class KinoPublisher implements EventPublisher3 {
+ private final EventBus createBus;
+ private final EventBus updateBus;
+ private final EventBus deleteBus;
+
+ private final EventNotification notification;
+
+ public KinoPublisher(final EventBusFactory eventBusFactory,
+ final EventNotification notification) {
+ this.createBus = eventBusFactory.createOrGetEventBusFor(KinoCreatedEvent.class);
+ this.updateBus = eventBusFactory.createOrGetEventBusFor(KinoUpdatedEvent.class);
+ this.deleteBus = eventBusFactory.createOrGetEventBusFor(KinoDeletedEvent.class);
+
+ this.notification = notification;
+ }
+
+ public void publishNewKinos(final Collection kinos) {
+ publishCRUDEvents(kinos, KinoCRUDEvent.CRUD.CREATE);
+ }
+
+ public void publishUpdatedKinos(final Collection kinos) {
+ publishCRUDEvents(kinos, KinoCRUDEvent.CRUD.UPDATE);
+ }
+
+ // currently we do not publish Deletions events
+ public void publishDeletedKinos(final Collection kinos) {
+ publishCRUDEvents(kinos, KinoCRUDEvent.CRUD.DELETE);
+ }
+
+ private void publishCRUDEvents(final Collection changedKinos, final KinoCRUDEvent.CRUD mode) {
+ for (Kino kino: changedKinos) {
+ KinoTO kinoTO = KinoKinoSaal2KinoKinoSaalTOMapper.mapKino(kino);
+ switch (mode) {
+ case CREATE -> this.post (KinoCreatedEvent.class, new KinoCreatedEvent(kinoTO));
+ case UPDATE -> this.post2(KinoUpdatedEvent.class, new KinoUpdatedEvent(kinoTO));
+ case DELETE -> this.post3(KinoDeletedEvent.class, new KinoDeletedEvent(kinoTO));
+ }
+ }
+ }
+
+ @Override
+ public String getName() {
+ return KinoPublisher.class.getName();
+ }
+
+ @Override
+ public void post(final Class eventType, final KinoCreatedEvent event) {
+ this.createBus.publish(eventType, event);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void post2(final Class eventType, final KinoUpdatedEvent event) {
+ this.updateBus.publish(eventType, event);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void post3(final Class eventType, final KinoDeletedEvent event) {
+ this.deleteBus.publish(eventType, event);
+ this.notification.notify(event);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/VorfuehrungPublisher.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/VorfuehrungPublisher.java
new file mode 100644
index 0000000..cbdb6e5
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/VorfuehrungPublisher.java
@@ -0,0 +1,80 @@
+package de.accso.flexinale.backoffice.api_out.event;
+
+import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungCRUDEvent;
+import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungCreatedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungDeletedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungUpdatedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.backoffice.api_out.event.mapper.Vorfuehrung2VorfuehrungTOMapper;
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+import de.accso.flexinale.common.api.eventbus.EventBus;
+import de.accso.flexinale.common.api.eventbus.EventBusFactory;
+import de.accso.flexinale.common.api.eventbus.EventNotification;
+import de.accso.flexinale.common.api.eventbus.EventPublisher.EventPublisher3;
+
+import java.util.Collection;
+
+@SuppressWarnings("unused")
+public class VorfuehrungPublisher implements EventPublisher3 {
+ private final EventBus createBus;
+ private final EventBus updateBus;
+ private final EventBus deleteBus;
+
+ private final EventNotification notification;
+
+ public VorfuehrungPublisher(final EventBusFactory eventBusFactory,
+ final EventNotification notification) {
+ this.createBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungCreatedEvent.class);
+ this.updateBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungUpdatedEvent.class);
+ this.deleteBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungDeletedEvent.class);
+
+ this.notification = notification;
+ }
+
+ public void publishNewVorfuehrungen(final Collection vorfuehrungen) {
+ publishCRUDEvents(vorfuehrungen, VorfuehrungCRUDEvent.CRUD.CREATE);
+ }
+
+ public void publishUpdatedVorfuehrungen(final Collection vorfuehrungen) {
+ publishCRUDEvents(vorfuehrungen, VorfuehrungCRUDEvent.CRUD.UPDATE);
+ }
+
+ // currently we do not publish Deletions events
+ public void publishDeletedVorfuehrungen(final Collection vorfuehrungen) {
+ publishCRUDEvents(vorfuehrungen, VorfuehrungCRUDEvent.CRUD.DELETE);
+ }
+
+ private void publishCRUDEvents(final Collection changedVorfuehrungen, final VorfuehrungCRUDEvent.CRUD mode) {
+ for (Vorfuehrung vorfuehrung: changedVorfuehrungen) {
+ VorfuehrungTO vorfuehrungTO = Vorfuehrung2VorfuehrungTOMapper.map(vorfuehrung);
+ switch (mode) {
+ case CREATE -> this.post (VorfuehrungCreatedEvent.class, new VorfuehrungCreatedEvent(vorfuehrungTO));
+ case UPDATE -> this.post2(VorfuehrungUpdatedEvent.class, new VorfuehrungUpdatedEvent(vorfuehrungTO));
+ case DELETE -> this.post3(VorfuehrungDeletedEvent.class, new VorfuehrungDeletedEvent(vorfuehrungTO));
+ }
+ }
+ }
+
+ @Override
+ public String getName() {
+ return VorfuehrungPublisher.class.getName();
+ }
+
+ @Override
+ public void post(final Class eventType, final VorfuehrungCreatedEvent event) {
+ this.createBus.publish(eventType, event);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void post2(final Class eventType, final VorfuehrungUpdatedEvent event) {
+ this.updateBus.publish(eventType, event);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void post3(final Class eventType, final VorfuehrungDeletedEvent event) {
+ this.deleteBus.publish(eventType, event);
+ this.notification.notify(event);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Film2FilmTOMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Film2FilmTOMapper.java
new file mode 100644
index 0000000..6132d37
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Film2FilmTOMapper.java
@@ -0,0 +1,16 @@
+package de.accso.flexinale.backoffice.api_out.event.mapper;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.backoffice.domain.model.Film;
+
+import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
+
+public final class Film2FilmTOMapper {
+
+ public static FilmTO map(final Film film) {
+ return new FilmTO(film.id(), film.version,
+ new FilmTO.Titel(getRawOrNull(film.titel)),
+ new FilmTO.ImdbUrl(getRawOrNull(film.imdbUrl)),
+ new FilmTO.DauerInMinuten(getRawOrNull(film.dauerInMinuten)));
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/KinoKinoSaal2KinoKinoSaalTOMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/KinoKinoSaal2KinoKinoSaalTOMapper.java
new file mode 100644
index 0000000..bfff4bd
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/KinoKinoSaal2KinoKinoSaalTOMapper.java
@@ -0,0 +1,43 @@
+package de.accso.flexinale.backoffice.api_out.event.mapper;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.backoffice.domain.model.Kino;
+import de.accso.flexinale.backoffice.domain.model.KinoSaal;
+
+import java.util.HashSet;
+
+import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
+
+public final class KinoKinoSaal2KinoKinoSaalTOMapper {
+
+ public static KinoTO mapKino(final Kino kino) {
+ final KinoTO mappedKino = new KinoTO(kino.id(), kino.version(),
+ new KinoTO.Name(getRawOrNull(kino.name)),
+ new KinoTO.Adresse(getRawOrNull(kino.adresse)),
+ new KinoTO.EmailAdresse(getRawOrNull(kino.emailAdresse)),
+ new HashSet<>());
+
+ kino.kinoSaele.stream()
+ .map(kinoSaal -> mapKinoSaal(kinoSaal, mappedKino))
+ .forEach(kinoSaal -> mappedKino.kinoSaele().add(kinoSaal));
+
+ return mappedKino;
+ }
+
+ static KinoSaalTO mapKinoSaal(final KinoSaal kinoSaal) {
+ Kino kino = kinoSaal.kino;
+ KinoTO mappedKino = mapKino(kino);
+ return new KinoSaalTO(kinoSaal.id(), kino.version,
+ new KinoSaalTO.Name(getRawOrNull(kinoSaal.name)),
+ new KinoSaalTO.AnzahlPlaetze(getRawOrNull(kinoSaal.anzahlPlaetze)),
+ mappedKino);
+ }
+
+ private static KinoSaalTO mapKinoSaal(final KinoSaal kinoSaal, final KinoTO kino) {
+ return new KinoSaalTO(kinoSaal.id(), kinoSaal.version,
+ new KinoSaalTO.Name(getRawOrNull(kinoSaal.name)),
+ new KinoSaalTO.AnzahlPlaetze(getRawOrNull(kinoSaal.anzahlPlaetze)),
+ kino);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Vorfuehrung2VorfuehrungTOMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Vorfuehrung2VorfuehrungTOMapper.java
new file mode 100644
index 0000000..8fa2606
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Vorfuehrung2VorfuehrungTOMapper.java
@@ -0,0 +1,20 @@
+package de.accso.flexinale.backoffice.api_out.event.mapper;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+
+import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
+
+public final class Vorfuehrung2VorfuehrungTOMapper {
+
+ public static VorfuehrungTO map(final Vorfuehrung vorfuehrung) {
+ FilmTO filmTO = Film2FilmTOMapper.map(vorfuehrung.film);
+ KinoSaalTO kinoSaalTO = KinoKinoSaal2KinoKinoSaalTOMapper.mapKinoSaal(vorfuehrung.kinoSaal);
+
+ return new VorfuehrungTO(vorfuehrung.id(), vorfuehrung.version(),
+ new VorfuehrungTO.Zeit(getRawOrNull(vorfuehrung.zeit)),
+ filmTO, kinoSaalTO, kinoSaalTO.kino().id());
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmService.java
new file mode 100644
index 0000000..59919cc
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmService.java
@@ -0,0 +1,25 @@
+package de.accso.flexinale.backoffice.application.services;
+
+import de.accso.flexinale.backoffice.domain.model.Film;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import jakarta.transaction.Transactional;
+
+import java.util.List;
+
+@Transactional
+public class FilmService {
+
+ private final de.accso.flexinale.backoffice.domain.services.FilmService filmService;
+
+ public FilmService(de.accso.flexinale.backoffice.domain.services.FilmService filmService) {
+ this.filmService = filmService;
+ }
+
+ public List filme() {
+ return filmService.filme();
+ }
+
+ public Film film(Identifiable.Id filmId) {
+ return filmService.film(filmId);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmUploadService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmUploadService.java
new file mode 100644
index 0000000..fb0d94d
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmUploadService.java
@@ -0,0 +1,58 @@
+package de.accso.flexinale.backoffice.application.services;
+
+import de.accso.flexinale.backoffice.domain.dao.FilmDao;
+import de.accso.flexinale.backoffice.domain.model.Film;
+import de.accso.flexinale.common.domain.model.AbstractDao;
+import de.accso.flexinale.common.application.services.AbstractExcelDataUploadService;
+import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import jakarta.transaction.Transactional;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Transactional
+public class FilmUploadService extends AbstractExcelDataUploadService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(FilmUploadService.class);
+
+ private final FilmDao filmDao;
+
+ public FilmUploadService(final FilmDao filmDao) {
+ this.filmDao = filmDao;
+ }
+
+ @Override
+ public AbstractDao getDao() {
+ return filmDao;
+ }
+
+ @Override
+ public String getNameOfExcelDataType() {
+ return Film.class.getSimpleName();
+ }
+
+ @Override
+ public Film createDataFromExcelRow(final Row excelRow) {
+ String filmId = excelRow.getCell(0).getStringCellValue();
+
+ Cell cellDauer = excelRow.getCell(3);
+ int dauerInMinuten =
+ switch(cellDauer.getCellType()) {
+ case NUMERIC -> (int) cellDauer.getNumericCellValue();
+ case STRING -> Integer.parseInt(cellDauer.getStringCellValue());
+ default -> throw new FlexinaleIllegalArgumentException("Dauer is not a number");
+ };
+
+ Film film = new Film(
+ Identifiable.Id.of(filmId),
+ new Film.Titel(excelRow.getCell(1).getStringCellValue()),
+ new Film.ImdbUrl(excelRow.getCell(2).getStringCellValue()),
+ new Film.DauerInMinuten(dauerInMinuten)
+ );
+
+ LOGGER.debug("New Film created " + film);
+
+ return film;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoSaalUploadService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoSaalUploadService.java
new file mode 100644
index 0000000..df3b4d7
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoSaalUploadService.java
@@ -0,0 +1,95 @@
+package de.accso.flexinale.backoffice.application.services;
+
+import de.accso.flexinale.backoffice.domain.dao.KinoSaalDao;
+import de.accso.flexinale.backoffice.domain.model.Kino;
+import de.accso.flexinale.backoffice.domain.model.KinoSaal;
+import de.accso.flexinale.common.application.PersistMode;
+import de.accso.flexinale.common.application.PersistedEntityAndResult;
+import de.accso.flexinale.common.domain.model.AbstractDao;
+import de.accso.flexinale.common.application.services.AbstractExcelDataUploadService;
+import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
+import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import jakarta.transaction.Transactional;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+@Transactional
+public class KinoSaalUploadService extends AbstractExcelDataUploadService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(KinoSaalUploadService.class);
+
+ private final KinoSaalDao kinoSaalDao;
+
+ public KinoSaalUploadService(final KinoSaalDao kinoSaalDao) {
+ this.kinoSaalDao = kinoSaalDao;
+ }
+
+ @Override
+ public AbstractDao getDao() {
+ return kinoSaalDao;
+ }
+
+ @Override
+ public String getNameOfExcelDataType() {
+ return KinoSaal.class.getSimpleName();
+ }
+
+ @Override
+ public Collection> loadDataFromExcelSheetAndPersistAndReturn(
+ final InputStream stream, final PersistMode mode) {
+ throw new DeveloperMistakeException("method must not be called on KinoSaalUploadService");
+ }
+
+ @Override
+ public KinoSaal createDataFromExcelRow(final Row excelRow) {
+ String kinoSaalId = excelRow.getCell(0).getStringCellValue();
+
+ String kinoId = excelRow.getCell(3).getStringCellValue();
+
+ Cell cellAnzahlPlaetze = excelRow.getCell(2);
+ int anzahlPlaetze =
+ switch(cellAnzahlPlaetze.getCellType()) {
+ case NUMERIC -> (int) cellAnzahlPlaetze.getNumericCellValue();
+ case STRING -> Integer.parseInt(cellAnzahlPlaetze.getStringCellValue());
+ default -> throw new FlexinaleIllegalArgumentException("Anzahl Plaetze is not a number");
+ };
+
+ KinoSaal kinoSaal = new KinoSaal(
+ Identifiable.Id.of(kinoSaalId),
+ new KinoSaal.Name(excelRow.getCell(1).getStringCellValue()),
+ new KinoSaal.AnzahlPlaetze(anzahlPlaetze),
+ new Kino(Identifiable.Id.of(kinoId))
+ );
+
+ LOGGER.debug("New KinoSaal created: " + kinoSaal);
+
+ return kinoSaal;
+ }
+
+ public Map> mapKinoSaeleToKino(final Collection kinoSaele) {
+ Map> saeleUndKinos = new HashMap<>();
+ for (KinoSaal saal : kinoSaele) {
+ Identifiable.Id key = saal.kino.id();
+
+ Collection kinoSaeleAlreadyMapped = saeleUndKinos.get(key);
+ if (kinoSaeleAlreadyMapped == null) {
+ HashSet saele = new HashSet<>();
+ saele.add(saal);
+ saeleUndKinos.put(key, saele);
+ }
+ else {
+ kinoSaeleAlreadyMapped.add(saal);
+ saeleUndKinos.put(key, kinoSaeleAlreadyMapped);
+ }
+ }
+ return saeleUndKinos;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoService.java
new file mode 100644
index 0000000..313deed
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoService.java
@@ -0,0 +1,26 @@
+package de.accso.flexinale.backoffice.application.services;
+
+import de.accso.flexinale.backoffice.domain.model.Kino;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import jakarta.transaction.Transactional;
+
+import java.util.List;
+
+@Transactional
+public class KinoService {
+
+ private final de.accso.flexinale.backoffice.domain.services.KinoService kinoService;
+
+ public KinoService(de.accso.flexinale.backoffice.domain.services.KinoService kinoService) {
+ this.kinoService = kinoService;
+ }
+
+
+ public Kino kino(Identifiable.Id kinoId) {
+ return kinoService.kino(kinoId);
+ }
+
+ public List kinos() {
+ return kinoService.kinos();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoUploadService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoUploadService.java
new file mode 100644
index 0000000..98afa69
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoUploadService.java
@@ -0,0 +1,81 @@
+package de.accso.flexinale.backoffice.application.services;
+
+import de.accso.flexinale.backoffice.domain.dao.KinoDao;
+import de.accso.flexinale.backoffice.domain.model.Kino;
+import de.accso.flexinale.backoffice.domain.model.KinoSaal;
+import de.accso.flexinale.common.domain.model.AbstractDao;
+import de.accso.flexinale.common.application.services.AbstractExcelDataUploadService;
+import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
+import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import jakarta.transaction.Transactional;
+import org.apache.poi.ss.usermodel.Row;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+@Transactional
+public class KinoUploadService extends AbstractExcelDataUploadService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(KinoUploadService.class);
+
+ private final KinoDao kinoDao;
+
+ private final Map> alleKinosUndIhreSaele = new HashMap<>();
+
+ public KinoUploadService(KinoDao kinoDao) {
+ this.kinoDao = kinoDao;
+ }
+
+ @Override
+ public AbstractDao getDao() {
+ return kinoDao;
+ }
+
+ @Override
+ public String getNameOfExcelDataType() {
+ return Kino.class.getSimpleName();
+ }
+
+ @Override
+ public void beforeLoad(Object... o) {
+ if ((o.length != 1) && (!(o[0] instanceof Map))) {
+ throw new DeveloperMistakeException("wrong type: expecting a " +
+ "Map> of Kino and their KinoSaele");
+ }
+
+ @SuppressWarnings("unchecked")
+ Map> newKinoAndKinoSaele = (Map>) o[0];
+ this.alleKinosUndIhreSaele.putAll(newKinoAndKinoSaele);
+ }
+
+ @Override
+ public void afterLoad(Object... o) {
+ alleKinosUndIhreSaele.clear();
+ }
+
+ @Override
+ public Kino createDataFromExcelRow(final Row excelRow) {
+ String kinoId = excelRow.getCell(0).getStringCellValue();
+
+ Collection saeleImKino = alleKinosUndIhreSaele.get(Identifiable.Id.of(kinoId));
+ if (saeleImKino == null || saeleImKino.isEmpty()) {
+ throw new FlexinaleIllegalStateException("no Saele for Kino %s found".formatted(kinoId));
+ }
+
+ Kino kino = new Kino(
+ Identifiable.Id.of(kinoId),
+ new Kino.Name(excelRow.getCell(1).getStringCellValue()),
+ new Kino.Adresse(excelRow.getCell(2).getStringCellValue()),
+ new Kino.EmailAdresse(excelRow.getCell(3).getStringCellValue()),
+ Set.copyOf(saeleImKino)
+ );
+
+ LOGGER.debug("New Kino created: " + kino);
+
+ return kino;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungService.java
new file mode 100644
index 0000000..e4b0560
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungService.java
@@ -0,0 +1,26 @@
+package de.accso.flexinale.backoffice.application.services;
+
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import jakarta.transaction.Transactional;
+
+import java.util.List;
+
+@Transactional
+public class VorfuehrungService {
+
+ private final de.accso.flexinale.backoffice.domain.services.VorfuehrungService vorfuehrungService;
+
+ public VorfuehrungService(de.accso.flexinale.backoffice.domain.services.VorfuehrungService vorfuehrungService) {
+ this.vorfuehrungService = vorfuehrungService;
+ }
+
+
+ public List vorfuehrungen() {
+ return vorfuehrungService.vorfuehrungen();
+ }
+
+ public Vorfuehrung vorfuehrung(Identifiable.Id vorfuehrungId) {
+ return vorfuehrungService.vorfuehrung(vorfuehrungId);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungUploadService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungUploadService.java
new file mode 100644
index 0000000..eb954f3
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungUploadService.java
@@ -0,0 +1,65 @@
+package de.accso.flexinale.backoffice.application.services;
+
+import de.accso.flexinale.backoffice.domain.dao.FilmDao;
+import de.accso.flexinale.backoffice.domain.dao.KinoSaalDao;
+import de.accso.flexinale.backoffice.domain.dao.VorfuehrungDao;
+import de.accso.flexinale.backoffice.domain.model.Film;
+import de.accso.flexinale.backoffice.domain.model.KinoSaal;
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+import de.accso.flexinale.common.domain.model.AbstractDao;
+import de.accso.flexinale.common.application.services.AbstractExcelDataUploadService;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import jakarta.transaction.Transactional;
+import org.apache.poi.ss.usermodel.Row;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDateTime;
+
+@Transactional
+public class VorfuehrungUploadService extends AbstractExcelDataUploadService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(VorfuehrungUploadService.class);
+
+ private final FilmDao filmDao;
+ private final KinoSaalDao kinoSaalDao;
+
+ private final VorfuehrungDao vorfuehrungDao;
+
+ public VorfuehrungUploadService(final VorfuehrungDao vorfuehrungDao,
+ final FilmDao filmDao,
+ final KinoSaalDao kinoSaalDao) {
+ this.filmDao = filmDao;
+ this.kinoSaalDao = kinoSaalDao;
+ this.vorfuehrungDao = vorfuehrungDao;
+ }
+
+ @Override
+ public AbstractDao getDao() {
+ return vorfuehrungDao;
+ }
+
+ @Override
+ public String getNameOfExcelDataType() {
+ return Vorfuehrung.class.getSimpleName();
+ }
+
+ @Override
+ public Vorfuehrung createDataFromExcelRow(final Row excelRow) {
+ String vorfuehrungId = excelRow.getCell(0).getStringCellValue();
+
+ // expects date/time in format like for example 2023-02-20T17:45
+ LocalDateTime zeit = LocalDateTime.parse(excelRow.getCell(1).getStringCellValue());
+
+ String filmId = excelRow.getCell(2).getStringCellValue();
+ Film film = filmDao.findById(Identifiable.Id.of(filmId)).orElseThrow(IllegalStateException::new);
+
+ String kinoSaalId = excelRow.getCell(3).getStringCellValue();
+ KinoSaal kinoSaal = kinoSaalDao.findById(Identifiable.Id.of(kinoSaalId)).orElseThrow(IllegalStateException::new);
+
+ Vorfuehrung vorfuehrung = new Vorfuehrung(Identifiable.Id.of(vorfuehrungId), new Vorfuehrung.Zeit(zeit), film, kinoSaal);
+
+ LOGGER.debug("New Vorfuehrung created: " + vorfuehrung);
+
+ return vorfuehrung;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/FilmDao.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/FilmDao.java
new file mode 100644
index 0000000..21762c6
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/FilmDao.java
@@ -0,0 +1,13 @@
+package de.accso.flexinale.backoffice.domain.dao;
+
+import de.accso.flexinale.backoffice.domain.model.Film;
+import de.accso.flexinale.common.domain.model.AbstractDao;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.Optional;
+
+public interface FilmDao extends AbstractDao {
+
+ @Override
+ Optional findById(final Identifiable.Id id);
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoDao.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoDao.java
new file mode 100644
index 0000000..59a61a3
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoDao.java
@@ -0,0 +1,13 @@
+package de.accso.flexinale.backoffice.domain.dao;
+
+import de.accso.flexinale.backoffice.domain.model.Kino;
+import de.accso.flexinale.common.domain.model.AbstractDao;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.Optional;
+
+public interface KinoDao extends AbstractDao {
+
+ @Override
+ Optional findById(final Identifiable.Id id);
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoSaalDao.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoSaalDao.java
new file mode 100644
index 0000000..a9cdb73
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoSaalDao.java
@@ -0,0 +1,13 @@
+package de.accso.flexinale.backoffice.domain.dao;
+
+import de.accso.flexinale.backoffice.domain.model.KinoSaal;
+import de.accso.flexinale.common.domain.model.AbstractDao;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.Optional;
+
+public interface KinoSaalDao extends AbstractDao {
+
+ @Override
+ Optional findById(final Identifiable.Id id);
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/VorfuehrungDao.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/VorfuehrungDao.java
new file mode 100644
index 0000000..eaa9e26
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/VorfuehrungDao.java
@@ -0,0 +1,17 @@
+package de.accso.flexinale.backoffice.domain.dao;
+
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+import de.accso.flexinale.common.domain.model.AbstractDao;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.List;
+import java.util.Optional;
+
+@SuppressWarnings("unused")
+public interface VorfuehrungDao extends AbstractDao {
+
+ @Override
+ Optional findById(final Identifiable.Id id);
+
+ List findByFilmId(final Identifiable.Id filmId);
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Film.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Film.java
new file mode 100644
index 0000000..1b15336
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Film.java
@@ -0,0 +1,95 @@
+package de.accso.flexinale.backoffice.domain.model;
+
+import de.accso.flexinale.common.shared_kernel.*;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+public class Film implements Identifiable, Versionable, Mergeable, EqualsByContent {
+ public record Titel(String raw) implements RawWrapper {}
+ public record ImdbUrl(String raw) implements RawWrapper {}
+ public record DauerInMinuten(Integer raw) implements RawWrapper {}
+
+ public final Id id;
+ public final Version version;
+
+ public Titel titel;
+ public ImdbUrl imdbUrl;
+ public DauerInMinuten dauerInMinuten;
+
+ public Film(final Id id) {
+ this(id, Versionable.unknownVersion());
+ }
+
+ public Film(final Id id, final Version version) {
+ this.id = id;
+ this.version = version;
+ }
+
+ public Film(final Id id,
+ final Titel titel, final ImdbUrl imdbUrl, final DauerInMinuten dauerInMinuten) {
+ this(id, Versionable.unknownVersion(), titel, imdbUrl, dauerInMinuten);
+ }
+
+ public Film(final Id id, final Version version,
+ final Titel titel, final ImdbUrl imdbUrl, final DauerInMinuten dauerInMinuten) {
+ this.id = id;
+ this.version = version;
+ this.titel = titel;
+ this.imdbUrl = imdbUrl;
+ this.dauerInMinuten = dauerInMinuten;
+ }
+
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ @Override
+ public Version version() {
+ return version;
+ }
+
+ @Override
+ public Film merge(final Film newData) {
+ return new Film(this.id, this.version,
+ newData.titel, newData.imdbUrl, newData.dauerInMinuten);
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (!equalsByContent(o)) return false;
+
+ if (this == o) {return true;}
+ if (o == null || getClass() != o.getClass()) {return false;}
+ final Film that = (Film) o;
+ return new EqualsBuilder().append(version, that.version).isEquals();
+ }
+
+ @Override
+ public boolean equalsByContent(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ final Film that = (Film) o;
+
+ return new EqualsBuilder()
+ .append(id, that.id)
+ .append(titel, that.titel).append(imdbUrl, that.imdbUrl)
+ .append(dauerInMinuten, that.dauerInMinuten).isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(id).append(version)
+ .append(titel).append(imdbUrl).append(dauerInMinuten).toHashCode();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Kino.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Kino.java
new file mode 100644
index 0000000..aeadf51
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Kino.java
@@ -0,0 +1,145 @@
+package de.accso.flexinale.backoffice.domain.model;
+
+import de.accso.flexinale.common.shared_kernel.*;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class Kino implements Identifiable, Versionable, Mergeable, EqualsByContent {
+ public record Name(String raw) implements RawWrapper {}
+ public record Adresse(String raw) implements RawWrapper {}
+ public record EmailAdresse(String raw) implements RawWrapper {}
+
+ public final Id id;
+ public final Version version;
+
+ public Name name;
+ public Adresse adresse;
+ public EmailAdresse emailAdresse;
+ @DoNotCheckInArchitectureTests
+ public Set kinoSaele = new HashSet<>();
+
+ public Kino(final Id id) {
+ this(id, Versionable.unknownVersion());
+ }
+
+ public Kino(final Id id, final Version version) {
+ this.id = id;
+ this.version = version;
+ }
+
+ public Kino(final Id id,
+ final Name name, final Adresse adresse,
+ final EmailAdresse emailAdresse, final Set kinoSaele) {
+ this(id, Versionable.unknownVersion(), name, adresse, emailAdresse, kinoSaele);
+ }
+
+ public Kino(final Id id, final Version version,
+ final Name name, final Adresse adresse,
+ final EmailAdresse emailAdresse, final Set kinoSaele) {
+ this.id = id;
+ this.version = version;
+ this.name = name;
+ this.adresse = adresse;
+ this.emailAdresse = emailAdresse;
+ this.kinoSaele = kinoSaele;
+ }
+
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ @Override
+ public Version version() {
+ return version;
+ }
+
+ @Override
+ public Kino merge(final Kino newData) {
+ Map mapIdToOldKinoSaal = new HashMap<>();
+ this.kinoSaele.forEach(oldKinoSaal -> {
+ mapIdToOldKinoSaal.put(oldKinoSaal.id(), oldKinoSaal);
+ });
+
+ Set mergedKinoSaele = newData.kinoSaele.stream().map(newKinoSaal -> {
+ KinoSaal oldKinoSaal = mapIdToOldKinoSaal.get(newKinoSaal.id);
+ if (oldKinoSaal != null) {
+ return oldKinoSaal.merge(newKinoSaal);
+ }
+ else {
+ return newKinoSaal;
+ }
+ })
+ .collect(Collectors.toSet());
+
+ Kino mergedKino = new Kino(this.id, this.version,
+ newData.name, newData.adresse, newData.emailAdresse, mergedKinoSaele);
+ mergedKinoSaele.forEach(kinoSaal -> kinoSaal.kino = mergedKino);
+
+ return mergedKino;
+ }
+
+ public void addSaele(Collection saele) {
+ kinoSaele.addAll(saele);
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (!equalsByContent(o)) return false;
+
+ if (this == o) {return true;}
+ if (o == null || getClass() != o.getClass()) {return false;}
+ Kino that = (Kino) o;
+ return new EqualsBuilder().append(version, that.version).isEquals();
+ }
+
+ @SuppressWarnings("ConstantValue")
+ @Override
+ public boolean equalsByContent(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ Kino that = (Kino) o;
+
+ boolean result = new EqualsBuilder()
+ .append(id, that.id)
+ .append(name, that.name)
+ .append(adresse, that.adresse).append(emailAdresse, that.emailAdresse)
+ .isEquals();
+ if (!result) return false;
+
+ if (kinoSaele == null && that.kinoSaele == null) return true;
+ if (kinoSaele == null && that.kinoSaele != null) return false;
+ if (kinoSaele != null && that.kinoSaele == null) return false;
+
+ List thisKSList = kinoSaele.stream().toList();
+ List thatKSList = that.kinoSaele.stream().toList();
+ if (thisKSList.size() != thatKSList.size()) return false;
+ for (int counter = 0; counter < thisKSList.size(); counter++) {
+ if (!thisKSList.get(counter).equalsByContent(thatKSList.get(counter))) return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(id).append(version)
+ .append(name).append(adresse).append(emailAdresse)
+ .append(kinoSaele.stream().toList()) // need to use list, as Set does not support deep equals
+ .toHashCode();
+ }
+
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/KinoSaal.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/KinoSaal.java
new file mode 100644
index 0000000..52bf17f
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/KinoSaal.java
@@ -0,0 +1,118 @@
+package de.accso.flexinale.backoffice.domain.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import de.accso.flexinale.common.shared_kernel.*;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+public class KinoSaal implements Identifiable, Versionable, Mergeable, EqualsByContent {
+ public record Name(String raw) implements RawWrapper {}
+ public record AnzahlPlaetze(Integer raw) implements RawWrapper {}
+
+ public final Id id;
+ public final Version version;
+
+ public Name name;
+ public AnzahlPlaetze anzahlPlaetze = new KinoSaal.AnzahlPlaetze(0);
+
+ @JsonIgnore
+ @DoNotCheckInArchitectureTests
+ public Kino kino;
+
+ public KinoSaal(final Id id) {
+ this(id, Versionable.unknownVersion());
+ }
+
+ public KinoSaal(final Id id, final Version version) {
+ this.id = id;
+ this.version = version;
+ }
+
+ public KinoSaal(final Id id,
+ final Name name, final AnzahlPlaetze anzahlPlaetze, final Kino kino) {
+ this(id, Versionable.unknownVersion(), name, anzahlPlaetze, kino);
+ }
+
+ public KinoSaal(final Id id, final Version version,
+ final Name name, final AnzahlPlaetze anzahlPlaetze, final Kino kino) {
+ this.id = id;
+ this.version = version;
+ this.name = name;
+ this.anzahlPlaetze = anzahlPlaetze;
+ this.kino = kino;
+ }
+
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ @Override
+ public Version version() {
+ return version;
+ }
+
+ @Override
+ public KinoSaal merge(final KinoSaal newData) {
+ return new KinoSaal(this.id,
+ newData.name, newData.anzahlPlaetze,
+ /* needs to be corrected outside */ null);
+ }
+
+ @Override
+ public String toString() {
+ return "KinoSaal{" +
+ "id='" + id + '\'' +
+ ", version=" + version +
+ ", name='" + name + '\'' +
+ ", anzahlPlaetze=" + anzahlPlaetze +
+ (kino != null ? ", kino.id=" + kino.id : ", kino=null") +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (!equalsByContent(o)) return false;
+
+ if (this == o) {return true;}
+ if (o == null || getClass() != o.getClass()) {return false;}
+ KinoSaal that = (KinoSaal) o;
+ return new EqualsBuilder().append(version, that.version).isEquals();
+ }
+
+ @SuppressWarnings("ConstantValue")
+ @Override
+ public boolean equalsByContent(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ KinoSaal that = (KinoSaal) o;
+
+ boolean result = new EqualsBuilder()
+ .append(id, that.id)
+ .append(name, that.name)
+ .append(anzahlPlaetze, that.anzahlPlaetze).
+ isEquals();
+ if (!result) return false;
+
+ if (kino == null && that.kino == null) return true;
+ if (kino == null && that.kino != null) return false;
+ if (kino != null) {
+ result = kino.id.equals(that.kino.id); // do not check kino but only its id (otherwise Stackoverflow error)
+ }
+
+ return result;
+ }
+
+ @Override
+ public int hashCode() {
+ HashCodeBuilder builder = new HashCodeBuilder(17, 37)
+ .append(id).append(version)
+ .append(name).append(anzahlPlaetze);
+ if (kino != null) {
+ builder.append(kino.id); // do not use kino but only its id (otherwise Stackoverflow error)
+ }
+ return builder.toHashCode();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Vorfuehrung.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Vorfuehrung.java
new file mode 100644
index 0000000..dad4745
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Vorfuehrung.java
@@ -0,0 +1,141 @@
+package de.accso.flexinale.backoffice.domain.model;
+
+import de.accso.flexinale.common.shared_kernel.*;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+public final class Vorfuehrung implements Identifiable, Versionable, Mergeable, EqualsByContent { // TODO is only final as otherwise Spotbugs complains, get rid of Exception in constructor!
+ public record Zeit(LocalDateTime raw) implements RawWrapper {
+ public Zeit(LocalDateTime raw) {
+ this.raw = raw.withNano(0); // precision is second
+ }
+ }
+
+ public final Id id;
+ public final Version version;
+
+ public final Zeit zeit;
+
+ @DoNotCheckInArchitectureTests
+ public final Film film;
+ @DoNotCheckInArchitectureTests
+ public final KinoSaal kinoSaal;
+
+ public Vorfuehrung(final Id id,
+ final Zeit zeit,
+ final Film film,
+ final KinoSaal kinoSaal) {
+ this(id, Versionable.unknownVersion(), zeit, film, kinoSaal);
+ }
+
+ public Vorfuehrung(final Id id, final Version version,
+ final Zeit zeit,
+ final Film film,
+ final KinoSaal kinoSaal) {
+ this.id = id;
+ this.version = version;
+ this.zeit = zeit;
+ this.film = film;
+ this.kinoSaal = kinoSaal;
+
+ //TODO this validation should not be done here. If class is not final, Spotbugs complains. For DB entities use "nullable=false" and "optional=false" for DB checks. In domain classes check with jakarta.annotation NonNull? Also: Why is KinoSaal obligatory but not Film?
+ if (kinoSaal == null) {
+ throw new FlexinaleIllegalArgumentException("KinoSaal of Vorfuehrung " + this +
+ " must not be null");
+ }
+ //TODO this validation should not be done here but in KinoSaal itself
+ if (kinoSaal.anzahlPlaetze == null) {
+ throw new FlexinaleIllegalArgumentException("KinoSaal's Anzahl Plaetze of Vorfuehrung " + this +
+ " must not be null");
+ }
+ }
+
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ @Override
+ public Version version() {
+ return version;
+ }
+
+ @Override
+ public Vorfuehrung merge(final Vorfuehrung newData) {
+ // merging Vorfuehrung has some business flaws - like when its film or its kinoSaal is changed,
+ // then all sold tickets might get obsolete - we ignore this here but care for that in Ticketing
+ Film mergedFilm = this.film.merge(newData.film);
+
+ Kino mergedKino = this.kinoSaal.kino.merge(newData.kinoSaal.kino);
+ Optional mergedKinoSaal = mergedKino.kinoSaele.stream().filter(
+ kinoSaal -> kinoSaal.id.equals(this.kinoSaal.id)).findFirst();
+ if (mergedKinoSaal.isEmpty()) {
+ throw new DeveloperMistakeException("merged KinoSaal from Vorfuehrung could not be retrieved");
+ }
+
+ return new Vorfuehrung(this.id, this.version,
+ newData.zeit, mergedFilm, mergedKinoSaal.get());
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (!equalsByContent(o)) return false;
+
+ if (this == o) {return true;}
+ if (o == null || getClass() != o.getClass()) {return false;}
+ Vorfuehrung that = (Vorfuehrung) o;
+ return new EqualsBuilder().append(version, that.version).isEquals();
+ }
+
+ @SuppressWarnings({"UnusedAssignment", "DataFlowIssue"})
+ @Override
+ public boolean equalsByContent(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ Vorfuehrung that = (Vorfuehrung) o;
+
+ boolean result = new EqualsBuilder()
+ .append(id, that.id)
+ .append(zeit, that.zeit)
+ .isEquals();
+ if (!result) return false;
+
+ if (film == null && that.film == null) result = true;
+ if (film == null && that.film != null) return false;
+ if (film != null) {
+ result = film.equalsByContent(that.film);
+ if (!result) return false;
+ }
+
+ if (kinoSaal == null && that.kinoSaal == null) result = true;
+ if (kinoSaal == null && that.kinoSaal != null) return false;
+ if (kinoSaal != null) {
+ result = kinoSaal.equalsByContent(that.kinoSaal);
+ return result;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(id).append(version)
+ .append(zeit)
+ .append(film)
+ .append(kinoSaal)
+ .toHashCode();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/FilmService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/FilmService.java
new file mode 100644
index 0000000..a6bf116
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/FilmService.java
@@ -0,0 +1,24 @@
+package de.accso.flexinale.backoffice.domain.services;
+
+import de.accso.flexinale.backoffice.domain.dao.FilmDao;
+import de.accso.flexinale.backoffice.domain.model.Film;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.List;
+
+public class FilmService {
+
+ private final FilmDao filmDao;
+
+ public FilmService(final FilmDao filmDao) {
+ this.filmDao = filmDao;
+ }
+
+ public List filme() {
+ return filmDao.findAll();
+ }
+
+ public Film film(final Identifiable.Id id) {
+ return filmDao.findById(id).orElse(null);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/KinoService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/KinoService.java
new file mode 100644
index 0000000..4b0a46b
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/KinoService.java
@@ -0,0 +1,24 @@
+package de.accso.flexinale.backoffice.domain.services;
+
+import de.accso.flexinale.backoffice.domain.dao.KinoDao;
+import de.accso.flexinale.backoffice.domain.model.Kino;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.List;
+
+public class KinoService {
+
+ private final KinoDao kinoDao;
+
+ public KinoService(final KinoDao kinoDao) {
+ this.kinoDao = kinoDao;
+ }
+
+ public List kinos() {
+ return kinoDao.findAll();
+ }
+
+ public Kino kino(final Identifiable.Id id) {
+ return kinoDao.findById(id).orElse(null);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/VorfuehrungService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/VorfuehrungService.java
new file mode 100644
index 0000000..90e9ded
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/VorfuehrungService.java
@@ -0,0 +1,24 @@
+package de.accso.flexinale.backoffice.domain.services;
+
+import de.accso.flexinale.backoffice.domain.dao.VorfuehrungDao;
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.List;
+
+public class VorfuehrungService {
+
+ private final VorfuehrungDao vorfuehrungDao;
+
+ public VorfuehrungService(final VorfuehrungDao vorfuehrungDao) {
+ this.vorfuehrungDao = vorfuehrungDao;
+ }
+
+ public List vorfuehrungen() {
+ return vorfuehrungDao.findAll();
+ }
+
+ public Vorfuehrung vorfuehrung(final Identifiable.Id id) {
+ return vorfuehrungDao.findById(id).orElse(null);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeActuatorEndpointEventsPublished.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeActuatorEndpointEventsPublished.java
new file mode 100644
index 0000000..cea991f
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeActuatorEndpointEventsPublished.java
@@ -0,0 +1,34 @@
+package de.accso.flexinale.backoffice.infrastructure;
+
+import de.accso.flexinale.common.api.eventbus.EventNotification;
+import de.accso.flexinale.common.api.event.Event;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
+import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
+import org.springframework.boot.actuate.endpoint.annotation.Selector;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+@Component
+@Endpoint(id = "backofficeEventsPublished")
+public class FlexinaleBackofficeActuatorEndpointEventsPublished implements EventNotification {
+ private final Queue eventsPublished = new ConcurrentLinkedQueue<>(); // event list is ordered by production time
+
+ @ReadOperation
+ public List eventsPublished() {
+ return eventsPublished.stream().toList();
+ }
+
+ @ReadOperation
+ public List eventsPublishedFilteredBy(@Selector Identifiable.Id correlationId) {
+ return eventsPublished.stream().filter(event -> event.correlationId().equals(correlationId)).toList();
+ }
+
+ @Override
+ public void notify(final Event event) {// might want to use a generic subscriber
+ eventsPublished.add(event);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeSpringFactory.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeSpringFactory.java
new file mode 100644
index 0000000..7c21e87
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeSpringFactory.java
@@ -0,0 +1,137 @@
+package de.accso.flexinale.backoffice.infrastructure;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import de.accso.flexinale.backoffice.api_out.event.FilmPublisher;
+import de.accso.flexinale.backoffice.api_out.event.KinoPublisher;
+import de.accso.flexinale.backoffice.api_out.event.VorfuehrungPublisher;
+import de.accso.flexinale.backoffice.application.services.FilmService;
+import de.accso.flexinale.backoffice.application.services.KinoService;
+import de.accso.flexinale.backoffice.application.services.VorfuehrungService;
+import de.accso.flexinale.backoffice.application.services.FilmUploadService;
+import de.accso.flexinale.backoffice.application.services.KinoUploadService;
+import de.accso.flexinale.backoffice.application.services.KinoSaalUploadService;
+import de.accso.flexinale.backoffice.application.services.VorfuehrungUploadService;
+import de.accso.flexinale.backoffice.domain.dao.FilmDao;
+import de.accso.flexinale.backoffice.domain.dao.KinoDao;
+import de.accso.flexinale.backoffice.domain.dao.KinoSaalDao;
+import de.accso.flexinale.backoffice.domain.dao.VorfuehrungDao;
+import de.accso.flexinale.backoffice.infrastructure.persistence.*;
+import de.accso.flexinale.common.api.eventbus.EventBusFactory;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+@Configuration
+@Profile({ "!testdata-besucherportal & !testdata-ticketing" })
+@EnableJpaRepositories({"de.accso.flexinale.backoffice.infrastructure.persistence"})
+@EnableTransactionManagement
+@EntityScan(basePackages={"de.accso.flexinale.backoffice.infrastructure.persistence"})
+public class FlexinaleBackofficeSpringFactory {
+
+ @Bean
+ public FilmService createApplicationFilmService(final de.accso.flexinale.backoffice.domain.services.FilmService filmService) {
+ return new FilmService(filmService);
+ }
+
+ @Bean
+ public de.accso.flexinale.backoffice.domain.services.FilmService createBackofficeFilmService(final FilmDao filmDao) {
+ return new de.accso.flexinale.backoffice.domain.services.FilmService(filmDao);
+ }
+
+ @Bean
+ public FilmUploadService createBackofficeFilmUploadService(final FilmDao filmDao) {
+ return new FilmUploadService(filmDao);
+ }
+
+ @Bean
+ public FilmDao createBackofficeFilmDao(final FilmJpaRepository filmJpaRepository) {
+ return new FilmJpaRepositoryDelegate(filmJpaRepository);
+ }
+
+ @Bean
+ public FilmPublisher createBackofficeFilmPublisher(final EventBusFactory eventBusFactory,
+ final FlexinaleBackofficeActuatorEndpointEventsPublished endpointEventsPublished) {
+ return new FilmPublisher(eventBusFactory, endpointEventsPublished);
+ }
+
+ // ------------------------------------------------------------------------------------------------
+
+ @Bean
+ public KinoService createApplicationKinoService(final de.accso.flexinale.backoffice.domain.services.KinoService kinoService) {
+ return new KinoService(kinoService);
+ }
+
+ @Bean
+ public de.accso.flexinale.backoffice.domain.services.KinoService createBackofficeKinoService(final KinoDao KinoDao) {
+ return new de.accso.flexinale.backoffice.domain.services.KinoService(KinoDao);
+ }
+
+ @Bean
+ public KinoUploadService createBackofficeKinoUploadService(final KinoDao kinoDao) {
+ return new KinoUploadService(kinoDao);
+ }
+
+ @Bean
+ public KinoSaalUploadService createBackofficeKinoSaalUploadService(final KinoSaalDao kinoSaalDao) {
+ return new KinoSaalUploadService(kinoSaalDao);
+ }
+
+ @Bean
+ public KinoDao createBackofficeKinoDao(final KinoJpaRepository kinoJpaRepository) {
+ return new KinoJpaRepositoryDelegate(kinoJpaRepository);
+ }
+
+ @Bean
+ public KinoSaalDao createBackofficeKinoSaalDao(final KinoSaalJpaRepository kinoSaalJpaRepository) {
+ return new KinoSaalJpaRepositoryDelegate(kinoSaalJpaRepository);
+ }
+
+ @Bean
+ public KinoPublisher createBackofficeKinoPublisher(final EventBusFactory eventBusFactory,
+ final FlexinaleBackofficeActuatorEndpointEventsPublished endpointEventsPublished) {
+ return new KinoPublisher(eventBusFactory, endpointEventsPublished);
+ }
+
+ // ------------------------------------------------------------------------------------------------
+
+ @Bean
+ public VorfuehrungService createApplicationVorfuehrungService(final de.accso.flexinale.backoffice.domain.services.VorfuehrungService vorfuehrungService) {
+ return new VorfuehrungService(vorfuehrungService);
+ }
+
+ @Bean
+ public de.accso.flexinale.backoffice.domain.services.VorfuehrungService createBackofficeVorfuehrungService(final VorfuehrungDao vorfuehrungDao) {
+ return new de.accso.flexinale.backoffice.domain.services.VorfuehrungService(vorfuehrungDao);
+ }
+
+ @Bean
+ public VorfuehrungUploadService createBackofficeVorfuehrungUploadService(final VorfuehrungDao vorfuehrungDao,
+ final FilmDao filmDao,
+ final KinoSaalDao kinoSaalDao) {
+ return new VorfuehrungUploadService(vorfuehrungDao, filmDao, kinoSaalDao);
+ }
+
+ @Bean
+ public VorfuehrungDao createBackofficeVorfuehrungDao(final VorfuehrungJpaRepository vorfuehrungJpaRepository) {
+ return new VorfuehrungJpaRepositoryDelegate(vorfuehrungJpaRepository);
+ }
+
+ @Bean
+ public VorfuehrungPublisher createBackofficeVorfuehrungPublisher(final EventBusFactory eventBusFactory,
+ final FlexinaleBackofficeActuatorEndpointEventsPublished endpointEventsPublished) {
+ return new VorfuehrungPublisher(eventBusFactory, endpointEventsPublished);
+ }
+
+ // ------------------------------------------------------------------------------------------------
+
+ // Actuator Event serialization
+ @Bean
+ public Jackson2ObjectMapperBuilderCustomizer jsonBackofficeCustomizer() {
+ return builder -> builder.visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmEntity.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmEntity.java
new file mode 100644
index 0000000..ea33dd6
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmEntity.java
@@ -0,0 +1,78 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.io.Serializable;
+
+@jakarta.persistence.Entity(name = "Film")
+public class FilmEntity implements Identifiable, Versionable, Serializable {
+ @jakarta.persistence.Id
+ public String id; // primary key
+
+ @jakarta.persistence.Column
+ public String titel;
+
+ @jakarta.persistence.Column
+ public String imdbUrl;
+
+ @jakarta.persistence.Column
+ public Integer dauerInMinuten;
+
+ @jakarta.persistence.Version
+ private Integer version = 0;
+
+ protected FilmEntity() {
+ }
+
+ public FilmEntity(final String id, final Integer version,
+ final String titel, final String imdbUrl, final Integer dauerInMinuten) {
+ this.id = id;
+ this.version = version;
+ this.titel = titel;
+ this.imdbUrl = imdbUrl;
+ this.dauerInMinuten = dauerInMinuten;
+ }
+
+ @Override
+ public Id id() {
+ return Identifiable.Id.of(id);
+ }
+
+ @Override
+ public Version version() {
+ return Versionable.Version.of(version);
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ FilmEntity that = (FilmEntity) o;
+
+ return new EqualsBuilder()
+ .append(id, that.id).append(version, that.version)
+ .append(titel, that.titel)
+ .append(imdbUrl, that.imdbUrl)
+ .append(dauerInMinuten, that.dauerInMinuten)
+ .isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(id).append(version)
+ .append(titel).append(imdbUrl).append(dauerInMinuten).toHashCode();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepository.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepository.java
new file mode 100644
index 0000000..e10323f
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepository.java
@@ -0,0 +1,8 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface FilmJpaRepository extends JpaRepository {
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepositoryDelegate.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepositoryDelegate.java
new file mode 100644
index 0000000..74481e8
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepositoryDelegate.java
@@ -0,0 +1,59 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import de.accso.flexinale.backoffice.domain.dao.FilmDao;
+import de.accso.flexinale.backoffice.domain.model.Film;
+import de.accso.flexinale.backoffice.infrastructure.persistence.mapper.FilmEntity2FilmMapper;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@SuppressWarnings("unused")
+public class FilmJpaRepositoryDelegate implements FilmDao {
+
+ private final FilmJpaRepository filmJpaRepository;
+
+ public FilmJpaRepositoryDelegate(final FilmJpaRepository filmJpaRepository) {
+ this.filmJpaRepository = filmJpaRepository;
+ }
+
+ @Override
+ public List findAll() {
+ List filmEntities = filmJpaRepository.findAll();
+
+ return filmEntities.stream()
+ .map(FilmEntity2FilmMapper::map)
+ .collect(Collectors.toCollection(ArrayList::new));
+ }
+
+ @Override
+ public Optional findById(final Identifiable.Id id) {
+ Optional filmEntity = filmJpaRepository.findById(id.id());
+ return FilmEntity2FilmMapper.map(filmEntity);
+ }
+
+ @Override
+ public Film save(final Film film) {
+ FilmEntity filmEntity = FilmEntity2FilmMapper.map(film);
+ FilmEntity savedEntity = filmJpaRepository.save(filmEntity);
+ return FilmEntity2FilmMapper.map(savedEntity);
+ }
+
+ @Override
+ public void delete(final Film film) {
+ FilmEntity filmEntity = FilmEntity2FilmMapper.map(film);
+ filmJpaRepository.delete(filmEntity);
+ }
+
+ @Override
+ public void deleteById(final Identifiable.Id id) {
+ filmJpaRepository.deleteById(id.id());
+ }
+
+ @Override
+ public void deleteAll() {
+ filmJpaRepository.deleteAll();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoEntity.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoEntity.java
new file mode 100644
index 0000000..29cccbf
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoEntity.java
@@ -0,0 +1,89 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.FetchType;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
+
+@jakarta.persistence.Entity(name = "Kino")
+public class KinoEntity implements Identifiable, Versionable, Serializable {
+ @jakarta.persistence.Id
+ public String id; // primary key
+
+ @jakarta.persistence.Column
+ public String name;
+
+ @jakarta.persistence.Column
+ public String adresse;
+
+ @jakarta.persistence.Column
+ public String emailAdresse;
+
+ @jakarta.persistence.Version
+ private Integer version = 0;
+
+ @jakarta.persistence.OneToMany(mappedBy = "kino", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
+ public Set kinoSaele = new HashSet<>();
+
+ protected KinoEntity() {
+ }
+
+ public KinoEntity(final String id, final Integer version,
+ final String name, final String adresse, final String emailAdresse) {
+ this.id = id;
+ this.version = version;
+ this.name = name;
+ this.adresse = adresse;
+ this.emailAdresse = emailAdresse;
+ }
+
+ @Override
+ public Id id() {
+ return Identifiable.Id.of(id);
+ }
+
+ @Override
+ public Version version() {
+ return Versionable.Version.of(version);
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ KinoEntity that = (KinoEntity) o;
+
+ return new EqualsBuilder()
+ .append(id, that.id).append(version, that.version)
+ .append(name, that.name)
+ .append(adresse, that.adresse)
+ .append(emailAdresse, that.emailAdresse)
+ .append(kinoSaele.stream().toList(), that.kinoSaele.stream().toList()) // need to use list, as Set does not support deep equals
+ .isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(id).append(version)
+ .append(name).append(adresse)
+ .append(emailAdresse)
+ .append(kinoSaele.stream().toList()) // need to use list, as Set does not support deep equals
+ .toHashCode();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepository.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepository.java
new file mode 100644
index 0000000..9838433
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepository.java
@@ -0,0 +1,8 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface KinoJpaRepository extends JpaRepository {
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepositoryDelegate.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepositoryDelegate.java
new file mode 100644
index 0000000..40c268d
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepositoryDelegate.java
@@ -0,0 +1,59 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import de.accso.flexinale.backoffice.domain.dao.KinoDao;
+import de.accso.flexinale.backoffice.domain.model.Kino;
+import de.accso.flexinale.backoffice.infrastructure.persistence.mapper.KinoKinoSaalEntity2KinoKinoSaalMapper;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@SuppressWarnings("unused")
+public class KinoJpaRepositoryDelegate implements KinoDao {
+
+ private final KinoJpaRepository kinoJpaRepository;
+
+ public KinoJpaRepositoryDelegate(final KinoJpaRepository KinoJpaRepository) {
+ this.kinoJpaRepository = KinoJpaRepository;
+ }
+
+ @Override
+ public List findAll() {
+ List kinoEntities = kinoJpaRepository.findAll();
+
+ return kinoEntities.stream()
+ .map(KinoKinoSaalEntity2KinoKinoSaalMapper::mapKino)
+ .collect(Collectors.toCollection(ArrayList::new));
+ }
+
+ @Override
+ public Optional findById(final Identifiable.Id id) {
+ Optional kinoEntity = kinoJpaRepository.findById(id.id());
+ return KinoKinoSaalEntity2KinoKinoSaalMapper.mapKino(kinoEntity);
+ }
+
+ @Override
+ public Kino save(final Kino kino) {
+ KinoEntity kinoEntity = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKino(kino);
+ KinoEntity savedEntity = kinoJpaRepository.save(kinoEntity);
+ return KinoKinoSaalEntity2KinoKinoSaalMapper.mapKino(savedEntity);
+ }
+
+ @Override
+ public void delete(final Kino kino) {
+ KinoEntity kinoEntity = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKino(kino);
+ kinoJpaRepository.delete(kinoEntity);
+ }
+
+ @Override
+ public void deleteById(final Identifiable.Id id) {
+ kinoJpaRepository.deleteById(id.id());
+ }
+
+ @Override
+ public void deleteAll() {
+ kinoJpaRepository.deleteAll();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalEntity.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalEntity.java
new file mode 100644
index 0000000..f58de25
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalEntity.java
@@ -0,0 +1,88 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+import java.io.Serializable;
+
+@jakarta.persistence.Entity(name = "KinoSaal")
+public class KinoSaalEntity implements Identifiable, Versionable, Serializable {
+ @jakarta.persistence.Id
+ public String id; // primary key
+
+ @jakarta.persistence.Column
+ public String name;
+
+ @jakarta.persistence.Column
+ public Integer anzahlPlaetze = 0;
+
+ @jakarta.persistence.ManyToOne
+ @jakarta.persistence.JoinColumn(name = "kino", nullable = false)
+ public KinoEntity kino;
+
+ @jakarta.persistence.Version
+ private Integer version = 0;
+
+ protected KinoSaalEntity() {
+ }
+
+ public KinoSaalEntity(final String id, final Integer version,
+ final String name, final Integer anzahlPlaetze, final KinoEntity kino) {
+ this.id = id;
+ this.version = version;
+ this.name = name;
+ this.anzahlPlaetze = anzahlPlaetze;
+ this.kino = kino;
+ }
+
+ @Override
+ public Id id() {
+ return Identifiable.Id.of(id);
+ }
+
+ @Override
+ public Version version() {
+ return Versionable.Version.of(version);
+ }
+
+ @Override
+ public String toString() {
+ return "KinoSaalEntity{" +
+ "id='" + id + '\'' +
+ ", name='" + name + '\'' +
+ ", anzahlPlaetze=" + anzahlPlaetze +
+ (kino != null ? ", kino.id=" + kino.id : ", kino=null") +
+ ", version=" + version +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ KinoSaalEntity that = (KinoSaalEntity) o;
+
+ EqualsBuilder builder = new EqualsBuilder()
+ .append(id, that.id).append(version, that.version)
+ .append(name, that.name).append(anzahlPlaetze, that.anzahlPlaetze);
+ if (kino != null) {
+ builder.append(kino.id, that.kino.id); // do not check kino but only its id (otherwise Stackoverflow error)
+ }
+ return builder.isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ HashCodeBuilder builder = new HashCodeBuilder(17, 37)
+ .append(id).append(version)
+ .append(name).append(anzahlPlaetze);
+ if (kino != null) {
+ builder.append(kino.id); // do not check kino but only its id (otherwise Stackoverflow error)
+ }
+ return builder.toHashCode();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepository.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepository.java
new file mode 100644
index 0000000..41a0eb3
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepository.java
@@ -0,0 +1,8 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface KinoSaalJpaRepository extends JpaRepository {
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepositoryDelegate.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepositoryDelegate.java
new file mode 100644
index 0000000..c612d65
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepositoryDelegate.java
@@ -0,0 +1,58 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import de.accso.flexinale.backoffice.domain.dao.KinoSaalDao;
+import de.accso.flexinale.backoffice.domain.model.KinoSaal;
+import de.accso.flexinale.backoffice.infrastructure.persistence.mapper.KinoKinoSaalEntity2KinoKinoSaalMapper;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@SuppressWarnings("unused")
+public class KinoSaalJpaRepositoryDelegate implements KinoSaalDao {
+
+ private final KinoSaalJpaRepository kinoSaalJpaRepository;
+
+ public KinoSaalJpaRepositoryDelegate(final KinoSaalJpaRepository kinoSaalJpaRepository) {
+ this.kinoSaalJpaRepository = kinoSaalJpaRepository;
+ }
+
+ @Override
+ public List findAll() {
+ List kinoSaalEntities = kinoSaalJpaRepository.findAll();
+
+ return kinoSaalEntities.stream()
+ .map(KinoKinoSaalEntity2KinoKinoSaalMapper::mapKinoSaal)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public Optional findById(final Identifiable.Id id) {
+ Optional kinoSaalEntity = kinoSaalJpaRepository.findById(id.id());
+ return KinoKinoSaalEntity2KinoKinoSaalMapper.mapToKinoSaal(kinoSaalEntity);
+ }
+
+ @Override
+ public KinoSaal save(final KinoSaal kinoSaal) {
+ KinoSaalEntity kinoSaalEntity = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKinoSaal(kinoSaal);
+ KinoSaalEntity savedEntity = kinoSaalJpaRepository.save(kinoSaalEntity);
+ return KinoKinoSaalEntity2KinoKinoSaalMapper.mapKinoSaal(savedEntity);
+ }
+
+ @Override
+ public void delete(final KinoSaal kinoSaal) {
+ KinoSaalEntity kinoSaalEntity = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKinoSaal(kinoSaal);
+ kinoSaalJpaRepository.delete(kinoSaalEntity);
+ }
+
+ @Override
+ public void deleteById(final Identifiable.Id id) {
+ kinoSaalJpaRepository.deleteById(id.id());
+ }
+
+ @Override
+ public void deleteAll() {
+ kinoSaalJpaRepository.deleteAll();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungEntity.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungEntity.java
new file mode 100644
index 0000000..48fdb6e
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungEntity.java
@@ -0,0 +1,96 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import de.accso.flexinale.common.shared_kernel.DateTimeHelper;
+import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@jakarta.persistence.Entity(name = "Vorfuehrung")
+public final class VorfuehrungEntity implements Identifiable, Versionable, Serializable { // TODO is only final as otherwise Spotbugs complains, get rid of Exception in constructor!
+ @jakarta.persistence.Id
+ public String id; // primary key
+
+ @jakarta.persistence.Column
+ public Long zeit; // save EPOCH seconds instead of Date/Time (Timezone is UTC)
+
+ @jakarta.persistence.ManyToOne
+ public FilmEntity film;
+
+ @jakarta.persistence.ManyToOne
+ public KinoSaalEntity kinoSaal;
+
+ @jakarta.persistence.Version
+ private Integer version = 0;
+
+ protected VorfuehrungEntity() {
+ }
+
+ public VorfuehrungEntity(final String id, final Integer version,
+ final LocalDateTime zeit,
+ final FilmEntity film,
+ final KinoSaalEntity kinoSaal) {
+ this.id = id;
+ this.version = version;
+ this.zeit = DateTimeHelper.toEpochSeconds(zeit);
+ this.film = film;
+ this.kinoSaal = kinoSaal;
+
+ //TODO this validation should not be done here. If class is not final, Spotbugs complains. For DB entities use "nullable=false" and "optional=false" for DB checks. In domain classes check with jakarta.annotation NonNull? Also: Why is KinoSaal obligatory but not Film?
+ if (kinoSaal == null) {
+ throw new FlexinaleIllegalArgumentException("KinoSaal of Vorfuehrung " + this +
+ " must not be null");
+ }
+ //TODO this validation should not be done here but in KinoSaal itself
+ if (kinoSaal.anzahlPlaetze == null) {
+ throw new FlexinaleIllegalArgumentException("KinoSaal's Anzahl Plaetze of Vorfuehrung " + this +
+ " must not be null");
+ }
+ }
+
+ @Override
+ public Id id() {
+ return Identifiable.Id.of(id);
+ }
+
+ @Override
+ public Version version() {
+ return Versionable.Version.of(version);
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ VorfuehrungEntity that = (VorfuehrungEntity) o;
+
+ return new EqualsBuilder()
+ .append(id, that.id).append(version, that.version)
+ .append(zeit, that.zeit)
+ .append(film, that.film).append(kinoSaal, that.kinoSaal)
+ .isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(id).append(version)
+ .append(zeit).append(film).append(kinoSaal)
+ .toHashCode();
+ }
+}
+
+
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepository.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepository.java
new file mode 100644
index 0000000..46e6164
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepository.java
@@ -0,0 +1,14 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface VorfuehrungJpaRepository extends JpaRepository {
+
+ @Query("SELECT v FROM Vorfuehrung v WHERE v.film.id = :filmId ORDER BY v.zeit")
+ List findByFilmId(final String filmId);
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepositoryDelegate.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepositoryDelegate.java
new file mode 100644
index 0000000..4e5e1c3
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepositoryDelegate.java
@@ -0,0 +1,65 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence;
+
+import de.accso.flexinale.backoffice.domain.dao.VorfuehrungDao;
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+import de.accso.flexinale.backoffice.infrastructure.persistence.mapper.VorfuehrungEntity2VorfuehrungMapper;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@SuppressWarnings("unused")
+public class VorfuehrungJpaRepositoryDelegate implements VorfuehrungDao {
+
+ private final VorfuehrungJpaRepository vorfuehrungJpaRepository;
+
+ public VorfuehrungJpaRepositoryDelegate(final VorfuehrungJpaRepository vorfuehrungJpaRepository) {
+ this.vorfuehrungJpaRepository = vorfuehrungJpaRepository;
+ }
+
+ @Override
+ public List findAll() {
+ List vorfuehrungEntities = vorfuehrungJpaRepository.findAll();
+
+ return vorfuehrungEntities.stream()
+ .map(VorfuehrungEntity2VorfuehrungMapper::map)
+ .collect(Collectors.toCollection(ArrayList::new));
+ }
+
+ @Override
+ public Optional findById(final Identifiable.Id id) {
+ Optional vorfuehrungEntity = vorfuehrungJpaRepository.findById(id.id());
+ return VorfuehrungEntity2VorfuehrungMapper.map(vorfuehrungEntity);
+ }
+
+ @Override
+ public List findByFilmId(final Identifiable.Id filmId) {
+ List vorfuehrungEntities = vorfuehrungJpaRepository.findByFilmId(filmId.id());
+ return VorfuehrungEntity2VorfuehrungMapper.map(vorfuehrungEntities);
+ }
+
+ @Override
+ public Vorfuehrung save(final Vorfuehrung vorfuehrung) {
+ VorfuehrungEntity vorfuehrungEntity = VorfuehrungEntity2VorfuehrungMapper.map(vorfuehrung);
+ VorfuehrungEntity savedEntity = vorfuehrungJpaRepository.save(vorfuehrungEntity);
+ return VorfuehrungEntity2VorfuehrungMapper.map(savedEntity);
+ }
+
+ @Override
+ public void delete(final Vorfuehrung vorfuehrung) {
+ VorfuehrungEntity vorfuehrungEntity = VorfuehrungEntity2VorfuehrungMapper.map(vorfuehrung);
+ vorfuehrungJpaRepository.delete(vorfuehrungEntity);
+ }
+
+ @Override
+ public void deleteById(final Identifiable.Id id) {
+ vorfuehrungJpaRepository.deleteById(id.id());
+ }
+
+ @Override
+ public void deleteAll() {
+ vorfuehrungJpaRepository.deleteAll();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/FilmEntity2FilmMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/FilmEntity2FilmMapper.java
new file mode 100644
index 0000000..6b0343d
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/FilmEntity2FilmMapper.java
@@ -0,0 +1,33 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence.mapper;
+
+import de.accso.flexinale.backoffice.domain.model.Film;
+import de.accso.flexinale.backoffice.infrastructure.persistence.FilmEntity;
+
+import java.util.Optional;
+
+import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
+
+public final class FilmEntity2FilmMapper {
+
+ public static Film map(final FilmEntity filmEntity) {
+ return new Film(filmEntity.id(), filmEntity.version(),
+ new Film.Titel(filmEntity.titel), new Film.ImdbUrl(filmEntity.imdbUrl),
+ new Film.DauerInMinuten(filmEntity.dauerInMinuten));
+ }
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ public static Optional map(final Optional optionalFilmEntity) {
+ if (optionalFilmEntity.isEmpty()) {
+ return Optional.empty();
+ }
+ else {
+ FilmEntity filmEntity = optionalFilmEntity.get();
+ return Optional.of(map(filmEntity));
+ }
+ }
+
+ public static FilmEntity map(final Film film) {
+ return new FilmEntity(film.id().id(), film.version().version(),
+ getRawOrNull(film.titel), getRawOrNull(film.imdbUrl), getRawOrNull(film.dauerInMinuten));
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapper.java
new file mode 100644
index 0000000..2adee13
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapper.java
@@ -0,0 +1,86 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence.mapper;
+
+import de.accso.flexinale.backoffice.domain.model.Kino;
+import de.accso.flexinale.backoffice.domain.model.KinoSaal;
+import de.accso.flexinale.backoffice.infrastructure.persistence.KinoEntity;
+import de.accso.flexinale.backoffice.infrastructure.persistence.KinoSaalEntity;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
+
+public final class KinoKinoSaalEntity2KinoKinoSaalMapper {
+
+ public static Kino mapKino(final KinoEntity kinoEntity) {
+ final Kino kino = new Kino(kinoEntity.id(), kinoEntity.version());
+ Set mappedKinoSaele = kinoEntity.kinoSaele.stream()
+ .map(kinoSaalEntity -> mapKinoSaal(kinoSaalEntity, kino))
+ .collect(Collectors.toCollection(HashSet::new));
+
+ kino.name = new Kino.Name(kinoEntity.name);
+ kino.adresse = new Kino.Adresse(kinoEntity.adresse);
+ kino.emailAdresse = new Kino.EmailAdresse(kinoEntity.emailAdresse);
+ kino.kinoSaele = mappedKinoSaele;
+
+ return kino;
+ }
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ public static Optional mapKino(final Optional optionalKinoEntity) {
+ if (optionalKinoEntity.isEmpty()) {
+ return Optional.empty();
+ }
+ else {
+ KinoEntity kinoEntity = optionalKinoEntity.get();
+ return Optional.of(mapKino(kinoEntity));
+ }
+ }
+
+ public static KinoEntity mapKino(final Kino kino) {
+ final KinoEntity kinoEntity = new KinoEntity(kino.id().id(), kino.version().version(),
+ getRawOrNull(kino.name), getRawOrNull(kino.adresse), getRawOrNull(kino.emailAdresse));
+
+ kinoEntity.kinoSaele = kino.kinoSaele.stream()
+ .map(kinoSaal -> mapKinoSaal(kinoSaal, kinoEntity))
+ .collect(Collectors.toCollection(HashSet::new));
+
+ return kinoEntity;
+ }
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ public static Optional mapToKinoSaal(final Optional kinoSaalEntity) {
+ return kinoSaalEntity.map(KinoKinoSaalEntity2KinoKinoSaalMapper::mapKinoSaal);
+ }
+
+ public static KinoSaal mapKinoSaal(KinoSaalEntity kinoSaalEntity) {
+ KinoEntity kinoEntity = kinoSaalEntity.kino;
+ Kino mappedKino = mapKino(kinoEntity);
+ return new KinoSaal(kinoSaalEntity.id(), kinoEntity.version(),
+ new KinoSaal.Name(kinoSaalEntity.name), new KinoSaal.AnzahlPlaetze(kinoSaalEntity.anzahlPlaetze), mappedKino);
+ }
+
+ private static KinoSaal mapKinoSaal(final KinoSaalEntity kinoSaalEntity, Kino kino) {
+ return new KinoSaal(kinoSaalEntity.id(), kinoSaalEntity.version(),
+ new KinoSaal.Name(kinoSaalEntity.name), new KinoSaal.AnzahlPlaetze(kinoSaalEntity.anzahlPlaetze), kino);
+ }
+
+ @SuppressWarnings({"OptionalUsedAsFieldOrParameterType", "unused"})
+ public static Optional mapFromKinoSaal(final Optional kinoSaal) {
+ return kinoSaal.map(KinoKinoSaalEntity2KinoKinoSaalMapper::mapKinoSaal);
+ }
+
+ public static KinoSaalEntity mapKinoSaal(final KinoSaal kinoSaal) {
+ Kino kino = kinoSaal.kino;
+ KinoEntity mappedKino = mapKino(kino);
+ return new KinoSaalEntity(kinoSaal.id().id(), kinoSaal.version().version(),
+ getRawOrNull(kinoSaal.name), getRawOrNull(kinoSaal.anzahlPlaetze), mappedKino);
+ }
+
+ private static KinoSaalEntity mapKinoSaal(final KinoSaal kinoSaal, final KinoEntity kinoEntity) {
+ return new KinoSaalEntity(kinoSaal.id().id(), kinoEntity.version().version(),
+ getRawOrNull(kinoSaal.name), getRawOrNull(kinoSaal.anzahlPlaetze), kinoEntity);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/VorfuehrungEntity2VorfuehrungMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/VorfuehrungEntity2VorfuehrungMapper.java
new file mode 100644
index 0000000..a63695d
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/VorfuehrungEntity2VorfuehrungMapper.java
@@ -0,0 +1,52 @@
+package de.accso.flexinale.backoffice.infrastructure.persistence.mapper;
+
+import de.accso.flexinale.backoffice.domain.model.Film;
+import de.accso.flexinale.backoffice.domain.model.KinoSaal;
+import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
+import de.accso.flexinale.backoffice.infrastructure.persistence.FilmEntity;
+import de.accso.flexinale.backoffice.infrastructure.persistence.KinoSaalEntity;
+import de.accso.flexinale.backoffice.infrastructure.persistence.VorfuehrungEntity;
+import de.accso.flexinale.common.shared_kernel.DateTimeHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
+
+public class VorfuehrungEntity2VorfuehrungMapper {
+
+ public static Vorfuehrung map(final VorfuehrungEntity vorfuehrungEntity) {
+ Film mappedFilm = FilmEntity2FilmMapper.map(vorfuehrungEntity.film);
+ KinoSaal mappedKinoSaal = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKinoSaal(vorfuehrungEntity.kinoSaal);
+
+ return new Vorfuehrung(vorfuehrungEntity.id(), vorfuehrungEntity.version(),
+ new Vorfuehrung.Zeit(DateTimeHelper.fromEpochSeconds(vorfuehrungEntity.zeit)),
+ mappedFilm, mappedKinoSaal);
+ }
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ public static Optional map(final Optional optionalVorfuehrungEntity) {
+ if (optionalVorfuehrungEntity.isEmpty()) {
+ return Optional.empty();
+ }
+ else {
+ VorfuehrungEntity vorfuehrungEntity = optionalVorfuehrungEntity.get();
+ return Optional.of(map(vorfuehrungEntity));
+ }
+ }
+
+ public static VorfuehrungEntity map(final Vorfuehrung vorfuehrung) {
+ FilmEntity mappedFilm = FilmEntity2FilmMapper.map(vorfuehrung.film);
+ KinoSaalEntity mappedKinoSaal = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKinoSaal(vorfuehrung.kinoSaal);
+
+ return new VorfuehrungEntity(vorfuehrung.id().id(), vorfuehrung.version().version(),
+ getRawOrNull(vorfuehrung.zeit), mappedFilm, mappedKinoSaal);
+ }
+
+ public static List map(final List vorfuehrungEntities) {
+ return vorfuehrungEntities.stream().map(VorfuehrungEntity2VorfuehrungMapper::map)
+ .collect(Collectors.toCollection(ArrayList::new));
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/application.properties b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/application.properties
new file mode 100644
index 0000000..5cc44ae
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/application.properties
@@ -0,0 +1,70 @@
+application.title=FLEXinale as Distributed Services, Backoffice
+application.version=@pom.version@ @maven.build.timestamp@
+spring.banner.location=classpath:/flexinale-banner.txt
+
+#########################################################################
+# For endpoint /version
+#########################################################################
+build.version=@pom.version@
+build.date=@maven.build.timestamp@
+
+#########################################################################
+# Persistence
+#########################################################################
+spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/distributed-backoffice
+spring.datasource.name=flexinale
+spring.datasource.username=flexinale
+spring.datasource.password=flexinale
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.jpa.database=postgresql
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
+spring.jpa.generate-ddl=true
+# uses schema if existing (or creates anew)
+# in a real production environment one should use 'validate' which does just validation but doesn't change anything
+spring.jpa.hibernate.ddl-auto=update
+# Pros and Cons: See https://www.baeldung.com/spring-open-session-in-view,
+# https://stackoverflow.com/questions/30549489/what-is-this-spring-jpa-open-in-view-true-property-in-spring-boot
+spring.jpa.open-in-view=false
+# spring.jpa.show-sql=true
+
+#########################################################################
+# Web
+#########################################################################
+server.error.path=/error
+server.error.include-stacktrace=always
+server.error.include-exception=true
+server.error.include-message=always
+server.error.whitelabel.enabled=false
+server.port=8081
+
+#########################################################################
+# Security
+#########################################################################
+security.enable-csrf=true
+
+#########################################################################
+# Kafka
+#########################################################################
+spring.kafka.consumer.group-id=flexinale-distributed-backoffice
+
+# Spring Kafka Consumer
+spring.kafka.consumer.bootstrap-servers=localhost:29092
+spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
+
+# Spring Kafka Producer
+spring.kafka.producer.bootstrap-servers=localhost:29092
+spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
+spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
+
+#########################################################################
+# Metrics endpoints, micrometer/prometheus/grafana
+#########################################################################
+# enable and expose
+management.endpoints.web.exposure.include=*
+management.endpoint.health.show-details=always
+spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false
+
+#########################################################################
+# flexinale properties
+#########################################################################
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/flexinale-banner.txt b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/flexinale-banner.txt
new file mode 100644
index 0000000..cac6e52
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/flexinale-banner.txt
@@ -0,0 +1,10 @@
+-------------------------------------------------------------------------
+,------. ,--. ,------. ,--. ,--. ,--. ,--.
+| .---' | | | .---' \ `.' / `--' ,--,--, ,--,--. | | ,---.
+| `--, | | | `--, .' \ ,--. | \ ' ,-. | | | | .-. :
+| |` | '--. | `---. / .'. \ | | | || | \ '-' | | | \ --.
+`--' `-----' `------' '--' '--' `--' `--''--' `--`--' `--' `----'
+${application.title} on port ${server.port}
+(version ${application.version})
+Powered by Spring Boot ${spring-boot.version}
+-------------------------------------------------------------------------
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/logback.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/logback.xml
new file mode 100644
index 0000000..87e8669
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/logback.xml
@@ -0,0 +1,20 @@
+
+
+
+ %d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/static/index.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/static/index.html
new file mode 100644
index 0000000..31ea037
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/static/index.html
@@ -0,0 +1,12 @@
+
+
+
+ Flexinale Distributed - Backoffice
+
+
+
+
+Flexinale Distributed - Backoffice
+
+
+
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-events-published.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-events-published.http
new file mode 100644
index 0000000..61a6207
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-events-published.http
@@ -0,0 +1,3 @@
+# curl -v -X GET -u admin:admin1 http://localhost:8081/actuator/backofficeEventsPublished
+GET http://localhost:8081/actuator/backofficeEventsPublished
+Authorization: Basic admin admin1
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-health.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-health.http
new file mode 100644
index 0000000..af9cf0d
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-health.http
@@ -0,0 +1,3 @@
+# curl -v -X GET -u admin:admin1 http://localhost:8081/actuator/health
+GET http://localhost:8081/actuator/health
+Authorization: Basic admin admin1
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-info.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-info.http
new file mode 100644
index 0000000..aa1b43a
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-info.http
@@ -0,0 +1,3 @@
+# curl -v -X GET -u admin:admin1 http://localhost:8081/actuator/info
+GET http://localhost:8081/actuator/info
+Authorization: Basic admin admin1
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-metrics.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-metrics.http
new file mode 100644
index 0000000..5498552
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-metrics.http
@@ -0,0 +1,3 @@
+# curl -v -X GET -u admin:admin1 http://localhost:8081/actuator/metrics
+GET http://localhost:8081/actuator/metrics
+Authorization: Basic admin admin1
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-prometheus.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-prometheus.http
new file mode 100644
index 0000000..73fbf35
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-prometheus.http
@@ -0,0 +1,3 @@
+# curl -v -X GET -u admin:admin1 http://localhost:8081/actuator/prometheus
+GET http://localhost:8081/actuator/prometheus
+Authorization: Basic admin admin1
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java
new file mode 100644
index 0000000..cd1709b
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java
@@ -0,0 +1,21 @@
+package de.accso.flexinale;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(classes = {FlexinaleDistributedApplicationBackoffice.class})
+@ActiveProfiles("smoketest")
+public class ApplicationPropertiesFileTest {
+
+ @Value("${application.version}")
+ private String propertyApplicationVersion;
+
+ @Test
+ public void testReadsTestPropertiesFile() {
+ assertThat(propertyApplicationVersion).isEqualTo("test");
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSmokeTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSmokeTest.java
new file mode 100644
index 0000000..f35dc4a
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSmokeTest.java
@@ -0,0 +1,20 @@
+package de.accso.flexinale;
+
+import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(classes = {FlexinaleDistributedApplicationBackoffice.class})
+@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
+@ActiveProfiles("smoketest")
+@DoNotCheckInArchitectureTests
+class FlexinaleDistributedApplicationBackofficeSmokeTest {
+
+ @Test
+ @SuppressWarnings("EmptyMethod")
+ void testLoadInitialApplicationContext() {
+ // nope
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSpringConfigTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSpringConfigTest.java
new file mode 100644
index 0000000..d268b31
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSpringConfigTest.java
@@ -0,0 +1,31 @@
+package de.accso.flexinale;
+
+import de.accso.flexinale.common.application.Config;
+import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(classes = {FlexinaleDistributedApplicationBackoffice.class})
+@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
+@ActiveProfiles("configtest")
+@DoNotCheckInArchitectureTests
+class FlexinaleDistributedApplicationBackofficeSpringConfigTest {
+
+ @Autowired
+ Config config;
+
+ @Test
+ void testLoadSpringConfig() {
+ // arrange, act
+ // nope, is loaded auto-magically by Spring, see application-configtest.properties
+
+ // assert
+ assertThat(config.getQuoteOnline()).isEqualTo(33); // default
+ assertThat(config.getMinZeitZwischenVorfuehrungenInMinuten()).isEqualTo(30); // default
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application-configtest.properties b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application-configtest.properties
new file mode 100644
index 0000000..1bb8bf6
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application-configtest.properties
@@ -0,0 +1 @@
+# empty
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application.properties b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application.properties
new file mode 100644
index 0000000..5c60591
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application.properties
@@ -0,0 +1,39 @@
+application.title=FLEXinale as Distributed Services, Backoffice
+application.version=test
+spring.banner.location=classpath:/flexinale-banner.txt
+
+#########################################################################
+# For endpoint /version
+#########################################################################
+build.version=@pom.version@
+build.date=@maven.build.timestamp@
+
+#########################################################################
+# Persistence
+#########################################################################
+spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/distributed-backoffice
+spring.datasource.name=flexinale
+spring.datasource.username=flexinale
+spring.datasource.password=flexinale
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.jpa.database=postgresql
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
+spring.jpa.generate-ddl=true
+# spring.jpa.show-sql=true
+spring.jpa.hibernate.ddl-auto=create
+
+#########################################################################
+# Web
+#########################################################################
+spring.thymeleaf.prefix=classpath:/templates/
+spring.thymeleaf.suffix=.html
+server.error.path=/error
+server.port=9091
+
+#########################################################################
+# flexinale properties
+#########################################################################
+# Quote for online kontingent in percent
+de.accso.flexinale.kontingent.quote.online=33
+# time in minutes
+de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten=30
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/logback-test.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..ac74e21
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/logback-test.xml
@@ -0,0 +1,20 @@
+
+
+
+ %d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/.gitignore b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/.gitignore
new file mode 100644
index 0000000..b8de55e
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/.gitignore
@@ -0,0 +1,4 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/SpotbugsExcludeFilter.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/SpotbugsExcludeFilter.xml
new file mode 100644
index 0000000..029f096
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/SpotbugsExcludeFilter.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/pom.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/pom.xml
new file mode 100644
index 0000000..0b58a16
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/pom.xml
@@ -0,0 +1,71 @@
+
+
+ 4.0.0
+
+
+ de.accso
+ flexinale-distributed
+ 2024.3.0
+ ../pom.xml
+
+
+ flexinale-distributed-backoffice_api_contract
+ 2024.3.0
+ Flexinale Distributed Backoffice API Contract
+ Flexinale - FLEX case-study "film festival", distributed services, backoffice_api-contract
+
+ jar
+
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang3.version}
+
+
+
+ de.accso
+ flexinale-distributed-common
+ 2024.3.0
+
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs-maven-plugin.version}
+
+
+
+ com.h3xstream.findsecbugs
+ findsecbugs-plugin
+ ${findsecbugs-maven-plugin.version}
+
+
+ SpotbugsExcludeFilter.xml
+
+
+
+
+ com.github.spotbugs
+ spotbugs
+ ${spotbugs.version}
+
+
+
+
+ verify
+
+ check
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/AllBackofficeEvents.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/AllBackofficeEvents.java
new file mode 100644
index 0000000..18ae24c
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/AllBackofficeEvents.java
@@ -0,0 +1,21 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import de.accso.flexinale.common.api.event.Event;
+
+import java.util.Set;
+
+public final class AllBackofficeEvents {
+
+ public static final Set> eventTypes =
+ Set.of(
+ FilmCreatedEvent.class,
+ FilmUpdatedEvent.class,
+ FilmDeletedEvent.class,
+ KinoCreatedEvent.class,
+ KinoUpdatedEvent.class,
+ KinoDeletedEvent.class,
+ VorfuehrungCreatedEvent.class,
+ VorfuehrungUpdatedEvent.class,
+ VorfuehrungDeletedEvent.class
+ );
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCRUDEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCRUDEvent.java
new file mode 100644
index 0000000..2c8224b
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCRUDEvent.java
@@ -0,0 +1,42 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.common.api.event.AbstractEvent;
+import de.accso.flexinale.common.api.event.Event;
+import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+public abstract sealed class FilmCRUDEvent extends AbstractEvent implements Event
+ permits FilmCreatedEvent, FilmUpdatedEvent, FilmDeletedEvent {
+
+ public enum CRUD { CREATE, UPDATE, DELETE }
+
+ @DoNotCheckInArchitectureTests
+ public final FilmTO film;
+
+ protected FilmCRUDEvent(final FilmTO film) {
+ this.film = film;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ final FilmCRUDEvent that = (FilmCRUDEvent) o;
+
+ return new EqualsBuilder().appendSuper(super.equals(o))
+ .append(version(), that.version())
+ .append(film, that.film).isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .appendSuper(super.hashCode()).append(film)
+ .append(version())
+ .toHashCode();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCreatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCreatedEvent.java
new file mode 100644
index 0000000..64e050f
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCreatedEvent.java
@@ -0,0 +1,22 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+
+@SuppressWarnings({"unused", "CanBeFinal"})
+public non-sealed class FilmCreatedEvent extends FilmCRUDEvent {
+ private static Version version = Versionable.initialVersion().inc();
+
+ private FilmCreatedEvent() { super(null); } // needed for (de)serialization via Jackson
+
+ public FilmCreatedEvent(final FilmTO film) {
+ super(film);
+ }
+
+ @Override
+ @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized
+ public Version version() {
+ return version;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmDeletedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmDeletedEvent.java
new file mode 100644
index 0000000..e8ce85a
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmDeletedEvent.java
@@ -0,0 +1,22 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+
+@SuppressWarnings({"unused", "CanBeFinal"})
+public non-sealed class FilmDeletedEvent extends FilmCRUDEvent {
+ private static Version version = Versionable.initialVersion().inc();
+
+ private FilmDeletedEvent() { super(null); } // needed for (de)serialization via Jackson
+
+ public FilmDeletedEvent(final FilmTO film) {
+ super(film);
+ }
+
+ @Override
+ @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized
+ public Version version() {
+ return version;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmUpdatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmUpdatedEvent.java
new file mode 100644
index 0000000..de74c2f
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmUpdatedEvent.java
@@ -0,0 +1,22 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+
+@SuppressWarnings({"unused", "CanBeFinal"})
+public non-sealed class FilmUpdatedEvent extends FilmCRUDEvent {
+ private static Version version = Versionable.initialVersion().inc();
+
+ private FilmUpdatedEvent() { super(null); } // needed for (de)serialization via Jackson
+
+ public FilmUpdatedEvent(final FilmTO film) {
+ super(film);
+ }
+
+ @Override
+ @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized
+ public Version version() {
+ return version;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCRUDEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCRUDEvent.java
new file mode 100644
index 0000000..a84192a
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCRUDEvent.java
@@ -0,0 +1,42 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.common.api.event.AbstractEvent;
+import de.accso.flexinale.common.api.event.Event;
+import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+public abstract sealed class KinoCRUDEvent extends AbstractEvent implements Event
+ permits KinoCreatedEvent, KinoUpdatedEvent, KinoDeletedEvent {
+
+ public enum CRUD { CREATE, UPDATE, DELETE }
+
+ @DoNotCheckInArchitectureTests
+ public final KinoTO kino;
+
+ protected KinoCRUDEvent(final KinoTO kino) {
+ this.kino = kino;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ final KinoCRUDEvent that = (KinoCRUDEvent) o;
+
+ return new EqualsBuilder().appendSuper(super.equals(o))
+ .append(version(), that.version())
+ .append(kino, that.kino).isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .appendSuper(super.hashCode()).append(kino)
+ .append(version())
+ .toHashCode();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCreatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCreatedEvent.java
new file mode 100644
index 0000000..7768abd
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCreatedEvent.java
@@ -0,0 +1,22 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+
+@SuppressWarnings({"unused", "CanBeFinal"})
+public non-sealed class KinoCreatedEvent extends KinoCRUDEvent {
+ private static Version version = Versionable.initialVersion().inc();
+
+ private KinoCreatedEvent() { super(null); } // needed for (de)serialization via Jackson
+
+ public KinoCreatedEvent(final KinoTO kino) {
+ super(kino);
+ }
+
+ @Override
+ @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized
+ public Version version() {
+ return version;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoDeletedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoDeletedEvent.java
new file mode 100644
index 0000000..81a1ab7
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoDeletedEvent.java
@@ -0,0 +1,22 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+
+@SuppressWarnings({"unused", "CanBeFinal"})
+public non-sealed class KinoDeletedEvent extends KinoCRUDEvent {
+ private static Version version = Versionable.initialVersion().inc();
+
+ private KinoDeletedEvent() { super(null); } // needed for (de)serialization via Jackson
+
+ public KinoDeletedEvent(final KinoTO kino) {
+ super(kino);
+ }
+
+ @Override
+ @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized
+ public Version version() {
+ return version;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoUpdatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoUpdatedEvent.java
new file mode 100644
index 0000000..6c579ba
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoUpdatedEvent.java
@@ -0,0 +1,22 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+
+@SuppressWarnings({"unused", "CanBeFinal"})
+public non-sealed class KinoUpdatedEvent extends KinoCRUDEvent {
+ private static Version version = Versionable.initialVersion().inc();
+
+ private KinoUpdatedEvent() { super(null); } // needed for (de)serialization via Jackson
+
+ public KinoUpdatedEvent(final KinoTO kino) {
+ super(kino);
+ }
+
+ @Override
+ @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized
+ public Version version() {
+ return version;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCRUDEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCRUDEvent.java
new file mode 100644
index 0000000..6b57e15
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCRUDEvent.java
@@ -0,0 +1,42 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.common.api.event.AbstractEvent;
+import de.accso.flexinale.common.api.event.Event;
+import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+public abstract sealed class VorfuehrungCRUDEvent extends AbstractEvent implements Event
+ permits VorfuehrungCreatedEvent, VorfuehrungUpdatedEvent, VorfuehrungDeletedEvent {
+
+ public enum CRUD { CREATE, UPDATE, DELETE }
+
+ @DoNotCheckInArchitectureTests
+ public final VorfuehrungTO vorfuehrung;
+
+ protected VorfuehrungCRUDEvent(final VorfuehrungTO vorfuehrung) {
+ this.vorfuehrung = vorfuehrung;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ final VorfuehrungCRUDEvent that = (VorfuehrungCRUDEvent) o;
+
+ return new EqualsBuilder().appendSuper(super.equals(o))
+ .append(version(), that.version())
+ .append(vorfuehrung, that.vorfuehrung).isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .appendSuper(super.hashCode()).append(vorfuehrung)
+ .append(version())
+ .toHashCode();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCreatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCreatedEvent.java
new file mode 100644
index 0000000..7c29121
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCreatedEvent.java
@@ -0,0 +1,22 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+
+@SuppressWarnings({"unused", "CanBeFinal"})
+public non-sealed class VorfuehrungCreatedEvent extends VorfuehrungCRUDEvent {
+ private static Version version = Versionable.initialVersion().inc();
+
+ private VorfuehrungCreatedEvent() { super(null); } // needed for (de)serialization via Jackson
+
+ public VorfuehrungCreatedEvent(final VorfuehrungTO vorfuehrung) {
+ super(vorfuehrung);
+ }
+
+ @Override
+ @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized
+ public Version version() {
+ return version;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungDeletedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungDeletedEvent.java
new file mode 100644
index 0000000..3ca39ca
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungDeletedEvent.java
@@ -0,0 +1,22 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+
+@SuppressWarnings({"unused", "CanBeFinal"})
+public non-sealed class VorfuehrungDeletedEvent extends VorfuehrungCRUDEvent {
+ private static Version version = Versionable.initialVersion().inc();
+
+ private VorfuehrungDeletedEvent() { super(null); } // needed for (de)serialization via Jackson
+
+ public VorfuehrungDeletedEvent(final VorfuehrungTO vorfuehrung) {
+ super(vorfuehrung);
+ }
+
+ @Override
+ @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized
+ public Version version() {
+ return version;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungUpdatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungUpdatedEvent.java
new file mode 100644
index 0000000..99880ac
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungUpdatedEvent.java
@@ -0,0 +1,22 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+
+@SuppressWarnings({"unused", "CanBeFinal"})
+public non-sealed class VorfuehrungUpdatedEvent extends VorfuehrungCRUDEvent {
+ private static Version version = Versionable.initialVersion().inc();
+
+ private VorfuehrungUpdatedEvent() { super(null); } // needed for (de)serialization via Jackson
+
+ public VorfuehrungUpdatedEvent(final VorfuehrungTO vorfuehrung) {
+ super(vorfuehrung);
+ }
+
+ @Override
+ @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized
+ public Version version() {
+ return version;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/FilmTO.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/FilmTO.java
new file mode 100644
index 0000000..12a573b
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/FilmTO.java
@@ -0,0 +1,33 @@
+package de.accso.flexinale.backoffice.api_contract.event.model;
+
+import de.accso.flexinale.common.shared_kernel.EqualsByContent;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import de.accso.flexinale.common.shared_kernel.RawWrapper;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+
+import java.io.Serializable;
+
+@SuppressWarnings("unused")
+public record FilmTO(Id id, Version version,
+ Titel titel, ImdbUrl imdbUrl, DauerInMinuten dauerInMinuten)
+ implements Identifiable, Versionable, EqualsByContent, Serializable
+{
+ public record Titel(String raw) implements RawWrapper {}
+ public record ImdbUrl(String raw) implements RawWrapper {}
+ public record DauerInMinuten(Integer raw) implements RawWrapper {}
+
+ @Override
+ public boolean equalsByContent(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ final FilmTO that = (FilmTO) o;
+
+ return new EqualsBuilder()
+ .append(id, that.id)
+ .append(titel, that.titel).append(imdbUrl, that.imdbUrl)
+ .append(dauerInMinuten, that.dauerInMinuten).isEquals();
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoSaalTO.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoSaalTO.java
new file mode 100644
index 0000000..24c40a0
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoSaalTO.java
@@ -0,0 +1,73 @@
+package de.accso.flexinale.backoffice.api_contract.event.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import de.accso.flexinale.common.shared_kernel.*;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+import java.io.Serializable;
+
+public record KinoSaalTO(Id id, Version version,
+ Name name, AnzahlPlaetze anzahlPlaetze,
+ @JsonIgnore @DoNotCheckInArchitectureTests KinoTO kino)
+ implements Identifiable, Versionable, EqualsByContent, Serializable
+{
+ public record Name(String raw) implements RawWrapper {}
+ public record AnzahlPlaetze(Integer raw) implements RawWrapper {}
+
+ @Override
+ public String toString() {
+ return "KinoSaalTO{" +
+ "id='" + id + '\'' +
+ ", version=" + version +
+ ", name='" + name + '\'' +
+ ", anzahlPlaetze=" + anzahlPlaetze +
+ (kino != null ? ", kino.id=" + kino.id() : ", kino=null") +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (!equalsByContent(o)) return false;
+
+ if (this == o) {return true;}
+ if (o == null || getClass() != o.getClass()) {return false;}
+ KinoSaalTO that = (KinoSaalTO) o;
+ return new EqualsBuilder().append(version, that.version).isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ HashCodeBuilder builder = new HashCodeBuilder(17, 37).append(id).append(version)
+ .append(name).append(anzahlPlaetze);
+ if (kino != null) {
+ builder.append(kino.id()); // do not use kino but only its id (otherwise Stackoverflow error)
+ }
+ return builder.toHashCode();
+ }
+
+ @SuppressWarnings("ConstantValue")
+ @Override
+ public boolean equalsByContent(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ KinoSaalTO that = (KinoSaalTO) o;
+
+ boolean result = new EqualsBuilder()
+ .append(id, that.id)
+ .append(name, that.name)
+ .append(anzahlPlaetze, that.anzahlPlaetze).
+ isEquals();
+ if (!result) return false;
+
+ if (kino == null && that.kino == null) return true;
+ if (kino == null && that.kino != null) return false;
+ if (kino != null) {
+ result = kino.id().equals(that.kino.id()); // do not check kino but only its id (otherwise Stackoverflow error)
+ }
+
+ return result;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoTO.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoTO.java
new file mode 100644
index 0000000..5cca8c0
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoTO.java
@@ -0,0 +1,61 @@
+package de.accso.flexinale.backoffice.api_contract.event.model;
+
+import de.accso.flexinale.common.shared_kernel.*;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public record KinoTO(Id id, Version version,
+ Name name, Adresse adresse, EmailAdresse emailAdresse,
+ @DoNotCheckInArchitectureTests Set kinoSaele)
+ implements Identifiable, Versionable, EqualsByContent, Serializable
+{
+ public record Name(String raw) implements RawWrapper {}
+ public record Adresse(String raw) implements RawWrapper {}
+ public record EmailAdresse(String raw) implements RawWrapper {}
+
+ public KinoTO(final Id id, final Version version) {
+ this(id, version, new KinoTO.Name(""), new KinoTO.Adresse(""), new KinoTO.EmailAdresse(""),
+ new HashSet<>()); // cannot use Set.of() as this would be an ImmutableCollections.EMPTY_SET
+ }
+
+ public KinoTO(final Id id,
+ final Name name, final Adresse adresse,
+ final EmailAdresse emailAdresse, final Set kinoSaele) {
+ this(id, Versionable.unknownVersion(), name, adresse, emailAdresse, kinoSaele);
+ }
+
+ @SuppressWarnings({"ConstantValue", "unused"})
+ @Override
+ public boolean equalsByContent(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ KinoTO that = (KinoTO) o;
+
+ boolean result = new EqualsBuilder()
+ .append(id, that.id)
+ .append(name, that.name)
+ .append(adresse, that.adresse).append(emailAdresse, that.emailAdresse)
+ .isEquals();
+ if (!result) return false;
+
+ if (kinoSaele == null && that.kinoSaele == null) return true;
+ if (kinoSaele == null && that.kinoSaele != null) return false;
+ if (kinoSaele != null && that.kinoSaele == null) return false;
+
+ List thisKSList = kinoSaele.stream().toList();
+ List thatKSList = that.kinoSaele.stream().toList();
+ if (thisKSList.size() != thatKSList.size()) return false;
+ for (int counter = 0; counter < thisKSList.size(); counter++) {
+ if (!thisKSList.get(counter).equalsByContent(thatKSList.get(counter))) return false;
+ }
+
+ return true;
+ }
+
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/VorfuehrungTO.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/VorfuehrungTO.java
new file mode 100644
index 0000000..a995be2
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/VorfuehrungTO.java
@@ -0,0 +1,80 @@
+package de.accso.flexinale.backoffice.api_contract.event.model;
+
+import de.accso.flexinale.common.shared_kernel.*;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+public record VorfuehrungTO(Id id, Version version,
+ Zeit zeit,
+ @DoNotCheckInArchitectureTests FilmTO film,
+ @DoNotCheckInArchitectureTests KinoSaalTO kinoSaal,
+ Id kinoId)
+ implements Identifiable, Versionable, EqualsByContent, Serializable
+{
+ public record Zeit(LocalDateTime raw) implements RawWrapper {
+ public Zeit(LocalDateTime raw) {
+ this.raw = raw.withNano(0); // precision is second
+ }
+ }
+
+ public VorfuehrungTO(final Id id, final Version version,
+ final Zeit zeit,
+ final FilmTO film,
+ final KinoSaalTO kinoSaal,
+ final Id kinoId) {
+ this.id = id;
+ this.version = version;
+ this.zeit = zeit;
+ this.film = film;
+ this.kinoSaal = kinoSaal;
+
+ // VorfuehrungTO also gets kinoId, which is actually redundant (as a KinoSaalTO already contains the Kino).
+ // But during serialization the Kino is set to null! Therefore, we add the extra kinoId here!
+ this.kinoId = kinoId;
+
+ //TODO this validation should not be done here. If class is not final, Spotbugs complains. For DB entities use "nullable=false" and "optional=false" for DB checks. In domain classes check with jakarta.annotation NonNull? Also: Why is KinoSaal obligatory but not Film?
+ if (kinoSaal == null) {
+ throw new FlexinaleIllegalArgumentException("KinoSaal of Vorfuehrung " + this +
+ " must not be null");
+ }
+ //TODO this validation should not be done here but in KinoSaal itself
+ if (kinoSaal.anzahlPlaetze() == null) {
+ throw new FlexinaleIllegalArgumentException("KinoSaal's Anzahl Plaetze of Vorfuehrung " + this +
+ " must not be null");
+ }
+ //TODO Why is this here (check not done in any other Vorfuehrung* class)?
+ if (kinoId == null) {
+ throw new FlexinaleIllegalArgumentException("Kino Id of Vorfuehrung " + this +
+ " must not be null");
+ }
+ }
+
+ @SuppressWarnings({"UnusedAssignment", "DataFlowIssue", "unused"})
+ @Override
+ public boolean equalsByContent(final Object o) {
+ if (this == o) {return true;}
+
+ if (o == null || getClass() != o.getClass()) {return false;}
+
+ VorfuehrungTO that = (VorfuehrungTO) o;
+
+ boolean result = new EqualsBuilder()
+ .append(id, that.id)
+ .append(film, that.film)
+ .append(zeit, that.zeit)
+ .append(kinoId, that.kinoId)
+ .isEquals();
+ if (!result) return false;
+
+ if (kinoSaal == null && that.kinoSaal == null) result = true;
+ if (kinoSaal == null && that.kinoSaal != null) return false;
+ if (kinoSaal != null) {
+ result = kinoSaal.equalsByContent(that.kinoSaal);
+ return result;
+ }
+
+ return true;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/resources/logback.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/resources/logback.xml
new file mode 100644
index 0000000..87e8669
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/resources/logback.xml
@@ -0,0 +1,20 @@
+
+
+
+ %d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventSerializationTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventSerializationTest.java
new file mode 100644
index 0000000..23430b1
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventSerializationTest.java
@@ -0,0 +1,176 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.common.api.event.Event;
+import de.accso.flexinale.common.infrastructure.eventbus.EventSerializationHelper;
+import static de.accso.flexinale.backoffice.api_contract.event.ReflectionHelper.setField;
+import de.accso.flexinale.common.shared_kernel.*;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static de.accso.flexinale.common.shared_kernel.Identifiable.Id.uuidString;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class EventSerializationTest {
+ private static Stream allFilmEventTypes() {
+ return Set.of(FilmCreatedEvent.class, FilmUpdatedEvent.class, FilmDeletedEvent.class).stream().map(Arguments::of);
+ }
+
+ private static Stream allKinoEventTypes() {
+ return Set.of(KinoCreatedEvent.class, KinoUpdatedEvent.class, KinoDeletedEvent.class).stream().map(Arguments::of);
+ }
+
+ private static Stream allVorfuehrungEventTypes() {
+ return Set.of(VorfuehrungCreatedEvent.class, VorfuehrungUpdatedEvent.class, VorfuehrungDeletedEvent.class).stream().map(Arguments::of);
+ }
+
+ @ParameterizedTest
+ @MethodSource("allFilmEventTypes")
+ void testSerializeFilmEventClass2JsonString(final Class filmEventType)
+ throws JsonProcessingException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
+ // arrange
+ Constructor constructor = filmEventType.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ Event event = constructor.newInstance();
+
+ FilmTO filmTO = createFilmTO();
+ setField(filmEventType, event, "film", filmTO, true);
+
+ // act - serialize to JSON
+ String jsonString = EventSerializationHelper.serializeEvent2JsonString(event);
+
+ // assert
+ assertThat(jsonString).contains(getExpectedJsonStringForVersion(event.version()));
+ assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("titel", filmTO.titel()));
+ assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("imdbUrl", filmTO.imdbUrl()));
+ assertThat(jsonString).contains(getExpectedJsonStringForRawTypeInteger("dauerInMinuten", filmTO.dauerInMinuten()));
+
+ // act - deserialize back from JSON
+ E deserializedEvent = EventSerializationHelper.deserializeJsonString2Event(jsonString, filmEventType);
+
+ // assert
+ assertThat(deserializedEvent).isInstanceOf(filmEventType);
+ assertThat(deserializedEvent).isInstanceOf(FilmCRUDEvent.class);
+ FilmCRUDEvent castedDeserializedEvent = (FilmCRUDEvent) deserializedEvent;
+ assertThat(castedDeserializedEvent.version()).isEqualTo(event.version());
+ assertThat(castedDeserializedEvent.film).isEqualTo(filmTO);
+ }
+
+ @ParameterizedTest
+ @MethodSource("allKinoEventTypes")
+ void testSerializeKinoEventClass2JsonString(final Class kinoEventType)
+ throws JsonProcessingException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
+ // arrange
+ Constructor constructor = kinoEventType.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ Event event = constructor.newInstance();
+
+ KinoTO kinoTO = createKinoTO();
+ setField(kinoEventType, event, "kino", kinoTO, true);
+
+ // act
+ String jsonString = EventSerializationHelper.serializeEvent2JsonString(event);
+
+ // assert
+ assertThat(jsonString).contains(getExpectedJsonStringForVersion(event.version()));
+ assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("name", kinoTO.name()));
+ assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("adresse", kinoTO.adresse()));
+ assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("emailAdresse", kinoTO.emailAdresse()));
+
+ // act - deserialize back from JSON
+ E deserializedEvent = EventSerializationHelper.deserializeJsonString2Event(jsonString, kinoEventType);
+
+ // assert
+ assertThat(deserializedEvent).isInstanceOf(kinoEventType);
+ assertThat(deserializedEvent).isInstanceOf(KinoCRUDEvent.class);
+ KinoCRUDEvent castedDeserializedEvent = (KinoCRUDEvent) deserializedEvent;
+ assertThat(castedDeserializedEvent.version()).isEqualTo(event.version());
+//TODO bidirectional relation K<->KS, assertion commented out, as does not work as the de/serialization is not working correctly for bidirectional relation between Kino and KinoSaal
+ // assertThat(castedDeserializedEvent.kino).isEqualTo(kinoTO);
+ }
+
+ @ParameterizedTest
+ @MethodSource("allVorfuehrungEventTypes")
+ void testSerializeVorfuehrungEventClass2JsonString(final Class vorfuehrungEventType)
+ throws JsonProcessingException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
+ // arrange
+ Constructor constructor = vorfuehrungEventType.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ Event event = constructor.newInstance();
+
+ VorfuehrungTO vorfuehrungTO = createVorfuehrungTO();
+ setField(vorfuehrungEventType, event, "vorfuehrung", vorfuehrungTO, true);
+
+ // act
+ String jsonString = EventSerializationHelper.serializeEvent2JsonString(event);
+
+ // assert
+ assertThat(jsonString).contains(getExpectedJsonStringForVersion(event.version()));
+ assertThat(jsonString).contains(getExpectedJsonStringForRawTypeLocalDateTime("zeit", vorfuehrungTO.zeit()));
+
+ // act - deserialize back from JSON
+ E deserializedEvent = EventSerializationHelper.deserializeJsonString2Event(jsonString, vorfuehrungEventType);
+
+ // assert
+ assertThat(deserializedEvent).isInstanceOf(vorfuehrungEventType);
+ assertThat(deserializedEvent).isInstanceOf(VorfuehrungCRUDEvent.class);
+ VorfuehrungCRUDEvent castedDeserializedEvent = (VorfuehrungCRUDEvent) deserializedEvent;
+ assertThat(castedDeserializedEvent.version()).isEqualTo(event.version());
+ assertThat(castedDeserializedEvent.vorfuehrung).isEqualTo(vorfuehrungTO);
+ }
+
+ private static FilmTO createFilmTO() {
+ FilmTO.Titel titel = new FilmTO.Titel(uuidString());
+ FilmTO.ImdbUrl imdbUrl = new FilmTO.ImdbUrl(uuidString());
+ FilmTO.DauerInMinuten dauerInMinuten = new FilmTO.DauerInMinuten(42);
+ return new FilmTO(Identifiable.Id.of(), Versionable.unknownVersion(), titel, imdbUrl, dauerInMinuten);
+ }
+
+ private static KinoTO createKinoTO() {
+ KinoTO.Name name = new KinoTO.Name(uuidString());
+ KinoTO.Adresse adresse = new KinoTO.Adresse(uuidString());
+ KinoTO.EmailAdresse emailAdresse = new KinoTO.EmailAdresse(uuidString());
+ KinoTO kinoTO = new KinoTO(Identifiable.Id.of(), Versionable.unknownVersion(), name, adresse, emailAdresse, new HashSet<>());
+ kinoTO.kinoSaele().add(createKinoSaalTO(kinoTO));
+ return kinoTO;
+ }
+
+ private static KinoSaalTO createKinoSaalTO(KinoTO kinoTO) {
+ KinoSaalTO.Name name = new KinoSaalTO.Name(uuidString());
+ KinoSaalTO.AnzahlPlaetze adresse = new KinoSaalTO.AnzahlPlaetze(42);
+ return new KinoSaalTO(Identifiable.Id.of(), Versionable.unknownVersion(), name, adresse, kinoTO);
+ }
+
+ private static VorfuehrungTO createVorfuehrungTO() {
+ VorfuehrungTO.Zeit zeit = new VorfuehrungTO.Zeit(LocalDateTime.now().withSecond(0)); // intentionally as ISO format changes then!
+ KinoSaalTO kinoSaalTO = createKinoSaalTO(null);
+ return new VorfuehrungTO(Identifiable.Id.of(), Versionable.unknownVersion(), zeit,
+ createFilmTO(), kinoSaalTO, Identifiable.Id.of());
+ }
+
+ private static String getExpectedJsonStringForVersion(Versionable.Version field) {
+ return String.format("\"%s\":{\"version\":%d", "version", field.version());
+ }
+ private static String getExpectedJsonStringForRawTypeString(String fieldName, RawWrapper field) {
+ return String.format("\"%s\":{\"raw\":\"%s\"", fieldName, field.raw());
+ }
+ private static String getExpectedJsonStringForRawTypeInteger(String fieldName, RawWrapper field) {
+ return String.format("\"%s\":{\"raw\":%d", fieldName, field.raw());
+ }
+ private static String getExpectedJsonStringForRawTypeLocalDateTime(String fieldName, RawWrapper field) {
+ return String.format("\"%s\":{\"raw\":\"%s\"", fieldName, field.raw().format(DateTimeFormatter.ISO_DATE_TIME));
+ }
+}
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventStructureTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventStructureTest.java
new file mode 100644
index 0000000..52c0d0e
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventStructureTest.java
@@ -0,0 +1,54 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import de.accso.flexinale.common.api.event.Event;
+import de.accso.flexinale.common.infrastructure.eventbus.EventSerializationHelper;
+import de.accso.flexinale.common.shared_kernel.EventClassHelper;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.time.LocalDateTime;
+import java.util.stream.Stream;
+
+import static de.accso.flexinale.backoffice.api_contract.event.ReflectionHelper.setField;
+import static org.assertj.core.api.Assertions.*;
+
+class EventStructureTest {
+ private static Stream allEventTypes() {
+ return AllBackofficeEvents.eventTypes.stream().map(Arguments::of);
+ }
+
+ // check for all Event subclasses if no exception is thrown for (de)serialization and reflection access
+ // a) default constructor is needed
+ // b) field 'version' is needed
+
+ @ParameterizedTest
+ @MethodSource("allEventTypes")
+ void testCheckEventStructure(final Class eventType)
+ throws InstantiationException, IllegalAccessException, JsonProcessingException, NoSuchMethodException, InvocationTargetException {
+ // arrange
+ Constructor constructor = eventType.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ Event event = constructor.newInstance();
+
+ // set id, timestamp and version explicitely, so that (de)serialization is somewhat more realistic
+ setField(eventType, event, "id", Identifiable.Id.of(), false);
+ setField(eventType, event, "timestamp", LocalDateTime.now(), false);
+
+ // act - check serialization and deserialization
+ String jsonString = EventSerializationHelper.serializeEvent2JsonString(event);
+ EventSerializationHelper.deserializeJsonString2Event(jsonString, eventType);
+
+ // act - check instantiation and version attribute
+ assertThatCode(() -> {
+ Versionable.Version eventClazzVersion = EventClassHelper.getEventClazzVersion(eventType);
+ assertThat(eventClazzVersion).isEqualTo(event.version());
+ })
+ .doesNotThrowAnyException();
+ }
+}
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/ReflectionHelper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/ReflectionHelper.java
new file mode 100644
index 0000000..9ae774b
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/ReflectionHelper.java
@@ -0,0 +1,25 @@
+package de.accso.flexinale.backoffice.api_contract.event;
+
+import de.accso.flexinale.common.api.event.Event;
+import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException;
+
+import java.lang.reflect.Field;
+
+public final class ReflectionHelper {
+ public static void setField(final Class> eventType, final Event event, final String fieldName, final Object fieldValue,
+ boolean includeDerivedFieldsFromSuperClasses) throws IllegalAccessException {
+ if (eventType.equals(Object.class)) {
+ throw new FlexinaleIllegalArgumentException("event hierarchy does not have a field " + fieldName);
+ }
+ try {
+ Field field = (includeDerivedFieldsFromSuperClasses)
+ ? eventType.getField(fieldName)
+ : eventType.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(event, fieldValue);
+ }
+ catch (NoSuchFieldException nosfex) {
+ setField(eventType.getSuperclass(), event, fieldName, fieldValue, includeDerivedFieldsFromSuperClasses);
+ }
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/SpotbugsExcludeFilter.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/SpotbugsExcludeFilter.xml
new file mode 100644
index 0000000..a307e71
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/SpotbugsExcludeFilter.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/Dockerfile b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/Dockerfile
new file mode 100644
index 0000000..5c6bb98
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/Dockerfile
@@ -0,0 +1,8 @@
+FROM amazoncorretto:21.0.1-alpine3.18
+
+WORKDIR /app
+COPY ../../target/flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar /app
+
+EXPOSE 8080
+
+CMD ["java", "-jar", "flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar"]
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/docker_build.sh b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/docker_build.sh
new file mode 100644
index 0000000..9b89feb
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/docker_build.sh
@@ -0,0 +1 @@
+docker build -t de.accso/flexinale-distributed-besucherportal:2024.3.0 -f Dockerfile ../../
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/Dockerfile b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/Dockerfile
new file mode 100644
index 0000000..5c6bb98
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/Dockerfile
@@ -0,0 +1,8 @@
+FROM amazoncorretto:21.0.1-alpine3.18
+
+WORKDIR /app
+COPY ../../target/flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar /app
+
+EXPOSE 8080
+
+CMD ["java", "-jar", "flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar"]
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/podman_build.bat b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/podman_build.bat
new file mode 100644
index 0000000..8df13e6
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/podman_build.bat
@@ -0,0 +1 @@
+podman build -t de.accso/flexinale-distributed-besucherportal:2024.3.0 -f Dockerfile ../../
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/pom.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/pom.xml
new file mode 100644
index 0000000..ebc8ede
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/pom.xml
@@ -0,0 +1,162 @@
+
+
+ 4.0.0
+
+
+ de.accso
+ flexinale-distributed
+ 2024.3.0
+ ../pom.xml
+
+
+ flexinale-distributed-besucherportal
+ 2024.3.0
+ Flexinale Distributed Besucherportal
+ Flexinale - FLEX case-study "film festival", distributed services, besucherportal
+
+ jar
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.thymeleaf.extras
+ thymeleaf-extras-springsecurity6
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+ ${micrometer.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
+ de.accso
+ flexinale-distributed-common
+ 2024.3.0
+
+
+ de.accso
+ flexinale-distributed-common
+ 2024.3.0
+ test-jar
+ test
+
+
+ de.accso
+ flexinale-distributed-security
+ runtime
+ 2024.3.0
+
+
+ de.accso
+ flexinale-distributed-security_api_contract
+ 2024.3.0
+
+
+ de.accso
+ flexinale-distributed-besucherportal_api_contract
+ 2024.3.0
+
+
+ de.accso
+ flexinale-distributed-backoffice_api_contract
+ 2024.3.0
+
+
+ de.accso
+ flexinale-distributed-ticketing_api_contract
+ 2024.3.0
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ spring-boot-fat-jar
+
+
+
+
+ build-info
+
+
+
+ Distributed
+ Besucherportal
+
+
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs-maven-plugin.version}
+
+
+
+ com.h3xstream.findsecbugs
+ findsecbugs-plugin
+ ${findsecbugs-maven-plugin.version}
+
+
+ SpotbugsExcludeFilter.xml
+
+
+
+
+ com.github.spotbugs
+ spotbugs
+ ${spotbugs.version}
+
+
+
+
+ verify
+
+ check
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed - start Besucherportal app (_FlexinaleDistributedApplicationBesucherportal_).run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed - start Besucherportal app (_FlexinaleDistributedApplicationBesucherportal_).run.xml
new file mode 100644
index 0000000..5fdf369
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed - start Besucherportal app (_FlexinaleDistributedApplicationBesucherportal_).run.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Cache Contents.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Cache Contents.run.xml
new file mode 100644
index 0000000..ed0ce44
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Cache Contents.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Consumed.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Consumed.run.xml
new file mode 100644
index 0000000..64c9ff6
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Consumed.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Published.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Published.run.xml
new file mode 100644
index 0000000..8812ecd
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Published.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Health.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Health.run.xml
new file mode 100644
index 0000000..3baa27c
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Health.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Info.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Info.run.xml
new file mode 100644
index 0000000..3cc0f9b
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Info.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Metrics.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Metrics.run.xml
new file mode 100644
index 0000000..578d5fa
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Metrics.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Prometheus.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Prometheus.run.xml
new file mode 100644
index 0000000..a2eb186
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Prometheus.run.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportal.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportal.java
new file mode 100644
index 0000000..689c940
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportal.java
@@ -0,0 +1,14 @@
+package de.accso.flexinale;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Profile;
+
+@SpringBootApplication
+@Profile({"!test-integrated &!test-distributed & !testdata"})
+public class FlexinaleDistributedApplicationBesucherportal {
+
+ public static void main(String[] args) {
+ SpringApplication.run(FlexinaleDistributedApplicationBesucherportal.class, args);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/FilmSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/FilmSubscriber.java
new file mode 100644
index 0000000..f2edeef
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/FilmSubscriber.java
@@ -0,0 +1,78 @@
+package de.accso.flexinale.besucherportal.api_in.event;
+
+import de.accso.flexinale.backoffice.api_contract.event.FilmCreatedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.FilmDeletedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.FilmUpdatedEvent;
+import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
+import de.accso.flexinale.common.api.eventbus.EventBus;
+import de.accso.flexinale.common.api.eventbus.EventBusFactory;
+import de.accso.flexinale.common.api.eventbus.EventNotification;
+import de.accso.flexinale.common.api.eventbus.EventSubscriber;
+import de.accso.flexinale.common.api.eventbus.EventSubscriber.EventSubscriber3;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING;
+
+@SuppressWarnings("unused")
+public class FilmSubscriber implements EventSubscriber3 {
+ private static final Logger LOGGER = LoggerFactory.getLogger(FilmSubscriber.class);
+
+ private final FKKsVTInMemoryCache cache;
+
+ private final EventNotification notification;
+
+ @SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"})
+ public FilmSubscriber(final EventBusFactory eventBusFactory,
+ final FKKsVTInMemoryCache cache,
+ final EventNotification notification) {
+ EventBus createBus = eventBusFactory.createOrGetEventBusFor(FilmCreatedEvent.class);
+ EventBus updateBus = eventBusFactory.createOrGetEventBusFor(FilmUpdatedEvent.class);
+ EventBus deleteBus = eventBusFactory.createOrGetEventBusFor(FilmDeletedEvent.class);
+
+ createBus.subscribe(FilmCreatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+ updateBus.subscribe(FilmUpdatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+ deleteBus.subscribe(FilmDeletedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+
+ this.cache = cache;
+ this.notification = notification;
+ }
+
+ @Override
+ public String getName() {
+ return FilmSubscriber.class.getName();
+ }
+
+ @Override
+ public String getGroupName() {
+ return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties
+ }
+
+ @Override
+ public void receive(final FilmCreatedEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ cache.addOrUpdate(event.film);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void receive2(final FilmUpdatedEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ cache.addOrUpdate(event.film);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void receive3(final FilmDeletedEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ cache.delete(event.film);
+ this.notification.notify(event);
+ }
+
+ public FKKsVTInMemoryCache getCache() {
+ return cache;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KinoSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KinoSubscriber.java
new file mode 100644
index 0000000..c60fde5
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KinoSubscriber.java
@@ -0,0 +1,101 @@
+package de.accso.flexinale.besucherportal.api_in.event;
+
+import de.accso.flexinale.backoffice.api_contract.event.KinoCreatedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.KinoDeletedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.KinoUpdatedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
+import de.accso.flexinale.common.api.eventbus.EventBus;
+import de.accso.flexinale.common.api.eventbus.EventBusFactory;
+import de.accso.flexinale.common.api.eventbus.EventNotification;
+import de.accso.flexinale.common.api.eventbus.EventSubscriber;
+import de.accso.flexinale.common.api.eventbus.EventSubscriber.EventSubscriber3;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+
+import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING;
+
+@SuppressWarnings("unused")
+public class KinoSubscriber implements EventSubscriber3 {
+ private static final Logger LOGGER = LoggerFactory.getLogger(KinoSubscriber.class);
+
+ private final FKKsVTInMemoryCache cache;
+
+ private final EventNotification notification;
+
+ @SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"})
+ public KinoSubscriber(final EventBusFactory eventBusFactory,
+ final FKKsVTInMemoryCache cache,
+ final EventNotification notification) {
+ EventBus createBus = eventBusFactory.createOrGetEventBusFor(KinoCreatedEvent.class);
+ EventBus updateBus = eventBusFactory.createOrGetEventBusFor(KinoUpdatedEvent.class);
+ EventBus deleteBus = eventBusFactory.createOrGetEventBusFor(KinoDeletedEvent.class);
+
+ createBus.subscribe(KinoCreatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+ updateBus.subscribe(KinoUpdatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+ deleteBus.subscribe(KinoDeletedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+
+ this.cache = cache;
+ this.notification = notification;
+ }
+
+ @Override
+ public String getName() {
+ return KinoSubscriber.class.getName();
+ }
+
+ @Override
+ public String getGroupName() {
+ return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties
+ }
+
+ @Override
+ public void receive(final KinoCreatedEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ KinoTO fixedKino = fixKinoSaalTO2KinoTORelation(event.kino);
+ cache.addOrUpdate(fixedKino);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void receive2(final KinoUpdatedEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ KinoTO fixedKino = fixKinoSaalTO2KinoTORelation(event.kino);
+ cache.addOrUpdate(fixedKino);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void receive3(final KinoDeletedEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ cache.delete(event.kino);
+ this.notification.notify(event);
+ }
+
+ // need to fix in all KinoSaalTO the relation to the correct KinoTO (as it is null after Json deserialization)
+ private KinoTO fixKinoSaalTO2KinoTORelation(final KinoTO kinoTOFromEvent) {
+ if (kinoTOFromEvent == null) return null;
+
+ final KinoTO newKinoTO = new KinoTO(kinoTOFromEvent.id(), kinoTOFromEvent.version(), kinoTOFromEvent.name(),
+ kinoTOFromEvent.adresse(), kinoTOFromEvent.emailAdresse(), new HashSet<>());
+ kinoTOFromEvent.kinoSaele()
+ .stream()
+ .map(kinoSaalTOFromEvent ->
+ new KinoSaalTO(kinoSaalTOFromEvent.id(), kinoSaalTOFromEvent.version(),
+ kinoSaalTOFromEvent.name(), kinoSaalTOFromEvent.anzahlPlaetze(), newKinoTO))
+ .forEach(kinoSaalTOFromEvent ->
+ newKinoTO.kinoSaele().add(kinoSaalTOFromEvent));
+
+ return newKinoTO;
+ }
+
+ public FKKsVTInMemoryCache getCache() {
+ return cache;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KontingentSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KontingentSubscriber.java
new file mode 100644
index 0000000..5f383a0
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KontingentSubscriber.java
@@ -0,0 +1,54 @@
+package de.accso.flexinale.besucherportal.api_in.event;
+
+import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
+import de.accso.flexinale.common.api.eventbus.EventBus;
+import de.accso.flexinale.common.api.eventbus.EventBusFactory;
+import de.accso.flexinale.common.api.eventbus.EventNotification;
+import de.accso.flexinale.common.api.eventbus.EventSubscriber;
+import de.accso.flexinale.ticketing.api_contract.event.OnlineKontingentChangedEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING;
+import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
+
+@SuppressWarnings("unused")
+public class KontingentSubscriber implements EventSubscriber {
+ private static final Logger LOGGER = LoggerFactory.getLogger(KontingentSubscriber.class);
+
+ private final FKKsVTInMemoryCache cache;
+
+ private final EventNotification notification;
+
+ public KontingentSubscriber(final EventBusFactory eventBusFactory,
+ final FKKsVTInMemoryCache cache,
+ final EventNotification notification) {
+ EventBus kontingentBus = eventBusFactory.createOrGetEventBusFor(OnlineKontingentChangedEvent.class);
+ kontingentBus.subscribe(OnlineKontingentChangedEvent.class, this, START_READING_FROM_BEGINNING);
+
+ this.cache = cache;
+ this.notification = notification;
+ }
+
+ @Override
+ public String getName() {
+ return KontingentSubscriber.class.getName();
+ }
+
+ @Override
+ public String getGroupName() {
+ return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties
+ }
+
+ @Override
+ public void receive(final OnlineKontingentChangedEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ cache.addOrUpdate(event.onlineKontingent.vorfuehrungId(), getRawOrNull(event.onlineKontingent.neuesOnlineRestKontingent()));
+ this.notification.notify(event);
+ }
+
+ public FKKsVTInMemoryCache getCache() {
+ return cache;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/TicketSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/TicketSubscriber.java
new file mode 100644
index 0000000..c690bb9
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/TicketSubscriber.java
@@ -0,0 +1,66 @@
+package de.accso.flexinale.besucherportal.api_in.event;
+
+import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
+import de.accso.flexinale.common.api.eventbus.EventBus;
+import de.accso.flexinale.common.api.eventbus.EventBusFactory;
+import de.accso.flexinale.common.api.eventbus.EventNotification;
+import de.accso.flexinale.common.api.eventbus.EventSubscriber;
+import de.accso.flexinale.ticketing.api_contract.event.TicketGekauftEvent;
+import de.accso.flexinale.ticketing.api_contract.event.TicketUngueltigEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING;
+
+@SuppressWarnings("unused")
+public class TicketSubscriber implements EventSubscriber.EventSubscriber2 {
+ private static final Logger LOGGER = LoggerFactory.getLogger(TicketSubscriber.class);
+
+ private final FKKsVTInMemoryCache cache;
+
+ private final EventNotification notification;
+
+ @SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"})
+ public TicketSubscriber(final EventBusFactory eventBusFactory,
+ final FKKsVTInMemoryCache cache,
+ final EventNotification notification) {
+ EventBus createBus = eventBusFactory.createOrGetEventBusFor(TicketGekauftEvent.class);
+ EventBus ungueltigBus = eventBusFactory.createOrGetEventBusFor(TicketUngueltigEvent.class);
+
+ createBus.subscribe(TicketGekauftEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+ ungueltigBus.subscribe(TicketUngueltigEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+
+ this.cache = cache;
+ this.notification = notification;
+ }
+
+ @Override
+ public String getName() {
+ return TicketSubscriber.class.getName();
+ }
+
+ @Override
+ public String getGroupName() {
+ return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties
+ }
+
+ @Override
+ public void receive(final TicketGekauftEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ cache.addOrUpdate(event.ticket);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void receive2(final TicketUngueltigEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ cache.addOrUpdate(event.ticket);
+ this.notification.notify(event);
+ }
+
+ public FKKsVTInMemoryCache getCache() {
+ return cache;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/VorfuehrungSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/VorfuehrungSubscriber.java
new file mode 100644
index 0000000..f60bfcf
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/VorfuehrungSubscriber.java
@@ -0,0 +1,77 @@
+package de.accso.flexinale.besucherportal.api_in.event;
+
+import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungCreatedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungDeletedEvent;
+import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungUpdatedEvent;
+import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
+import de.accso.flexinale.common.api.eventbus.EventBus;
+import de.accso.flexinale.common.api.eventbus.EventBusFactory;
+import de.accso.flexinale.common.api.eventbus.EventNotification;
+import de.accso.flexinale.common.api.eventbus.EventSubscriber;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING;
+
+@SuppressWarnings("unused")
+public class VorfuehrungSubscriber implements EventSubscriber.EventSubscriber3 {
+ private static final Logger LOGGER = LoggerFactory.getLogger(VorfuehrungSubscriber.class);
+
+ private final FKKsVTInMemoryCache cache;
+
+ private final EventNotification notification;
+
+ @SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"})
+ public VorfuehrungSubscriber(final EventBusFactory eventBusFactory,
+ final FKKsVTInMemoryCache cache,
+ final EventNotification notification) {
+ EventBus createBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungCreatedEvent.class);
+ EventBus updateBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungUpdatedEvent.class);
+ EventBus deleteBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungDeletedEvent.class);
+
+ createBus.subscribe(VorfuehrungCreatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+ updateBus.subscribe(VorfuehrungUpdatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+ deleteBus.subscribe(VorfuehrungDeletedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING);
+
+ this.cache = cache;
+ this.notification = notification;
+ }
+
+ @Override
+ public String getName() {
+ return VorfuehrungSubscriber.class.getName();
+ }
+
+ @Override
+ public String getGroupName() {
+ return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties
+ }
+
+ @Override
+ public void receive(final VorfuehrungCreatedEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ cache.addOrUpdate(event.vorfuehrung);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void receive2(final VorfuehrungUpdatedEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ cache.addOrUpdate(event.vorfuehrung);
+ this.notification.notify(event);
+ }
+
+ @Override
+ public void receive3(final VorfuehrungDeletedEvent event) {
+ LOGGER.debug("received new event " + event);
+
+ cache.delete(event.vorfuehrung);
+ this.notification.notify(event);
+ }
+
+ public FKKsVTInMemoryCache getCache() {
+ return cache;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/FilmWebController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/FilmWebController.java
new file mode 100644
index 0000000..fdcd854
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/FilmWebController.java
@@ -0,0 +1,110 @@
+package de.accso.flexinale.besucherportal.api_in.web;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
+import de.accso.flexinale.besucherportal.application.services.FilmService;
+import de.accso.flexinale.besucherportal.application.services.KinoService;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import de.accso.flexinale.common.shared_kernel.RawWrapper;
+import de.accso.flexinale.security.api_contract.BesucherRetriever;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
+
+@Controller
+@Profile({ "!testdata-backoffice & !testdata-ticketing" })
+public class FilmWebController {
+
+ private record VorfuehrungMitRestkontingentTO(Identifiable.Id vorfuehrungId, FilmTO film, Zeit zeit,
+ KinoSaalTO kinoSaal, KinoTO kino, RestKontingent restkontingentOnline) {
+ public record Zeit(LocalDateTime raw) implements RawWrapper {
+ public Zeit(LocalDateTime raw) {
+ this.raw = raw.withNano(0); // precision is second
+ }
+ }
+
+ public record RestKontingent(Integer raw) implements RawWrapper {}
+ }
+
+ @Autowired
+ private FilmService filmService;
+
+ @Autowired
+ private KinoService kinoService;
+
+ @Autowired
+ private BesucherRetriever besucherRetriever;
+
+ @Autowired
+ private FKKsVTInMemoryCache cache;
+
+ @GetMapping(value="/filme")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ @SuppressWarnings("SameReturnValue")
+ public String filme(final Model model) {
+ List filme = filmService.filme();
+ model.addAttribute("filme", filme);
+ return "filme";
+ }
+
+ @GetMapping(value="/film/{id}")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ @SuppressWarnings("SameReturnValue")
+ public String film(@PathVariable("id") final String id, final Model model) {
+ Identifiable.Id filmId = Identifiable.Id.of(id);
+
+ FilmTO film = filmService.film(filmId);
+ if (film == null) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "film %s not available".formatted(id));
+ }
+ else {
+ model.addAttribute("film", film);
+ List vorfuehrungenOfFilm = filmService.vorfuehrungenFuer(filmId);
+
+ List vorfuehrungenMitRestkontingent = vorfuehrungenOfFilm
+ .stream()
+ .map(v -> getRestkontingentOnlineAndMap(v, film))
+ .collect(Collectors.toList());
+
+ model.addAttribute("vorfuehrungenMitRestkontingent", vorfuehrungenMitRestkontingent);
+
+ if (!vorfuehrungenOfFilm.isEmpty()) {
+ Identifiable.Id idOfLoggedInBesucher = besucherRetriever.getIdOfLoggedInBesucher();
+ List vorfuehrungenMitUeberlapp =
+ filmService.vorfuehrungenMitUeberlapp(vorfuehrungenOfFilm, idOfLoggedInBesucher);
+ model.addAttribute("vorfuehrungenMitUeberlapp", vorfuehrungenMitUeberlapp);
+ List vorfuehrungenMitTicket =
+ filmService.vorfuehrungenFuerDieDerBenutzerEinTicketHat(vorfuehrungenOfFilm, idOfLoggedInBesucher);
+ model.addAttribute("vorfuehrungenMitTicket", vorfuehrungenMitTicket);
+ }
+ }
+
+ return "film";
+ }
+
+ private VorfuehrungMitRestkontingentTO getRestkontingentOnlineAndMap(final VorfuehrungTO vorfuehrung,
+ final FilmTO film) {
+ VorfuehrungMitRestkontingentTO.RestKontingent restKontingentOnline =
+ new VorfuehrungMitRestkontingentTO.RestKontingent(cache.getRestkontingentOnline(vorfuehrung.id()));
+ KinoTO kino = kinoService.kino(vorfuehrung.kinoId());
+
+ return new VorfuehrungMitRestkontingentTO(vorfuehrung.id(), film,
+ new VorfuehrungMitRestkontingentTO.Zeit(getRawOrNull(vorfuehrung.zeit())),
+ vorfuehrung.kinoSaal(), kino, restKontingentOnline);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/IndexWebController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/IndexWebController.java
new file mode 100644
index 0000000..43f2b73
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/IndexWebController.java
@@ -0,0 +1,25 @@
+package de.accso.flexinale.besucherportal.api_in.web;
+
+import de.accso.flexinale.common.application.Config;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+@Profile({ "!testdata-backoffice & !testdata-ticketing" })
+public class IndexWebController {
+ @Autowired
+ private Config config;
+
+ @GetMapping(value="/")
+ @SuppressWarnings("SameReturnValue")
+ public String index(final Model model) {
+ model.addAttribute("applicationTitle", config.getApplicationTitle());
+ model.addAttribute("buildVersion", config.getBuildVersion());
+ model.addAttribute("buildDate", config.getBuildDate());
+
+ return "index";
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/KinoWebController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/KinoWebController.java
new file mode 100644
index 0000000..6e005d4
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/KinoWebController.java
@@ -0,0 +1,48 @@
+package de.accso.flexinale.besucherportal.api_in.web;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.besucherportal.application.services.KinoService;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.List;
+
+@Controller
+@Profile({ "!testdata-backoffice & !testdata-ticketing" })
+public class KinoWebController {
+
+ @Autowired
+ private KinoService kinoService;
+
+ @GetMapping(value="/kinos")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ @SuppressWarnings("SameReturnValue")
+ public String kinos(final Model model) {
+ List kinos = kinoService.kinos();
+ model.addAttribute("kinos", kinos);
+ return "kinos";
+ }
+
+ @GetMapping(value="/kino/{id}")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ @SuppressWarnings("SameReturnValue")
+ public String kino(@PathVariable("id") final String id, final Model model) {
+ Identifiable.Id kinoId = Identifiable.Id.of(id);
+ KinoTO kino = kinoService.kino(kinoId);
+ if (kino == null) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "kino %s not available".formatted(id));
+ }
+ else {
+ model.addAttribute("kino", kino);
+ return "kino";
+ }
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketSorter.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketSorter.java
new file mode 100644
index 0000000..0659b86
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketSorter.java
@@ -0,0 +1,166 @@
+package de.accso.flexinale.besucherportal.api_in.web;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
+import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
+import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.*;
+
+import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
+
+class TicketSorter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TicketSorter.class);
+
+ /**
+ * return a list of list of tickets, sorted by time (inner list) and date (outer list).
+ * each list contains the tickets for a day
+ */
+ public static List> sortAndMapTicketsPerDay(final List allTickets,
+ final FKKsVTInMemoryCache cache) {
+ if (allTickets == null || allTickets.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ Map ticket2Vorfuehrung = createTicketToVorfuehrungMap(allTickets, cache);
+
+ // 0) sort tickets by time (not done in database SQL query as before)
+ allTickets.sort(new TicketTOByZeitSortingComparator(ticket2Vorfuehrung));
+
+ // 1) tickets by day
+ List> ticketsProTag = new ArrayList<>();
+
+ LocalDate previousDate = LocalDate.MIN; // far past :-)
+ for (TicketTO ticket : allTickets) {
+ LocalDateTime zeit = getRawOrNull(ticket2Vorfuehrung.get(ticket).zeit());
+ LocalDate currentDate = zeit.toLocalDate();
+
+ if (currentDate.equals(previousDate)) {
+ ticketsProTag.getLast().add(ticket);
+ }
+ else {
+ List tickets = new ArrayList<>();
+ tickets.add(ticket);
+ ticketsProTag.add(tickets);
+ }
+ previousDate = currentDate;
+ }
+
+ // 2) inner structure: Vorfuehrung and tickets
+ List> vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag = new ArrayList<>();
+
+ for (List ticketsForOneDay : ticketsProTag) {
+ List vorfuehrungenUndAnzahlTicketsFuerEinenTag = new ArrayList<>();
+ int anzahlGueltigeTicketsFuerVorfuehrung = 0;
+ int anzahlUngueltigeTicketsFuerVorfuehrung = 0;
+ VorfuehrungTO previousVorfuehrung = ticket2Vorfuehrung.get(ticketsForOneDay.getFirst());
+
+ for (TicketTO ticket : ticketsForOneDay) {
+ // inside each day, Vorfuehrungen are ordered by zeit.
+ // There is never more than one Vorfuehrung at a zeit for a user.
+ // So tickets for the same Vorfuehrung are all in a row - that's why the following works.
+ VorfuehrungTO currentVorfuehrung = ticket2Vorfuehrung.get(ticket);
+
+ if (currentVorfuehrung.equals(previousVorfuehrung)) {
+ Boolean ticketGueltig = getRawOrNull(ticket.gueltig());
+
+ if (ticketGueltig) {
+ anzahlGueltigeTicketsFuerVorfuehrung++;
+ }
+ else {
+ anzahlUngueltigeTicketsFuerVorfuehrung++;
+ }
+ }
+ else {
+ Boolean ticketGueltig = getRawOrNull(ticket.gueltig());
+
+ // add the information for previous Vorfuehrung
+ VorfuehrungMitAnzahlTicketsTO to = mapVorfuehrungToTO(
+ anzahlGueltigeTicketsFuerVorfuehrung, anzahlUngueltigeTicketsFuerVorfuehrung,
+ previousVorfuehrung, cache);
+ vorfuehrungenUndAnzahlTicketsFuerEinenTag.add(to);
+
+ // ... we also have a new ticket for the next Vorfuehrung
+ if (ticketGueltig) {
+ anzahlGueltigeTicketsFuerVorfuehrung = 1;
+ anzahlUngueltigeTicketsFuerVorfuehrung = 0;
+ } else {
+ anzahlGueltigeTicketsFuerVorfuehrung = 0;
+ anzahlUngueltigeTicketsFuerVorfuehrung = 1;
+ }
+ }
+
+ previousVorfuehrung = currentVorfuehrung;
+ }
+
+ // Also add the last Vorfuehrung an anzahlTickets when day is over
+ VorfuehrungMitAnzahlTicketsTO to = mapVorfuehrungToTO(
+ anzahlGueltigeTicketsFuerVorfuehrung, anzahlUngueltigeTicketsFuerVorfuehrung, previousVorfuehrung, cache);
+ vorfuehrungenUndAnzahlTicketsFuerEinenTag.add(to);
+ vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag.add(vorfuehrungenUndAnzahlTicketsFuerEinenTag);
+ }
+ return vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag;
+ }
+
+ private static VorfuehrungMitAnzahlTicketsTO mapVorfuehrungToTO(final int anzahlGueltigeTicketsFuerVorfuehrung,
+ final int anzahlUngueltigeTicketsFuerVorfuehrung,
+ final VorfuehrungTO vorfuehrung,
+ final FKKsVTInMemoryCache cache) {
+ FilmTO film = vorfuehrung.film();
+ KinoTO kino = cache.kino(vorfuehrung.kinoId());
+ if (film == null) {
+ throw new FlexinaleIllegalStateException("no Film for Vorfuehrung %s found".formatted(vorfuehrung.id()));
+ }
+ else {
+ return new VorfuehrungMitAnzahlTicketsTO(
+ new VorfuehrungMitAnzahlTicketsTO.Zeit(getRawOrNull(vorfuehrung.zeit())),
+ film,
+ vorfuehrung.kinoSaal(),
+ kino,
+ new VorfuehrungMitAnzahlTicketsTO.AnzahlTickets(anzahlGueltigeTicketsFuerVorfuehrung),
+ new VorfuehrungMitAnzahlTicketsTO.AnzahlTickets(anzahlUngueltigeTicketsFuerVorfuehrung)
+ );
+ }
+ }
+
+ private static Map createTicketToVorfuehrungMap(final List allTickets,
+ final FKKsVTInMemoryCache cache) {
+ Map ticketToVorfuehrung = new HashMap<>();
+ for (TicketTO ticket : allTickets) {
+ VorfuehrungTO vorfuehrung = cache.vorfuehrung(ticket.vorfuehrungId());
+ if (vorfuehrung == null) {
+ LOGGER.error("Vorfuehrung %s of ticket %s cannot be found.".formatted(ticket.vorfuehrungId(), ticket.id()));
+ }
+ else {
+ ticketToVorfuehrung.put(ticket, vorfuehrung);
+ }
+ }
+ return ticketToVorfuehrung;
+ }
+}
+
+class TicketTOByZeitSortingComparator implements Comparator, Serializable {
+
+ private final Map ticket2Vorfuehrung;
+
+ TicketTOByZeitSortingComparator(final Map ticket2Vorfuehrung) {
+ this.ticket2Vorfuehrung = ticket2Vorfuehrung;
+ }
+
+ @Override
+ public int compare(final TicketTO t1, final TicketTO t2) {
+ VorfuehrungTO v1 = ticket2Vorfuehrung.get(t1);
+ VorfuehrungTO v2 = ticket2Vorfuehrung.get(t2);
+ LocalDateTime v1Zeit = getRawOrNull(v1.zeit());
+ LocalDateTime v2Zeit = getRawOrNull(v2.zeit());
+ return v1Zeit.compareTo(v2Zeit);
+ }
+}
\ No newline at end of file
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketWebController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketWebController.java
new file mode 100644
index 0000000..d1dbf00
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketWebController.java
@@ -0,0 +1,89 @@
+package de.accso.flexinale.besucherportal.api_in.web;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache;
+import de.accso.flexinale.besucherportal.application.services.TicketService;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import de.accso.flexinale.common.shared_kernel.RawWrapper;
+import de.accso.flexinale.security.api_contract.BesucherRetriever;
+import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Controller
+@Profile({ "!testdata-backoffice & !testdata-ticketing" })
+public class TicketWebController {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TicketWebController.class);
+
+ @Autowired
+ private TicketService ticketService;
+
+ @Autowired
+ private FKKsVTInMemoryCache cache;
+
+ @Autowired
+ private BesucherRetriever besucherRetriever;
+
+ @GetMapping(value="/tickets")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ public String listTickets(final Model model) {
+ Identifiable.Id besucherId = besucherRetriever.getIdOfLoggedInBesucher();
+
+ List allTickets = cache.ticketsByBesucher(besucherId);
+
+ List> ticketsByDay
+ = TicketSorter.sortAndMapTicketsPerDay(allTickets, cache);
+ model.addAttribute("ticketsByDay", ticketsByDay);
+
+ int totalNumberOfTickets = cache.gesamtZahlDerTicketsFuer(besucherId);
+ model.addAttribute("totalNumberOfTickets", totalNumberOfTickets);
+
+ return ("tickets");
+ }
+
+ @PostMapping("/vorfuehrung/loeseGutscheineOnlineEin")
+ @PreAuthorize("hasRole('ROLE_BESUCHER')")
+ public String loeseGutscheineOnlineEin(
+ @RequestParam("vorfuehrungId") final String vId,
+ @RequestParam("filmId") final String fId,
+ @RequestParam("anzahl") final int anzahl,
+ RedirectAttributes redirAttrs) {
+ Identifiable.Id vorfuehrungId = Identifiable.Id.of(vId);
+ Identifiable.Id filmId = Identifiable.Id.of(fId);
+ Identifiable.Id besucherId = besucherRetriever.getIdOfLoggedInBesucher();
+
+ ticketService.loeseGutscheineOnlineFuerVorfuehrungEin(vorfuehrungId, filmId, besucherId, anzahl);
+
+ redirAttrs.addFlashAttribute("success",
+ "Kauf von %d Ticket(s) beauftragt".formatted(anzahl));
+ return "redirect:/tickets";
+ }
+}
+
+// class used to "flatten" Vorfuehrung and number of Tickets for Vorfuehrung for Clients
+record VorfuehrungMitAnzahlTicketsTO(Zeit zeit, FilmTO film,
+ KinoSaalTO kinoSaal, KinoTO kino,
+ AnzahlTickets anzahlGueltigeTickets, AnzahlTickets anzahlUngueltigeTickets) {
+ public record Zeit(LocalDateTime raw) implements RawWrapper {
+ public Zeit(LocalDateTime raw) {
+ this.raw = raw.withNano(0); // precision is second
+ }
+ }
+
+ public record AnzahlTickets(Integer raw) implements RawWrapper {}
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_out/event/GutscheinEinloesenBeauftragtPublisher.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_out/event/GutscheinEinloesenBeauftragtPublisher.java
new file mode 100644
index 0000000..7ee5bed
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_out/event/GutscheinEinloesenBeauftragtPublisher.java
@@ -0,0 +1,46 @@
+package de.accso.flexinale.besucherportal.api_out.event;
+
+import de.accso.flexinale.besucherportal.api_contract.event.GutscheinEinloesenBeauftragtEvent;
+import de.accso.flexinale.besucherportal.api_contract.event.model.GutscheinEinloesenAuftragTO;
+import de.accso.flexinale.besucherportal.application.services.GutscheinEinloesenBeauftragtPublication;
+import de.accso.flexinale.common.api.eventbus.EventBus;
+import de.accso.flexinale.common.api.eventbus.EventBusFactory;
+import de.accso.flexinale.common.api.eventbus.EventNotification;
+import de.accso.flexinale.common.api.eventbus.EventPublisher;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import de.accso.flexinale.common.shared_kernel.Versionable;
+
+public class GutscheinEinloesenBeauftragtPublisher implements EventPublisher, GutscheinEinloesenBeauftragtPublication {
+ private final EventBus gutscheinEinloesenBeauftragtEventBus;
+ private final EventNotification notification;
+
+ public GutscheinEinloesenBeauftragtPublisher(final EventBusFactory eventBusFactory,
+ final EventNotification notification) {
+ this.gutscheinEinloesenBeauftragtEventBus = eventBusFactory.createOrGetEventBusFor(GutscheinEinloesenBeauftragtEvent.class);
+ this.notification = notification;
+ }
+
+ @Override
+ public void publishGutscheinEinloesenBeauftragtEvent(final Identifiable.Id vorfuehrungId,
+ final Identifiable.Id filmId,
+ final Identifiable.Id besucherId,
+ final int anzahlGutscheine) {
+ GutscheinEinloesenAuftragTO gutschein = new GutscheinEinloesenAuftragTO(Identifiable.Id.of(), Versionable.initialVersion(),
+ filmId, vorfuehrungId, besucherId,
+ new GutscheinEinloesenAuftragTO.AnzahlTickets(anzahlGutscheine));
+
+ post(GutscheinEinloesenBeauftragtEvent.class,
+ new GutscheinEinloesenBeauftragtEvent(gutschein));
+ }
+
+ @Override
+ public String getName() {
+ return GutscheinEinloesenBeauftragtPublisher.class.getName();
+ }
+
+ @Override
+ public void post(Class eventType, GutscheinEinloesenBeauftragtEvent event) {
+ this.gutscheinEinloesenBeauftragtEventBus.publish(eventType, event);
+ this.notification.notify(event);
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FKKsVTInMemoryCache.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FKKsVTInMemoryCache.java
new file mode 100644
index 0000000..fdbc573
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FKKsVTInMemoryCache.java
@@ -0,0 +1,127 @@
+package de.accso.flexinale.besucherportal.application.services;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.common.application.caching.InMemoryCache;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
+
+public final class FKKsVTInMemoryCache {
+
+ // caches are currently never cleared - might want to use Cache implementation with automatic time-to-live
+ private final InMemoryCache filmCache = new InMemoryCache<>();
+ private final InMemoryCache kinoCache = new InMemoryCache<>();
+ private final InMemoryCache vorfuehrungCache = new InMemoryCache<>();
+ private final InMemoryCache ticketCache = new InMemoryCache<>();
+
+ private final InMemoryCache vorfuehrung2RestKontingentOnlineCache = new InMemoryCache<>();
+
+ public void addOrUpdate(final FilmTO film) {
+ filmCache.put(film.id(), film);
+ }
+
+ public void delete(final FilmTO film) {
+ filmCache.remove(film.id());
+ }
+
+ public FilmTO film(final Identifiable.Id id) {
+ return filmCache.get(id);
+ }
+
+ public List filme() {
+ return filmCache.values().stream().toList();
+ }
+
+ // -------------------------------------------------------------------------------------------------------------
+
+ public void addOrUpdate(final KinoTO kino) {
+ kinoCache.put(kino.id(), kino);
+ }
+
+ public void delete(final KinoTO kino) {
+ kinoCache.remove(kino.id());
+ }
+
+
+ public KinoTO kino(final Identifiable.Id id) {
+ return kinoCache.get(id);
+ }
+
+ public List kinos() {
+ return kinoCache.values().stream().toList();
+ }
+
+ // -------------------------------------------------------------------------------------------------------------
+
+ public void addOrUpdate(final VorfuehrungTO vorfuehrung) {
+ vorfuehrungCache.put(vorfuehrung.id(), vorfuehrung);
+ }
+
+ public void delete(final VorfuehrungTO vorfuehrung) {
+ vorfuehrungCache.remove(vorfuehrung.id());
+ }
+
+ public List vorfuehrungen() {
+ return vorfuehrungCache.values().stream().toList();
+ }
+
+ public VorfuehrungTO vorfuehrung(final Identifiable.Id id) {
+ return vorfuehrungCache.get(id);
+ }
+
+ public List vorfuehrungenByFilmId(final Identifiable.Id filmId) {
+ return vorfuehrungCache.values()
+ .stream()
+ .filter(v -> v.film().id().equals(filmId))
+ .toList();
+ }
+
+ // -------------------------------------------------------------------------------------------------------------
+
+ public void addOrUpdate(final TicketTO ticket) {
+ ticketCache.put(ticket.id(), ticket);
+ }
+
+ public List tickets() {
+ return ticketCache.values().stream().toList();
+ }
+
+ public List ticketsByBesucher(final Identifiable.Id besucherId) {
+ return ticketCache.values()
+ .stream()
+ .filter(t -> t.besucherId().equals(besucherId))
+ .collect(Collectors.toList()); // don't use .toList() here as we need to sort the list later
+ }
+
+ public List gueltigeTicketsByBesucher(final Identifiable.Id besucherId) {
+ return ticketsByBesucher(besucherId)
+ .stream()
+ .filter((t -> getRawOrNull(t.gueltig())))
+ .collect(Collectors.toList());
+ }
+
+ public int gesamtZahlDerTicketsFuer(final Identifiable.Id besucherId) {
+ return (int) ticketCache.values()
+ .stream()
+ .filter(t -> t.besucherId().equals(besucherId))
+ .filter((t -> getRawOrNull(t.gueltig())))
+ .count();
+ }
+
+ // -------------------------------------------------------------------------------------------------------------
+
+ public void addOrUpdate(final Identifiable.Id vorfuehrungId, final Integer onlineRestKontingent) {
+ vorfuehrung2RestKontingentOnlineCache.put(vorfuehrungId, onlineRestKontingent);
+ }
+
+ public Integer getRestkontingentOnline(final Identifiable.Id vorfuehrungId) {
+ Integer restKontingentFromCache = vorfuehrung2RestKontingentOnlineCache.get(vorfuehrungId);
+ return (restKontingentFromCache != null) ? restKontingentFromCache : 0;
+ }
+}
diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FilmService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FilmService.java
new file mode 100644
index 0000000..c976102
--- /dev/null
+++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FilmService.java
@@ -0,0 +1,65 @@
+package de.accso.flexinale.besucherportal.application.services;
+
+import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
+import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
+import de.accso.flexinale.common.shared_kernel.Identifiable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FilmService {
+
+ private final FKKsVTInMemoryCache cache;
+
+ private final VorfuehrungService vorfuehrungService;
+
+ private final long minZeitZwischenVorfuehrungenInMinuten;
+
+ public FilmService(final FKKsVTInMemoryCache cache,
+ final VorfuehrungService vorfuehrungService,
+ final long minZeitZwischenVorfuehrungenInMinuten) {
+ this.cache = cache;
+ this.vorfuehrungService = vorfuehrungService;
+ this.minZeitZwischenVorfuehrungenInMinuten = minZeitZwischenVorfuehrungenInMinuten;
+ }
+
+ public List filme() {
+ return cache.filme();
+ }
+
+ public FilmTO film(final Identifiable.Id id) {
+ return cache.film(id);
+ }
+
+ public List vorfuehrungenFuer(final Identifiable.Id filmId) {
+ return cache.vorfuehrungenByFilmId(filmId);
+ }
+
+ public List vorfuehrungenMitUeberlapp(final List vorfuehrungen,
+ final Identifiable.Id besucherId) {
+ List