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>
This commit is contained in:
Michael Hoennig 2025-01-15 13:43:20 +01:00
parent a7ffee9348
commit 9c8d7616e3
111 changed files with 1095 additions and 758 deletions

View File

@ -93,7 +93,8 @@ alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResource
alias gw-test='. .aliases; ./gradlew test'
alias gw-check='. .aliases; gw test check -x pitest'
alias cas-curl='bin/cas-curl'
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='
@ -108,3 +109,4 @@ 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-open='open https://hsngdev.hs-example.de/scenarios/office'

View File

@ -4,5 +4,5 @@ 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 LIQUIBASE_COMMAND_CONTEXT_FILTER=
export LANG=en_US.UTF-8

View File

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

View File

@ -617,6 +617,11 @@ 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 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.7'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
id 'io.openapiprocessor.openapi-processor' version '2023.2'
id 'com.github.jk1.dependency-license-report' version '2.9'
id "org.owasp.dependencycheck" version "11.1.1"
id "com.diffplug.spotless" version "7.0.0"
id "org.owasp.dependencycheck" version "12.0.0"
id "com.diffplug.spotless" version "7.0.2"
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'
}
// 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
@ -61,23 +67,23 @@ dependencies {
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.10.0'
implementation 'org.springdoc:springdoc-openapi:2.6.0'
implementation 'org.postgresql:postgresql:42.7.4'
implementation 'org.liquibase:liquibase-core:4.30.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:2.18.2'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
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.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3'
implementation 'org.reflections:reflections:0.10.2'
compileOnly 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
// FIXME: developmentOnly 'org.springframework.boot:spring-boot-devtools'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
@ -89,7 +95,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'

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

@ -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

@ -7,7 +7,7 @@ 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.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
@ -26,7 +26,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsBookingProjectRbacRepository bookingProjectRepo;

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,7 +12,7 @@ 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.http.ResponseEntity;
@ -36,7 +36,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
private Context context;
@Autowired
private StandardMapper mapper;
private StrictMapper mapper;
@Autowired
private HsHostingAssetRbacRepository rbacAssetRepo;

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,7 +9,7 @@ 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;
@ -25,7 +25,7 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
private ObjectMapper jsonMapper;
@Autowired
private StandardMapper standardMapper;
private StrictMapper StrictMapper;
@Override
@SneakyThrows
@ -44,9 +44,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 +62,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;

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;