add SEPA-mandate API+Controller

This commit is contained in:
Michael Hoennig 2022-10-15 15:32:32 +02:00
parent 67e850f9b2
commit e305c3c935
11 changed files with 913 additions and 47 deletions

View File

@ -15,9 +15,12 @@ public interface HsOfficeBankAccountRepository extends Repository<HsOfficeBankAc
SELECT c FROM HsOfficeBankAccountEntity c SELECT c FROM HsOfficeBankAccountEntity c
WHERE :holder is null WHERE :holder is null
OR lower(c.holder) like lower(concat(:holder, '%')) OR lower(c.holder) like lower(concat(:holder, '%'))
ORDER BY c.holder
""") """)
List<HsOfficeBankAccountEntity> findByOptionalHolderLike(String holder); List<HsOfficeBankAccountEntity> findByOptionalHolderLike(String holder);
List<HsOfficeBankAccountEntity> findByIbanOrderByIban(String iban);
HsOfficeBankAccountEntity save(final HsOfficeBankAccountEntity entity); HsOfficeBankAccountEntity save(final HsOfficeBankAccountEntity entity);
int deleteByUuid(final UUID uuid); int deleteByUuid(final UUID uuid);

View File

@ -0,0 +1,151 @@
package net.hostsharing.hsadminng.hs.office.sepamandate;
import com.vladmihalcea.hibernate.type.range.Range;
import net.hostsharing.hsadminng.Mapper;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMandatesApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
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 javax.persistence.EntityManager;
import javax.validation.Valid;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.Mapper.map;
@RestController
public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
@Autowired
private Context context;
@Autowired
private HsOfficeSepaMandateRepository SepaMandateRepo;
@Autowired
private EntityManager em;
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeSepaMandateResource>> listSepaMandatesByIban(
final String currentUser,
final String assumedRoles,
final String iban) {
context.define(currentUser, assumedRoles);
final var entities = SepaMandateRepo.findSepaMandateByOptionalIban(iban);
final var resources = Mapper.mapList(entities, HsOfficeSepaMandateResource.class,
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
}
@Override
@Transactional
public ResponseEntity<HsOfficeSepaMandateResource> addSepaMandate(
final String currentUser,
final String assumedRoles,
@Valid final HsOfficeSepaMandateInsertResource body) {
context.define(currentUser, assumedRoles);
final var entityToSave = map(body, HsOfficeSepaMandateEntity.class, SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER);
entityToSave.setUuid(UUID.randomUUID());
final var saved = SepaMandateRepo.save(entityToSave);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/office/SepaMandates/{id}")
.buildAndExpand(entityToSave.getUuid())
.toUri();
final var mapped = map(saved, HsOfficeSepaMandateResource.class,
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.created(uri).body(mapped);
}
@Override
@Transactional(readOnly = true)
public ResponseEntity<HsOfficeSepaMandateResource> getSepaMandateByUuid(
final String currentUser,
final String assumedRoles,
final UUID sepaMandateUuid) {
context.define(currentUser, assumedRoles);
final var result = SepaMandateRepo.findByUuid(sepaMandateUuid);
if (result.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(map(result.get(), HsOfficeSepaMandateResource.class,
SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER));
}
@Override
@Transactional
public ResponseEntity<Void> deleteSepaMandateByUuid(
final String currentUser,
final String assumedRoles,
final UUID sepaMandateUuid) {
context.define(currentUser, assumedRoles);
final var result = SepaMandateRepo.deleteByUuid(sepaMandateUuid);
if (result == 0) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
}
@Override
@Transactional
public ResponseEntity<HsOfficeSepaMandateResource> patchSepaMandate(
final String currentUser,
final String assumedRoles,
final UUID sepaMandateUuid,
final HsOfficeSepaMandatePatchResource body) {
context.define(currentUser, assumedRoles);
final var current = SepaMandateRepo.findByUuid(sepaMandateUuid).orElseThrow();
current.setValidity(toPostgresDateRange(current.getValidity().lower(), body.getValidTo()));
final var saved = SepaMandateRepo.save(current);
final var mapped = map(saved, HsOfficeSepaMandateResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}
private static Range<LocalDate> toPostgresDateRange(
final LocalDate validFrom,
final LocalDate validTo) {
return validTo != null
? Range.closedOpen(validFrom, validTo.plusDays(1))
: Range.closedInfinite(validFrom);
}
final BiConsumer<HsOfficeSepaMandateEntity, HsOfficeSepaMandateResource> SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
resource.setDebitor(map(entity.getDebitor(), HsOfficeDebitorResource.class));
resource.setBankAccount(map(entity.getBankAccount(), HsOfficeBankAccountResource.class));
resource.setValidFrom(entity.getValidity().lower());
if (entity.getValidity().hasUpperBound()) {
resource.setValidTo(entity.getValidity().upper().minusDays(1));
}
};
final BiConsumer<HsOfficeSepaMandateInsertResource, HsOfficeSepaMandateEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.setDebitor(em.getReference(HsOfficeDebitorEntity.class, resource.getDebitorUuid()));
entity.setBankAccount(em.getReference(HsOfficeBankAccountEntity.class, resource.getBankAccountUuid()));
entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo()));
};
}

View File

@ -15,8 +15,9 @@ public interface HsOfficeSepaMandateRepository extends Repository<HsOfficeSepaMa
SELECT mandate FROM HsOfficeSepaMandateEntity mandate SELECT mandate FROM HsOfficeSepaMandateEntity mandate
WHERE :iban is null WHERE :iban is null
OR mandate.bankAccount.iban like concat(:iban, '%') OR mandate.bankAccount.iban like concat(:iban, '%')
ORDER BY mandate.bankAccount.iban
""") """)
List<HsOfficeSepaMandateEntity> findSepaMandateByOptionalIBAN(String iban); List<HsOfficeSepaMandateEntity> findSepaMandateByOptionalIban(String iban);
HsOfficeSepaMandateEntity save(final HsOfficeSepaMandateEntity entity); HsOfficeSepaMandateEntity save(final HsOfficeSepaMandateEntity entity);

View File

@ -3,6 +3,7 @@ openapi-processor-mapping: v2
options: options:
package-name: net.hostsharing.hsadminng.hs.office.generated.api.v1 package-name: net.hostsharing.hsadminng.hs.office.generated.api.v1
model-name-suffix: Resource model-name-suffix: Resource
bean-validation: true
map: map:
result: org.springframework.http.ResponseEntity result: org.springframework.http.ResponseEntity
@ -10,6 +11,7 @@ map:
types: types:
- type: array => java.util.List - type: array => java.util.List
- type: string:uuid => java.util.UUID - type: string:uuid => java.util.UUID
- type: string:format => java.lang.String
paths: paths:
/api/hs/office/partners/{partnerUUID}: /api/hs/office/partners/{partnerUUID}:
@ -24,3 +26,5 @@ map:
null: org.openapitools.jackson.nullable.JsonNullable null: org.openapitools.jackson.nullable.JsonNullable
/api/hs/office/debitors/{debitorUUID}: /api/hs/office/debitors/{debitorUUID}:
null: org.openapitools.jackson.nullable.JsonNullable null: org.openapitools.jackson.nullable.JsonNullable
/api/hs/office/sepamandates/{debitorUUID}:
null: org.openapitools.jackson.nullable.JsonNullable

View File

@ -0,0 +1,60 @@
components:
schemas:
HsOfficeSepaMandate:
type: object
properties:
uuid:
type: string
format: uuid
debitor:
$ref: './hs-office-debitor-schemas.yaml#/components/schemas/HsOfficeDebitor'
bankAccount:
$ref: './hs-office-bankaccount-schemas.yaml#/components/schemas/HsOfficeBankAccount'
reference:
type: string
validFrom:
type: string
format: date
validTo:
type: string
format: date
HsOfficeSepaMandatePatch:
type: object
properties:
validTo:
type: string
format: date
additionalProperties: false
HsOfficeSepaMandateInsert:
type: object
properties:
debitorUuid:
type: string
format: uuid
nullable: false
bankAccountUuid:
type: string
format: uuid
nullable: false
reference:
type: string
nullable: false
validFrom:
type: string
format: date
nullable: false
validTo:
type: string
format: date
nullable: true
required:
- debitorUuid
- bankAccountUuid
- reference
- validFrom
additionalProperties: false

View File

@ -0,0 +1,83 @@
get:
tags:
- hs-office-sepaMandates
description: 'Fetch a single SEPA Mandate by its uuid, if visible for the current subject.'
operationId: getSepaMandateByUuid
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
- name: sepaMandateUUID
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the SEPA Mandate to fetch.
responses:
"200":
description: OK
content:
'application/json':
schema:
$ref: './hs-office-sepamandate-schemas.yaml#/components/schemas/HsOfficeSepaMandate'
"401":
$ref: './error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './error-responses.yaml#/components/responses/Forbidden'
patch:
tags:
- hs-office-sepaMandates
description: 'Updates a single SEPA Mandate by its uuid, if permitted for the current subject.'
operationId: patchSepaMandate
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
- name: sepaMandateUUID
in: path
required: true
schema:
type: string
format: uuid
requestBody:
content:
'application/json':
schema:
$ref: './hs-office-sepamandate-schemas.yaml#/components/schemas/HsOfficeSepaMandatePatch'
responses:
"200":
description: OK
content:
'application/json':
schema:
$ref: './hs-office-sepamandate-schemas.yaml#/components/schemas/HsOfficeSepaMandate'
"401":
$ref: './error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './error-responses.yaml#/components/responses/Forbidden'
delete:
tags:
- hs-office-sepaMandates
description: 'Delete a single SEPA Mandate by its uuid, if permitted for the current subject.'
operationId: deleteSepaMandateByUuid
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
- name: sepaMandateUUID
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the sepaMandate 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'

View File

@ -0,0 +1,57 @@
get:
summary: Returns a list of (optionally filtered) SEPA Mandates.
description: Returns the list of (optionally filtered) SEPA Mandates which are visible to the current user or any of it's assumed roles.
tags:
- hs-office-sepaMandates
operationId: listSepaMandatesByIBAN
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
- name: name
in: query
required: false
schema:
type: string
description: (Beginning of) IBAN to filter the results.
responses:
"200":
description: OK
content:
'application/json':
schema:
type: array
items:
$ref: './hs-office-sepamandate-schemas.yaml#/components/schemas/HsOfficeSepaMandate'
"401":
$ref: './error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './error-responses.yaml#/components/responses/Forbidden'
post:
summary: Adds a new SEPA Mandate.
tags:
- hs-office-sepaMandates
operationId: addSepaMandate
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
requestBody:
description: A JSON object describing the new SEPA-Mandate.
required: true
content:
application/json:
schema:
$ref: '/hs-office-sepamandate-schemas.yaml#/components/schemas/HsOfficeSepaMandateInsert'
responses:
"201":
description: Created
content:
'application/json':
schema:
$ref: './hs-office-sepamandate-schemas.yaml#/components/schemas/HsOfficeSepaMandate'
"401":
$ref: './error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './error-responses.yaml#/components/responses/Forbidden'
"409":
$ref: './error-responses.yaml#/components/responses/Conflict'

View File

@ -60,3 +60,12 @@ paths:
/api/hs/office/debitors/{debitorUUID}: /api/hs/office/debitors/{debitorUUID}:
$ref: "./hs-office-debitors-with-uuid.yaml" $ref: "./hs-office-debitors-with-uuid.yaml"
# SepaMandates
/api/hs/office/sepamandates:
$ref: "./hs-office-sepamandates.yaml"
/api/hs/office/sepamandates/{sepaMandateUUID}:
$ref: "./hs-office-sepamandates-with-uuid.yaml"

View File

@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
@Autowired @Autowired
HsOfficeBankAccountRepository bankaccountRepo; HsOfficeBankAccountRepository bankAccountRepo;
@Autowired @Autowired
RawRbacRoleRepository rawRoleRepo; RawRbacRoleRepository rawRoleRepo;
@ -60,37 +60,37 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
class CreateBankAccount { class CreateBankAccount {
@Test @Test
public void globalAdmin_withoutAssumedRole_canCreateNewBankAccount() { public void globalAdmin_canCreateNewBankAccount() {
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
final var count = bankaccountRepo.count(); final var count = bankAccountRepo.count();
// when // when
final var result = attempt(em, () -> bankaccountRepo.save( final var result = attempt(em, () -> bankAccountRepo.save(
hsOfficeBankAccount("some temp acc A", "DE37500105177419788228", ""))); hsOfficeBankAccount("some temp acc A", "DE37500105177419788228", "")));
// then // then
result.assertSuccessful(); result.assertSuccessful();
assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeBankAccountEntity::getUuid).isNotNull(); assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeBankAccountEntity::getUuid).isNotNull();
assertThatBankAccountIsPersisted(result.returnedValue()); assertThatBankAccountIsPersisted(result.returnedValue());
assertThat(bankaccountRepo.count()).isEqualTo(count + 1); assertThat(bankAccountRepo.count()).isEqualTo(count + 1);
} }
@Test @Test
public void arbitraryUser_canCreateNewBankAccount() { public void arbitraryUser_canCreateNewBankAccount() {
// given // given
context("selfregistered-user-drew@hostsharing.org"); context("selfregistered-user-drew@hostsharing.org");
final var count = bankaccountRepo.count(); final var count = bankAccountRepo.count();
// when // when
final var result = attempt(em, () -> bankaccountRepo.save( final var result = attempt(em, () -> bankAccountRepo.save(
hsOfficeBankAccount("some temp acc B", "DE49500105174516484892", "INGDDEFFXXX"))); hsOfficeBankAccount("some temp acc B", "DE49500105174516484892", "INGDDEFFXXX")));
// then // then
result.assertSuccessful(); result.assertSuccessful();
assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeBankAccountEntity::getUuid).isNotNull(); assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeBankAccountEntity::getUuid).isNotNull();
assertThatBankAccountIsPersisted(result.returnedValue()); assertThatBankAccountIsPersisted(result.returnedValue());
assertThat(bankaccountRepo.count()).isEqualTo(count + 1); assertThat(bankAccountRepo.count()).isEqualTo(count + 1);
} }
@Test @Test
@ -101,7 +101,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
final var initialGrantNames = grantDisplaysOf(rawGrantRepo.findAll()); final var initialGrantNames = grantDisplaysOf(rawGrantRepo.findAll());
// when // when
attempt(em, () -> bankaccountRepo.save( attempt(em, () -> bankAccountRepo.save(
hsOfficeBankAccount("some temp acc C", "DE25500105176934832579", "INGDDEFFXXX")) hsOfficeBankAccount("some temp acc C", "DE25500105176934832579", "INGDDEFFXXX"))
).assumeSuccessful(); ).assumeSuccessful();
@ -131,21 +131,21 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
} }
private void assertThatBankAccountIsPersisted(final HsOfficeBankAccountEntity saved) { private void assertThatBankAccountIsPersisted(final HsOfficeBankAccountEntity saved) {
final var found = bankaccountRepo.findByUuid(saved.getUuid()); final var found = bankAccountRepo.findByUuid(saved.getUuid());
assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved); assertThat(found).isNotEmpty().get().usingRecursiveComparison().isEqualTo(saved);
} }
} }
@Nested @Nested
class FindAllBankAccounts { class ListBankAccounts {
@Test @Test
public void globalAdmin_withoutAssumedRole_canViewAllBankAccounts() { public void globalAdmin_canViewAllBankAccounts() {
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
// when // when
final var result = bankaccountRepo.findByOptionalHolderLike(null); final var result = bankAccountRepo.findByOptionalHolderLike(null);
// then // then
allTheseBankAccountsAreReturned( allTheseBankAccountsAreReturned(
@ -167,45 +167,32 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
// when: // when:
context("selfregistered-user-drew@hostsharing.org"); context("selfregistered-user-drew@hostsharing.org");
final var result = bankaccountRepo.findByOptionalHolderLike(null); final var result = bankAccountRepo.findByOptionalHolderLike(null);
// then: // then:
exactlyTheseBankAccountsAreReturned(result, givenBankAccount.getHolder()); exactlyTheseBankAccountsAreReturned(result, givenBankAccount.getHolder());
} }
}
@Nested
class FindByLabelLike {
@Test @Test
public void globalAdmin_withoutAssumedRole_canViewAllBankAccounts() { public void globalAdmin_canViewBankAccountsByIban() {
// given // given
context("superuser-alex@hostsharing.net", null); context("superuser-alex@hostsharing.net", null);
// when // when
final var result = bankaccountRepo.findByOptionalHolderLike(null); final var result = bankAccountRepo.findByIbanOrderByIban("DE02120300000000202051");
// then // then
exactlyTheseBankAccountsAreReturned( exactlyTheseBankAccountsAreReturned(result, "First GmbH");
result,
"Anita Bessler",
"First GmbH",
"Fourth e.G.",
"Mel Bessler",
"Paul Winkler",
"Peter Smith",
"Second e.K.",
"Third OHG");
} }
@Test @Test
public void arbitraryUser_withoutAssumedRole_canViewOnlyItsOwnBankAccount() { public void arbitraryUser_canViewItsOwnBankAccount() {
// given: // given:
final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org");
// when: // when:
context("selfregistered-user-drew@hostsharing.org"); context("selfregistered-user-drew@hostsharing.org");
final var result = bankaccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); final var result = bankAccountRepo.findByIbanOrderByIban(givenBankAccount.getIban());
// then: // then:
exactlyTheseBankAccountsAreReturned(result, givenBankAccount.getHolder()); exactlyTheseBankAccountsAreReturned(result, givenBankAccount.getHolder());
@ -216,7 +203,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
class DeleteByUuid { class DeleteByUuid {
@Test @Test
public void globalAdmin_withoutAssumedRole_canDeleteAnyBankAccount() { public void globalAdmin_canDeleteAnyBankAccount() {
// given // given
context("superuser-alex@hostsharing.net", null); context("superuser-alex@hostsharing.net", null);
final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org");
@ -224,33 +211,33 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
// when // when
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", null); context("superuser-alex@hostsharing.net", null);
bankaccountRepo.deleteByUuid(givenBankAccount.getUuid()); bankAccountRepo.deleteByUuid(givenBankAccount.getUuid());
}); });
// then // then
result.assertSuccessful(); result.assertSuccessful();
assertThat(jpaAttempt.transacted(() -> { assertThat(jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", null); context("superuser-alex@hostsharing.net", null);
return bankaccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); return bankAccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder());
}).assertSuccessful().returnedValue()).hasSize(0); }).assertSuccessful().returnedValue()).hasSize(0);
} }
@Test @Test
public void arbitraryUser_withoutAssumedRole_canDeleteABankAccountCreatedByItself() { public void arbitraryUser_canDeleteABankAccountCreatedByItself() {
// given // given
final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org");
// when // when
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {
context("selfregistered-user-drew@hostsharing.org", null); context("selfregistered-user-drew@hostsharing.org", null);
bankaccountRepo.deleteByUuid(givenBankAccount.getUuid()); bankAccountRepo.deleteByUuid(givenBankAccount.getUuid());
}); });
// then // then
result.assertSuccessful(); result.assertSuccessful();
assertThat(jpaAttempt.transacted(() -> { assertThat(jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", null); context("superuser-alex@hostsharing.net", null);
return bankaccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); return bankAccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder());
}).assertSuccessful().returnedValue()).hasSize(0); }).assertSuccessful().returnedValue()).hasSize(0);
} }
@ -269,7 +256,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
// when // when
final var result = jpaAttempt.transacted(() -> { final var result = jpaAttempt.transacted(() -> {
context("selfregistered-user-drew@hostsharing.org", null); context("selfregistered-user-drew@hostsharing.org", null);
return bankaccountRepo.deleteByUuid(givenBankAccount.getUuid()); return bankAccountRepo.deleteByUuid(givenBankAccount.getUuid());
}); });
// then // then
@ -289,7 +276,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
Supplier<HsOfficeBankAccountEntity> entitySupplier) { Supplier<HsOfficeBankAccountEntity> entitySupplier) {
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {
context(createdByUser); context(createdByUser);
return bankaccountRepo.save(entitySupplier.get()); return bankAccountRepo.save(entitySupplier.get());
}).assertSuccessful().returnedValue(); }).assertSuccessful().returnedValue();
} }
@ -316,10 +303,10 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest {
@AfterEach @AfterEach
void cleanup() { void cleanup() {
context("superuser-alex@hostsharing.net", null); context("superuser-alex@hostsharing.net", null);
final var result = bankaccountRepo.findByOptionalHolderLike("some temp acc"); final var result = bankAccountRepo.findByOptionalHolderLike("some temp acc");
result.forEach(tempPerson -> { result.forEach(tempPerson -> {
System.out.println("DELETING temporary bankaccount: " + tempPerson.getHolder()); System.out.println("DELETING temporary bankaccount: " + tempPerson.getHolder());
bankaccountRepo.deleteByUuid(tempPerson.getUuid()); bankAccountRepo.deleteByUuid(tempPerson.getUuid());
}); });
} }

View File

@ -0,0 +1,511 @@
package net.hostsharing.hsadminng.hs.office.sepamandate;
import com.vladmihalcea.hibernate.type.range.Range;
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 net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountRepository;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
import net.hostsharing.test.JpaAttempt;
import org.json.JSONException;
import org.junit.jupiter.api.AfterEach;
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.time.LocalDate;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid;
import static net.hostsharing.test.JsonMatcher.lenientlyEquals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.*;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class }
)
@Transactional
class HsOfficeSepaMandateControllerAcceptanceTest {
@LocalServerPort
private Integer port;
@Autowired
Context context;
@Autowired
Context contextMock;
@Autowired
HsOfficeSepaMandateRepository sepaMandateRepo;
@Autowired
HsOfficeDebitorRepository debitorRepo;
@Autowired
HsOfficeBankAccountRepository bankAccountRepo;
@Autowired
JpaAttempt jpaAttempt;
Set<UUID> tempSepaMandateUuids = new HashSet<>();
@Nested
@Accepts({ "SepaMandate:F(Find)" })
class ListSepaMandates {
@Test
void globalAdmin_canViewAllSepaMandates_ifNoCriteriaGiven() throws JSONException {
RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.port(port)
.when()
.get("http://localhost/api/hs/office/sepamandates")
.then().log().all().assertThat()
.statusCode(200)
.contentType("application/json")
.body("", lenientlyEquals("""
[
{
"debitor": {
"debitorNumber": 10002,
"billingContact": { "label": "second contact" }
},
"bankAccount": { "holder": "Second e.K." },
"reference": "refSeconde.K.",
"validFrom": "2022-10-01",
"validTo": "2026-12-31"
},
{
"debitor": {
"debitorNumber": 10001,
"billingContact": { "label": "first contact" }
},
"bankAccount": { "holder": "First GmbH" },
"reference": "refFirstGmbH",
"validFrom": "2022-10-01",
"validTo": "2026-12-31"
},
{
"debitor": {
"debitorNumber": 10003,
"billingContact": { "label": "third contact" }
},
"bankAccount": { "holder": "Third OHG" },
"reference": "refThirdOHG",
"validFrom": "2022-10-01",
"validTo": "2026-12-31"
}
]
"""));
// @formatter:on
}
}
@Nested
@Accepts({ "SepaMandate:C(Create)" })
class AddSepaMandate {
@Test
void globalAdmin_canAddSepaMandate() {
context.define("superuser-alex@hostsharing.net");
final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("Third").get(0);
final var givenBankAccount = bankAccountRepo.findByIbanOrderByIban("DE02200505501015871393").get(0);
final var location = RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"debitorUuid": "%s",
"bankAccountUuid": "%s",
"reference": "temp ref A",
"validFrom": "2022-10-13"
}
""".formatted(givenDebitor.getUuid(), givenBankAccount.getUuid()))
.port(port)
.when()
.post("http://localhost/api/hs/office/sepamandates")
.then().log().all().assertThat()
.statusCode(201)
.contentType(ContentType.JSON)
.body("uuid", isUuidValid())
.body("debitor.partner.person.tradeName", is("Third OHG"))
.body("bankAccount.iban", is("DE02200505501015871393"))
.body("reference", is("temp ref A"))
.body("validFrom", is("2022-10-13"))
.body("validTo", equalTo(null))
.header("Location", startsWith("http://localhost"))
.extract().header("Location"); // @formatter:on
// finally, the new sepaMandate can be accessed under the generated UUID
final var newUserUuid = toCleanup(UUID.fromString(
location.substring(location.lastIndexOf('/') + 1)));
assertThat(newUserUuid).isNotNull();
}
// TODO.test: move validation tests to a ...WebMvcTest
@Test
void globalAdmin_canNotAddSepaMandateWhenDebitorUuidIsMissing() {
context.define("superuser-alex@hostsharing.net");
final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("Third").get(0);
final var givenBankAccount = bankAccountRepo.findByIbanOrderByIban("DE02200505501015871393").get(0);
final var location = RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"bankAccountUuid": "%s",
"reference": "temp ref A",
"validFrom": "2022-10-13"
}
""".formatted(givenBankAccount.getUuid()))
.port(port)
.when()
.post("http://localhost/api/hs/office/sepamandates")
.then().assertThat()
.statusCode(400); // @formatter:on
}
@Test
void globalAdmin_canNotAddSepaMandate_ifBankAccountDoesNotExist() {
context.define("superuser-alex@hostsharing.net");
final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("Third").get(0);
final var givenBankAccountUuid = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6");
final var location = RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"debitorUuid": "%s",
"bankAccountUuid": "%s",
"reference": "temp ref A",
"validFrom": "2022-10-13",
"validTo": "2024-12-31"
}
""".formatted(givenDebitor.getUuid(), givenBankAccountUuid))
.port(port)
.when()
.post("http://localhost/api/hs/office/sepamandates")
.then().log().all().assertThat()
.statusCode(400)
.body("message", is("Unable to find BankAccount with uuid 3fa85f64-5717-4562-b3fc-2c963f66afa6"));
// @formatter:on
}
@Test
void globalAdmin_canNotAddSepaMandate_ifPersonDoesNotExist() {
context.define("superuser-alex@hostsharing.net");
final var givenDebitorUuid = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6");
final var givenBankAccount = bankAccountRepo.findByIbanOrderByIban("DE02200505501015871393").get(0);
final var location = RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"debitorUuid": "%s",
"bankAccountUuid": "%s",
"reference": "temp ref A",
"validFrom": "2022-10-13",
"validTo": "2024-12-31"
}
""".formatted(givenDebitorUuid, givenBankAccount.getUuid()))
.port(port)
.when()
.post("http://localhost/api/hs/office/sepamandates")
.then().log().all().assertThat()
.statusCode(400)
.body("message", is("Unable to find Debitor with uuid 3fa85f64-5717-4562-b3fc-2c963f66afa6"));
// @formatter:on
}
}
@Nested
@Accepts({ "SepaMandate:R(Read)" })
class GetSepaMandate {
@Test
void globalAdmin_canGetArbitrarySepaMandate() {
context.define("superuser-alex@hostsharing.net");
final var givenSepaMandateUuid = sepaMandateRepo.findSepaMandateByOptionalIban("DE02120300000000202051")
.get(0)
.getUuid();
RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.port(port)
.when()
.get("http://localhost/api/hs/office/sepamandates/" + givenSepaMandateUuid)
.then().log().body().assertThat()
.statusCode(200)
.contentType("application/json")
.body("", lenientlyEquals("""
{
"debitor": {
"debitorNumber": 10001,
"billingContact": { "label": "first contact" }
},
"bankAccount": {
"holder": "First GmbH",
"iban": "DE02120300000000202051"
},
"reference": "refFirstGmbH",
"validFrom": "2022-10-01",
"validTo": "2026-12-31"
}
""")); // @formatter:on
}
@Test
@Accepts({ "SepaMandate:X(Access Control)" })
void normalUser_canNotGetUnrelatedSepaMandate() {
context.define("superuser-alex@hostsharing.net");
final var givenSepaMandateUuid = sepaMandateRepo.findSepaMandateByOptionalIban("DE02120300000000202051")
.get(0)
.getUuid();
RestAssured // @formatter:off
.given()
.header("current-user", "selfregistered-user-drew@hostsharing.org")
.port(port)
.when()
.get("http://localhost/api/hs/office/sepamandates/" + givenSepaMandateUuid)
.then().log().body().assertThat()
.statusCode(404); // @formatter:on
}
@Test
@Accepts({ "SepaMandate:X(Access Control)" })
void bankAccountAdminUser_canGetRelatedSepaMandate() {
context.define("superuser-alex@hostsharing.net");
final var givenSepaMandateUuid = sepaMandateRepo.findSepaMandateByOptionalIban("DE02120300000000202051")
.get(0)
.getUuid();
RestAssured // @formatter:off
.given()
.header("current-user", "bankaccount-admin@FirstGmbH.example.com")
.port(port)
.when()
.get("http://localhost/api/hs/office/sepamandates/" + givenSepaMandateUuid)
.then().log().body().assertThat()
.statusCode(200)
.contentType("application/json")
.body("", lenientlyEquals("""
{
"debitor": {
"debitorNumber": 10001,
"billingContact": { "label": "first contact" }
},
"bankAccount": {
"holder": "First GmbH",
"iban": "DE02120300000000202051"
},
"reference": "refFirstGmbH",
"validFrom": "2022-10-01",
"validTo": "2026-12-31"
}
""")); // @formatter:on
}
}
@Nested
@Accepts({ "SepaMandate:U(Update)" })
class PatchSepaMandate {
@Test
void globalAdmin_canPatchValidToOfArbitrarySepaMandate() {
context.define("superuser-alex@hostsharing.net");
final var givenSepaMandate = givenSomeTemporarySepaMandateBessler();
final var location = RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"validTo": "2022-12-31"
}
""")
.port(port)
.when()
.patch("http://localhost/api/hs/office/sepamandates/" + givenSepaMandate.getUuid())
.then().log().all().assertThat()
.statusCode(200)
.contentType(ContentType.JSON)
.body("uuid", isUuidValid())
.body("debitor.partner.person.tradeName", is("First GmbH"))
.body("bankAccount.iban", is("DE02120300000000202051"))
.body("reference", is("temp ref X"))
.body("validFrom", is("2022-11-01"))
.body("validTo", is("2022-12-31"));
// @formatter:on
// finally, the sepaMandate is actually updated
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get()
.matches(mandate -> {
assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(10001: First GmbH)");
assertThat(mandate.getBankAccount().toShortString()).isEqualTo("First GmbH");
assertThat(mandate.getReference()).isEqualTo("temp ref X");
assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2023-01-01)");
return true;
});
}
@Test
void globalAdmin_canNotPatchReferenceOfArbitrarySepaMandate() {
context.define("superuser-alex@hostsharing.net");
final var givenSepaMandate = givenSomeTemporarySepaMandateBessler();
final var location = RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"reference": "new ref"
}
""")
.port(port)
.when()
.patch("http://localhost/api/hs/office/sepamandates/" + givenSepaMandate.getUuid())
.then().assertThat()
// TODO.impl: I'd prefer a 400,
// but OpenApi Spring Code Gen does not convert additonalProperties=false into a validation
.statusCode(200); // @formatter:on
// finally, the sepaMandate is actually updated
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get()
.matches(mandate -> {
assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,)");
return true;
});
}
}
@Nested
@Accepts({ "SepaMandate:D(Delete)" })
class DeleteSepaMandate {
@Test
void globalAdmin_canDeleteArbitrarySepaMandate() {
context.define("superuser-alex@hostsharing.net");
final var givenSepaMandate = givenSomeTemporarySepaMandateBessler();
RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.port(port)
.when()
.delete("http://localhost/api/hs/office/sepamandates/" + givenSepaMandate.getUuid())
.then().log().body().assertThat()
.statusCode(204); // @formatter:on
// then the given sepaMandate is gone
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isEmpty();
}
@Test
@Accepts({ "SepaMandate:X(Access Control)" })
void bankAccountAdminUser_canNotDeleteRelatedSepaMandate() {
context.define("superuser-alex@hostsharing.net");
final var givenSepaMandate = givenSomeTemporarySepaMandateBessler();
assertThat(givenSepaMandate.getReference()).isEqualTo("temp ref X");
RestAssured // @formatter:off
.given()
.header("current-user", "bankaccount-admin@FirstGmbH.example.com")
.port(port)
.when()
.delete("http://localhost/api/hs/office/sepamandates/" + givenSepaMandate.getUuid())
.then().log().body().assertThat()
.statusCode(403); // @formatter:on
// then the given sepaMandate is still there
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isNotEmpty();
}
@Test
@Accepts({ "SepaMandate:X(Access Control)" })
void normalUser_canNotDeleteUnrelatedSepaMandate() {
context.define("superuser-alex@hostsharing.net");
final var givenSepaMandate = givenSomeTemporarySepaMandateBessler();
assertThat(givenSepaMandate.getReference()).isEqualTo("temp ref X");
RestAssured // @formatter:off
.given()
.header("current-user", "selfregistered-user-drew@hostsharing.org")
.port(port)
.when()
.delete("http://localhost/api/hs/office/sepamandates/" + givenSepaMandate.getUuid())
.then().log().body().assertThat()
.statusCode(404); // @formatter:on
// then the given sepaMandate is still there
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isNotEmpty();
}
}
private HsOfficeSepaMandateEntity givenSomeTemporarySepaMandateBessler() {
return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0);
final var givenBankAccount = bankAccountRepo.findByOptionalHolderLike("First").get(0);
final var newSepaMandate = HsOfficeSepaMandateEntity.builder()
.uuid(UUID.randomUUID())
.debitor(givenDebitor)
.bankAccount(givenBankAccount)
.reference("temp ref X")
.validity(Range.closedOpen(
LocalDate.parse("2022-11-01"), LocalDate.parse("2023-03-31")))
.build();
toCleanup(newSepaMandate.getUuid());
return sepaMandateRepo.save(newSepaMandate);
}).assertSuccessful().returnedValue();
}
private UUID toCleanup(final UUID tempSepaMandateUuid) {
tempSepaMandateUuids.add(tempSepaMandateUuid);
return tempSepaMandateUuid;
}
@AfterEach
void cleanup() {
tempSepaMandateUuids.forEach(uuid -> {
jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net", null);
System.out.println("DELETING temporary sepaMandate: " + uuid);
final var count = sepaMandateRepo.deleteByUuid(uuid);
System.out.println("DELETED temporary sepaMandate: " + uuid + (count > 0 ? " successful" : " failed"));
});
});
}
}

View File

@ -176,7 +176,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTest {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
// when // when
final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null);
// then // then
allTheseSepaMandatesAreReturned( allTheseSepaMandatesAreReturned(
@ -192,7 +192,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTest {
context("bankaccount-admin@FirstGmbH.example.com"); context("bankaccount-admin@FirstGmbH.example.com");
// when: // when:
final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null);
// then: // then:
exactlyTheseSepaMandatesAreReturned( exactlyTheseSepaMandatesAreReturned(
@ -210,7 +210,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTest {
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
// when // when
final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null);
// then // then
exactlyTheseSepaMandatesAreReturned( exactlyTheseSepaMandatesAreReturned(
@ -226,7 +226,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTest {
context("bankaccount-admin@ThirdOHG.example.com"); context("bankaccount-admin@ThirdOHG.example.com");
// when // when
final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null);
// then // then
exactlyTheseSepaMandatesAreReturned( exactlyTheseSepaMandatesAreReturned(