Compare commits

...

17 Commits

Author SHA1 Message Date
ce7e3741bd feature/test-liquibase-migration-from-a-prod-dump (#152)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #152
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-01-28 12:27:54 +01:00
2a61686918 use-latest-versions and improved test-code-coverage (#151)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #151
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
2025-01-24 09:28:52 +01:00
cd01d0ab8f amend contact-caption-import and add person-type organizational-unit (#150)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #150
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
2025-01-22 13:53:07 +01:00
9d251f88e9 test legacy-id-triggers (#149)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #149
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-01-22 10:08:19 +01:00
c1d3d583e7 feature/run-office-module-without-booking-and-hosting (#148)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Co-authored-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
Co-authored-by: Timotheus Pokorra <timotheus.pokorra@solidcharity.com>
Reviewed-on: #148
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-01-21 14:36:49 +01:00
ad61f2af59 Merge pull request 'add view for list subscriptions' (#146) from TP-20250113-list-subscription-view into master
Reviewed-on: #146
Reviewed-by: Michael Hoennig <michael.hoennig@hostsharing.net>
2025-01-16 13:57:27 +01:00
abddebd7c3 Merge pull request 'post new contact: process postalAddress' (#145) from TP-20250109_addcontact_putpostaladdress into master
Reviewed-on: #145
Reviewed-by: Michael Hoennig <michael.hoennig@hostsharing.net>
2025-01-16 13:57:05 +01:00
Dev und Test fuer hsadminng
02495de36f Merge branch 'master' into TP-20250113-list-subscription-view 2025-01-16 09:51:33 +01:00
Timotheus Pokorra
19c1e1ba5c Merge branch 'master' into TP-20250109_addcontact_putpostaladdress 2025-01-16 09:49:06 +01:00
Timotheus Pokorra
a31159eb5b improve Test for addRelation with postalAddress 2025-01-16 09:47:08 +01:00
Timotheus Pokorra
15e94a1800 fix previous commit 2025-01-16 09:42:07 +01:00
78cc729a97 improve test for adding contact with postal address 2025-01-16 08:54:19 +01:00
d6edfa4dc7 add sql file for mlmmj integration view to db changelog 2025-01-16 08:36:30 +01:00
9c8d7616e3 Upgrade to SpringBoot 3.4.1 and dependencies (#147)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #147
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-01-15 13:43:20 +01:00
6a673c66d4 add view for list subscriptions 2025-01-13 16:26:34 +01:00
27de4ce634 post new contact: process postalAddress 2025-01-09 10:21:48 +01:00
a7ffee9348 dependency-versions-upgrade and exclusion (#144)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #144
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-01-09 09:28:30 +01:00
180 changed files with 19350 additions and 1032 deletions

View File

@ -90,10 +90,51 @@ alias pg-sql-restore='gunzip --stdout | docker exec -i hsadmin-ng-postgres psql
alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l'
alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources'
alias gw-test='. .aliases; ./gradlew test'
alias gw-check='. .aliases; gw test check -x pitest'
alias gw-check='. .aliases; . .tc-environment; gw test check -x pitest'
alias cas-curl='bin/cas-curl'
# HOWTO: run all 'normal' tests (no scenario+import-tests): `gw-test`
# You can also mention specific targets: `gw-test importOfficeData`.
# This will always use the environment from `.tc-environment`.
#
# HOWTO: re-run tests even if no changed can be detected: `gw-test --rerun`
# You can also mention specific targets: `gw-test scenarioTest --rerun`.
# This will always use the environment from `.tc-environment`.
#
# HOWTO: run all tests (unit, integration+acceptance, import and scenario): `gw-test --all`
# You can also re-run all these tests, which will take ~20min: `gw-test --all --rerun`
# This will always use the environment from `.tc-environment`.
#
function _gwTest1() {
echo
printf -- '=%0.s' {1..80}; echo
echo "RUNNING gw $@"
printf -- '-%0.s' {1..80}; echo
./gradlew "$@"
printf -- '-%0.s' {1..80}; echo
echo "DONE gw $@"
}
function _gwTest() {
. .aliases;
. .tc-environment;
rm /tmp/gwTest.tmp
if [ "$1" == "--all" ]; then
shift # to remove the --all from $@
# delierately in separate gradlew-calls to avoid Testcontains-PostgreSQL problem spillover
time (_gwTest1 unitTest "$@" &&
_gwTest1 officeIntegrationTest bookingIntegrationTest hostingIntegrationTest "$@" &&
_gwTest1 scenarioTest "$@" &&
_gwTest1 importOfficeData importHostingAssets "$@");
elif [ $# -eq 0 ] || [[ $1 == -* ]]; then
time _gwTest1 test "$@";
else
time _gwTest1 "$@";
fi
printf -- '=%0.s' {1..80}; echo
}
alias gw-test=_gwTest
alias howto=bin/howto
alias cas-curl=bin/cas-curl
# etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries
alias gw-importOfficeData-in-docker-compose='
@ -106,5 +147,6 @@ if [ ! -f .environment ]; then
fi
source .environment
alias scenario-reports-upload='./gradlew scenarioTests convertMarkdownToHtml && ssh hsh03-hsngdev@h50.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@h50.hostsharing.net:doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office'
alias scenario-reports-upload='./gradlew scenarioTest convertMarkdownToHtml && ssh hsh03-hsngdev@h50.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@h50.hostsharing.net:doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office'
alias scenario-reports-open='open https://hsngdev.hs-example.de/scenarios/office'

View File

@ -4,5 +4,4 @@ export HSADMINNG_POSTGRES_ADMIN_PASSWORD=
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted
export HSADMINNG_SUPERUSER=superuser-alex@hostsharing.net
export HSADMINNG_MIGRATION_DATA_PATH=migration
export LIQUIBASE_CONTEXT=
export LANG=en_US.UTF-8

View File

@ -4,5 +4,4 @@ unset HSADMINNG_POSTGRES_ADMIN_PASSWORD
unset HSADMINNG_POSTGRES_RESTRICTED_USERNAME
unset HSADMINNG_SUPERUSER
unset HSADMINNG_MIGRATION_DATA_PATH
unset LIQUIBASE_CONTEXT

21
Jenkinsfile vendored
View File

@ -35,9 +35,24 @@ pipeline {
stage ('Tests') {
parallel {
stage('Unit-/Integration/Acceptance-Tests') {
stage('Unit-Tests') {
steps {
sh './gradlew check --no-daemon -x pitest -x dependencyCheckAnalyze -x importOfficeData -x importHostingAssets'
sh './gradlew unitTest --no-daemon'
}
}
stage('General-Tests') {
steps {
sh './gradlew generalTest --no-daemon'
}
}
stage('Office-Tests') {
steps {
sh './gradlew officeIntegrationTest --no-daemon'
}
}
stage('Booking+Hosting-Tests') {
steps {
sh './gradlew bookingIntegrationTest hostingIntegrationTest --no-daemon'
}
}
stage('Import-Tests') {
@ -47,7 +62,7 @@ pipeline {
}
stage ('Scenario-Tests') {
steps {
sh './gradlew scenarioTests --no-daemon'
sh './gradlew scenarioTest --no-daemon'
}
}
}

177
README.md
View File

@ -5,41 +5,48 @@ For architecture consider the files in the `doc` and `adr` folder.
<!-- generated TOC begin: -->
- [Setting up the Development Environment](#setting-up-the-development-environment)
- [PostgreSQL Server](#postgresql-server)
- [Markdown](#markdown)
- [Render Markdown embedded PlantUML](#render-markdown-embedded-plantuml)
- [Render Markdown Embedded Mermaid Diagrams](#render-markdown-embedded-mermaid-diagrams)
- [IDE Specific Settings](#ide-specific-settings)
- [IntelliJ IDEA](#intellij-idea)
- [Other Tools](#other-tools)
- [PostgreSQL Server](#postgresql-server)
- [Markdown](#markdown)
- [Render Markdown embedded PlantUML](#render-markdown-embedded-plantuml)
- [Render Markdown Embedded Mermaid Diagrams](#render-markdown-embedded-mermaid-diagrams)
- [IDE Specific Settings](#ide-specific-settings)
- [IntelliJ IDEA](#intellij-idea)
- [Other Tools](#other-tools)
- [Running the SQL files](#running-the-sql-files)
- [For RBAC](#for-rbac)
- [For Historization](#for-historization)
- [For RBAC](#for-rbac)
- [For Historization](#for-historization)
- [Coding Guidelines](#coding-guidelines)
- [Directory and Package Structure](#directory-and-package-structure)
- [General Directory Structure](#general-directory-structure)
- [Source Code Package Structure](#source-code-package-structure)
- [Run Tests from Command Line](#run-tests-from-command-line)
- [Spotless Code Formatting](#spotless-code-formatting)
- [JaCoCo Test Code Coverage Check](#jacoco-test-code-coverage-check)
- [PiTest Mutation Testing](#pitest-mutation-testing)
- [Remark](#remark)
- [OWASP Security Vulnerability Check](#owasp-security-vulnerability-check)
- [Dependency-License-Compatibility](#dependency-license-compatibility)
- [Dependency Version Upgrade](#dependency-version-upgrade)
- [Directory and Package Structure](#directory-and-package-structure)
- [General Directory Structure](#general-directory-structure)
- [Source Code Package Structure](#source-code-package-structure)
- [Run Tests from Command Line](#run-tests-from-command-line)
- [Spotless Code Formatting](#spotless-code-formatting)
- [JaCoCo Test Code Coverage Check](#jacoco-test-code-coverage-check)
- [PiTest Mutation Testing](#pitest-mutation-testing)
- [Remark](#remark)
- [OWASP Security Vulnerability Check](#owasp-security-vulnerability-check)
- [Dependency-License-Compatibility](#dependency-license-compatibility)
- [Dependency Version Upgrade](#dependency-version-upgrade)
- [Biggest Flaws in our Architecture](#biggest-flaws-in-our-architecture)
- [The RBAC System is too Complicated](#the-rbac-system-is-too-complicated)
- [The Mapper is Error-Prone](#the-mapper-is-error-prone)
- [Too Many Business-Rules Implemented in Controllers](#too-many-business-rules-implemented-in-controllers)
- [How To ...](#how-to-...)
- [How to Configure .pgpass for the Default PostgreSQL Database?](#how-to-configure-.pgpass-for-the-default-postgresql-database?)
- [How to Run the Tests Against a Local User-Space Podman Daemon?](#how-to-run-the-tests-against-a-local-user-space-podman-daemon?)
- [Install and Run Podman](#install-and-run-podman)
- [Use the Command Line to Run the Tests Against the Podman Daemon ](#use-the-command-line-to-run-the-tests-against-the-podman-daemon-)
- [Use IntelliJ IDEA Run the Tests Against the Podman Daemon](#use-intellij-idea-run-the-tests-against-the-podman-daemon)
- [~/.testcontainers.properties](#~/.testcontainers.properties)
- [How to Run the Tests Against a Remote Podman or Docker Daemon?](#how-to-run-the-tests-against-a-remote-podman-or-docker-daemon?)
- [How to Run the Application on a Different Port?](#how-to-run-the-application-on-a-different-port?)
- [How to Use a Persistent Database for Integration Tests?](#how-to-use-a-persistent-database-for-integration-tests?)
- [How to Amend Liquibase SQL Changesets?](#how-to-amend-liquibase-sql-changesets?)
- [How to Re-Generate Spring-Controller-Interfaces from OpenAPI specs?](#how-to-re-generate-spring-controller-interfaces-from-openapi-specs?)
- [How to Generate Database Table Diagrams?](#how-to-generate-database-table-diagrams?)
- [How to Run the Application With Other Profiles, e.g. production](#)
- [How to Do a Clean Run of the Application](#how-to-do-a-clean-run-of-the-application)
- [How to Configure .pgpass for the Default PostgreSQL Database?](#how-to-configure-.pgpass-for-the-default-postgresql-database?)
- [How to Run the Tests Against a Local User-Space Podman Daemon?](#how-to-run-the-tests-against-a-local-user-space-podman-daemon?)
- [Install and Run Podman](#install-and-run-podman)
- [Use the Command Line to Run the Tests Against the Podman Daemon ](#use-the-command-line-to-run-the-tests-against-the-podman-daemon-)
- [Use IntelliJ IDEA Run the Tests Against the Podman Daemon](#use-intellij-idea-run-the-tests-against-the-podman-daemon)
- [~/.testcontainers.properties](#~/.testcontainers.properties)
- [How to Run the Tests Against a Remote Podman or Docker Daemon?](#how-to-run-the-tests-against-a-remote-podman-or-docker-daemon?)
- [How to Run the Application on a Different Port?](#how-to-run-the-application-on-a-different-port?)
- [How to Use a Persistent Database for Integration Tests?](#how-to-use-a-persistent-database-for-integration-tests?)
- [How to Amend Liquibase SQL Changesets?](#how-to-amend-liquibase-sql-changesets?)
- [How to Re-Generate Spring-Controller-Interfaces from OpenAPI specs?](#how-to-re-generate-spring-controller-interfaces-from-openapi-specs?)
- [How to Generate Database Table Diagrams?](#how-to-generate-database-table-diagrams?)
- [How to Add (Real) Admin Users](#how-to-add-(real)-admin-users)
- [Further Documentation](#further-documentation)
<!-- generated TOC end. -->
@ -51,10 +58,11 @@ Everything is tested on _Ubuntu Linux 22.04_ and _MacOS Monterey (12.4)_.
To be able to build and run the Java Spring Boot application, you need the following tools:
- Docker 20.x (on MacOS you also need *Docker Desktop* or similar) or Podman
- optionally: PostgreSQL Server 15.5-bookworm
- optionally: PostgreSQL Server 15.5-bookworm, if you want to use the database directly, not just via Docker
(see instructions below to install and run in Docker)
- The matching Java JDK at will be automatically installed by Gradle toolchain support to `~/.gradle/jdks/`.
- You also might need an IDE (e.g. *IntelliJ IDEA* or *Eclipse* or *VS Code* with *[STS](https://spring.io/tools)* and a GUI Frontend for *PostgreSQL* like *Postbird*.
- Python 3 is expected in /usr/bin/python3 if you want to run the `howto` tool (see `bin/howto`)
If you have at least Docker and the Java JDK installed in appropriate versions and in your `PATH`, then you can start like this:
@ -64,7 +72,12 @@ If you have at least Docker and the Java JDK installed in appropriate versions a
gw # initially downloads the configured Gradle version into the project
gw test # compiles and runs unit- and integration-tests - takes >10min even on a fast machine
gw scenarioTests # compiles and scenario-tests - takes ~1min on a decent machine
# `gw test` does NOT run import- and scenario-tests.
# Use `gw-test` instead to make sure .tc-environment is sourced.
gw scenarioTest # compiles and scenario-tests - takes ~1min on a decent machine
# Use `gw-test scenarioTest` instead to make sure .tc-environment is sourced.
howto test # shows more test information about how to run tests
# if the container has not been built yet, run this:
pg-sql-run # downloads + runs PostgreSQL in a Docker container on localhost:5432
@ -72,12 +85,22 @@ If you have at least Docker and the Java JDK installed in appropriate versions a
# if the container has been built already and you want to keep the data, run this:
pg-sql-start
Next, compile and run the application without CAS-authentication on `localhost:8080`:
Next, compile and run the application on `localhost:8080` and the management server on `localhost:8081`:
# this disables CAS-authentication, for using the REST-API with CAS-authentication, see `bin/cas-curl`.
export HSADMINNG_CAS_SERVER=
gw bootRun
For using the REST-API with CAS-authentication, see `bin/cas-curl`.
# this runs the application with test-data and all modules:
gw bootRun --args='--spring.profiles.active=dev,complete,test-data'
The meaning of these profiles is:
- **dev**: the PostgreSQL users are created via Liquibase
- **complete**: all modules are started
- **test-data**: some test data inserted
Running just `gw bootRun` would just run the *office* module, not insert any test-data and
require the PostgreSQL users created in the database (see env-vars in `.aliases`).
Now we can access the REST API, e.g. using curl:
@ -109,7 +132,7 @@ Also try for example 'admin@xxx.example.com' or 'unknown@example.org'.
If you want a formatted JSON output, you can pipe the result to `jq` or similar.
And to see the full, currently implemented, API, open http://localhost:8080/swagger-ui/index.html.
And to see the full, currently implemented, API, open http://localhost:8081/actuator/swagger-ui/index.html (uses management-port and thus bypasses authentication).
If you still need to install some of these tools, find some hints in the next chapters.
@ -189,7 +212,7 @@ To generate the TOC (Table of Contents), a little bash script from a
Given this is in PATH as `md-toc`, use:
```shell
md-toc <README.md 2 4 | cut -c5-'
md-toc <README.md 2 4 | cut -c5-
```
To render the Markdown files, especially to watch embedded PlantUML diagrams, you can use one of the following methods:
@ -427,36 +450,42 @@ Some of these rules are checked with *ArchUnit* unit tests.
### Run Tests from Command Line
Run all tests which have not yet been passed with the current source code:
Run all unit-, integration- and acceptance-tests which have not yet been passed with the current source code:
```shell
gw test
gw test # uses the current environment, especially HSADMINNG_POSTGRES_JDBC_URL
```
If the referenced database is not empty, the tests might fail.
To explicitly use the Testcontainers-environment, run:
```shell
gw-test # uses the environment from .tc-environment
```
Force running all tests:
```shell
gw cleanTest test
gw-test --rerun
```
To find more options about running tests, try `howto test`.
### Spotless Code Formatting
Code formatting for Java is checked via *spotless*.
The formatting style can be checked with this command:
```shell
gw spotlessCheck
```
This task is also included in `gw build` and `gw check`.
To apply formatting rules, use:
```shell
gw spotlessApply
gw-spotless
```
The gradle task spotlessCheck is also included in `gw build` and `gw check`,
thus if the formatting is not compliant to the rules, the build is going to fail.
### JaCoCo Test Code Coverage Check
This project uses the JaCoCo test code coverage report with limit checks.
@ -494,13 +523,12 @@ Classes to be scanned, tests to be executed and thresholds are configured in [bu
A report is generated under [build/reports/pitest/index.html](./build/reports/pitest/index.html).
A link to the report is also printed after the `pitest` run.
This task is also executed as part of `gw check`.
<!-- TODO.test: This task is also executed as part of `gw check`. -->
#### Remark
In this project, there is little business logic in *Java* code;
most business code is in *plsql*
and *Java* ist mostly used for mapping REST calls to database queries.
In this project, there is a large amount of code is in *plsql*, especially for RBAC.
*Java* ist mostly used for mapping and validating REST calls to database queries.
This mapping ist mostly done through *Spring* annotations and other implicit code.
Therefore, there are only few unit tests and thus mutation testing has limited value.
@ -534,7 +562,7 @@ In case of suppression, a note must be added to explain why it does not apply to
See also: https://jeremylong.github.io/DependencyCheck/dependency-check-gradle/index.html.
### Dependency-License-Compatibility
### How to Check Dependency-License-Compatibility
The `gw check` phase depends on a dependency-license-compatibility check.
If any dependency violates the configured [list of allowed licenses](etc/allowed-licenses.json), the build will fail.
@ -564,7 +592,7 @@ The generated license can be found here: [index.html](build/reports/dependency-l
More information can be found on the [project's website](https://github.com/jk1/Gradle-License-Report).
### Dependency Version Upgrade
### How to Upgrade Versions of Dependencies
Dependency versions can be automatically upgraded to the latest available version:
@ -592,8 +620,9 @@ This way we would get rid of all explicit grants within the same DB-row
and would not need the `rbac.role` table anymore.
We would also reduce the depth of the expensive recursive CTE-query.
This has to be explored further.
For now, we just keep it in mind and
This has to be explored further. For now, we just keep it in mind and avoid roles+grants
which would not fit into a simplified system with a fixed role-type-system.
### The Mapper is Error-Prone
@ -617,6 +646,36 @@ Besides the following *How Tos* you can also find several *How Tos* in the sourc
grep -r HOWTO src
```
also try this (assumed you've sourced .aliases):
```sh
howto
```
### How to Run the Application With Other Profiles, e.g. production:
Add `--args='--spring.profiles.active=...` with the wanted profile selector:
```sh
gw bootRun --args='--spring.profiles.active=external-db,only -office,without-test-data'
```
These profiles mean:
- **external-db**: an external PostgreSQL database is used with the PostgreSQL users already created as specified in the environment
- **only-office**: only the Office module is started, but neither the Booking nor the Hosting modules
- **without-test-data**: no test-data is inserted
### How to Do a Clean Run of the Application
If you frequently need to run with a fresh database and a clean build, you can use this:
```sh
export HSADMINNG_CAS_SERVER=
gw clean && pg-sql-reset && sleep 5 && gw bootRun' 2>&1 | tee log
```
### How to Configure .pgpass for the Default PostgreSQL Database?
To access the default database schema as used during development, add this line to your `.pgpass` file in your users home directory:

86
bin/howto Executable file
View File

@ -0,0 +1,86 @@
#!/usr/bin/python3
import os
import sys
from urllib.parse import urljoin, quote
def path_to_file_uri(path):
"""
Converts a file path to a file URI.
Handles absolute and relative paths.
"""
abs_path = os.path.abspath(path)
return urljoin("file://", quote(abs_path))
def is_binary_file(filepath, chunk_size=1024):
"""
Prüft, ob eine Datei binär ist, indem sie den Inhalt der Datei auf nicht-druckbare Zeichen untersucht.
"""
try:
with open(filepath, "rb") as file:
chunk = file.read(chunk_size)
if b"\0" in chunk: # Nullbyte ist ein typisches Zeichen für Binärdateien
return True
return False
except Exception as e:
print(f"Fehler beim Prüfen, ob Datei binär ist: {filepath}: {e}")
return True
def search_keywords_in_files(keywords):
if not keywords:
print("Bitte geben Sie mindestens ein Stichwort an.")
sys.exit(1)
# Allowed comment symbols
comment_symbols = {"//", "#", ";"}
for root, dirs, files in os.walk("."):
# Ausschließen bestimmter Verzeichnisse
dirs[:] = [d for d in dirs if d not in {".git", "build"}]
for file in files:
filepath = os.path.join(root, file)
# Überspringen von Binärdateien
if is_binary_file(filepath):
continue
try:
with open(filepath, "r", encoding="utf-8") as f:
lines = f.readlines()
for line_number, line in enumerate(lines, start=1):
stripped_line = line.lstrip() # Entfernt führende Leerzeichen
for symbol in comment_symbols:
if stripped_line.startswith(symbol):
# Entfernt das Kommentarzeichen und nachfolgende Leerzeichen
howtoMatch = stripped_line[len(symbol):].lstrip()
if howtoMatch.startswith(("HOWTO ", "HOWTO: ", "How to ")):
if all(keyword in howtoMatch.lower() for keyword in keywords):
# Titelzeile ohne Kommentarzeichen
print(howtoMatch.rstrip())
# Ausgabe nachfolgender Zeilen mit dem gleichen Kommentar-Präfix
for subsequent_line in lines[line_number:]:
subsequent_line = subsequent_line.lstrip()
if subsequent_line.startswith(symbol):
# Entfernt Kommentarzeichen aus Folgezeilen
print("\t" + subsequent_line[len(symbol):].strip())
else:
break
# Link mit Zeilennummer
print(f"--> {path_to_file_uri(filepath)}:{line_number}")
# Abstand zwischen Matches
print()
break
except Exception as e:
print(f"Fehler beim Lesen der Datei {filepath}: {e}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Verwendung: bin/howto <Stichwort1> <Stichwort2> ...")
sys.exit(1)
search_keywords_in_files([arg.lower() for arg in sys.argv[1:]])

View File

@ -1,17 +1,20 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
id 'io.openapiprocessor.openapi-processor' version '2023.2'
id 'com.github.jk1.dependency-license-report' version '2.9'
id "org.owasp.dependencycheck" version "10.0.4"
id "com.diffplug.spotless" version "6.25.0"
id 'jacoco'
id 'info.solidsoft.pitest' version '1.15.0'
id 'se.patrikerdes.use-latest-versions' version '0.2.18'
id 'com.github.ben-manes.versions' version '0.51.0'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7' // manages implicit dependencies
id 'io.openapiprocessor.openapi-processor' version '2023.2' // generates Controller-interface and resources from API-spec
id 'com.github.jk1.dependency-license-report' version '2.9' // checks dependency-license compatibility
id "org.owasp.dependencycheck" version "12.0.1" // checks dependencies for known vulnerabilities
id "com.diffplug.spotless" version "7.0.2" // formats + checks formatting for source-code
id 'jacoco' // determines code-coverage of tests
id 'info.solidsoft.pitest' version '1.15.0' // performs mutation testing
id 'se.patrikerdes.use-latest-versions' version '0.2.18' // updates module and plugin versions
id 'com.github.ben-manes.versions' version '0.52.0' // determines which dependencies have updates
}
// HOWTO: find out which dependency versions are managed by Spring Boot:
// https://docs.spring.io/spring-boot/appendix/dependency-versions/coordinates.html
group = 'net.hostsharing'
version = '0.0.1-SNAPSHOT'
@ -20,6 +23,9 @@ wrapper {
gradleVersion = '8.5'
}
// TODO.impl: self-attaching is deprecated, see:
// https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3
configurations {
compileOnly {
extendsFrom annotationProcessor
@ -60,25 +66,25 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2'
implementation 'org.springdoc:springdoc-openapi:2.6.0'
implementation 'org.postgresql:postgresql:42.7.4'
implementation 'org.liquibase:liquibase-core:4.29.2'
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.8.3'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0'
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0'
implementation 'org.springdoc:springdoc-openapi:2.8.3'
implementation 'org.postgresql:postgresql'
implementation 'org.liquibase:liquibase-core'
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
implementation 'org.apache.commons:commons-text:1.12.0'
implementation 'net.java.dev.jna:jna:5.15.0'
implementation 'org.modelmapper:modelmapper:3.2.1'
implementation 'org.apache.commons:commons-text:1.13.0'
implementation 'net.java.dev.jna:jna:5.16.0'
implementation 'org.modelmapper:modelmapper:3.2.2'
implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
implementation 'org.webjars:swagger-ui:5.17.14'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3'
implementation 'org.reflections:reflections:0.10.2'
compileOnly 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// TODO.impl: version conflict with SpringDoc, check later and re-enable if fixed
// developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
@ -90,7 +96,7 @@ dependencies {
testImplementation 'org.testcontainers:postgresql'
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
testImplementation 'io.rest-assured:spring-mock-mvc'
testImplementation 'org.hamcrest:hamcrest-core:3.0'
testImplementation 'org.hamcrest:hamcrest-core'
testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.wiremock:wiremock-standalone:3.10.0'
@ -174,7 +180,9 @@ openapiProcessor {
}
}
sourceSets.main.java.srcDir 'build/generated/sources/openapi'
abstract class ProcessSpring extends DefaultTask {}
tasks.register('processSpring', ProcessSpring)
['processSpringRoot',
'processSpringRbac',
@ -205,7 +213,7 @@ openApiGenerate.dependsOn processSpring
spotless {
java {
removeUnusedImports()
indentWithSpaces(4)
leadingTabsToSpaces(4)
endWithNewline()
toggleOffOn()
@ -219,7 +227,7 @@ project.tasks.check.dependsOn(spotlessCheck)
// HACK: no idea why spotless uses the output of these tasks, but we get warnings without those
project.tasks.spotlessJava.dependsOn(
tasks.generateLicenseReport,
tasks.pitest,
// tasks.pitest, TODO.test: PiTest currently does not work, needs to be fixed
tasks.jacocoTestReport,
tasks.processResources,
tasks.processTestResources)
@ -248,19 +256,21 @@ licenseReport {
}
project.tasks.check.dependsOn(checkLicense)
// JaCoCo Test Code Coverage
jacoco {
toolVersion = "0.8.10"
}
// HOWTO: run all tests except import- and scenario-tests: gw test
test {
finalizedBy jacocoTestReport // generate report after tests
excludes = [
'net.hostsharing.hsadminng.**.generated.**',
]
useJUnitPlatform {
excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest'
excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest'
}
}
// JaCoCo Test Code Coverage for unit-tests
jacoco {
toolVersion = "0.8.10"
}
jacocoTestReport {
dependsOn test
afterEvaluate {
@ -325,6 +335,67 @@ jacocoTestCoverageVerification {
}
}
// HOWTO: run all unit-tests which don't need a database: gw-test unitTest
tasks.register('unitTest', Test) {
useJUnitPlatform {
excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest', 'generalIntegrationTest',
'officeIntegrationTest', 'bookingIntegrationTest', 'hostingIntegrationTest'
}
group 'verification'
description 'runs all unit-tests which do not need a database'
mustRunAfter spotlessJava
}
// HOWTO: run all integration tests which are not specific to a module, like base, rbac, config etc.
tasks.register('generalIntegrationTest', Test) {
useJUnitPlatform {
includeTags 'generalIntegrationTest'
}
group 'verification'
description 'runs integration tests which are not specific to a module, like base, rbac, config etc.'
mustRunAfter spotlessJava
}
// HOWTO: run all integration tests of the office module: gw-test officeIntegrationTest
tasks.register('officeIntegrationTest', Test) {
useJUnitPlatform {
includeTags 'officeIntegrationTest'
}
group 'verification'
description 'runs integration tests of the office module'
mustRunAfter spotlessJava
}
// HOWTO: run all integration tests of the booking module: gw-test bookingIntegrationTest
tasks.register('bookingIntegrationTest', Test) {
useJUnitPlatform {
includeTags 'bookingIntegrationTest'
}
group 'verification'
description 'runs integration tests of the booking module'
mustRunAfter spotlessJava
}
// HOWTO: run all integration tests of the hosting module: gw-test hostingIntegrationTest
tasks.register('hostingIntegrationTest', Test) {
useJUnitPlatform {
includeTags 'hostingIntegrationTest'
}
group 'verification'
description 'runs integration tests of the hosting module'
mustRunAfter spotlessJava
}
tasks.register('importOfficeData', Test) {
useJUnitPlatform {
includeTags 'importOfficeData'
@ -347,7 +418,7 @@ tasks.register('importHostingAssets', Test) {
mustRunAfter spotlessJava
}
tasks.register('scenarioTests', Test) {
tasks.register('scenarioTest', Test) {
useJUnitPlatform {
includeTags 'scenarioTest'
}
@ -368,7 +439,7 @@ pitest {
]
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*']
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*', '**ImportOfficeData', '**ImportHostingAssets']
pitestVersion = '1.17.0'
junit5PluginVersion = '1.1.0'
@ -383,7 +454,7 @@ pitest {
outputFormats = ['XML', 'HTML']
timestampedReports = false
}
project.tasks.check.dependsOn(project.tasks.pitest)
// project.tasks.check.dependsOn(project.tasks.pitest) TODO.test: PiTest currently does not work, needs to be fixed
project.tasks.pitest.doFirst { // Why not doLast? See README.md!
println "PiTest Mutation Report: file:///${project.rootDir}/build/reports/pitest/index.html"
}
@ -447,7 +518,7 @@ tasks.register('convertMarkdownToHtml') {
}
}
}
convertMarkdownToHtml.dependsOn scenarioTests
convertMarkdownToHtml.dependsOn scenarioTest
// shortcut for compiling all files
tasks.register('compile') {

View File

@ -50,6 +50,7 @@ classDiagram
UNKNOWN: nur für Import
NATURAL_PERSON: natürliche Person
LEGAL_PERSON: z.B. GmbH, e.K., eG, e.V.
ORGANIZATIONAL_UNIT: z.B. "Admin-Team", "Buchhaltung"
INCORORATED_FIRM: z.B. OHG, Partnerschaftsgesellschaft
UNINCORPORATED_FIRM: z.B. GbR, ARGE, Erbengemeinschaft
PUBLIC_INSTITUTION: KdöR, AöR [ohne Registergericht/Registernummer]

View File

@ -5,9 +5,23 @@
{ "moduleLicense": "Apache-2.0" },
{ "moduleLicense": "Apache License 2.0" },
{ "moduleLicense": "Apache License v2.0" },
{ "moduleLicense": "Apache License Version 2.0" },
{ "moduleLicense": "Apache License, Version 2.0" },
{ "moduleLicense": "The Apache License, Version 2.0" },
{ "moduleLicense": "The Apache Software License, Version 2.0" },
{
"moduleLicense": null,
"#moduleLicense": "Apache License 2.0, see https://github.com/springdoc/springdoc-openapi/blob/main/LICENSE",
"moduleVersion": "2.4.0",
"moduleName": "org.springdoc:springdoc-openapi"
},
{
"moduleLicense": null,
"moduleVersion": "1.0.0",
"moduleName": "org.jspecify:jspecify"
},
{ "moduleLicense": "BSD License" },
{ "moduleLicense": "BSD-2-Clause" },
{ "moduleLicense": "BSD-3-Clause" },
@ -46,14 +60,8 @@
{
"moduleLicense": "Public Domain, per Creative Commons CC0",
"moduleVersion": "2.0.3"
},
{
"moduleLicense": null,
"#moduleLicense": "Apache License 2.0, see https://github.com/springdoc/springdoc-openapi/blob/main/LICENSE",
"moduleVersion": "2.4.0",
"moduleName": "org.springdoc:springdoc-openapi"
}
]
}

View File

@ -9,8 +9,12 @@
</suppress>
<suppress>
<notes><![CDATA[
Malicious HTTP redirect in JAXB on a REST-endpoint is not that dangerous.
file name: logback-core-1.5.12.jar
A successful attack requires the user to have write access to a configuration file or environment vars.
]]></notes>
<cve>CVE-2024-9329</cve>
<packageUrl regex="true">^pkg:maven/ch\.qos\.logback/logback-core@.*$</packageUrl>
<cpe>cpe:/a:qos:logback</cpe>
<cve>CVE-2024-12798</cve>
</suppress>
</suppressions>

View File

@ -0,0 +1,105 @@
package net.hostsharing.hsadminng.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
// HOWTO: exclude sensitive values, like passwords and other secrets, from being show by actuator endpoints:
// either use: add your custom keys to management.endpoint.additionalKeysToSanitize,
// or, if you need more heuristics, amend this code down here.
@Component
public class ActuatorSanitizer implements SanitizingFunction {
private static final String[] REGEX_PARTS = {"*", "$", "^", "+"};
private static final Set<String> DEFAULT_KEYS_TO_SANITIZE = Set.of(
"password", "secret", "token", ".*credentials.*", "vcap_services", "^vcap\\.services.*$", "sun.java.command", "^spring[._]application[._]json$"
);
private static final Set<String> URI_USERINFO_KEYS = Set.of(
"uri", "uris", "url", "urls", "address", "addresses"
);
private static final Pattern URI_USERINFO_PATTERN = Pattern.compile("^\\[?[A-Za-z][A-Za-z0-9\\+\\.\\-]+://.+:(.*)@.+$");
private final List<Pattern> keysToSanitize = new ArrayList<>();
public ActuatorSanitizer(@Value("${management.endpoint.additionalKeysToSanitize:}") final List<String> additionalKeysToSanitize) {
addKeysToSanitize(DEFAULT_KEYS_TO_SANITIZE);
addKeysToSanitize(URI_USERINFO_KEYS);
addKeysToSanitize(additionalKeysToSanitize);
}
@Override
public SanitizableData apply(final SanitizableData data) {
if (data.getValue() == null) {
return data;
}
for (final Pattern pattern : keysToSanitize) {
if (pattern.matcher(data.getKey()).matches()) {
if (keyIsUriWithUserInfo(pattern)) {
return data.withValue(sanitizeUris(data.getValue().toString()));
}
return data.withValue(SanitizableData.SANITIZED_VALUE);
}
}
return data;
}
private void addKeysToSanitize(final Collection<String> keysToSanitize) {
for (final String key : keysToSanitize) {
this.keysToSanitize.add(getPattern(key));
}
}
private Pattern getPattern(final String value) {
if (isRegex(value)) {
return Pattern.compile(value, Pattern.CASE_INSENSITIVE);
}
return Pattern.compile(".*" + value + "$", Pattern.CASE_INSENSITIVE);
}
private boolean isRegex(final String value) {
for (final String part : REGEX_PARTS) {
if (value.contains(part)) {
return true;
}
}
return false;
}
private boolean keyIsUriWithUserInfo(final Pattern pattern) {
for (String uriKey : URI_USERINFO_KEYS) {
if (pattern.matcher(uriKey).matches()) {
return true;
}
}
return false;
}
private Object sanitizeUris(final String value) {
return Arrays.stream(value.split(",")).map(this::sanitizeUri).collect(Collectors.joining(","));
}
private String sanitizeUri(final String value) {
final var matcher = URI_USERINFO_PATTERN.matcher(value);
final var password = matcher.matches() ? matcher.group(1) : null;
if (password != null) {
return StringUtils.replace(value, ":" + password + "@", ":" + SanitizableData.SANITIZED_VALUE + "@");
}
return value;
}
}

View File

@ -97,6 +97,7 @@ public class RestResponseEntityExceptionHandler
return errorResponse(request, HttpStatus.valueOf(statusCode.value()),
Optional.ofNullable(response.getBody()).map(Object::toString).orElse(firstMessageLine(exc)));
}
@Override
@SuppressWarnings("unchecked,rawtypes")
protected ResponseEntity handleHttpMessageNotReadable(
@ -131,7 +132,7 @@ public class RestResponseEntityExceptionHandler
final HttpStatusCode status,
final WebRequest request) {
final var errorList = exc
.getAllValidationResults()
.getParameterValidationResults()
.stream()
.map(ParameterValidationResult::getResolvableErrors)
.flatMap(Collection::stream)

View File

@ -5,6 +5,7 @@ import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
@ -24,7 +25,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayAs("BookingDebitor")
public class HsBookingDebitorEntity implements Stringifyable {
public class HsBookingDebitorEntity implements Stringifyable, WithRoleId {
public static final String DEBITOR_NUMBER_TAG = "D-";

View File

@ -2,12 +2,14 @@ package net.hostsharing.hsadminng.hs.booking.debitor;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.repository.Repository;
import org.springframework.context.annotation.Profile;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> {
@Profile("!only-office")
public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> {
@Timed("app.booking.debitor.repo.findByUuid")
Optional<HsBookingDebitorEntity> findByUuid(UUID id);

View File

@ -1,10 +1,12 @@
package net.hostsharing.hsadminng.hs.booking.item;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository;
import java.util.UUID;
@Profile("!only-office")
public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> {
@Timed("app.booking.items.repo.save")

View File

@ -16,6 +16,7 @@ import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
@ -30,6 +31,7 @@ import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@RestController
@Profile("!only-office")
public class HsBookingItemController implements HsBookingItemsApi {
@Autowired

View File

@ -1,12 +1,14 @@
package net.hostsharing.hsadminng.hs.booking.item;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsBookingItemRbacRepository extends HsBookingItemRepository<HsBookingItemRbacEntity>,
Repository<HsBookingItemRbacEntity, UUID> {

View File

@ -1,12 +1,14 @@
package net.hostsharing.hsadminng.hs.booking.item;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsBookingItemRealRepository extends HsBookingItemRepository<HsBookingItemRealEntity>,
Repository<HsBookingItemRealEntity, UUID> {

View File

@ -1,10 +1,13 @@
package net.hostsharing.hsadminng.hs.booking.item;
import org.springframework.context.annotation.Profile;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsBookingItemRepository<E extends HsBookingItem> {
Optional<E> findByUuid(final UUID bookingItemUuid);

View File

@ -7,8 +7,9 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjec
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
@ -20,13 +21,14 @@ import java.util.UUID;
import java.util.function.BiConsumer;
@RestController
@Profile("!only-office")
public class HsBookingProjectController implements HsBookingProjectsApi {
@Autowired
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsBookingProjectRbacRepository bookingProjectRepo;

View File

@ -1,12 +1,14 @@
package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsBookingProjectRbacRepository extends HsBookingProjectRepository<HsBookingProjectRbacEntity>,
Repository<HsBookingProjectRbacEntity, UUID> {

View File

@ -1,12 +1,14 @@
package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsBookingProjectRealRepository extends HsBookingProjectRepository<HsBookingProjectRealEntity>,
Repository<HsBookingProjectRealEntity, UUID> {

View File

@ -1,11 +1,13 @@
package net.hostsharing.hsadminng.hs.booking.project;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsBookingProjectRepository<E extends HsBookingProject> {
@Timed("app.booking.projects.repo.findByUuid")

View File

@ -89,7 +89,7 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
@JoinColumn(name = "alarmcontactuuid")
private HsOfficeContactRealEntity alarmContact;
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY)
@OneToMany(cascade = { CascadeType.PERSIST, CascadeType.REFRESH }, orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid")
private List<HsHostingAssetRealEntity> subHostingAssets;

View File

@ -12,9 +12,10 @@ import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAsse
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
import net.hostsharing.hsadminng.mapper.KeyValueMap;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
@ -27,6 +28,7 @@ import java.util.UUID;
import java.util.function.BiConsumer;
@RestController
@Profile("!only-office")
public class HsHostingAssetController implements HsHostingAssetsApi {
@Autowired
@ -36,7 +38,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsHostingAssetRbacRepository rbacAssetRepo;

View File

@ -4,6 +4,7 @@ import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
@ -12,6 +13,7 @@ import java.util.Map;
@RestController
@Profile("!only-office")
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
@ -8,7 +9,7 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<HsHostingAssetRbacEntity>, Repository<HsHostingAssetRbacEntity, UUID> {
@Timed("app.hostingAsset.repo.findByUuid.rbac")

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
@ -9,6 +10,7 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<HsHostingAssetRealEntity>, Repository<HsHostingAssetRealEntity, UUID> {
@Timed("app.hostingAsset.repo.findByUuid.real")

View File

@ -1,11 +1,13 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import io.micrometer.core.annotation.Timed;
import org.springframework.context.annotation.Profile;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Profile("!only-office")
public interface HsHostingAssetRepository<E extends HsHostingAsset> {
@Timed("app.hosting.assets.repo.findByUuid")

View File

@ -9,7 +9,7 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
import net.hostsharing.hsadminng.lambda.Reducer;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.mapper.ToStringConverter;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
@ -31,8 +31,8 @@ public class DomainSetupHostingAssetFactory extends HostingAssetFactory {
final EntityManagerWrapper emw,
final HsBookingItemRealEntity newBookingItemRealEntity,
final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper) {
super(emw, newBookingItemRealEntity, asset, standardMapper);
final StrictMapper StrictMapper) {
super(emw, newBookingItemRealEntity, asset, StrictMapper);
}
@Override

View File

@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAsse
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
@ -16,7 +16,7 @@ abstract class HostingAssetFactory {
final EntityManagerWrapper emw;
final HsBookingItemRealEntity fromBookingItem;
final HsHostingAssetAutoInsertResource asset;
final StandardMapper standardMapper;
final StrictMapper StrictMapper;
protected abstract HsHostingAsset create();

View File

@ -9,13 +9,15 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAsse
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Component
@Profile("!only-office")
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
@Autowired
@ -25,7 +27,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
private ObjectMapper jsonMapper;
@Autowired
private StandardMapper standardMapper;
private StrictMapper StrictMapper;
@Override
@SneakyThrows
@ -44,9 +46,9 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class);
final var factory = switch (newBookingItemRealEntity.getType()) {
case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER ->
forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, standardMapper);
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, StrictMapper);
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, StrictMapper);
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, StrictMapper);
};
if (factory != null) {
final var statusMessage = factory.createAndPersist();
@ -62,9 +64,9 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
final EntityManagerWrapper emw,
final HsBookingItemRealEntity fromBookingItem,
final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper
final StrictMapper StrictMapper
) {
return new HostingAssetFactory(emw, fromBookingItem, asset, standardMapper) {
return new HostingAssetFactory(emw, fromBookingItem, asset, StrictMapper) {
@Override
protected HsHostingAsset create() {

View File

@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAsse
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import jakarta.validation.ValidationException;
@ -19,8 +19,8 @@ public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
final EntityManagerWrapper emw,
final HsBookingItemRealEntity newBookingItemRealEntity,
final HsHostingAssetAutoInsertResource asset,
final StandardMapper standardMapper) {
super(emw, newBookingItemRealEntity, asset, standardMapper);
final StrictMapper StrictMapper) {
super(emw, newBookingItemRealEntity, asset, StrictMapper);
}
@Override
@ -32,7 +32,7 @@ public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
.map(Enum::name)
.orElse(null));
}
final var managedWebspaceHostingAsset = standardMapper.map(asset, HsHostingAssetRealEntity.class);
final var managedWebspaceHostingAsset = StrictMapper.map(asset, HsHostingAssetRealEntity.class);
managedWebspaceHostingAsset.setBookingItem(fromBookingItem);
emw.createQuery(
"SELECT asset FROM HsHostingAssetRealEntity asset WHERE asset.bookingItem.uuid=:bookingItemUuid",

View File

@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.iban4j.BicUtil;
import org.iban4j.IbanUtil;
import org.springframework.beans.factory.annotation.Autowired;
@ -25,7 +25,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeBankAccountRepository bankAccountRepo;

View File

@ -12,6 +12,7 @@ import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
@ -37,7 +38,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@SuperBuilder(toBuilder = true)
@FieldNameConstants
@DisplayAs("Contact")
public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContact> {
public class HsOfficeContact implements Stringifyable, BaseEntity<HsOfficeContact>, WithRoleId {
private static Stringify<HsOfficeContact> toString = stringify(HsOfficeContact.class, "contact")
.withProp(Fields.caption, HsOfficeContact::getCaption)

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.contact;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
@ -28,7 +28,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeContactRbacRepository contactRepo;
@ -131,6 +131,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
@SuppressWarnings("unchecked")
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putPostalAddress(from(resource.getPostalAddress()));
entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
};

View File

@ -1,4 +1,3 @@
package net.hostsharing.hsadminng.hs.office.coopassets;
import lombok.AllArgsConstructor;
@ -10,11 +9,22 @@ import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
import jakarta.persistence.*;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
@ -57,8 +67,7 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
.quotedValues(false);
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
@GeneratedValue
private UUID uuid;
@Version
@ -122,15 +131,15 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
return this;
}
public String getTaggedMemberNumber() {
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-???????");
}
@Override
public String toString() {
return stringify.apply(this);
}
public String getTaggedMemberNumber() {
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-???????");
}
@Override
public String toShortString() {
return "%s:%.3s:%+1.2f".formatted(
@ -141,7 +150,7 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, BaseE
public static RbacSpec rbac() {
return rbacViewFor("coopAssetsTransaction", HsOfficeCoopAssetsTransactionEntity.class)
.withIdentityView(RbacSpec.SQL.projection("reference"))
.withIdentityView(SQL.projection("reference"))
.withUpdatableColumns("comment")
.importEntityAlias("membership", HsOfficeMembershipEntity.class, usingDefaultCase(),
dependsOnColumn("membershipUuid"),

View File

@ -1,14 +1,13 @@
package net.hostsharing.hsadminng.hs.office.coopshares;
import jakarta.persistence.EntityNotFoundException;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
@ -25,6 +24,7 @@ import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.CANCELLATION;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION;
import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
@RestController
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
@ -33,14 +33,16 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
@Autowired
private HsOfficeMembershipRepository membershipRepo;
@Override
@Transactional(readOnly = true)
@Timed("app.office.coopShares.api.getListOfCoopShares")
public ResponseEntity<List<HsOfficeCoopSharesTransactionResource>> getListOfCoopShares(
final String currentSubject,
@ -55,7 +57,10 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
fromValueDate,
toValueDate);
final var resources = mapper.mapList(entities, HsOfficeCoopSharesTransactionResource.class);
final var resources = mapper.mapList(
entities,
HsOfficeCoopSharesTransactionResource.class,
ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
}
@ -70,7 +75,10 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
context.define(currentSubject, assumedRoles);
validate(requestBody);
final var entityToSave = mapper.map(requestBody, HsOfficeCoopSharesTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var entityToSave = mapper.map(
requestBody,
HsOfficeCoopSharesTransactionEntity.class,
RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = coopSharesTransactionRepo.save(entityToSave);
@ -79,7 +87,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
.path("/api/hs/office/coopsharestransactions/{id}")
.buildAndExpand(saved.getUuid())
.toUri();
final var mapped = mapper.map(saved, HsOfficeCoopSharesTransactionResource.class);
final var mapped = mapper.map(saved, HsOfficeCoopSharesTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.created(uri).body(mapped);
}
@ -87,15 +95,18 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
@Transactional(readOnly = true)
@Timed("app.office.coopShares.repo.getSingleCoopShareTransactionByUuid")
public ResponseEntity<HsOfficeCoopSharesTransactionResource> getSingleCoopShareTransactionByUuid(
final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
final String currentSubject, final String assumedRoles, final UUID shareTransactionUuid) {
context.define(currentSubject, assumedRoles);
context.define(currentSubject, assumedRoles);
final var result = coopSharesTransactionRepo.findByUuid(shareTransactionUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeCoopSharesTransactionResource.class));
final var result = coopSharesTransactionRepo.findByUuid(shareTransactionUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(mapper.map(
result.get(),
HsOfficeCoopSharesTransactionResource.class,
ENTITY_TO_RESOURCE_POSTMAPPER));
}
@ -137,9 +148,16 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
}
final BiConsumer<HsOfficeCoopSharesTransactionInsertResource, HsOfficeCoopSharesTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if ( resource.getRevertedShareTxUuid() != null ) {
entity.setRevertedShareTx(coopSharesTransactionRepo.findByUuid(resource.getRevertedShareTxUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] revertedShareTxUuid %s not found".formatted(resource.getRevertedShareTxUuid()))));
entity.setMembership(resolve("membership.uuid", resource.getMembershipUuid(), membershipRepo::findByUuid));
if (resource.getRevertedShareTxUuid() != null) {
entity.setRevertedShareTx(resolve(
"revertedShareTx.uuid",
resource.getRevertedShareTxUuid(),
coopSharesTransactionRepo::findByUuid));
}
};
final BiConsumer<HsOfficeCoopSharesTransactionEntity, HsOfficeCoopSharesTransactionResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setMembershipUuid(entity.getMembership().getUuid());
};
}

View File

@ -7,13 +7,23 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import jakarta.persistence.*;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.io.IOException;
import java.time.LocalDate;
import java.util.UUID;
@ -132,6 +142,7 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, BaseE
directlyFetchedByDependsOnColumn(),
NOT_NULL)
// the membership:ADMIN is not to be confused with the member itself, it's an account manager of the coop
.toRole("membership", ADMIN).grantPermission(INSERT)
.toRole("membership", ADMIN).grantPermission(UPDATE)
.toRole("membership", AGENT).grantPermission(SELECT);

View File

@ -2,14 +2,16 @@ package net.hostsharing.hsadminng.hs.office.debitor;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitorsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.persistence.EntityExistsValidator;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.apache.commons.lang3.Validate;
import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired;
@ -26,6 +28,7 @@ import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
import static net.hostsharing.hsadminng.hs.validation.UuidResolver.resolve;
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@RestController
@ -36,16 +39,22 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeDebitorRepository debitorRepo;
@Autowired
private HsOfficeRelationRealRepository relrealRepo;
private HsOfficeRelationRealRepository realRelRepo;
@Autowired
private EntityExistsValidator entityValidator;
private HsOfficePersonRealRepository realPersonRepo;
@Autowired
private HsOfficeContactRealRepository realContactRepo;
@Autowired
private HsOfficeBankAccountRepository bankAccountRepo;
@PersistenceContext
private EntityManager em;
@ -63,9 +72,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
final var entities = partnerNumber != null
? debitorRepo.findDebitorsByPartnerNumber(cropTag("P-", partnerNumber))
: partnerUuid != null
? debitorRepo.findDebitorsByPartnerUuid(partnerUuid)
: debitorRepo.findDebitorsByOptionalNameLike(name);
: partnerUuid != null
? debitorRepo.findDebitorsByPartnerUuid(partnerUuid)
: debitorRepo.findDebitorsByOptionalNameLike(name);
final var resources = mapper.mapList(entities, HsOfficeDebitorResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
@ -81,34 +90,19 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
context.define(currentSubject, assumedRoles);
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRelUuid() == null,
Validate.isTrue(
body.getDebitorRel() == null || body.getDebitorRelUuid() == null,
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
Validate.isTrue(
body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found none");
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
Validate.isTrue(
body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
"ERROR: [400] debitorRel.mark must be null");
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
if (body.getDebitorRel() != null) {
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
debitorRel.setType(DEBITOR);
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
} else {
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
debitorRelOptional.ifPresentOrElse(
debitorRel -> {entityToSave.setDebitorRel(relrealRepo.save(debitorRel));},
() -> {
throw new ValidationException(
"Unable to find RealRelation by debitorRelUuid: " + body.getDebitorRelUuid());
});
}
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var savedEntity = debitorRepo.save(entityToSave);
em.flush();
em.refresh(savedEntity);
final var savedEntity = debitorRepo.save(entityToSave).reload(em);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@ -181,7 +175,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
context.define(currentSubject, assumedRoles);
final var current = debitorRepo.findByUuid(debitorUuid).orElseThrow();
final var current = debitorRepo.findByUuid(debitorUuid).orElseThrow().reload(em);
new HsOfficeDebitorEntityPatcher(em, current).apply(body);
@ -191,7 +185,39 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
return ResponseEntity.ok(mapped);
}
final BiConsumer<HsOfficeDebitorInsertResource, HsOfficeDebitorEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if (resource.getDebitorRel() != null) {
final var debitorRel = realRelRepo.save(HsOfficeRelationRealEntity.builder()
.type(DEBITOR)
.anchor(resolve(
"debitorRel.anchor.uuid", resource.getDebitorRel().getAnchorUuid(), realPersonRepo::findByUuid))
.holder(resolve(
"debitorRel.holder.uuid", resource.getDebitorRel().getHolderUuid(), realPersonRepo::findByUuid))
.contact(resolve(
"debitorRel.contact.uuid", resource.getDebitorRel().getContactUuid(), realContactRepo::findByUuid))
.build());
entity.setDebitorRel(debitorRel);
} else {
final var debitorRelOptional = realRelRepo.findByUuid(resource.getDebitorRelUuid());
debitorRelOptional.ifPresentOrElse(
debitorRel -> {
entity.setDebitorRel(realRelRepo.save(debitorRel));
},
() -> {
throw new ValidationException(
"Unable to find debitorRel.uuid: " + resource.getDebitorRelUuid());
});
}
if (resource.getRefundBankAccountUuid() != null) {
entity.setRefundBankAccount(resolve(
"refundBankAccount.uuid", resource.getRefundBankAccountUuid(), bankAccountRepo::findByUuid));
}
};
final BiConsumer<HsOfficeDebitorEntity, HsOfficeDebitorResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setDebitorNumber(entity.getTaggedDebitorNumber());
resource.getPartner().setPartnerNumber(entity.getPartner().getTaggedPartnerNumber());
};
}

View File

@ -7,7 +7,8 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartner;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
@ -16,7 +17,6 @@ import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.JoinFormula;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
@ -75,7 +75,6 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
@Id
@GeneratedValue
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private UUID uuid;
@Version
@ -87,16 +86,16 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
value = """
(
SELECT DISTINCT partner.uuid
FROM hs_office.partner_rv partner
FROM hs_office.partner partner
JOIN hs_office.relation dRel
ON dRel.uuid = debitorreluuid AND dRel.type = 'DEBITOR'
ON dRel.uuid = debitorRelUuid AND dRel.type = 'DEBITOR'
JOIN hs_office.relation pRel
ON pRel.uuid = partner.partnerRelUuid AND pRel.type = 'PARTNER'
WHERE pRel.holderUuid = dRel.anchorUuid
)
""")
@NotFound(action = NotFoundAction.IGNORE) // TODO.impl: map a simplified raw-PartnerEntity, just for the partner-number
private HsOfficePartnerEntity partner;
@NotFound(action = NotFoundAction.EXCEPTION) // TODO.impl: map a simplified raw-PartnerEntity, just for the partner-number
private HsOfficePartnerRealEntity partner;
@Column(name = "debitornumbersuffix", length = 2)
@Pattern(regexp = TWO_DECIMAL_DIGITS)
@ -132,9 +131,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
@Override
public HsOfficeDebitorEntity load() {
BaseEntity.super.load();
if (partner != null) {
partner.load();
}
partner.load();
debitorRel.load();
if (refundBankAccount != null) {
refundBankAccount.load();
@ -145,7 +142,7 @@ public class HsOfficeDebitorEntity implements BaseEntity<HsOfficeDebitorEntity>,
public String getTaggedDebitorNumber() {
return ofNullable(partner)
.filter(partner -> debitorNumberSuffix != null)
.map(HsOfficePartnerEntity::getPartnerNumber)
.map(HsOfficePartner::getPartnerNumber)
.map(partnerNumber -> DEBITOR_NUMBER_TAG + partnerNumber + debitorNumberSuffix)
.orElse(null);
}

View File

@ -19,7 +19,7 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
@Query("""
SELECT debitor FROM HsOfficeDebitorEntity debitor
JOIN HsOfficePartnerEntity partner
JOIN HsOfficePartnerRealEntity partner
ON partner.partnerRel.holder = debitor.debitorRel.anchor
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
WHERE partner.partnerNumber = :partnerNumber
@ -42,7 +42,7 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
@Query("""
SELECT debitor FROM HsOfficeDebitorEntity debitor
JOIN HsOfficePartnerEntity partner
JOIN HsOfficePartnerRealEntity partner
ON partner.partnerRel.holder = debitor.debitorRel.anchor
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
JOIN HsOfficePersonRealEntity person

View File

@ -6,14 +6,16 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeMembersh
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipResource;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRbacEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealRepository;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityNotFoundException;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
@ -28,7 +30,10 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficePartnerRealRepository partnerRepo;
@Autowired
private HsOfficeMembershipRepository membershipRepo;
@ -47,7 +52,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
final var entities = partnerNumber != null
? membershipRepo.findMembershipsByPartnerNumber(
cropTag(HsOfficePartnerEntity.PARTNER_NUMBER_TAG, partnerNumber))
cropTag(HsOfficePartnerRbacEntity.PARTNER_NUMBER_TAG, partnerNumber))
: partnerUuid != null
? membershipRepo.findMembershipsByPartnerUuid(partnerUuid)
: membershipRepo.findAll();
@ -68,7 +73,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
context.define(currentSubject, assumedRoles);
final var entityToSave = mapper.map(body, HsOfficeMembershipEntity.class);
final var entityToSave = mapper.map(body, HsOfficeMembershipEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = membershipRepo.save(entityToSave);
@ -164,5 +169,12 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
if (entity.getValidity().hasUpperBound()) {
resource.setValidTo(entity.getValidity().upper().minusDays(1));
}
resource.getPartner().setPartnerNumber(entity.getPartner().getTaggedPartnerNumber()); // TODO.refa: use partner mapper?
};
final BiConsumer<HsOfficeMembershipInsertResource, HsOfficeMembershipEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.setPartner(partnerRepo.findByUuid(resource.getPartnerUuid())
.orElseThrow(() -> new EntityNotFoundException(
"ERROR: [400] partnerUuid %s not found".formatted(resource.getPartnerUuid()))));
};
}

View File

@ -8,11 +8,12 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.rbac.role.WithRoleId;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.Type;
@ -63,7 +64,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@NoArgsConstructor
@AllArgsConstructor
@DisplayAs("Membership")
public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEntity>, Stringifyable {
public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEntity>, Stringifyable, WithRoleId {
public static final String MEMBER_NUMBER_TAG = "M-";
public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
@ -84,7 +85,7 @@ public class HsOfficeMembershipEntity implements BaseEntity<HsOfficeMembershipEn
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "partneruuid")
private HsOfficePartnerEntity partner;
private HsOfficePartnerRealEntity partner;
@Column(name = "membernumbersuffix", length = 2)
@Pattern(regexp = TWO_DECIMAL_DIGITS)

View File

@ -2,18 +2,18 @@ package net.hostsharing.hsadminng.hs.office.membership;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import java.util.Optional;
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
private final StandardMapper mapper;
private final StrictMapper mapper;
private final HsOfficeMembershipEntity entity;
public HsOfficeMembershipEntityPatcher(
final StandardMapper mapper,
final StrictMapper mapper,
final HsOfficeMembershipEntity entity) {
this.mapper = mapper;
this.entity = entity;

View File

@ -0,0 +1,103 @@
package net.hostsharing.hsadminng.hs.office.partner;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.Column;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Version;
import java.util.UUID;
import static jakarta.persistence.CascadeType.DETACH;
import static jakarta.persistence.CascadeType.MERGE;
import static jakarta.persistence.CascadeType.PERSIST;
import static jakarta.persistence.CascadeType.REFRESH;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@MappedSuperclass
@Getter
@Setter
@SuperBuilder(toBuilder = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@DisplayAs("Partner")
public class HsOfficePartner<T extends HsOfficePartner<?>> implements Stringifyable, BaseEntity<T> {
public static final String PARTNER_NUMBER_TAG = "P-";
protected static Stringify<HsOfficePartner> stringify = stringify(HsOfficePartner.class, "partner")
.withIdProp(HsOfficePartner::toShortString)
.withProp(p -> ofNullable(p.getPartnerRel())
.map(HsOfficeRelation::getHolder)
.map(HsOfficePerson::toShortString)
.orElse(null))
.withProp(p -> ofNullable(p.getPartnerRel())
.map(HsOfficeRelation::getContact)
.map(HsOfficeContact::toShortString)
.orElse(null))
.quotedValues(false);
@Id
@GeneratedValue
private UUID uuid;
@Version
private int version;
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
private Integer partnerNumber;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "partnerreluuid", nullable = false)
private HsOfficeRelationRealEntity partnerRel;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "detailsuuid")
@NotFound(action = NotFoundAction.IGNORE)
private HsOfficePartnerDetailsEntity details;
@Override
public T load() {
BaseEntity.super.load();
partnerRel.load();
if (details != null) {
details.load();
}
//noinspection unchecked
return (T) this;
}
public String getTaggedPartnerNumber() {
return PARTNER_NUMBER_TAG + partnerNumber;
}
@Override
public String toString() {
return stringify.apply(this);
}
@Override
public String toShortString() {
return getTaggedPartnerNumber();
}
}

View File

@ -13,7 +13,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@ -26,6 +26,7 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.EX_PARTNER;
import static net.hostsharing.hsadminng.repr.TaggedNumber.cropTag;
@ -38,10 +39,10 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficePartnerRepository partnerRepo;
private HsOfficePartnerRbacRepository partnerRepo;
@Autowired
private HsOfficeRelationRealRepository relationRepo;
@ -60,7 +61,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final var entities = partnerRepo.findPartnerByOptionalNameLike(name);
final var resources = mapper.mapList(entities, HsOfficePartnerResource.class);
final var resources = mapper.mapList(entities, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
}
@ -83,7 +84,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
.path("/api/hs/office/partners/{id}")
.buildAndExpand(saved.getUuid())
.toUri();
final var mapped = mapper.map(saved, HsOfficePartnerResource.class);
final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.created(uri).body(mapped);
}
@ -101,7 +102,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(mapper.map(result.get(), HsOfficePartnerResource.class));
final var mapped = mapper.map(result.get(), HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}
@Override
@ -118,7 +120,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(mapper.map(result.get(), HsOfficePartnerResource.class));
final var mapped = mapper.map(result.get(), HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}
@Override
@ -161,20 +164,20 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final var saved = partnerRepo.save(current);
optionallyCreateExPartnerRelation(saved, previousPartnerRel);
final var mapped = mapper.map(saved, HsOfficePartnerResource.class);
final var mapped = mapper.map(saved, HsOfficePartnerResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}
private void optionallyCreateExPartnerRelation(final HsOfficePartnerEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) {
private void optionallyCreateExPartnerRelation(final HsOfficePartnerRbacEntity saved, final HsOfficeRelationRealEntity previousPartnerRel) {
if (!saved.getPartnerRel().getUuid().equals(previousPartnerRel.getUuid())) {
// TODO.impl: we also need to use the new partner-person as the anchor
relationRepo.save(previousPartnerRel.toBuilder().uuid(null).type(EX_PARTNER).build());
}
}
private HsOfficePartnerEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
final var entityToSave = new HsOfficePartnerEntity();
entityToSave.setPartnerNumber(cropTag(HsOfficePartnerEntity.PARTNER_NUMBER_TAG, body.getPartnerNumber()));
private HsOfficePartnerRbacEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
final var entityToSave = new HsOfficePartnerRbacEntity();
entityToSave.setPartnerNumber(cropTag(HsOfficePartnerRbacEntity.PARTNER_NUMBER_TAG, body.getPartnerNumber()));
entityToSave.setPartnerRel(persistPartnerRel(body.getPartnerRel()));
entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class));
return entityToSave;
@ -197,4 +200,8 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
throw new ReferenceNotFoundException(entityClass, uuid, exc);
}
}
final BiConsumer<HsOfficePartnerRbacEntity, HsOfficePartnerResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setPartnerNumber(entity.getTaggedPartnerNumber());
};
}

View File

@ -1,128 +0,0 @@
package net.hostsharing.hsadminng.hs.office.partner;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.persistence.BaseEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import net.hostsharing.hsadminng.repr.Stringify;
import net.hostsharing.hsadminng.repr.Stringifyable;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID;
import static jakarta.persistence.CascadeType.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
@Entity
@Table(schema = "hs_office", name = "partner_rv")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DisplayAs("Partner")
public class HsOfficePartnerEntity implements Stringifyable, BaseEntity<HsOfficePartnerEntity> {
public static final String PARTNER_NUMBER_TAG = "P-";
private static Stringify<HsOfficePartnerEntity> stringify = stringify(HsOfficePartnerEntity.class, "partner")
.withIdProp(HsOfficePartnerEntity::toShortString)
.withProp(p -> ofNullable(p.getPartnerRel())
.map(HsOfficeRelation::getHolder)
.map(HsOfficePerson::toShortString)
.orElse(null))
.withProp(p -> ofNullable(p.getPartnerRel())
.map(HsOfficeRelation::getContact)
.map(HsOfficeContact::toShortString)
.orElse(null))
.quotedValues(false);
@Id
@GeneratedValue
private UUID uuid;
@Version
private int version;
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
private Integer partnerNumber;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "partnerreluuid", nullable = false)
private HsOfficeRelationRealEntity partnerRel;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "detailsuuid")
@NotFound(action = NotFoundAction.IGNORE)
private HsOfficePartnerDetailsEntity details;
@Override
public HsOfficePartnerEntity load() {
BaseEntity.super.load();
partnerRel.load();
details.load();
return this;
}
public String getTaggedPartnerNumber() {
return PARTNER_NUMBER_TAG + partnerNumber;
}
@Override
public String toString() {
return stringify.apply(this);
}
@Override
public String toShortString() {
return getTaggedPartnerNumber();
}
public static RbacSpec rbac() {
return rbacViewFor("partner", HsOfficePartnerEntity.class)
.withIdentityView(SQL.projection("'P-' || partnerNumber"))
.withUpdatableColumns("partnerRelUuid")
.toRole(GLOBAL, ADMIN).grantPermission(INSERT)
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationRbacEntity.class,
usingDefaultCase(),
directlyFetchedByDependsOnColumn(),
dependsOnColumn("partnerRelUuid"))
.createPermission(DELETE).grantedTo("partnerRel", OWNER)
.createPermission(UPDATE).grantedTo("partnerRel", ADMIN)
.createPermission(SELECT).grantedTo("partnerRel", TENANT)
.importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class,
directlyFetchedByDependsOnColumn(),
dependsOnColumn("detailsUuid"))
.createPermission("partnerDetails", DELETE).grantedTo("partnerRel", OWNER)
.createPermission("partnerDetails", UPDATE).grantedTo("partnerRel", AGENT)
.createPermission("partnerDetails", SELECT).grantedTo("partnerRel", AGENT); // not TENANT!
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("5-hs-office/504-partner/5043-hs-office-partner-rbac");
}
}

View File

@ -9,10 +9,10 @@ import jakarta.persistence.EntityManager;
class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatchResource> {
private final EntityManager em;
private final HsOfficePartnerEntity entity;
private final HsOfficePartnerRbacEntity entity;
HsOfficePartnerEntityPatcher(
final EntityManager em,
final HsOfficePartnerEntity entity) {
final HsOfficePartnerRbacEntity entity) {
this.em = em;
this.entity = entity;
}

View File

@ -0,0 +1,59 @@
package net.hostsharing.hsadminng.hs.office.partner;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec;
import net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL;
import jakarta.persistence.*;
import java.io.IOException;
import static jakarta.persistence.CascadeType.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.GLOBAL;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.Role.*;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
@Entity
@Table(schema = "hs_office", name = "partner_rv")
@Getter
@Setter
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@DisplayAs("RbacPartner")
public class HsOfficePartnerRbacEntity extends HsOfficePartner<HsOfficePartnerRbacEntity> {
public static RbacSpec rbac() {
return rbacViewFor("partner", HsOfficePartnerRbacEntity.class)
.withIdentityView(SQL.projection("'P-' || partnerNumber"))
.withUpdatableColumns("partnerRelUuid")
.toRole(GLOBAL, ADMIN).grantPermission(INSERT)
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationRbacEntity.class,
usingDefaultCase(),
directlyFetchedByDependsOnColumn(),
dependsOnColumn("partnerRelUuid"))
.createPermission(DELETE).grantedTo("partnerRel", OWNER)
.createPermission(UPDATE).grantedTo("partnerRel", ADMIN)
.createPermission(SELECT).grantedTo("partnerRel", TENANT)
.importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class,
directlyFetchedByDependsOnColumn(),
dependsOnColumn("detailsUuid"))
.createPermission("partnerDetails", DELETE).grantedTo("partnerRel", OWNER)
.createPermission("partnerDetails", UPDATE).grantedTo("partnerRel", AGENT)
.createPermission("partnerDetails", SELECT).grantedTo("partnerRel", AGENT); // not TENANT!
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("5-hs-office/504-partner/5043-hs-office-partner-rbac");
}
}

View File

@ -0,0 +1,47 @@
package net.hostsharing.hsadminng.hs.office.partner;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsOfficePartnerRbacRepository extends Repository<HsOfficePartnerRbacEntity, UUID> {
@Timed("app.office.partners.repo.findByUuid.rbac")
Optional<HsOfficePartnerRbacEntity> findByUuid(UUID id);
@Timed("app.office.partners.repo.findAll.rbac")
List<HsOfficePartnerRbacEntity> findAll(); // TODO.refa: move to a repo in test sources
@Query(value = """
select partner.uuid, partner.detailsuuid, partner.partnernumber, partner.partnerreluuid, partner.version
from hs_office.partner_rv partner
join hs_office.relation partnerRel on partnerRel.uuid = partner.partnerreluuid
join hs_office.contact contact on contact.uuid = partnerRel.contactuuid
join hs_office.person partnerPerson on partnerPerson.uuid = partnerRel.holderuuid
left join hs_office.partner_details_rv partnerDetails on partnerDetails.uuid = partner.detailsuuid
where :name is null
or (partnerDetails.uuid is not null and partnerDetails.birthname like (cast(:name as text) || '%') escape '')
or contact.caption like (cast(:name as text) || '%') escape ''
or partnerPerson.tradename like (cast(:name as text) || '%') escape ''
or partnerPerson.givenname like (cast(:name as text) || '%') escape ''
or partnerPerson.familyname like (cast(:name as text) || '%') escape ''
""", nativeQuery = true)
@Timed("app.office.partners.repo.findPartnerByOptionalNameLike.rbac")
List<HsOfficePartnerRbacEntity> findPartnerByOptionalNameLike(String name);
@Timed("app.office.partners.repo.findPartnerByPartnerNumber.rbac")
Optional<HsOfficePartnerRbacEntity> findPartnerByPartnerNumber(Integer partnerNumber);
@Timed("app.office.partners.repo.save.rbac")
HsOfficePartnerRbacEntity save(final HsOfficePartnerRbacEntity entity);
@Timed("app.office.partners.repo.count.rbac")
long count();
@Timed("app.office.partners.repo.deleteByUuid.rbac")
int deleteByUuid(UUID uuid);
}

View File

@ -0,0 +1,21 @@
package net.hostsharing.hsadminng.hs.office.partner;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(schema = "hs_office", name = "partner")
@Getter
@Setter
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@DisplayAs("RealPartner")
public class HsOfficePartnerRealEntity extends HsOfficePartner<HsOfficePartnerRealEntity> {
}

View File

@ -0,0 +1,41 @@
package net.hostsharing.hsadminng.hs.office.partner;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsOfficePartnerRealRepository extends Repository<HsOfficePartnerRealEntity, UUID> {
@Timed("app.office.partners.repo.findByUuid.real")
Optional<HsOfficePartnerRealEntity> findByUuid(UUID id);
@Timed("app.office.partners.repo.findAll.real")
List<HsOfficePartnerRbacEntity> findAll(); // TODO.refa: move to a repo in test sources
@Query(value = """
select partner.uuid, partner.detailsuuid, partner.partnernumber, partner.partnerreluuid, partner.version
from hs_office.partner partner
join hs_office.relation partnerRel on partnerRel.uuid = partner.partnerreluuid
join hs_office.contact contact on contact.uuid = partnerRel.contactuuid
join hs_office.person partnerPerson on partnerPerson.uuid = partnerRel.holderuuid
left join hs_office.partner_details_rv partnerDetails on partnerDetails.uuid = partner.detailsuuid
where :name is null
or (partnerDetails.uuid is not null and partnerDetails.birthname like (cast(:name as text) || '%') escape '')
or contact.caption like (cast(:name as text) || '%') escape ''
or partnerPerson.tradename like (cast(:name as text) || '%') escape ''
or partnerPerson.givenname like (cast(:name as text) || '%') escape ''
or partnerPerson.familyname like (cast(:name as text) || '%') escape ''
""", nativeQuery = true)
@Timed("app.office.partners.repo.findPartnerByOptionalNameLike.real")
List<HsOfficePartnerRealEntity> findPartnerByOptionalNameLike(String name);
@Timed("app.office.partners.repo.findPartnerByPartnerNumber.real")
Optional<HsOfficePartnerRealEntity> findPartnerByPartnerNumber(Integer partnerNumber);
@Timed("app.office.partners.repo.save.real")
HsOfficePartnerRealEntity save(final HsOfficePartnerRealEntity entity);
}

View File

@ -1,45 +0,0 @@
package net.hostsharing.hsadminng.hs.office.partner;
import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEntity, UUID> {
@Timed("app.office.partners.repo.findByUuid")
Optional<HsOfficePartnerEntity> findByUuid(UUID id);
@Timed("app.office.partners.repo.findAll")
List<HsOfficePartnerEntity> findAll(); // TODO.refa: move to a repo in test sources
@Query("""
SELECT partner FROM HsOfficePartnerEntity partner
JOIN HsOfficeRelationRealEntity rel ON rel.uuid = partner.partnerRel.uuid
JOIN HsOfficeContactRealEntity contact ON contact.uuid = rel.contact.uuid
JOIN HsOfficePersonRealEntity person ON person.uuid = rel.holder.uuid
WHERE :name is null
OR partner.details.birthName like concat(cast(:name as text), '%')
OR contact.caption like concat(cast(:name as text), '%')
OR person.tradeName like concat(cast(:name as text), '%')
OR person.givenName like concat(cast(:name as text), '%')
OR person.familyName like concat(cast(:name as text), '%')
""")
@Timed("app.office.partners.repo.findPartnerByOptionalNameLike")
List<HsOfficePartnerEntity> findPartnerByOptionalNameLike(String name);
@Timed("app.office.partners.repo.findPartnerByPartnerNumber")
Optional<HsOfficePartnerEntity> findPartnerByPartnerNumber(Integer partnerNumber);
@Timed("app.office.partners.repo.save")
HsOfficePartnerEntity save(final HsOfficePartnerEntity entity);
@Timed("app.office.partners.repo.count")
long count();
@Timed("app.office.partners.repo.deleteByUuid")
int deleteByUuid(UUID uuid);
}

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.person;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
@ -24,7 +24,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficePersonRbacRepository personRepo;

View File

@ -3,7 +3,6 @@ package net.hostsharing.hsadminng.hs.office.person;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import net.hostsharing.hsadminng.errors.DisplayAs;
@ -17,7 +16,6 @@ import jakarta.persistence.Table;
@Setter
@NoArgsConstructor
@SuperBuilder(toBuilder = true)
@FieldNameConstants
@DisplayAs("RealPerson")
public class HsOfficePersonRealEntity extends HsOfficePerson<HsOfficePersonRealEntity> {
}

View File

@ -4,6 +4,7 @@ public enum HsOfficePersonType {
UNKNOWN_PERSON_TYPE("??"),
NATURAL_PERSON("NP"), // a human being
LEGAL_PERSON("LP"), // incorporated legal entity like A/S, GmbH, e.K., eG, e.V.
ORGANIZATIONAL_UNIT("OU"), // groups of persons within an organization, e.g. "Admin-Team", "Buchhaltung"
INCORPORATED_FIRM("IF"), // registered business partnership like OHG, Partnerschaftsgesellschaft
UNINCORPORATED_FIRM("UF"), // unregistered partnership, association etc. like GbR, ARGE, community of heirs
PUBLIC_INSTITUTION("PI"); // entities under public law like government entities, KdöR, AöR

View File

@ -9,7 +9,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelation
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
@ -32,7 +32,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeRelationRbacRepository rbacRelationRepo;
@ -185,6 +185,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
@SuppressWarnings("unchecked")
final BiConsumer<HsOfficeContactInsertResource, HsOfficeContactRealEntity> CONTACT_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putPostalAddress(from(resource.getPostalAddress()));
entity.putEmailAddresses(from(resource.getEmailAddresses()));
entity.putPhoneNumbers(from(resource.getPhoneNumbers()));
};

View File

@ -2,11 +2,14 @@ package net.hostsharing.hsadminng.hs.office.sepamandate;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMandatesApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateResource;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
@ -15,6 +18,7 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.validation.ValidationException;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
@ -29,7 +33,13 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsOfficeDebitorRepository debitorRepo;
@Autowired
private HsOfficeBankAccountRepository bankAccountRepo;
@Autowired
private HsOfficeSepaMandateRepository sepaMandateRepo;
@ -137,10 +147,22 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
if (entity.getValidity().hasUpperBound()) {
resource.setValidTo(entity.getValidity().upper().minusDays(1));
}
resource.setDebitor(mapper.map(entity.getDebitor(), HsOfficeDebitorResource.class));
resource.getDebitor().setDebitorNumber(entity.getDebitor().getTaggedDebitorNumber());
resource.getDebitor().getPartner().setPartnerNumber(entity.getDebitor().getPartner().getTaggedPartnerNumber());
};
final BiConsumer<HsOfficeSepaMandateInsertResource, HsOfficeSepaMandateEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo()));
entity.setDebitor(debitorRepo.findByUuid(resource.getDebitorUuid()).orElseThrow( () ->
new ValidationException(
"debitor.uuid='" + resource.getDebitorUuid() + "' not found or not accessible"
)
));
entity.setBankAccount(bankAccountRepo.findByUuid(resource.getBankAccountUuid()).orElseThrow( () ->
new ValidationException(
"bankAccount.uuid='" + resource.getBankAccountUuid() + "' not found or not accessible"
)
));
};
}

View File

@ -0,0 +1,17 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.experimental.UtilityClass;
import jakarta.validation.ValidationException;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
@UtilityClass
public class UuidResolver {
public static <T> T resolve(final String jsonPath, final UUID uuid, final Function<UUID, Optional<T>> findByUuid) {
return findByUuid.apply(uuid)
.orElseThrow(() -> new ValidationException("Unable to find " + jsonPath + ": " + uuid));
}
}

View File

@ -1,17 +0,0 @@
package net.hostsharing.hsadminng.mapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* A nicer API for ModelMapper in standard mode.
*/
@Component
public class StandardMapper extends Mapper {
public StandardMapper(@Autowired final EntityManagerWrapper em) {
super(em);
getConfiguration().setAmbiguityIgnored(true);
}
}

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.persistence;
import org.hibernate.Hibernate;
import jakarta.persistence.EntityManager;
import java.util.UUID;
public interface BaseEntity<T extends BaseEntity<?>> {
@ -15,4 +16,10 @@ public interface BaseEntity<T extends BaseEntity<?>> {
//noinspection unchecked
return (T) this;
};
default T reload(final EntityManager em) {
em.flush();
em.refresh(this);
return load();
}
}

View File

@ -1,38 +0,0 @@
package net.hostsharing.hsadminng.persistence;
import net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import jakarta.persistence.Entity;
import jakarta.validation.ValidationException;
@Service
public class EntityExistsValidator {
@Autowired
private EntityManagerWrapper em;
public <T extends BaseEntity<T>> void validateEntityExists(final String property, final T entitySkeleton) {
final var foundEntity = em.find(entityClass(entitySkeleton), entitySkeleton.getUuid());
if ( foundEntity == null) {
throw new ValidationException("Unable to find " + DisplayName.of(entitySkeleton) + " by " + property + ": " + entitySkeleton.getUuid());
}
}
private static <T extends BaseEntity<T>> Class<?> entityClass(final T entityOrProxy) {
final var entityClass = entityClass(entityOrProxy.getClass());
if (entityClass == null) {
throw new IllegalArgumentException("@Entity not found in superclass hierarchy of " + entityOrProxy.getClass());
}
return entityClass;
}
private static Class<?> entityClass(final Class<?> entityOrProxyClass) {
return entityOrProxyClass.isAnnotationPresent(Entity.class)
? entityOrProxyClass
: entityOrProxyClass.getSuperclass() == null
? null
: entityClass(entityOrProxyClass.getSuperclass());
}
}

View File

@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.grant;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacGrantResource;
import org.springframework.beans.factory.annotation.Autowired;
@ -23,7 +23,7 @@ public class RbacGrantController implements RbacGrantsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private RbacGrantRepository rbacGrantRepository;

View File

@ -30,7 +30,7 @@ public class RbacGrantsDiagramService {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
writer.write("""
### all grants to %s
```mermaid
%s
```
@ -49,8 +49,18 @@ public class RbacGrantsDiagramService {
NON_TEST_ENTITIES;
public static final EnumSet<Include> ALL = EnumSet.allOf(Include.class);
public static final EnumSet<Include> ALL_TEST_ENTITY_RELATED = EnumSet.of(USERS, DETAILS, NOT_ASSUMED, TEST_ENTITIES, PERMISSIONS);
public static final EnumSet<Include> ALL_NON_TEST_ENTITY_RELATED = EnumSet.of(USERS, DETAILS, NOT_ASSUMED, NON_TEST_ENTITIES, PERMISSIONS);
public static final EnumSet<Include> ALL_TEST_ENTITY_RELATED = EnumSet.of(
USERS,
DETAILS,
NOT_ASSUMED,
TEST_ENTITIES,
PERMISSIONS);
public static final EnumSet<Include> ALL_NON_TEST_ENTITY_RELATED = EnumSet.of(
USERS,
DETAILS,
NOT_ASSUMED,
NON_TEST_ENTITIES,
PERMISSIONS);
}
@Autowired
@ -66,9 +76,9 @@ public class RbacGrantsDiagramService {
public String allGrantsTocurrentSubject(final EnumSet<Include> includes) {
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
for ( UUID subjectUuid: context.fetchCurrentSubjectOrAssumedRolesUuids() ) {
for (UUID subjectUuid : context.fetchCurrentSubjectOrAssumedRolesUuids()) {
traverseGrantsTo(graph, subjectUuid, includes);
}
}
return toMermaidFlowchart(graph, includes);
}
@ -78,7 +88,7 @@ public class RbacGrantsDiagramService {
if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm:")) {
return;
}
if ( !g.getDescendantIdName().startsWith("role:rbac.global")) {
if (!g.getDescendantIdName().startsWith("role:rbac.global")) {
if (!includes.contains(TEST_ENTITIES) && g.getDescendantIdName().contains(":rbactest.")) {
return;
}
@ -94,12 +104,17 @@ public class RbacGrantsDiagramService {
}
public String allGrantsFrom(final UUID targetObject, final String op, final EnumSet<Include> includes) {
final var refUuid = (UUID) em.createNativeQuery("SELECT uuid FROM rbac.permission WHERE objectuuid=:targetObject AND op=:op")
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
@SuppressWarnings("unchecked") // List -> List<List<UUID>>
final var refUuidLists = (List<List<UUID>>) em.createNativeQuery(
"select uuid from rbac.permission where objectUuid=:targetObject and op=:op",
List.class)
.setParameter("targetObject", targetObject)
.setParameter("op", op)
.getSingleResult();
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
traverseGrantsFrom(graph, refUuid, includes);
.getResultList();
refUuidLists.stream().flatMap(Collection::stream)
.forEach(refUuid -> traverseGrantsFrom(graph, refUuid, includes));
return toMermaidFlowchart(graph, includes);
}
@ -125,20 +140,20 @@ public class RbacGrantsDiagramService {
final var entities =
includes.contains(DETAILS)
? graph.stream()
.flatMap(g -> Stream.of(
new Node(g.getAscendantIdName(), g.getAscendingUuid()),
new Node(g.getDescendantIdName(), g.getDescendantUuid()))
)
.collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName))
.entrySet().stream()
.map(entity -> "subgraph " + cleanId(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n "
+ entity.getValue().stream()
.map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n "))
.sorted()
.distinct()
.collect(joining("\n\n ")))
.collect(joining("\n\nend\n\n"))
+ "\n\nend\n\n"
.flatMap(g -> Stream.of(
new Node(g.getAscendantIdName(), g.getAscendingUuid()),
new Node(g.getDescendantIdName(), g.getDescendantUuid()))
)
.collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName))
.entrySet().stream()
.map(entity -> "subgraph " + cleanId(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n "
+ entity.getValue().stream()
.map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n "))
.sorted()
.distinct()
.collect(joining("\n\n ")))
.collect(joining("\n\nend\n\n"))
+ "\n\nend\n\n"
: "";
final var grants = graph.stream()
@ -193,7 +208,7 @@ public class RbacGrantsDiagramService {
final var refType = refType(idName);
if (refType.equals("user")) {
final var displayName = idName.substring(refType.length()+1);
final var displayName = idName.substring(refType.length() + 1);
return "(" + displayName + "\nref:" + uuid + ")";
}
if (refType.equals("role")) {
@ -215,15 +230,20 @@ public class RbacGrantsDiagramService {
@NotNull
private static String cleanId(final String idName) {
return idName.replaceAll("@.*", "")
.replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", "").replace(">", ":").replace("|", "_");
.replace("[", "")
.replace("]", "")
.replace("(", "")
.replace(")", "")
.replace(",", "")
.replace(">", ":")
.replace("|", "_");
}
static class LimitedHashSet<T> extends HashSet<T> {
@Override
public boolean add(final T t) {
if (size() < GRANT_LIMIT ) {
if (size() < GRANT_LIMIT) {
return super.add(t);
} else {
return false;

View File

@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.role;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacRolesApi;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacRoleResource;
import org.springframework.beans.factory.annotation.Autowired;
@ -19,7 +19,7 @@ public class RbacRoleController implements RbacRolesApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private RbacRoleRepository rbacRoleRepository;

View File

@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.subject;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacSubjectsApi;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectPermissionResource;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectResource;
@ -22,7 +22,7 @@ public class RbacSubjectController implements RbacSubjectsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private RbacSubjectRepository rbacSubjectRepository;

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.rbac.test.cust;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi;
import net.hostsharing.hsadminng.test.generated.api.v1.model.TestCustomerResource;
import org.springframework.beans.factory.annotation.Autowired;
@ -21,7 +21,7 @@ public class TestCustomerController implements TestCustomersApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private TestCustomerRepository testCustomerRepository;

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.rbac.test.pac;
import net.hostsharing.hsadminng.mapper.StandardMapper;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestPackagesApi;
@ -21,7 +21,7 @@ public class TestPackageController implements TestPackagesApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private TestPackageRepository testPackageRepository;

View File

@ -90,6 +90,7 @@ components:
type: boolean
vatReverseCharge:
type: boolean
# TODO.feat: alternatively the complete refundBankAccount
refundBankAccount.uuid:
type: string
format: uuid

View File

@ -9,6 +9,7 @@ components:
- UNKNOWN_PERSON
- NATURAL_PERSON
- LEGAL_PERSON
- ORGANIZATIONAL_UNIT
- INCORPORATED_FIRM
- UNINCORPORATED_FIRM
- PUBLIC_INSTITUTION

View File

@ -8,17 +8,33 @@ management:
endpoints:
web:
exposure:
# HOWTO: view _clickable_ Spring Actuator (Micrometer) Metrics endpoints: http://localhost:8081/actuator/metric-links
include: info, health, metrics, metric-links
# HOWTO: view _clickable_ Spring Actuator (Micrometer) Metrics endpoints:
# http://localhost:8081/actuator/metric-links
# HOWTO: view all configured endpoints of the running application:
# http://localhost:8081/actuator/mappings
# HOWTO: view the effective application configuration properties:
# http://localhost:8081/actuator/configprops
include: info, health, metrics, metric-links, mappings, openapi, swaggerui, configprops, env
endpoint:
env:
# TODO.spec: check this, maybe set to when_authorized?
show-values: always
configprops:
# TODO.spec: check this, maybe set to when_authorized?
show-values: always
observations:
annotations:
enabled: true
spring:
datasource:
driver-class-name: org.postgresql.Driver
password: password
url: jdbc:postgresql://localhost:5432/postgres
url: ${HSADMINNG_POSTGRES_JDBC_URL}
username: postgres
sql:
@ -30,8 +46,12 @@ spring:
hibernate:
dialect: net.hostsharing.hsadminng.config.PostgresCustomDialect
liquibase:
contexts: dev
liquibase:
contexts: ${spring.profiles.active}
# keep this in sync with test/.../application.yml
springdoc:
use-management-port: true
hsadminng:
postgres:
@ -46,4 +66,3 @@ metrics:
http:
server:
requests: true

View File

@ -3,7 +3,7 @@
-- ============================================================================
-- NUMERIC-HASH-FUNCTIONS
--changeset michael.hoennig:hash endDelimiter:--//
--changeset michael.hoennig:hash runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ----------------------------------------------------------------------------
do $$

View File

@ -870,18 +870,23 @@ $$;
-- ============================================================================
--changeset michael.hoennig:rbac-base-PGSQL-ROLES context:dev,tc endDelimiter:--//
--changeset michael.hoennig:rbac-base-PGSQL-ROLES runOnChange:true validCheckSum:ANY context:!external-db endDelimiter:--//
-- ----------------------------------------------------------------------------
do $$
begin
if '${HSADMINNG_POSTGRES_ADMIN_USERNAME}'='admin' then
create role admin;
if not exists (select from pg_catalog.pg_roles where rolname = 'admin') then
create role admin;
end if;
grant all privileges on all tables in schema public to admin;
end if;
if '${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}'='restricted' then
create role restricted;
if not exists (select from pg_catalog.pg_roles where rolname = 'restricted') then
create role restricted;
end if;
grant all privileges on all tables in schema public to restricted;
end if;
end $$;

View File

@ -170,7 +170,7 @@ commit;
-- ============================================================================
--changeset michael.hoennig:rbac-global-ADMIN-USERS context:dev,tc endDelimiter:--//
--changeset michael.hoennig:rbac-global-ADMIN-USERS context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Create two users and assign both to the administrators' role.
@ -192,7 +192,7 @@ $$;
-- ============================================================================
--changeset michael.hoennig:rbac-global-TEST context:dev,tc runAlways:true endDelimiter:--//
--changeset michael.hoennig:rbac-global-TEST context:!without-test-data runAlways:true endDelimiter:--//
-- ----------------------------------------------------------------------------
/*

View File

@ -67,7 +67,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:test-customer-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:test-customer-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -59,7 +59,7 @@ $$;
-- ============================================================================
--changeset michael.hoennig:test-package-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:test-package-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -52,7 +52,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-domain-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-domain-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -4,13 +4,13 @@
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================
--changeset michael.hoennig:hs-office-contact-MIGRATION-mapping endDelimiter:--//
--changeset michael.hoennig:hs-office-contact-MIGRATION-legacy-mapping endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE TABLE hs_office.contact_legacy_id
(
uuid uuid NOT NULL REFERENCES hs_office.contact(uuid),
contact_id integer NOT NULL
uuid uuid PRIMARY KEY NOT NULL REFERENCES hs_office.contact(uuid),
contact_id integer UNIQUE NOT NULL
);
--//

View File

@ -55,7 +55,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-office-contact-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-office-contact-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -8,6 +8,7 @@ CREATE TYPE hs_office.PersonType AS ENUM (
'??', -- unknown
'NP', -- natural person
'LP', -- legal person
'OU', -- organizational unit
'IF', -- incorporated firm
'UF', -- unincorporated firm
'PI'); -- public institution

View File

@ -34,7 +34,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-office-person-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-office-person-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -80,7 +80,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-office-relation-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-office-relation-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -4,13 +4,13 @@
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================
--changeset michael.hoennig:hs-office-partner-MIGRATION-mapping endDelimiter:--//
--changeset michael.hoennig:hs-office-partner-MIGRATION-legacy-mapping endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE TABLE hs_office.partner_legacy_id
(
uuid uuid NOT NULL REFERENCES hs_office.partner(uuid),
bp_id integer NOT NULL
uuid uuid PRIMARY KEY NOT NULL REFERENCES hs_office.partner(uuid),
bp_id integer UNIQUE NOT NULL
);
--//

View File

@ -66,7 +66,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-office-partner-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-office-partner-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -26,7 +26,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-office-bankaccount-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-office-bankaccount-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -45,7 +45,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-office-debitor-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-office-debitor-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -4,13 +4,13 @@
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================
--changeset michael.hoennig:hs-office-sepamandate-MIGRATION-mapping endDelimiter:--//
--changeset michael.hoennig:hs-office-sepamandate-MIGRATION-legacy-mapping endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE TABLE hs_office.sepamandate_legacy_id
(
uuid uuid NOT NULL REFERENCES hs_office.sepamandate(uuid),
sepa_mandate_id integer NOT NULL
uuid uuid PRIMARY KEY NOT NULL REFERENCES hs_office.sepamandate(uuid),
sepa_mandate_id integer UNIQUE NOT NULL
);
--//

View File

@ -38,7 +38,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-office-sepaMandate-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-office-sepaMandate-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -28,7 +28,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-office-membership-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-office-membership-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -4,13 +4,13 @@
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================
--changeset michael.hoennig:hs-office-coopshares-MIGRATION-mapping endDelimiter:--//
--changeset michael.hoennig:hs-office-coopshares-MIGRATION-legacy-mapping endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE TABLE hs_office.coopsharetx_legacy_id
(
uuid uuid NOT NULL REFERENCES hs_office.coopsharetx(uuid),
member_share_id integer NOT NULL
uuid uuid PRIMARY KEY NOT NULL REFERENCES hs_office.coopsharetx(uuid),
member_share_id integer UNIQUE NOT NULL
);
--//

View File

@ -38,12 +38,15 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-office-coopSharesTransaction-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-office-coopSharesTransaction-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$
begin
call base.defineContext('creating coopSharesTransaction test-data');
call base.defineContext('creating coopSharesTransaction test-data',
null,
'superuser-alex@hostsharing.net',
'rbac.global#global:ADMIN');
SET CONSTRAINTS ALL DEFERRED;
call hs_office.coopsharetx_create_test_data(10001, '01');

View File

@ -4,13 +4,13 @@
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================
--changeset michael.hoennig:hs-office-coopassets-MIGRATION-mapping endDelimiter:--//
--changeset michael.hoennig:hs-office-coopassets-MIGRATION-legacy-mapping endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE TABLE hs_office.coopassettx_legacy_id
(
uuid uuid NOT NULL REFERENCES hs_office.coopassettx(uuid),
member_asset_id integer NOT NULL
uuid uuid PRIMARY KEY NOT NULL REFERENCES hs_office.coopassettx(uuid),
member_asset_id integer UNIQUE NOT NULL
);
--//

View File

@ -44,12 +44,15 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-office-coopAssetsTransaction-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-office-coopAssetsTransaction-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$
begin
call base.defineContext('creating coopAssetsTransaction test-data');
call base.defineContext('creating coopAssetsTransaction test-data',
null,
'superuser-alex@hostsharing.net',
'rbac.global#global:ADMIN');
SET CONSTRAINTS ALL DEFERRED;
call hs_office.coopassettx_create_test_data(10001, '01');

View File

@ -34,7 +34,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-booking-project-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-booking-project-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -40,7 +40,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-booking-item-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-booking-item-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -4,13 +4,13 @@
-- Once we don't need the external remote views anymore, create revert changesets.
-- ============================================================================
--changeset hs-hosting-asset-MIGRATION-mapping:1 endDelimiter:--//
--changeset hs-hosting-asset-MIGRATION-legacy-mapping:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE TABLE hs_hosting.asset_legacy_id
(
uuid uuid NOT NULL REFERENCES hs_hosting.asset(uuid),
legacy_id integer NOT NULL
uuid uuid PRIMARY KEY NOT NULL REFERENCES hs_hosting.asset(uuid),
legacy_id integer NOT NULL -- not unique because we sometimes create multiple asset types for the same legacy asset
);
--//

View File

@ -105,7 +105,7 @@ end; $$;
-- ============================================================================
--changeset michael.hoennig:hs-hosting-asset-TEST-DATA-GENERATION context=dev,tc endDelimiter:--//
--changeset michael.hoennig:hs-hosting-asset-TEST-DATA-GENERATION context:!without-test-data endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$

View File

@ -0,0 +1,14 @@
--liquibase formatted sql
-- ============================================================================
--changeset timotheus.pokorra:hs-global-integration-znuny endDelimiter:--//
CREATE OR REPLACE VIEW hs_integration.subscription AS
SELECT DISTINCT
relation.mark as subscription,
contact.emailaddresses->>'main' as email
FROM hs_office.contact AS contact
JOIN hs_office.relation AS relation ON relation.contactuuid = contact.uuid AND relation.type = 'SUBSCRIBER'
ORDER BY subscription, email;
--//

View File

@ -29,6 +29,7 @@ databaseChangeLog:
file: db/changelog/0-base/030-historization.sql
- include:
file: db/changelog/0-base/090-log-slow-queries-extensions.sql
- include:
file: db/changelog/1-rbac/1000-rbac-schema.sql
- include:
@ -49,26 +50,38 @@ databaseChangeLog:
file: db/changelog/1-rbac/1059-rbac-statistics.sql
- include:
file: db/changelog/1-rbac/1080-rbac-global.sql
- include:
file: db/changelog/2-rbactest/200-rbactest-schema.sql
context: "!without-test-data"
- include:
file: db/changelog/2-rbactest/201-rbactest-customer/2010-rbactest-customer.sql
context: "!without-test-data"
- include:
file: db/changelog/2-rbactest/201-rbactest-customer/2013-rbactest-customer-rbac.sql
context: "!without-test-data"
- include:
file: db/changelog/2-rbactest/201-rbactest-customer/2018-rbactest-customer-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/2-rbactest/202-rbactest-package/2020-rbactest-package.sql
context: "!without-test-data"
- include:
file: db/changelog/2-rbactest/202-rbactest-package/2023-rbactest-package-rbac.sql
context: "!without-test-data"
- include:
file: db/changelog/2-rbactest/202-rbactest-package/2028-rbactest-package-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/2-rbactest/203-rbactest-domain/2030-rbactest-domain.sql
context: "!without-test-data"
- include:
file: db/changelog/2-rbactest/203-rbactest-domain/2033-rbactest-domain-rbac.sql
context: "!without-test-data"
- include:
file: db/changelog/2-rbactest/203-rbactest-domain/2038-rbactest-domain-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/5-hs-office/500-hs-office-schema.sql
- include:
@ -79,18 +92,21 @@ databaseChangeLog:
file: db/changelog/5-hs-office/501-contact/5016-hs-office-contact-migration.sql
- include:
file: db/changelog/5-hs-office/501-contact/5018-hs-office-contact-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/5-hs-office/502-person/5020-hs-office-person.sql
- include:
file: db/changelog/5-hs-office/502-person/5023-hs-office-person-rbac.sql
- include:
file: db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/5-hs-office/503-relation/5030-hs-office-relation.sql
- include:
file: db/changelog/5-hs-office/503-relation/5033-hs-office-relation-rbac.sql
- include:
file: db/changelog/5-hs-office/503-relation/5038-hs-office-relation-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/5-hs-office/504-partner/5040-hs-office-partner.sql
- include:
@ -101,18 +117,21 @@ databaseChangeLog:
file: db/changelog/5-hs-office/504-partner/5046-hs-office-partner-migration.sql
- include:
file: db/changelog/5-hs-office/504-partner/5048-hs-office-partner-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/5-hs-office/505-bankaccount/5050-hs-office-bankaccount.sql
- include:
file: db/changelog/5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac.sql
- include:
file: db/changelog/5-hs-office/505-bankaccount/5058-hs-office-bankaccount-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/5-hs-office/506-debitor/5060-hs-office-debitor.sql
- include:
file: db/changelog/5-hs-office/506-debitor/5063-hs-office-debitor-rbac.sql
- include:
file: db/changelog/5-hs-office/506-debitor/5068-hs-office-debitor-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/5-hs-office/507-sepamandate/5070-hs-office-sepamandate.sql
- include:
@ -121,12 +140,14 @@ databaseChangeLog:
file: db/changelog/5-hs-office/507-sepamandate/5076-hs-office-sepamandate-migration.sql
- include:
file: db/changelog/5-hs-office/507-sepamandate/5078-hs-office-sepamandate-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/5-hs-office/510-membership/5100-hs-office-membership.sql
- include:
file: db/changelog/5-hs-office/510-membership/5103-hs-office-membership-rbac.sql
- include:
file: db/changelog/5-hs-office/510-membership/5108-hs-office-membership-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/5-hs-office/511-coopshares/5110-hs-office-coopshares.sql
- include:
@ -135,6 +156,7 @@ databaseChangeLog:
file: db/changelog/5-hs-office/511-coopshares/5116-hs-office-coopshares-migration.sql
- include:
file: db/changelog/5-hs-office/511-coopshares/5118-hs-office-coopshares-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/5-hs-office/512-coopassets/5120-hs-office-coopassets.sql
- include:
@ -143,37 +165,58 @@ databaseChangeLog:
file: db/changelog/5-hs-office/512-coopassets/5126-hs-office-coopassets-migration.sql
- include:
file: db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql
context: "!without-test-data"
- include:
file: db/changelog/6-hs-booking/600-hs-booking-schema.sql
context: "!only-office"
- include:
file: db/changelog/6-hs-booking/610-booking-debitor/6100-hs-booking-debitor.sql
context: "!only-office"
- include:
file: db/changelog/6-hs-booking/620-booking-project/6200-hs-booking-project.sql
context: "!only-office"
- include:
file: db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql
context: "!only-office"
- include:
file: db/changelog/6-hs-booking/620-booking-project/6208-hs-booking-project-test-data.sql
context: "!only-office and !without-test-data"
- include:
file: db/changelog/6-hs-booking/630-booking-item/6300-hs-booking-item.sql
context: "!only-office"
- include:
file: db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.sql
context: "!only-office"
- include:
file: db/changelog/6-hs-booking/630-booking-item/6308-hs-booking-item-test-data.sql
context: "!only-office and !without-test-data"
- include:
file: db/changelog/7-hs-hosting/700-hs-hosting-schema.sql
context: "!only-office"
- include:
file: db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql
context: "!only-office"
- include:
file: db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql
context: "!only-office"
- include:
file: db/changelog/7-hs-hosting/701-hosting-asset/7016-hs-hosting-asset-migration.sql
context: "!only-office"
- include:
file: db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql
context: "!only-office and !without-test-data"
- include:
file: db/changelog/9-hs-global/9000-statistics.sql
context: "!only-office"
- include:
file: db/changelog/9-hs-global/9100-hs-integration-schema.sql
- include:
file: db/changelog/9-hs-global/9110-integration-kimai.sql
- include:
file: db/changelog/9-hs-global/9120-integration-znuny.sql
file: db/changelog/9-hs-global/9120-integration-znuny.sql
- include:
file: db/changelog/9-hs-global/9130-integration-mlmmj.sql

Some files were not shown because too many files have changed in this diff Show More