introduce-partner-business-role (#16)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #16
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-02-01 14:48:15 +01:00
parent 188cb9733e
commit 2c0101b46d
72 changed files with 1666 additions and 930 deletions

View File

@ -46,7 +46,7 @@ postgresAutodoc () {
alias postgres-autodoc=postgresAutodoc
function importOfficeData() {
export HSADMINNG_POSTGRES_JDBC=jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers
export HSADMINNG_POSTGRES_JDBC_URL=jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers
export HSADMINNG_POSTGRES_ADMIN_USERNAME=admin
export HSADMINNG_POSTGRES_ADMIN_PASSWORD=password
export HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted

View File

@ -53,7 +53,7 @@ To be able to build and run the Java Spring Boot application, you need the follo
- Docker 20.x (on MacOS you also need *Docker Desktop* or similar) or Podman
- optionally: PostgreSQL Server 15.5-bookworm
(see instructions below to install and run in Docker)
- The matching Java JDK at will be automatically installed by Gradle toolchain support.
- 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*.
If you have at least Docker and the Java JDK installed in appropriate versions and in your `PATH`, then you can start like this:

View File

@ -308,6 +308,8 @@ tasks.register('importOfficeData', Test) {
group 'verification'
description 'run the import jobs as tests'
mustRunAfter spotlessJava
}

View File

@ -0,0 +1,21 @@
package net.hostsharing.hsadminng.errors;
import net.hostsharing.hsadminng.persistence.HasUuid;
import java.util.UUID;
public class ReferenceNotFoundException extends RuntimeException {
private final Class<?> entityClass;
private final UUID uuid;
public <E extends HasUuid> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
super(exc);
this.entityClass = entityClass;
this.uuid = uuid;
}
@Override
public String getMessage() {
return "Cannot resolve " + entityClass.getSimpleName() +" with uuid " + uuid;
}
}

View File

@ -45,7 +45,7 @@ public class RestResponseEntityExceptionHandler
protected ResponseEntity<CustomErrorResponse> handleJpaExceptions(
final RuntimeException exc, final WebRequest request) {
final var message = line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0);
return errorResponse(request, httpStatus(message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message);
return errorResponse(request, httpStatus(exc, message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message);
}
@ExceptionHandler(NoSuchElementException.class)
@ -55,6 +55,12 @@ public class RestResponseEntityExceptionHandler
return errorResponse(request, HttpStatus.NOT_FOUND, message);
}
@ExceptionHandler(ReferenceNotFoundException.class)
protected ResponseEntity<CustomErrorResponse> handleReferenceNotFoundException(
final ReferenceNotFoundException exc, final WebRequest request) {
return errorResponse(request, HttpStatus.BAD_REQUEST, exc.getMessage());
}
@ExceptionHandler({ JpaObjectRetrievalFailureException.class, EntityNotFoundException.class })
protected ResponseEntity<CustomErrorResponse> handleJpaObjectRetrievalFailureException(
final RuntimeException exc, final WebRequest request) {
@ -74,8 +80,9 @@ public class RestResponseEntityExceptionHandler
@ExceptionHandler(Throwable.class)
protected ResponseEntity<CustomErrorResponse> handleOtherExceptions(
final Throwable exc, final WebRequest request) {
final var message = firstMessageLine(NestedExceptionUtils.getMostSpecificCause(exc));
return errorResponse(request, httpStatus(message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message);
final var causingException = NestedExceptionUtils.getMostSpecificCause(exc);
final var message = firstMessageLine(causingException);
return errorResponse(request, httpStatus(causingException, message).orElse(HttpStatus.INTERNAL_SERVER_ERROR), message);
}
@Override
@ -138,7 +145,10 @@ public class RestResponseEntityExceptionHandler
}
}
private Optional<HttpStatus> httpStatus(final String message) {
private Optional<HttpStatus> httpStatus(final Throwable causingException, final String message) {
if ( EntityNotFoundException.class.isInstance(causingException) ) {
return Optional.of(HttpStatus.BAD_REQUEST);
}
if (message.startsWith("ERROR: [")) {
for (HttpStatus status : HttpStatus.values()) {
if (message.startsWith("ERROR: [" + status.value() + "]")) {

View File

@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;

View File

@ -13,13 +13,15 @@ public interface HsOfficeBankAccountRepository extends Repository<HsOfficeBankAc
@Query("""
SELECT c FROM HsOfficeBankAccountEntity c
WHERE :holder is null
OR lower(c.holder) like lower(concat(:holder, '%'))
WHERE lower(c.holder) like lower(concat(:holder, '%'))
ORDER BY c.holder
""")
List<HsOfficeBankAccountEntity> findByOptionalHolderLike(String holder);
List<HsOfficeBankAccountEntity> findByOptionalHolderLikeImpl(String holder);
default List<HsOfficeBankAccountEntity> findByOptionalHolderLike(String holder) {
return findByOptionalHolderLikeImpl(holder == null ? "" : holder);
}
List<HsOfficeBankAccountEntity> findByIbanOrderByIban(String iban);
List<HsOfficeBankAccountEntity> findByIbanOrderByIbanAsc(String iban);
<S extends HsOfficeBankAccountEntity> S save(S entity);

View File

@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.contact;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator;
@ -36,7 +36,7 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
private String label;
@Column(name = "postaladdress")
private String postalAddress;
private String postalAddress; // TODO: check if we really want multiple, if so: JSON-Array or Postgres-Array?
@Column(name = "emailaddresses", columnDefinition = "json")
private String emailAddresses; // TODO: check if we can really add multiple. format: ["eins@...", "zwei@..."]

View File

@ -4,7 +4,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.GenericGenerator;

View File

@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -25,7 +25,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUuid {
private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class)
.withProp(HsOfficeCoopSharesTransactionEntity::getMemberNumber)
.withProp(HsOfficeCoopSharesTransactionEntity::getMemberNumberTagged)
.withProp(HsOfficeCoopSharesTransactionEntity::getValueDate)
.withProp(HsOfficeCoopSharesTransactionEntity::getTransactionType)
.withProp(HsOfficeCoopSharesTransactionEntity::getShareCount)
@ -76,12 +76,12 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUu
return stringify.apply(this);
}
public Integer getMemberNumber() {
return ofNullable(membership).map(HsOfficeMembershipEntity::getMemberNumber).orElse(null);
private String getMemberNumberTagged() {
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse(null);
}
@Override
public String toShortString() {
return "M-%s%+d".formatted(getMemberNumber(), shareCount);
return "%s%+d".formatted(getMemberNumberTagged(), shareCount);
}
}

View File

@ -4,7 +4,7 @@ import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;

View File

@ -5,7 +5,7 @@ import com.vladmihalcea.hibernate.type.range.Range;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;

View File

@ -1,12 +1,21 @@
package net.hostsharing.hsadminng.hs.office.partner;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.errors.ReferenceNotFoundException;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePartnersApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerRoleInsertResource;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipRepository;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
@ -30,6 +39,9 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
@Autowired
private HsOfficePartnerRepository partnerRepo;
@Autowired
private HsOfficeRelationshipRepository relationshipRepo;
@PersistenceContext
private EntityManager em;
@ -56,7 +68,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
context.define(currentUser, assumedRoles);
final var entityToSave = mapper.map(body, HsOfficePartnerEntity.class);
final var entityToSave = createPartnerEntity(body);
final var saved = partnerRepo.save(entityToSave);
@ -93,11 +105,17 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final UUID partnerUuid) {
context.define(currentUser, assumedRoles);
final var result = partnerRepo.deleteByUuid(partnerUuid);
if (result == 0) {
final var partnerToDelete = partnerRepo.findByUuid(partnerUuid);
if (partnerToDelete.isEmpty()) {
return ResponseEntity.notFound().build();
}
if (partnerRepo.deleteByUuid(partnerUuid) != 1 ||
// TODO: move to after delete trigger in partner
relationshipRepo.deleteByUuid(partnerToDelete.get().getPartnerRole().getUuid()) != 1 ) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
return ResponseEntity.noContent().build();
}
@ -119,4 +137,32 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
final var mapped = mapper.map(saved, HsOfficePartnerResource.class);
return ResponseEntity.ok(mapped);
}
private HsOfficePartnerEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
final var entityToSave = new HsOfficePartnerEntity();
entityToSave.setPartnerNumber(body.getPartnerNumber());
entityToSave.setPartnerRole(persistPartnerRole(body.getPartnerRole()));
entityToSave.setContact(ref(HsOfficeContactEntity.class, body.getContactUuid()));
entityToSave.setPerson(ref(HsOfficePersonEntity.class, body.getPersonUuid()));
entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class));
return entityToSave;
}
private HsOfficeRelationshipEntity persistPartnerRole(final HsOfficePartnerRoleInsertResource resource) {
final var entity = new HsOfficeRelationshipEntity();
entity.setRelType(HsOfficeRelationshipType.PARTNER);
entity.setRelAnchor(ref(HsOfficePersonEntity.class, resource.getRelAnchorUuid()));
entity.setRelHolder(ref(HsOfficePersonEntity.class, resource.getRelHolderUuid()));
entity.setContact(ref(HsOfficeContactEntity.class, resource.getContactUuid()));
em.persist(entity);
return entity;
}
private <E extends HasUuid> E ref(final Class<E> entityClass, final UUID uuid) {
try {
return em.getReference(entityClass, uuid);
} catch (final Throwable exc) {
throw new ReferenceNotFoundException(entityClass, uuid, exc);
}
}
}

View File

@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.office.partner;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;

View File

@ -3,8 +3,9 @@ package net.hostsharing.hsadminng.hs.office.partner;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.NotFound;
@ -39,10 +40,16 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
private Integer partnerNumber;
@ManyToOne
@JoinColumn(name = "partnerroleuuid", nullable = false)
private HsOfficeRelationshipEntity partnerRole;
// TODO: remove, is replaced by partnerRole
@ManyToOne
@JoinColumn(name = "personuuid", nullable = false)
private HsOfficePersonEntity person;
// TODO: remove, is replaced by partnerRole
@ManyToOne
@JoinColumn(name = "contactuuid", nullable = false)
private HsOfficeContactEntity contact;

View File

@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.person;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.apache.commons.lang3.StringUtils;

View File

@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.relationship;
import lombok.*;
import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;

View File

@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.office.relationship;
public enum HsOfficeRelationshipType {
UNKNOWN,
PARTNER,
EX_PARTNER,
REPRESENTATIVE,
VIP_CONTACT,

View File

@ -6,7 +6,7 @@ import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.migration.HasUuid;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;

View File

@ -1,4 +1,4 @@
package net.hostsharing.hsadminng.hs.office.migration;
package net.hostsharing.hsadminng.persistence;
import java.util.UUID;

View File

@ -15,6 +15,8 @@ public interface RbacGrantRepository extends Repository<RbacGrantEntity, RbacGra
""")
RbacGrantEntity findById(RbacGrantId rbacGrantId);
long count();
List<RbacGrantEntity> findAll();
RbacGrantEntity save(final RbacGrantEntity grant);

View File

@ -8,9 +8,12 @@ import java.util.UUID;
public interface RbacRoleRepository extends Repository<RbacRoleEntity, UUID> {
/**
* Returns all instances of the type.
*
* @return all entities
* @return the number of persistent RbacRoleEntity instances, mostly for testing purposes.
*/
long count(); // TODO: move to test sources
/**
* @return all persistent RbacRoleEntity instances, assigned to the current subject (user or assumed roles)
*/
List<RbacRoleEntity> findAll();

View File

@ -96,6 +96,8 @@ components:
format: int8
minimum: 10000
maximum: 99999
partnerRole:
$ref: '#/components/schemas/HsOfficePartnerRoleInsert'
personUuid:
type: string
format: uuid
@ -110,6 +112,24 @@ components:
- contactUuid
- details
HsOfficePartnerRoleInsert:
type: object
nullable: false
properties:
relAnchorUuid:
type: string
format: uuid
relHolderUuid:
type: string
format: uuid
contactUuid:
type: string
format: uuid
required:
- relAnchorUuid
- relHolderUuid
- relContactUuid
HsOfficePartnerDetailsInsert:
type: object
nullable: false

View File

@ -7,6 +7,7 @@ components:
type: string
enum:
- UNKNOWN
- PARTNER
- EX_PARTNER
- REPRESENTATIVE,
- VIP_CONTACT
@ -61,3 +62,4 @@ components:
- relAnchorUuid
- relHolderUuid
- relType
- relContactUuid

View File

@ -53,6 +53,19 @@ create table tx_journal
create index on tx_journal (targetTable, targetUuid);
--//
-- ============================================================================
--changeset audit-TX-JOURNAL-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
A view combining tx_journal with tx_context.
*/
create view tx_journal_v as
select txc.*, txj.targettable, txj.targetop, txj.targetuuid, txj.targetdelta
from tx_journal txj
left join tx_context txc using (contextid)
order by txc.txtimestamp;
--//
-- ============================================================================
--changeset audit-TX-JOURNAL-TRIGGER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------

View File

@ -120,6 +120,7 @@ $$;
create table RbacObject
(
uuid uuid primary key default uuid_generate_v4(),
serialId serial, -- TODO: we might want to remove this once test data deletion works properly
objectTable varchar(64) not null,
unique (objectTable, uuid)
);

View File

@ -61,7 +61,7 @@ do language plpgsql $$
call createHsOfficeContactTestData('first contact');
call createHsOfficeContactTestData('second contact');
call createHsOfficeContactTestData('third contact');
call createHsOfficeContactTestData('forth contact');
call createHsOfficeContactTestData('fourth contact');
call createHsOfficeContactTestData('fifth contact');
call createHsOfficeContactTestData('sixth contact');
call createHsOfficeContactTestData('seventh contact');

View File

@ -46,7 +46,7 @@ create or replace procedure createTestPersonTestData(
begin
for t in startCount..endCount
loop
call createHsOfficePersonTestData('LEGAL', intToVarChar(t, 4));
call createHsOfficePersonTestData('LP', intToVarChar(t, 4));
commit;
end loop;
end; $$;
@ -59,11 +59,15 @@ end; $$;
do language plpgsql $$
begin
call createHsOfficePersonTestData('LP', 'Hostsharing eG');
call createHsOfficePersonTestData('LP', 'First GmbH');
call createHsOfficePersonTestData('NP', null, 'Firby', 'Susan');
call createHsOfficePersonTestData('NP', null, 'Smith', 'Peter');
call createHsOfficePersonTestData('LP', 'Second e.K.', 'Sandra', 'Miller');
call createHsOfficePersonTestData('NP', null, 'Tucker', 'Jack');
call createHsOfficePersonTestData('NP', null, 'Fouler', 'Ellie');
call createHsOfficePersonTestData('LP', 'Second e.K.', 'Smith', 'Peter');
call createHsOfficePersonTestData('IF', 'Third OHG');
call createHsOfficePersonTestData('IF', 'Fourth e.G.');
call createHsOfficePersonTestData('IF', 'Fourth eG');
call createHsOfficePersonTestData('UF', 'Erben Bessler', 'Mel', 'Bessler');
call createHsOfficePersonTestData('NP', null, 'Bessler', 'Anita');
call createHsOfficePersonTestData('NP', null, 'Winkler', 'Paul');

View File

@ -6,6 +6,7 @@
CREATE TYPE HsOfficeRelationshipType AS ENUM (
'UNKNOWN',
'PARTNER',
'EX_PARTNER',
'REPRESENTATIVE',
'VIP_CONTACT',

View File

@ -9,9 +9,9 @@
Creates a single relationship test record.
*/
create or replace procedure createHsOfficeRelationshipTestData(
anchorPersonTradeName varchar,
holderPersonFamilyName varchar,
holderPersonName varchar,
relationshipType HsOfficeRelationshipType,
anchorPersonTradeName varchar,
contactLabel varchar,
mark varchar default null)
language plpgsql as $$
@ -23,14 +23,27 @@ declare
contact hs_office_contact;
begin
idName := cleanIdentifier( anchorPersonTradeName || '-' || holderPersonFamilyName);
idName := cleanIdentifier( anchorPersonTradeName || '-' || holderPersonName);
currentTask := 'creating relationship test-data ' || idName;
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
execute format('set local hsadminng.currentTask to %L', currentTask);
select p.* from hs_office_person p where p.tradeName = anchorPersonTradeName into anchorPerson;
select p.* from hs_office_person p where p.familyName = holderPersonFamilyName into holderPerson;
if anchorPerson is null then
raise exception 'anchorPerson "%" not found', anchorPersonTradeName;
end if;
select p.* from hs_office_person p
where p.tradeName = holderPersonName or p.familyName = holderPersonName
into holderPerson;
if holderPerson is null then
raise exception 'holderPerson "%" not found', holderPersonName;
end if;
select c.* from hs_office_contact c where c.label = contactLabel into contact;
if contact is null then
raise exception 'contact "%" not found', contactLabel;
end if;
raise notice 'creating test relationship: %', idName;
raise notice '- using anchor person (%): %', anchorPerson.uuid, anchorPerson;
@ -72,13 +85,20 @@ end; $$;
do language plpgsql $$
begin
call createHsOfficeRelationshipTestData('First GmbH', 'Smith', 'REPRESENTATIVE', 'first contact');
call createHsOfficeRelationshipTestData('First GmbH', 'PARTNER', 'Hostsharing eG', 'first contact');
call createHsOfficeRelationshipTestData('Firby', 'REPRESENTATIVE', 'First GmbH', 'first contact');
call createHsOfficeRelationshipTestData('Second e.K.', 'Smith', 'REPRESENTATIVE', 'second contact');
call createHsOfficeRelationshipTestData('Second e.K.', 'PARTNER', 'Hostsharing eG', 'second contact');
call createHsOfficeRelationshipTestData('Smith', 'REPRESENTATIVE', 'Second e.K.', 'second contact');
call createHsOfficeRelationshipTestData('Third OHG', 'Smith', 'REPRESENTATIVE', 'third contact');
call createHsOfficeRelationshipTestData('Third OHG', 'PARTNER', 'Hostsharing eG', 'third contact');
call createHsOfficeRelationshipTestData('Tucker', 'REPRESENTATIVE', 'Third OHG', 'third contact');
call createHsOfficeRelationshipTestData('Third OHG', 'Smith', 'SUBSCRIBER', 'third contact', 'members-announce');
call createHsOfficeRelationshipTestData('Fourth eG', 'PARTNER', 'Hostsharing eG', 'fourth contact');
call createHsOfficeRelationshipTestData('Fouler', 'REPRESENTATIVE', 'Third OHG', 'third contact');
call createHsOfficeRelationshipTestData('Smith', 'PARTNER', 'Hostsharing eG', 'sixth contact');
call createHsOfficeRelationshipTestData('Smith', 'SUBSCRIBER', 'Third OHG', 'third contact', 'members-announce');
end;
$$;
--//

View File

@ -32,14 +32,47 @@ call create_journal('hs_office_partner_details');
create table hs_office_partner
(
uuid uuid unique references RbacObject (uuid) initially deferred,
partnerNumber numeric(5),
personUuid uuid not null references hs_office_person(uuid),
contactUuid uuid not null references hs_office_contact(uuid),
detailsUuid uuid not null references hs_office_partner_details(uuid) on delete cascade
partnerNumber numeric(5) unique not null,
partnerRoleUuid uuid not null references hs_office_relationship(uuid), -- TODO: delete in after delete trigger
personUuid uuid not null references hs_office_person(uuid), -- TODO: remove, replaced by partnerRoleUuid
contactUuid uuid not null references hs_office_contact(uuid), -- TODO: remove, replaced by partnerRoleUuid
detailsUuid uuid not null references hs_office_partner_details(uuid) -- deleted in after delete trigger
);
--//
-- ============================================================================
--changeset hs-office-partner-DELETE-DETAILS-TRIGGER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Trigger function to delete related details of a partner to delete.
*/
create or replace function deleteHsOfficeDetailsOnPartnerDelete()
returns trigger
language PLPGSQL
as $$
declare
counter integer;
begin
DELETE FROM hs_office_partner_details d WHERE d.uuid = OLD.detailsUuid;
GET DIAGNOSTICS counter = ROW_COUNT;
if counter = 0 then
raise exception 'partner details % could not be deleted', OLD.detailsUuid;
end if;
RETURN OLD;
end; $$;
/**
Triggers deletion of related details of a partner to delete.
*/
create trigger hs_office_partner_delete_details_trigger
after delete
on hs_office_partner
for each row
execute procedure deleteHsOfficeDetailsOnPartnerDelete();
-- ============================================================================
--changeset hs-office-partner-MAIN-TABLE-JOURNAL:1 endDelimiter:--//
-- ----------------------------------------------------------------------------

View File

@ -27,12 +27,17 @@ create or replace function hsOfficePartnerRbacRolesTrigger()
language plpgsql
strict as $$
declare
oldPartnerRole hs_office_relationship;
newPartnerRole hs_office_relationship;
oldPerson hs_office_person;
newPerson hs_office_person;
oldContact hs_office_contact;
newContact hs_office_contact;
begin
select * from hs_office_relationship as r where r.uuid = NEW.partnerroleuuid into newPartnerRole;
select * from hs_office_person as p where p.uuid = NEW.personUuid into newPerson;
select * from hs_office_contact as c where c.uuid = NEW.contactUuid into newContact;
@ -52,6 +57,7 @@ begin
incomingSuperRoles => array[
hsOfficePartnerOwner(NEW)],
outgoingSubRoles => array[
hsOfficeRelationshipTenant(newPartnerRole),
hsOfficePersonTenant(newPerson),
hsOfficeContactTenant(newContact)]
);
@ -60,6 +66,7 @@ begin
hsOfficePartnerAgent(NEW),
incomingSuperRoles => array[
hsOfficePartnerAdmin(NEW),
hsOfficeRelationshipAdmin(newPartnerRole),
hsOfficePersonAdmin(newPerson),
hsOfficeContactAdmin(newContact)]
);
@ -69,6 +76,7 @@ begin
incomingSuperRoles => array[
hsOfficePartnerAgent(NEW)],
outgoingSubRoles => array[
hsOfficeRelationshipTenant(newPartnerRole),
hsOfficePersonGuest(newPerson),
hsOfficeContactGuest(newContact)]
);
@ -109,6 +117,19 @@ begin
elsif TG_OP = 'UPDATE' then
if OLD.partnerRoleUuid <> NEW.partnerRoleUuid then
select * from hs_office_relationship as r where r.uuid = OLD.partnerRoleUuid into oldPartnerRole;
call revokeRoleFromRole(hsOfficeRelationshipTenant(oldPartnerRole), hsOfficePartnerAdmin(OLD));
call grantRoleToRole(hsOfficeRelationshipTenant(newPartnerRole), hsOfficePartnerAdmin(NEW));
call revokeRoleFromRole(hsOfficePartnerAgent(OLD), hsOfficeRelationshipAdmin(oldPartnerRole));
call grantRoleToRole(hsOfficePartnerAgent(NEW), hsOfficeRelationshipAdmin(newPartnerRole));
call revokeRoleFromRole(hsOfficeRelationshipGuest(oldPartnerRole), hsOfficePartnerTenant(OLD));
call grantRoleToRole(hsOfficeRelationshipGuest(newPartnerRole), hsOfficePartnerTenant(NEW));
end if;
if OLD.personUuid <> NEW.personUuid then
select * from hs_office_person as p where p.uuid = OLD.personUuid into oldPerson;
@ -179,6 +200,7 @@ call generateRbacIdentityView('hs_office_partner', $idName$
call generateRbacRestrictedView('hs_office_partner',
'(select idName from hs_office_person_iv p where p.uuid = target.personUuid)',
$updates$
partnerRoleUuid = new.partnerRoleUuid,
personUuid = new.personUuid,
contactUuid = new.contactUuid
$updates$);
@ -189,7 +211,7 @@ call generateRbacRestrictedView('hs_office_partner',
--changeset hs-office-partner-rbac-NEW-PARTNER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a global permission for new-partner and assigns it to the hostsharing admins role.
Creates a global permission for new-partner and assigns it to the Hostsharing admins role.
*/
do language plpgsql $$
declare

View File

@ -9,30 +9,49 @@
Creates a single partner test record.
*/
create or replace procedure createHsOfficePartnerTestData(
mandantTradeName varchar,
partnerNumber numeric(5),
personTradeOrFamilyName varchar,
partnerPersonName varchar,
contactLabel varchar )
language plpgsql as $$
declare
currentTask varchar;
idName varchar;
mandantPerson hs_office_person;
partnerRole hs_office_relationship;
relatedPerson hs_office_person;
relatedContact hs_office_contact;
relatedDetailsUuid uuid;
begin
idName := cleanIdentifier( personTradeOrFamilyName|| '-' || contactLabel);
idName := cleanIdentifier( partnerPersonName|| '-' || contactLabel);
currentTask := 'creating partner test-data ' || idName;
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
execute format('set local hsadminng.currentTask to %L', currentTask);
select p.* from hs_office_person p
where p.tradeName = personTradeOrFamilyName or p.familyName = personTradeOrFamilyName
where p.tradeName = mandantTradeName
into mandantPerson;
if mandantPerson is null then
raise exception 'mandant "%" not found', mandantTradeName;
end if;
select p.* from hs_office_person p
where p.tradeName = partnerPersonName or p.familyName = partnerPersonName
into relatedPerson;
select c.* from hs_office_contact c
where c.label = contactLabel
into relatedContact;
select r.* from hs_office_relationship r
where r.reltype = 'PARTNER'
and r.relanchoruuid = mandantPerson.uuid and r.relholderuuid = relatedPerson.uuid
into partnerRole;
if partnerRole is null then
raise exception 'partnerRole "%"-"%" not found', mandantPerson.tradename, partnerPersonName;
end if;
raise notice 'creating test partner: %', idName;
raise notice '- using partnerRole (%): %', partnerRole.uuid, partnerRole;
raise notice '- using person (%): %', relatedPerson.uuid, relatedPerson;
raise notice '- using contact (%): %', relatedContact.uuid, relatedContact;
@ -44,13 +63,13 @@ begin
else
insert
into hs_office_partner_details (uuid, registrationOffice, registrationNumber)
values (uuid_generate_v4(), 'Hamburg', '12345')
values (uuid_generate_v4(), 'Hamburg', 'RegNo123456789')
returning uuid into relatedDetailsUuid;
end if;
insert
into hs_office_partner (uuid, partnerNumber, personuuid, contactuuid, detailsUuid)
values (uuid_generate_v4(), partnerNumber, relatedPerson.uuid, relatedContact.uuid, relatedDetailsUuid);
into hs_office_partner (uuid, partnerNumber, partnerRoleUuid, personuuid, contactuuid, detailsUuid)
values (uuid_generate_v4(), partnerNumber, partnerRole.uuid, relatedPerson.uuid, relatedContact.uuid, relatedDetailsUuid);
end; $$;
--//
@ -62,11 +81,11 @@ end; $$;
do language plpgsql $$
begin
call createHsOfficePartnerTestData(10001, 'First GmbH', 'first contact');
call createHsOfficePartnerTestData(10002, 'Second e.K.', 'second contact');
call createHsOfficePartnerTestData(10003, 'Third OHG', 'third contact');
call createHsOfficePartnerTestData(10004, 'Fourth e.G.', 'forth contact');
call createHsOfficePartnerTestData(10010, 'Smith', 'fifth contact');
call createHsOfficePartnerTestData('Hostsharing eG', 10001, 'First GmbH', 'first contact');
call createHsOfficePartnerTestData('Hostsharing eG', 10002, 'Second e.K.', 'second contact');
call createHsOfficePartnerTestData('Hostsharing eG', 10003, 'Third OHG', 'third contact');
call createHsOfficePartnerTestData('Hostsharing eG', 10004, 'Fourth eG', 'fourth contact');
call createHsOfficePartnerTestData('Hostsharing eG', 10010, 'Smith', 'fifth contact');
end;
$$;
--//

View File

@ -41,7 +41,7 @@ do language plpgsql $$
call createHsOfficeBankAccountTestData('Peter Smith', 'DE02500105170137075030', 'INGDDEFF');
call createHsOfficeBankAccountTestData('Second e.K.', 'DE02100500000054540402', 'BELADEBE');
call createHsOfficeBankAccountTestData('Third OHG', 'DE02300209000106531065', 'CMCIDEDD');
call createHsOfficeBankAccountTestData('Fourth e.G.', 'DE02200505501015871393', 'HASPDEHH');
call createHsOfficeBankAccountTestData('Fourth eG', 'DE02200505501015871393', 'HASPDEHH');
call createHsOfficeBankAccountTestData('Mel Bessler', 'DE02100100100006820101', 'PBNKDEFF');
call createHsOfficeBankAccountTestData('Anita Bessler', 'DE02300606010002474689', 'DAAEDEDD');
call createHsOfficeBankAccountTestData('Paul Winkler', 'DE02600501010002034304', 'SOLADEST600');

View File

@ -66,21 +66,21 @@ databaseChangeLog:
- include:
file: db/changelog/218-hs-office-person-test-data.sql
- include:
file: db/changelog/220-hs-office-partner.sql
file: db/changelog/220-hs-office-relationship.sql
- include:
file: db/changelog/223-hs-office-partner-rbac.sql
file: db/changelog/223-hs-office-relationship-rbac.sql
- include:
file: db/changelog/224-hs-office-partner-details-rbac.sql
file: db/changelog/228-hs-office-relationship-test-data.sql
- include:
file: db/changelog/226-hs-office-partner-migration.sql
file: db/changelog/230-hs-office-partner.sql
- include:
file: db/changelog/228-hs-office-partner-test-data.sql
file: db/changelog/233-hs-office-partner-rbac.sql
- include:
file: db/changelog/230-hs-office-relationship.sql
file: db/changelog/234-hs-office-partner-details-rbac.sql
- include:
file: db/changelog/233-hs-office-relationship-rbac.sql
file: db/changelog/236-hs-office-partner-migration.sql
- include:
file: db/changelog/238-hs-office-relationship-test-data.sql
file: db/changelog/238-hs-office-partner-test-data.sql
- include:
file: db/changelog/240-hs-office-bankaccount.sql
- include:

View File

@ -30,6 +30,7 @@ public class ArchitectureTest {
"..test.pac",
"..context",
"..generated..",
"..persistence..",
"..hs.office.bankaccount",
"..hs.office.contact",
"..hs.office.coopassets",
@ -164,6 +165,7 @@ public class ArchitectureTest {
.that().resideInAPackage("..hs.office.relationship..")
.should().onlyBeAccessed().byClassesThat()
.resideInAnyPackage("..hs.office.relationship..",
"..hs.office.partner..",
"..hs.office.migration..");
@ArchTest

View File

@ -7,7 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
public abstract class ContextBasedTest {
@Autowired
Context context;
protected Context context;
TestInfo test;

View File

@ -4,6 +4,7 @@ import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup;
import net.hostsharing.test.Accepts;
import net.hostsharing.test.JpaAttempt;
import org.apache.commons.lang3.RandomStringUtils;
@ -29,7 +30,7 @@ import static org.hamcrest.Matchers.startsWith;
classes = { HsadminNgApplication.class, JpaAttempt.class }
)
@Transactional
class HsOfficeBankAccountControllerAcceptanceTest {
class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCleanup {
@LocalServerPort
private Integer port;
@ -51,7 +52,7 @@ class HsOfficeBankAccountControllerAcceptanceTest {
class ListBankAccounts {
@Test
void globalAdmin_withoutAssumedRoles_canViewAllBankAaccounts_ifNoCriteriaGiven() throws JSONException {
void globalAdmin_withoutAssumedRoles_canViewAllBankAccounts_ifNoCriteriaGiven() throws JSONException {
RestAssured // @formatter:off
.given()
@ -75,7 +76,7 @@ class HsOfficeBankAccountControllerAcceptanceTest {
"bic": "BYLADEM1001"
},
{
"holder": "Fourth e.G.",
"holder": "Fourth eG",
"iban": "DE02200505501015871393",
"bic": "HASPDEHH"
},

View File

@ -1,14 +1,12 @@
package net.hostsharing.hsadminng.hs.office.bankaccount;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.context.ContextBasedTest;
import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup;
import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository;
import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository;
import net.hostsharing.test.Array;
import net.hostsharing.test.JpaAttempt;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@ -24,14 +22,14 @@ import java.util.List;
import java.util.function.Supplier;
import static net.hostsharing.hsadminng.hs.office.bankaccount.TestHsOfficeBankAccount.hsOfficeBankAccount;
import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.grantDisplaysOf;
import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.roleNamesOf;
import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf;
import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf;
import static net.hostsharing.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@Import({ Context.class, JpaAttempt.class })
class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
@Autowired
HsOfficeBankAccountRepository bankAccountRepo;
@ -61,8 +59,8 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
final var count = bankAccountRepo.count();
// when
final var result = attempt(em, () -> bankAccountRepo.save(
hsOfficeBankAccount("some temp acc A", "DE37500105177419788228", "")));
final var result = attempt(em, () -> toCleanup(bankAccountRepo.save(
hsOfficeBankAccount("some temp acc A", "DE37500105177419788228", ""))));
// then
result.assertSuccessful();
@ -78,8 +76,8 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
final var count = bankAccountRepo.count();
// when
final var result = attempt(em, () -> bankAccountRepo.save(
hsOfficeBankAccount("some temp acc B", "DE49500105174516484892", "INGDDEFFXXX")));
final var result = attempt(em, () -> toCleanup(bankAccountRepo.save(
hsOfficeBankAccount("some temp acc B", "DE49500105174516484892", "INGDDEFFXXX"))))