Michael Hoennig
2022-09-06 af90fefd4921db2cd6e6f924a59b2ebbeef7cf9c
hs.admin.partner from API via Controller to Entity
22 files added
5 files modified
1212 ■■■■■ changed files
build.gradle 25 ●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/Mapper.java 33 ●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/admin/contact/HsAdminContactEntity.java 31 ●●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/admin/contact/HsAdminContactRepository.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/admin/partner/HsAdminPartnerController.java 155 ●●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/admin/partner/HsAdminPartnerEntity.java 35 ●●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/admin/partner/HsAdminPartnerRepository.java 30 ●●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonEntity.java 42 ●●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepository.java 26 ●●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-admin/api-mappings.yaml 16 ●●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-admin/auth.yaml 1 ●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-admin/error-responses.yaml 1 ●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-admin/hs-admin-contact-schemas.yaml 28 ●●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-admin/hs-admin-partner-schemas.yaml 45 ●●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-admin/hs-admin-partners-with-uuid.yaml 81 ●●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-admin/hs-admin-partners.yaml 56 ●●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-admin/hs-admin-person-schemas.yaml 33 ●●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-admin/hs-admin.yaml 16 ●●●●● patch | view | raw | blame | history
src/main/resources/db/changelog/200-hs-base.sql 83 ●●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/arch/ArchTest.java 18 ●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/admin/contact/HsAdminContactRepositoryIntegrationTest.java 14 ●●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/admin/partner/HsAdminPartnerControllerAcceptanceTest.java 195 ●●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/admin/partner/HsAdminPartnerRepositoryIntegrationTest.java 125 ●●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/admin/partner/TestHsAdminPartner.java 27 ●●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerRestTest.java 4 ●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/test/IsValidUuidMatcher.java 11 ●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/test/JsonBuilder.java 56 ●●●●● patch | view | raw | blame | history
build.gradle
@@ -124,12 +124,24 @@
        showWarnings true
        openApiNullable true
    }
    springHs {
        processorName 'spring'
        processor 'io.openapiprocessor:openapi-processor-spring:2022.4'
        apiPath "$projectDir/src/main/resources/api-definition/hs-admin/hs-admin.yaml"
        mapping "$projectDir/src/main/resources/api-definition/hs-admin/api-mappings.yaml"
        targetDir "$projectDir/build/generated/sources/openapi"
        showWarnings true
        openApiNullable true
    }
}
sourceSets.main.java.srcDir 'build/generated/sources/openapi'
['processSpringRoot', 'processSpringRbac', 'processSpringTest'].each {
    project.tasks.processResources.dependsOn it
    project.tasks.compileJava.dependsOn it
abstract class ProcessSpring extends DefaultTask {}
tasks.register('processSpring', ProcessSpring)
['processSpringRoot', 'processSpringRbac', 'processSpringTest', 'processSpringHs'].each {
    project.tasks.processSpring.dependsOn it
}
project.tasks.processResources.dependsOn processSpring
project.tasks.compileJava.dependsOn processSpring
// Spotless Code Formatting
spotless {
@@ -243,6 +255,7 @@
    targetClasses = ['net.hostsharing.hsadminng.**']
    excludedClasses = [
            'net.hostsharing.hsadminng.config.**',
            'net.hostsharing.hsadminng.**.*Controller',
            'net.hostsharing.hsadminng.**.generated.**'
    ]
@@ -255,9 +268,9 @@
    threads = 4
    // As Java unit tests are pretty pointless in our case, this maybe makes not much sense.
    mutationThreshold = 31
    coverageThreshold = 44
    testStrengthThreshold = 76
    mutationThreshold = 71
    coverageThreshold = 57
    testStrengthThreshold = 99
    outputFormats = ['XML', 'HTML']
    timestampedReports = false
src/main/java/net/hostsharing/hsadminng/Mapper.java
@@ -1,25 +1,48 @@
package net.hostsharing.hsadminng;
import net.hostsharing.hsadminng.hs.admin.generated.api.v1.model.HsAdminPersonResource;
import net.hostsharing.hsadminng.hs.admin.person.HsAdminPersonEntity;
import org.modelmapper.Converter;
import org.modelmapper.ModelMapper;
import org.modelmapper.spi.MappingContext;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
/**
 * A nicer API for ModelMapper.
 */
public abstract class Mapper {
    private final static ModelMapper modelMapper = new ModelMapper();
    public final static ModelMapper modelMapper = new ModelMapper();
    public static <S, T> List<T> mapList(final List<S> source, final Class<T> targetClass) {
        return mapList(source, targetClass, null);
    }
    public static <S, T> List<T> mapList(final List<S> source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) {
        return source
            .stream()
            .map(element -> modelMapper.map(element, targetClass))
            .collect(Collectors.toList());
                .stream()
                .map(element -> {
                    final var target = map(element, targetClass);
                    if (postMapper != null) {
                        postMapper.accept(element, target);
                    }
                    return target;
                })
                .collect(Collectors.toList());
    }
    public static <S, T> T map(final S source, final Class<T> targetClass) {
        return modelMapper.map(source, targetClass);
        return map(source, targetClass, null);
    }
    public static <S, T> T map(final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) {
        final var target = modelMapper.map(source, targetClass);
        if (postMapper != null) {
            postMapper.accept(source, target);
        }
        return target;
    }
}
src/main/java/net/hostsharing/hsadminng/hs/admin/contact/HsAdminContactEntity.java
New file
@@ -0,0 +1,31 @@
package net.hostsharing.hsadminng.hs.admin.contact;
import com.vladmihalcea.hibernate.type.array.ListArrayType;
import lombok.*;
import org.hibernate.annotations.TypeDef;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import java.util.UUID;
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HsAdminContactEntity {
    private @Id UUID uuid;
    private String label;
    @Column(name = "postaladdress")
    private String postalAddress;
    @Column(name = "emailaddresses", columnDefinition = "json")
    private String emailAddresses;
    @Column(name = "phonenumbers", columnDefinition = "json")
    private String phoneNumbers;
}
src/main/java/net/hostsharing/hsadminng/hs/admin/contact/HsAdminContactRepository.java
New file
@@ -0,0 +1,25 @@
package net.hostsharing.hsadminng.hs.admin.contact;
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 HsAdminContactRepository extends Repository<HsAdminContactEntity, UUID> {
    Optional<HsAdminContactEntity> findByUuid(UUID id);
    @Query("""
            SELECT c FROM HsAdminContactEntity c
                WHERE :label is null
                    OR c.label like concat(:label, '%')
               """)
        // TODO: join tables missing
    List<HsAdminContactEntity> findContactByOptionalLabelLike(String label);
    HsAdminContactEntity save(final HsAdminContactEntity entity);
    long count();
}
src/main/java/net/hostsharing/hsadminng/hs/admin/partner/HsAdminPartnerController.java
New file
@@ -0,0 +1,155 @@
package net.hostsharing.hsadminng.hs.admin.partner;
import net.hostsharing.hsadminng.Mapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.admin.contact.HsAdminContactEntity;
import net.hostsharing.hsadminng.hs.admin.generated.api.v1.api.HsAdminPartnersApi;
import net.hostsharing.hsadminng.hs.admin.generated.api.v1.model.HsAdminContactResource;
import net.hostsharing.hsadminng.hs.admin.generated.api.v1.model.HsAdminPartnerResource;
import net.hostsharing.hsadminng.hs.admin.generated.api.v1.model.HsAdminPartnerUpdateResource;
import net.hostsharing.hsadminng.hs.admin.generated.api.v1.model.HsAdminPersonResource;
import net.hostsharing.hsadminng.hs.admin.person.HsAdminPersonEntity;
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 java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.Mapper.map;
@RestController
public class HsAdminPartnerController implements HsAdminPartnersApi {
    @Autowired
    private Context context;
    @Autowired
    private HsAdminPartnerRepository partnerRepo;
    @Override
    @Transactional(readOnly = true)
    public ResponseEntity<List<HsAdminPartnerResource>> listPartners(
            final String currentUser,
            final String assumedRoles,
            final String name) {
        // TODO: context.define(currentUser, assumedRoles);
        // TODO: final var entities = partnerRepo.findPartnerByOptionalNameLike(name);
        final var entities = List.of(
                HsAdminPartnerEntity.builder()
                        .uuid(UUID.randomUUID())
                        .person(HsAdminPersonEntity.builder()
                                .tradeName("Ixx AG")
                                .build())
                        .contact(HsAdminContactEntity.builder()
                                .label("Ixx AG")
                                .build())
                        .build(),
                HsAdminPartnerEntity.builder()
                        .uuid(UUID.randomUUID())
                        .person(HsAdminPersonEntity.builder()
                                .tradeName("Ypsilon GmbH")
                                .build())
                        .contact(HsAdminContactEntity.builder()
                                .label("Ypsilon GmbH")
                                .build())
                        .build(),
                HsAdminPartnerEntity.builder()
                        .uuid(UUID.randomUUID())
                        .person(HsAdminPersonEntity.builder()
                                .tradeName("Zett OHG")
                                .build())
                        .contact(HsAdminContactEntity.builder()
                                .label("Zett OHG")
                                .build())
                        .build()
        );
        final var resources = Mapper.mapList(entities, HsAdminPartnerResource.class,
                PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER);
        return ResponseEntity.ok(resources);
    }
    @Override
    @Transactional
    public ResponseEntity<HsAdminPartnerResource> addPartner(
            final String currentUser,
            final String assumedRoles,
            final HsAdminPartnerResource body) {
        // TODO: context.define(currentUser, assumedRoles);
        if (body.getUuid() == null) {
            body.setUuid(UUID.randomUUID());
        }
        // TODO: final var saved = partnerRepo.save(map(body, HsAdminPartnerEntity.class));
        final var saved = map(body, HsAdminPartnerEntity.class, PARTNER_RESOURCE_TO_ENTITY_POSTMAPPER);
        final var uri =
                MvcUriComponentsBuilder.fromController(getClass())
                        .path("/api/hs/admin/partners/{id}")
                        .buildAndExpand(body.getUuid())
                        .toUri();
        final var mapped = map(saved, HsAdminPartnerResource.class,
                PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER);
        return ResponseEntity.created(uri).body(mapped);
    }
    @Override
    public ResponseEntity<HsAdminPartnerResource> getPartnerByUuid(
            final String currentUser,
            final String assumedRoles,
            final UUID partnerUuid) {
        // TODO: context.define(currentUser, assumedRoles);
        // TODO: final var result = partnerRepo.findByUuid(partnerUuid);
        final var result =
                partnerUuid.equals(UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")) ? null :
                        HsAdminPartnerEntity.builder()
                                .uuid(UUID.randomUUID())
                                .person(HsAdminPersonEntity.builder()
                                        .tradeName("Ixx AG")
                                        .build())
                                .contact(HsAdminContactEntity.builder()
                                        .label("Ixx AG")
                                        .build())
                                .build();
        if (result == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(map(result, HsAdminPartnerResource.class, PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER));
    }
    @Override
    public ResponseEntity<Void> deletePartnerByUuid(final String currentUser, final String assumedRoles, final UUID userUuid) {
        return null;
    }
    @Override
    public ResponseEntity<HsAdminPartnerResource> updatePartner(
            final String currentUser,
            final String assumedRoles,
            final UUID partnerUuid,
            final HsAdminPartnerUpdateResource body) {
        return null;
    }
    private final BiConsumer<HsAdminPartnerResource, HsAdminPartnerEntity> PARTNER_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
        entity.setPerson(map(resource.getPerson(), HsAdminPersonEntity.class));
        entity.setContact(map(resource.getContact(), HsAdminContactEntity.class));
    };
    private final BiConsumer<HsAdminPartnerEntity, HsAdminPartnerResource> PARTNER_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
        resource.setPerson(map(entity.getPerson(), HsAdminPersonResource.class));
        resource.setContact(map(entity.getContact(), HsAdminContactResource.class));
    };
}
src/main/java/net/hostsharing/hsadminng/hs/admin/partner/HsAdminPartnerEntity.java
New file
@@ -0,0 +1,35 @@
package net.hostsharing.hsadminng.hs.admin.partner;
import lombok.*;
import net.hostsharing.hsadminng.hs.admin.contact.HsAdminContactEntity;
import net.hostsharing.hsadminng.hs.admin.person.HsAdminPersonEntity;
import javax.persistence.*;
import java.time.LocalDate;
import java.util.UUID;
@Entity
@Table(name = "hs_admin_partner_rv")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HsAdminPartnerEntity {
    private @Id UUID uuid;
    @ManyToOne
    @JoinColumn(name = "personuuid")
    private HsAdminPersonEntity person;
    @ManyToOne
    @JoinColumn(name = "contactuuid")
    private HsAdminContactEntity contact;
    private @Column(name = "registrationoffice") String registrationOffice;
    private @Column(name = "registrationnumber") String registrationNumber;
    private @Column(name = "birthname") String birthName;
    private @Column(name = "birthday") LocalDate birthday;
    private @Column(name = "dateofdeath") LocalDate dateOfDeath;
}
src/main/java/net/hostsharing/hsadminng/hs/admin/partner/HsAdminPartnerRepository.java
New file
@@ -0,0 +1,30 @@
package net.hostsharing.hsadminng.hs.admin.partner;
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 HsAdminPartnerRepository extends Repository<HsAdminPartnerEntity, UUID> {
    Optional<HsAdminPartnerEntity> findByUuid(UUID id);
    @Query("""
            SELECT partner FROM HsAdminPartnerEntity partner
                JOIN HsAdminContactEntity contact ON contact.uuid = partner.contact
                JOIN HsAdminPersonEntity person ON person.uuid = partner.person
                WHERE :name is null
                    OR partner.birthName like concat(:name, '%')
                    OR contact.label like concat(:name, '%')
                    OR person.tradeName like concat(:name, '%')
                    OR person.givenName like concat(:name, '%')
                    OR person.familyName like concat(:name, '%')
               """)
    List<HsAdminPartnerEntity> findPartnerByOptionalNameLike(String name);
    HsAdminPartnerEntity save(final HsAdminPartnerEntity entity);
    long count();
}
src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonEntity.java
New file
@@ -0,0 +1,42 @@
package net.hostsharing.hsadminng.hs.admin.person;
import com.vladmihalcea.hibernate.type.array.ListArrayType;
import lombok.*;
import org.hibernate.annotations.TypeDef;
import javax.persistence.*;
import java.util.UUID;
@Entity
@Table(name = "hs_admin_person_rv")
@TypeDef(
        name = "list-array",
        typeClass = ListArrayType.class
)
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HsAdminPersonEntity {
    private @Id UUID uuid;
    @Enumerated(EnumType.STRING)
    private PersonType type;
    private String tradeName;
    @Column(name = "givenname")
    private String givenName;
    @Column(name = "familyname")
    private String familyName;
    public enum PersonType {
        NATURAL,
        LEGAL,
        SOLE_REPRESENTATION,
        JOINT_REPRESENTATION
    }
}
src/main/java/net/hostsharing/hsadminng/hs/admin/person/HsAdminPersonRepository.java
New file
@@ -0,0 +1,26 @@
package net.hostsharing.hsadminng.hs.admin.person;
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 HsAdminPersonRepository extends Repository<HsAdminPersonEntity, UUID> {
    Optional<HsAdminPersonEntity> findByUuid(UUID id);
    @Query("""
            SELECT p FROM HsAdminPersonEntity p
                WHERE :name is null
                    OR p.tradeName like concat(:name, '%')
                    OR p.givenName like concat(:name, '%')
                    OR p.familyName like concat(:name, '%')
               """)
    List<HsAdminPersonEntity> findPersonByOptionalNameLike(String name);
    HsAdminPersonEntity save(final HsAdminPersonEntity entity);
    long count();
}
src/main/resources/api-definition/hs-admin/api-mappings.yaml
New file
@@ -0,0 +1,16 @@
openapi-processor-mapping: v2
options:
    package-name: net.hostsharing.hsadminng.hs.admin.generated.api.v1
    model-name-suffix: Resource
map:
    result: org.springframework.http.ResponseEntity
    types:
        - type: array => java.util.List
        - type: string:uuid => java.util.UUID
    paths:
        /api/hs/admin/partners/{packageUUID}:
            null: org.openapitools.jackson.nullable.JsonNullable
src/main/resources/api-definition/hs-admin/auth.yaml
New file
@@ -0,0 +1 @@
../auth.yaml
src/main/resources/api-definition/hs-admin/error-responses.yaml
New file
@@ -0,0 +1 @@
../error-responses.yaml
src/main/resources/api-definition/hs-admin/hs-admin-contact-schemas.yaml
New file
@@ -0,0 +1,28 @@
components:
    schemas:
        HsAdminContactBase:
            type: object
            properties:
                label:
                    type: string
                postalAddress:
                    type: string
                emailAddresses:
                    type: string
                phoneNumbers:
                    type: string
        HsAdminContact:
            allOf:
                - type: object
                  properties:
                      uuid:
                          type: string
                          format: uuid
                - $ref: '#/components/schemas/HsAdminContactBase'
        HsAdminContactUpdate:
            $ref: '#/components/schemas/HsAdminContactBase'
src/main/resources/api-definition/hs-admin/hs-admin-partner-schemas.yaml
New file
@@ -0,0 +1,45 @@
components:
    schemas:
        HsAdminPartnerBase:
            type: object
            properties:
                registrationOffice:
                    type: string
                registrationNumber:
                    type: string
                birthName:
                    type: string
                birthday:
                    type: string
                    format: date
                dateOfDeath:
                    type: string
                    format: date
        HsAdminPartner:
            allOf:
                - type: object
                  properties:
                      uuid:
                          type: string
                          format: uuid
                      person:
                          $ref: './hs-admin-person-schemas.yaml#/components/schemas/HsAdminPerson'
                      contact:
                          $ref: './hs-admin-contact-schemas.yaml#/components/schemas/HsAdminContact'
                - $ref: '#/components/schemas/HsAdminPartnerBase'
        HsAdminPartnerUpdate:
            allOf:
                - type: object
                  properties:
                      personUuid:
                          type: string
                          format: uuid
                      contactUuid:
                          type: string
                          format: uuid
                - $ref: '#/components/schemas/HsAdminPartnerBase'
src/main/resources/api-definition/hs-admin/hs-admin-partners-with-uuid.yaml
New file
@@ -0,0 +1,81 @@
get:
    tags:
        - hs-admin-partners
    description: 'Fetch a single business partner by its uuid, if visible for the current subject.'
    operationId: getPartnerByUuid
    parameters:
        - $ref: './auth.yaml#/components/parameters/currentUser'
        - $ref: './auth.yaml#/components/parameters/assumedRoles'
        - name: partnerUUID
          in: path
          required: true
          schema:
              type: string
              format: uuid
    responses:
        "200":
            description: OK
            content:
                'application/json':
                    schema:
                        $ref: './hs-admin-partner-schemas.yaml#/components/schemas/HsAdminPartner'
        "401":
            $ref: './error-responses.yaml#/components/responses/Unauthorized'
        "403":
            $ref: './error-responses.yaml#/components/responses/Forbidden'
patch:
    tags:
        - hs-admin-partners
    operationId: updatePartner
    parameters:
        -   $ref: './auth.yaml#/components/parameters/currentUser'
        -   $ref: './auth.yaml#/components/parameters/assumedRoles'
        -   name: partnerUUID
            in: path
            required: true
            schema:
                type: string
                format: uuid
    requestBody:
        content:
            'application/json':
                schema:
                    $ref: './hs-admin-partner-schemas.yaml#/components/schemas/HsAdminPartnerUpdate'
    responses:
        "200":
            description: OK
            content:
                'application/json':
                    schema:
                        $ref: './hs-admin-partner-schemas.yaml#/components/schemas/HsAdminPartner'
        "401":
            $ref: './error-responses.yaml#/components/responses/Unauthorized'
        "403":
            $ref: './error-responses.yaml#/components/responses/Forbidden'
delete:
    tags:
        - hs-admin-partners
    operationId: deletePartnerByUuid
    parameters:
        - $ref: './auth.yaml#/components/parameters/currentUser'
        - $ref: './auth.yaml#/components/parameters/assumedRoles'
        - name: userUuid
          in: path
          required: true
          schema:
              type: string
              format: uuid
          description: UUID of the user to delete.
    responses:
        "204":
            description: No Content
        "401":
            $ref: './error-responses.yaml#/components/responses/Unauthorized'
        "403":
            $ref: './error-responses.yaml#/components/responses/Forbidden'
        "404":
            $ref: './error-responses.yaml#/components/responses/NotFound'
src/main/resources/api-definition/hs-admin/hs-admin-partners.yaml
New file
@@ -0,0 +1,56 @@
get:
    summary: Returns a list of (optionally filtered) business partners.
    description: Returns the list of (optionally filtered) business partners which are visible to the current user or any of it's assumed roles.
    tags:
        - hs-admin-partners
    operationId: listPartners
    parameters:
        - $ref: './auth.yaml#/components/parameters/currentUser'
        - $ref: './auth.yaml#/components/parameters/assumedRoles'
        - name: name
          in: query
          required: false
          schema:
              type: string
          description: Customer-prefix to filter the results. TODO
    responses:
        "200":
            description: OK
            content:
                'application/json':
                    schema:
                        type: array
                        items:
                            $ref: './hs-admin-partner-schemas.yaml#/components/schemas/HsAdminPartner'
        "401":
            $ref: './error-responses.yaml#/components/responses/Unauthorized'
        "403":
            $ref: './error-responses.yaml#/components/responses/Forbidden'
post:
    summary: Adds a new business partner.
    tags:
        - hs-admin-partners
    operationId: addPartner
    parameters:
        - $ref: './auth.yaml#/components/parameters/currentUser'
        - $ref: './auth.yaml#/components/parameters/assumedRoles'
    requestBody:
        content:
            'application/json':
                schema:
                    $ref: './hs-admin-partner-schemas.yaml#/components/schemas/HsAdminPartner'
        required: true
    responses:
        "201":
            description: Created
            content:
                'application/json':
                    schema:
                        $ref: './hs-admin-partner-schemas.yaml#/components/schemas/HsAdminPartner'
        "401":
            $ref: './error-responses.yaml#/components/responses/Unauthorized'
        "403":
            $ref: './error-responses.yaml#/components/responses/Forbidden'
        "409":
            $ref: './error-responses.yaml#/components/responses/Conflict'
src/main/resources/api-definition/hs-admin/hs-admin-person-schemas.yaml
New file
@@ -0,0 +1,33 @@
components:
    schemas:
        HsAdminPersonBase:
            type: object
            properties:
                personType:
                    type: string
                    enum:
                        - NATURAL               # a human
                        - LEGAL                 # e.g. Corp., Inc., AG, GmbH, eG
                        - SOLE_REPRESENTATION   # e.g. OHG, GbR
                        - JOINT_REPRESENTATION  # e.g. community of heirs
                tradeName:
                    type: string
                givenName:
                    type: string
                familyName:
                    type: string
        HsAdminPerson:
            allOf:
                - type: object
                  properties:
                      uuid:
                          type: string
                          format: uuid
                - $ref: '#/components/schemas/HsAdminPersonBase'
        HsAdminPersonUpdate:
            $ref: '#/components/schemas/HsAdminPersonBase'
src/main/resources/api-definition/hs-admin/hs-admin.yaml
New file
@@ -0,0 +1,16 @@
openapi: 3.0.1
info:
  title: Hostsharing hsadmin-ng API
  version: v0
servers:
  - url: http://localhost:8080
    description: Local development default URL.
paths:
  /api/hs/admin/partners:
    $ref: "./hs-admin-partners.yaml"
  /api/hs/admin/partners/{partnerUUID}:
    $ref: "./hs-admin-partners-with-uuid.yaml"
src/main/resources/db/changelog/200-hs-base.sql
New file
@@ -0,0 +1,83 @@
--liquibase formatted sql
-- ============================================================================
--changeset hs-base-GLOBAL-OBJECT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
  A single row to be referenced as a global object.
 */
begin transaction;
    call defineContext('initializing table "global"', null, null, null);
    insert
        into RbacObject (objecttable) values ('global');
    insert
        into Global (uuid, name) values ((select uuid from RbacObject where objectTable = 'global'), 'hostsharing');
commit;
--//
-- ============================================================================
--changeset hs-base-ADMIN-ROLE:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    A global administrator role.
 */
create or replace function hsHostsharingAdmin()
returns RbacRoleDescriptor
returns null on null input
    stable leakproof
    language sql as $$
select 'global', (select uuid from RbacObject where objectTable = 'global'), 'admin'::RbacRoleType;
$$;
begin transaction;
    call defineContext('creating Hostsharing admin role', null, null, null);
    select createRole(hsHostsharingAdmin());
commit;
-- ============================================================================
--changeset hs-base-ADMIN-USERS:1 context:dev,tc endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    Create two users and assign both to the administrators role.
 */
do language plpgsql $$
    declare
        admins uuid ;
    begin
        call defineContext('creating fake Hostsharing admin users', null, null, null);
        admins = findRoleId(hsHostsharingAdmin());
        call grantRoleToUserUnchecked(admins, admins, createRbacUser('mike@hostsharing.net'));
        call grantRoleToUserUnchecked(admins, admins, createRbacUser('sven@hostsharing.net'));
    end;
$$;
--//
-- ============================================================================
--changeset hs-base-hostsharing-TEST:1 context:dev,tc runAlways:true endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    Tests if currentUserUuid() can fetch the user from the session variable.
 */
do language plpgsql $$
    declare
        userName varchar;
    begin
        call defineContext('testing currentUserUuid', null, 'sven@hostsharing.net', null);
        select userName from RbacUser where uuid = currentUserUuid() into userName;
        if userName <> 'sven@hostsharing.net' then
            raise exception 'setting or fetching initial currentUser failed, got: %', userName;
        end if;
        call defineContext('testing currentUserUuid', null, 'mike@hostsharing.net', null);
        select userName from RbacUser where uuid = currentUserUuid() into userName;
        if userName = 'mike@ehostsharing.net' then
            raise exception 'currentUser should not change in one transaction, but did change, got: %', userName;
        end if;
    end; $$;
--//
src/test/java/net/hostsharing/hsadminng/arch/ArchTest.java
@@ -38,20 +38,34 @@
    @com.tngtech.archunit.junit.ArchTest
    @SuppressWarnings("unused")
    public static final ArchRule hsPackagesRule = classes()
    public static final ArchRule testPackagesRule = classes()
            .that().resideInAPackage("..test.(*)..")
            .should().onlyBeAccessed().byClassesThat()
            .resideInAnyPackage("..test.(*)..");
    @com.tngtech.archunit.junit.ArchTest
    @SuppressWarnings("unused")
    public static final ArchRule hsPackagePackageRule = classes()
    public static final ArchRule testPackagePackageRule = classes()
            .that().resideInAPackage("..test.pac..")
            .should().onlyBeAccessed().byClassesThat()
            .resideInAnyPackage("..test.pac..");
    @com.tngtech.archunit.junit.ArchTest
    @SuppressWarnings("unused")
    public static final ArchRule hsAdminPackagesRule = classes()
            .that().resideInAPackage("..hs.admin.(*)..")
            .should().onlyBeAccessed().byClassesThat()
            .resideInAnyPackage("..hs.admin.(*)..");
    @com.tngtech.archunit.junit.ArchTest
    @SuppressWarnings("unused")
    public static final ArchRule hsAdminPartnerPackageRule = classes()
            .that().resideInAPackage("..hs.admin.partner..")
            .should().onlyBeAccessed().byClassesThat()
            .resideInAnyPackage("..hs.admin.partner..");
    @com.tngtech.archunit.junit.ArchTest
    @SuppressWarnings("unused")
    public static final ArchRule acceptsAnnotationOnMethodsRule = methods()
            .that().areAnnotatedWith(Accepts.class)
            .should().beDeclaredInClassesThat().haveSimpleNameEndingWith("AcceptanceTest")
src/test/java/net/hostsharing/hsadminng/hs/admin/contact/HsAdminContactRepositoryIntegrationTest.java
New file
@@ -0,0 +1,14 @@
package net.hostsharing.hsadminng.hs.admin.contact;
import org.junit.jupiter.api.Test;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
class HsAdminContactRepositoryIntegrationTest {
    @Test
    void test() throws UnsupportedEncodingException, NoSuchAlgorithmException {
    }
}
src/test/java/net/hostsharing/hsadminng/hs/admin/partner/HsAdminPartnerControllerAcceptanceTest.java
New file
@@ -0,0 +1,195 @@
package net.hostsharing.hsadminng.hs.admin.partner;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.Accepts;
import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid;
import static net.hostsharing.test.JsonBuilder.jsonObject;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.*;
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
        classes = HsadminNgApplication.class
)
@Transactional
class HsAdminPartnerControllerAcceptanceTest {
    @LocalServerPort
    private Integer port;
    @Autowired
    Context context;
    @Autowired
    Context contextMock;
    @Autowired
    HsAdminPartnerRepository partnerRepository;
    @Nested
    @Accepts({ "Partner:F(Find)" })
    class ListPartners {
        @Test
        void testHostsharingAdmin_withoutAssumedRoles_canViewAllPartners_ifNoCriteriaGiven() {
            RestAssured // @formatter:off
                .given()
                    .header("current-user", "mike@hostsharing.net")
                    .port(port)
                .when()
                    .get("http://localhost/api/hs/admin/partners")
                .then().assertThat()
                    .statusCode(200)
                    .contentType("application/json")
                    .body("[0].contact.label", is("Ixx AG"))
                    .body("[0].person.tradeName", is("Ixx AG"))
                    .body("[1].contact.label", is("Ypsilon GmbH"))
                    .body("[1].person.tradeName", is("Ypsilon GmbH"))
                    .body("[2].contact.label", is("Zett OHG"))
                    .body("[2].person.tradeName", is("Zett OHG"))
                    .body("size()", greaterThanOrEqualTo(3));
                // @formatter:on
        }
    }
    @Nested
    @Accepts({ "Partner:C(Create)" })
    class AddPartner {
        private final static String NEW_PARTNER_JSON_WITHOUT_UUID =
                """
                                {
                                   "person": {
                                     "personType": "LEGAL",
                                     "tradeName": "Test Corp.",
                                     "givenName": null,
                                     "familyName": null
                                   },
                                   "contact": {
                                     "label": "Test Corp.",
                                     "postalAddress": "Test Corp.\\nTestweg 50\\n20001 Hamburg",
                                     "emailAddresses": "office@example.com",
                                     "phoneNumbers": "040 12345"
                                   },
                                   "registrationOffice": "Registergericht Hamburg",
                                   "registrationNumber": "123456",
                                   "birthName": null,
                                   "birthday": null,
                                   "dateOfDeath": null
                                 }
                        """;
        @Test
        void hostsharingAdmin_withoutAssumedRole_canAddPartner_withExplicitUuid() {
            final var givenUUID = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6");
            final var location = RestAssured // @formatter:off
                    .given()
                        .header("current-user", "mike@hostsharing.net")
                        .contentType(ContentType.JSON)
                        .body(jsonObject(NEW_PARTNER_JSON_WITHOUT_UUID)
                                .with("uuid", givenUUID.toString()).toString())
                        .port(port)
                    .when()
                        .post("http://localhost/api/hs/admin/partners")
                    .then().assertThat()
                        .statusCode(201)
                        .contentType(ContentType.JSON)
                        .body("uuid", is("3fa85f64-5717-4562-b3fc-2c963f66afa6"))
                        .body("registrationNumber", is("123456"))
                        .body("person.tradeName", is("Test Corp."))
                        .header("Location", startsWith("http://localhost"))
                    .extract().header("Location");  // @formatter:on
            // finally, the new partner can be viewed by its own admin
            final var newUserUuid = UUID.fromString(
                    location.substring(location.lastIndexOf('/') + 1));
            assertThat(newUserUuid).isEqualTo(givenUUID);
            // TODO: context.define("partner-admin@ttt.example.com");
            // assertThat(partnerRepository.findByUuid(newUserUuid))
            //        .hasValueSatisfying(c -> assertThat(c.getPerson().getTradeName()).isEqualTo("Test Corp."));
        }
        @Test
        void hostsharingAdmin_withoutAssumedRole_canAddPartner_withGeneratedUuid() {
            final var location = RestAssured // @formatter:off
                    .given()
                        .header("current-user", "mike@hostsharing.net")
                        .contentType(ContentType.JSON)
                        .body(NEW_PARTNER_JSON_WITHOUT_UUID)
                        .port(port)
                    .when()
                        .post("http://localhost/api/hs/admin/partners")
                    .then().assertThat()
                        .statusCode(201)
                        .contentType(ContentType.JSON)
                        .body("uuid", isUuidValid())
                        .body("registrationNumber", is("123456"))
                        .body("person.tradeName", is("Test Corp."))
                        .header("Location", startsWith("http://localhost"))
                    .extract().header("Location");  // @formatter:on
            // finally, the new partner can be viewed by its own admin
            final var newUserUuid = UUID.fromString(
                    location.substring(location.lastIndexOf('/') + 1));
            assertThat(newUserUuid).isNotNull();
            // TODO: context.define("partner-admin@ttt.example.com");
            // assertThat(partnerRepository.findByUuid(newUserUuid))
            //        .hasValueSatisfying(c -> assertThat(c.getPerson().getTradeName()).isEqualTo("Test Corp."));
        }
    }
    @Nested
    @Accepts({ "Partner:R(Read)" })
    class GetPartner {
        @Test
        void hostsharingAdmin_withoutAssumedRole_canGetArbitraryPartner() {
            // TODO: final var givenPartnerUuid = partnerRepository.findPartnerByOptionalNameLike("Ixx").get(0).getUuid();
            final var givenPartnerUuid = UUID.randomUUID();
            RestAssured // @formatter:off
                .given()
                    .header("current-user", "mike@hostsharing.net")
                    .port(port)
                .when()
                    .get("http://localhost/api/hs/admin/partners/" + givenPartnerUuid)
                .then().log().body().assertThat()
                    .statusCode(200)
                    .contentType("application/json")
                    .body("person.tradeName", is("Ixx AG"))
                    .body("contact.label", is("Ixx AG"));
                // @formatter:on
        }
        @Test
        @Accepts({ "Partner:X(Access Control)" })
        void normalUser_canNotGetUnrelatedPartner() {
            // TODO: final var givenPartnerUuid = partnerRepository.findPartnerByOptionalNameLike("Ixx").get(0).getUuid();
            final UUID givenPartnerUuid = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6");
            RestAssured // @formatter:off
                .given()
                    .header("current-user", "somebody@example.org")
                    .port(port)
                .when()
                    .get("http://localhost/api/hs/admin/partners/" + givenPartnerUuid)
                .then().log().body().assertThat()
                    .statusCode(404);
            // @formatter:on
        }
    }
}
src/test/java/net/hostsharing/hsadminng/hs/admin/partner/HsAdminPartnerRepositoryIntegrationTest.java
New file
@@ -0,0 +1,125 @@
package net.hostsharing.hsadminng.hs.admin.partner;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.context.ContextBasedTest;
import net.hostsharing.hsadminng.hs.admin.person.HsAdminPersonEntity;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.annotation.DirtiesContext;
import javax.persistence.EntityManager;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import static net.hostsharing.hsadminng.hs.admin.partner.TestHsAdminPartner.testLtd;
import static net.hostsharing.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat;
@Disabled
@DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, HsAdminPartnerRepository.class })
@DirtiesContext
class HsAdminPartnerRepositoryIntegrationTest extends ContextBasedTest {
    @Autowired
    HsAdminPartnerRepository partnerRepository;
    @Autowired
    EntityManager em;
    @MockBean
    HttpServletRequest request;
    @Nested
    class CreateCustomer {
        @Test
        public void testHostsharingAdmin_withoutAssumedRole_canCreateNewCustomer() {
            // given
            context("mike@example.org", null);
            final var count = partnerRepository.count();
            // when
            final var result = attempt(em, () -> {
                return partnerRepository.save(testLtd);
            });
            // then
            assertThat(result.wasSuccessful()).isTrue();
            assertThat(result.returnedValue()).isNotNull().extracting(HsAdminPartnerEntity::getUuid).isNotNull();
            assertThatPartnerIsPersisted(result.returnedValue());
            assertThat(partnerRepository.count()).isEqualTo(count + 1);
        }
        private void assertThatPartnerIsPersisted(final HsAdminPartnerEntity saved) {
            final var found = partnerRepository.findByUuid(saved.getUuid());
            assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
        }
    }
    @Nested
    class FindAllCustomers {
        @Test
        public void testGlobalAdmin_withoutAssumedRole_canViewAllCustomers() {
            // given
            context("mike@example.org", null);
            // when
            final var result = partnerRepository.findPartnerByOptionalNameLike(null);
            // then
            allThesePartnersAreReturned(result, "Ixx AG", "Ypsilon GmbH", "Zett OHG");
        }
    }
    @Nested
    class FindByPrefixLike {
        @Test
        public void testGlobalAdmin_withoutAssumedRole_canViewAllCustomers() {
            // given
            context("mike@example.org", null);
            // when
            final var result = partnerRepository.findPartnerByOptionalNameLike("Yps");
            // then
            exactlyTheseCustomersAreReturned(result, "Ypsilon GmbH");
        }
        @Test
        public void customerAdmin_withoutAssumedRole_canViewOnlyItsOwnCustomer() {
            // given:
            context("customer-admin@xxx.example.com", null);
            // when:
            final var result = partnerRepository.findPartnerByOptionalNameLike("Yps");
            // then:
            exactlyTheseCustomersAreReturned(result);
        }
    }
    void exactlyTheseCustomersAreReturned(final List<HsAdminPartnerEntity> actualResult, final String... partnerTradeNames) {
        assertThat(actualResult)
                .hasSize(partnerTradeNames.length)
                .extracting(HsAdminPartnerEntity::getPerson)
                .extracting(HsAdminPersonEntity::getTradeName)
                .containsExactlyInAnyOrder(partnerTradeNames);
    }
    void allThesePartnersAreReturned(final List<HsAdminPartnerEntity> actualResult, final String... partnerTradeNames) {
        assertThat(actualResult)
                .extracting(HsAdminPartnerEntity::getPerson)
                .extracting(HsAdminPersonEntity::getTradeName)
                .contains(partnerTradeNames);
    }
}
src/test/java/net/hostsharing/hsadminng/hs/admin/partner/TestHsAdminPartner.java
New file
@@ -0,0 +1,27 @@
package net.hostsharing.hsadminng.hs.admin.partner;
import net.hostsharing.hsadminng.hs.admin.contact.HsAdminContactEntity;
import net.hostsharing.hsadminng.hs.admin.partner.HsAdminPartnerEntity;
import net.hostsharing.hsadminng.hs.admin.person.HsAdminPersonEntity;
import java.util.UUID;
import static net.hostsharing.hsadminng.hs.admin.person.HsAdminPersonEntity.PersonType.LEGAL;
public class TestHsAdminPartner {
    public static final HsAdminPartnerEntity testLtd = hsAdminPartnerWithLegalPerson("Test Ltd.");
    static public HsAdminPartnerEntity hsAdminPartnerWithLegalPerson(final String tradeName) {
        return HsAdminPartnerEntity.builder()
                .uuid(UUID.randomUUID())
                .person(HsAdminPersonEntity.builder()
                        .type(LEGAL)
                        .tradeName(tradeName)
                        .build())
                .contact(HsAdminContactEntity.builder()
                        .label(tradeName)
                        .build())
                .build();
    }
}
src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerRestTest.java
@@ -11,7 +11,7 @@
import java.util.UUID;
import static net.hostsharing.test.IsValidUuidMatcher.isValidUuid;
import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
@@ -63,7 +63,7 @@
                // then
                .andExpect(status().isCreated())
                .andExpect(jsonPath("uuid", isValidUuid()));
                .andExpect(jsonPath("uuid", isUuidValid()));
        // then
        verify(rbacUserRepository).create(argThat(entity -> entity.getUuid() != null));
src/test/java/net/hostsharing/test/IsValidUuidMatcher.java
@@ -5,14 +5,15 @@
import org.hamcrest.Matcher;
import java.util.UUID;
import java.util.function.Predicate;
public class IsValidUuidMatcher extends BaseMatcher<CharSequence> {
    public static Matcher<CharSequence> isValidUuid() {
    public static Matcher<CharSequence> isUuidValid() {
        return new IsValidUuidMatcher();
    }
    public static boolean isValidUuid(final String actual) {
    public static boolean isUuidValid(final String actual) {
        try {
            UUID.fromString(actual);
        } catch (final IllegalArgumentException exc) {
@@ -21,12 +22,16 @@
        return true;
    }
    public static Predicate<String> isValidUuid() {
        return IsValidUuidMatcher::isUuidValid;
    }
    @Override
    public boolean matches(final Object actual) {
        if (actual == null || actual.getClass().isAssignableFrom(CharSequence.class)) {
            return false;
        }
        return isValidUuid(actual.toString());
        return isUuidValid(actual.toString());
    }
    @Override
src/test/java/net/hostsharing/test/JsonBuilder.java
New file
@@ -0,0 +1,56 @@
package net.hostsharing.test;
import org.json.JSONException;
import org.json.JSONObject;
/**
 * Build JSONObject with little boilerplate code.
 */
public class JsonBuilder {
    private final JSONObject jsonObject;
    /**
     * Create a JsonBuilder from a string.
     *
     * @param jsonString valid JSON
     * @return a new JsonBuilder
     */
    public static JsonBuilder jsonObject(final String jsonString) {
        return new JsonBuilder(jsonString);
    }
    /**
     * Add a property (key/value pair).
     *
     * @param key   JSON key
     * @param value JSON value
     * @return this JsonBuilder
     */
    public JsonBuilder with(final String key, final String value) {
        try {
            jsonObject.put(key, value);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        return this;
    }
    @Override
    public String toString() {
        try {
            return jsonObject.toString(4);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }
    private JsonBuilder(final String jsonString) {
        try {
            jsonObject = new JSONObject(jsonString);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }
}