From e305c3c935077d2e7409402c7a9a64aa9f14bd49 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 15 Oct 2022 15:32:32 +0200 Subject: [PATCH] add SEPA-mandate API+Controller --- .../HsOfficeBankAccountRepository.java | 3 + .../HsOfficeSepaMandateController.java | 151 ++++++ .../HsOfficeSepaMandateRepository.java | 3 +- .../hs-office/api-mappings.yaml | 4 + .../hs-office-sepamandate-schemas.yaml | 60 ++ .../hs-office-sepamandates-with-uuid.yaml | 83 +++ .../hs-office/hs-office-sepamandates.yaml | 57 ++ .../api-definition/hs-office/hs-office.yaml | 9 + ...eBankAccountRepositoryIntegrationTest.java | 71 +-- ...ceSepaMandateControllerAcceptanceTest.java | 511 ++++++++++++++++++ ...eSepaMandateRepositoryIntegrationTest.java | 8 +- 11 files changed, 913 insertions(+), 47 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java create mode 100644 src/main/resources/api-definition/hs-office/hs-office-sepamandate-schemas.yaml create mode 100644 src/main/resources/api-definition/hs-office/hs-office-sepamandates-with-uuid.yaml create mode 100644 src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepository.java index 19264a54..84e88949 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepository.java @@ -15,9 +15,12 @@ public interface HsOfficeBankAccountRepository extends Repository findByOptionalHolderLike(String holder); + List findByIbanOrderByIban(String iban); + HsOfficeBankAccountEntity save(final HsOfficeBankAccountEntity entity); int deleteByUuid(final UUID uuid); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java new file mode 100644 index 00000000..7dd7b159 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java @@ -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> 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 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 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 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 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 toPostgresDateRange( + final LocalDate validFrom, + final LocalDate validTo) { + return validTo != null + ? Range.closedOpen(validFrom, validTo.plusDays(1)) + : Range.closedInfinite(validFrom); + } + + final BiConsumer 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 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())); + }; +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepository.java index 1c7c16a2..d243a716 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepository.java @@ -15,8 +15,9 @@ public interface HsOfficeSepaMandateRepository extends Repository findSepaMandateByOptionalIBAN(String iban); + List findSepaMandateByOptionalIban(String iban); HsOfficeSepaMandateEntity save(final HsOfficeSepaMandateEntity entity); diff --git a/src/main/resources/api-definition/hs-office/api-mappings.yaml b/src/main/resources/api-definition/hs-office/api-mappings.yaml index efab5975..cc054e77 100644 --- a/src/main/resources/api-definition/hs-office/api-mappings.yaml +++ b/src/main/resources/api-definition/hs-office/api-mappings.yaml @@ -3,6 +3,7 @@ openapi-processor-mapping: v2 options: package-name: net.hostsharing.hsadminng.hs.office.generated.api.v1 model-name-suffix: Resource + bean-validation: true map: result: org.springframework.http.ResponseEntity @@ -10,6 +11,7 @@ map: types: - type: array => java.util.List - type: string:uuid => java.util.UUID + - type: string:format => java.lang.String paths: /api/hs/office/partners/{partnerUUID}: @@ -24,3 +26,5 @@ map: null: org.openapitools.jackson.nullable.JsonNullable /api/hs/office/debitors/{debitorUUID}: null: org.openapitools.jackson.nullable.JsonNullable + /api/hs/office/sepamandates/{debitorUUID}: + null: org.openapitools.jackson.nullable.JsonNullable diff --git a/src/main/resources/api-definition/hs-office/hs-office-sepamandate-schemas.yaml b/src/main/resources/api-definition/hs-office/hs-office-sepamandate-schemas.yaml new file mode 100644 index 00000000..e4189a0f --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-sepamandate-schemas.yaml @@ -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 diff --git a/src/main/resources/api-definition/hs-office/hs-office-sepamandates-with-uuid.yaml b/src/main/resources/api-definition/hs-office/hs-office-sepamandates-with-uuid.yaml new file mode 100644 index 00000000..4e21a9a2 --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-sepamandates-with-uuid.yaml @@ -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' diff --git a/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml b/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml new file mode 100644 index 00000000..08244629 --- /dev/null +++ b/src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml @@ -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' diff --git a/src/main/resources/api-definition/hs-office/hs-office.yaml b/src/main/resources/api-definition/hs-office/hs-office.yaml index cc7a51c2..f39ae8a0 100644 --- a/src/main/resources/api-definition/hs-office/hs-office.yaml +++ b/src/main/resources/api-definition/hs-office/hs-office.yaml @@ -60,3 +60,12 @@ paths: /api/hs/office/debitors/{debitorUUID}: $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" diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java index feb3c887..ba2846d8 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java @@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat; class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { @Autowired - HsOfficeBankAccountRepository bankaccountRepo; + HsOfficeBankAccountRepository bankAccountRepo; @Autowired RawRbacRoleRepository rawRoleRepo; @@ -60,37 +60,37 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { class CreateBankAccount { @Test - public void globalAdmin_withoutAssumedRole_canCreateNewBankAccount() { + public void globalAdmin_canCreateNewBankAccount() { // given context("superuser-alex@hostsharing.net"); - final var count = bankaccountRepo.count(); + final var count = bankAccountRepo.count(); // when - final var result = attempt(em, () -> bankaccountRepo.save( + final var result = attempt(em, () -> bankAccountRepo.save( hsOfficeBankAccount("some temp acc A", "DE37500105177419788228", ""))); // then result.assertSuccessful(); assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeBankAccountEntity::getUuid).isNotNull(); assertThatBankAccountIsPersisted(result.returnedValue()); - assertThat(bankaccountRepo.count()).isEqualTo(count + 1); + assertThat(bankAccountRepo.count()).isEqualTo(count + 1); } @Test public void arbitraryUser_canCreateNewBankAccount() { // given context("selfregistered-user-drew@hostsharing.org"); - final var count = bankaccountRepo.count(); + final var count = bankAccountRepo.count(); // when - final var result = attempt(em, () -> bankaccountRepo.save( + final var result = attempt(em, () -> bankAccountRepo.save( hsOfficeBankAccount("some temp acc B", "DE49500105174516484892", "INGDDEFFXXX"))); // then result.assertSuccessful(); assertThat(result.returnedValue()).isNotNull().extracting(HsOfficeBankAccountEntity::getUuid).isNotNull(); assertThatBankAccountIsPersisted(result.returnedValue()); - assertThat(bankaccountRepo.count()).isEqualTo(count + 1); + assertThat(bankAccountRepo.count()).isEqualTo(count + 1); } @Test @@ -101,7 +101,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { final var initialGrantNames = grantDisplaysOf(rawGrantRepo.findAll()); // when - attempt(em, () -> bankaccountRepo.save( + attempt(em, () -> bankAccountRepo.save( hsOfficeBankAccount("some temp acc C", "DE25500105176934832579", "INGDDEFFXXX")) ).assumeSuccessful(); @@ -131,21 +131,21 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { } 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); } } @Nested - class FindAllBankAccounts { + class ListBankAccounts { @Test - public void globalAdmin_withoutAssumedRole_canViewAllBankAccounts() { + public void globalAdmin_canViewAllBankAccounts() { // given context("superuser-alex@hostsharing.net"); // when - final var result = bankaccountRepo.findByOptionalHolderLike(null); + final var result = bankAccountRepo.findByOptionalHolderLike(null); // then allTheseBankAccountsAreReturned( @@ -167,45 +167,32 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { // when: context("selfregistered-user-drew@hostsharing.org"); - final var result = bankaccountRepo.findByOptionalHolderLike(null); + final var result = bankAccountRepo.findByOptionalHolderLike(null); // then: exactlyTheseBankAccountsAreReturned(result, givenBankAccount.getHolder()); } - } - - @Nested - class FindByLabelLike { @Test - public void globalAdmin_withoutAssumedRole_canViewAllBankAccounts() { + public void globalAdmin_canViewBankAccountsByIban() { // given context("superuser-alex@hostsharing.net", null); // when - final var result = bankaccountRepo.findByOptionalHolderLike(null); + final var result = bankAccountRepo.findByIbanOrderByIban("DE02120300000000202051"); // then - exactlyTheseBankAccountsAreReturned( - result, - "Anita Bessler", - "First GmbH", - "Fourth e.G.", - "Mel Bessler", - "Paul Winkler", - "Peter Smith", - "Second e.K.", - "Third OHG"); + exactlyTheseBankAccountsAreReturned(result, "First GmbH"); } @Test - public void arbitraryUser_withoutAssumedRole_canViewOnlyItsOwnBankAccount() { + public void arbitraryUser_canViewItsOwnBankAccount() { // given: final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); // when: context("selfregistered-user-drew@hostsharing.org"); - final var result = bankaccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); + final var result = bankAccountRepo.findByIbanOrderByIban(givenBankAccount.getIban()); // then: exactlyTheseBankAccountsAreReturned(result, givenBankAccount.getHolder()); @@ -216,7 +203,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { class DeleteByUuid { @Test - public void globalAdmin_withoutAssumedRole_canDeleteAnyBankAccount() { + public void globalAdmin_canDeleteAnyBankAccount() { // given context("superuser-alex@hostsharing.net", null); final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); @@ -224,33 +211,33 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { // when final var result = jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net", null); - bankaccountRepo.deleteByUuid(givenBankAccount.getUuid()); + bankAccountRepo.deleteByUuid(givenBankAccount.getUuid()); }); // then result.assertSuccessful(); assertThat(jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net", null); - return bankaccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); + return bankAccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); }).assertSuccessful().returnedValue()).hasSize(0); } @Test - public void arbitraryUser_withoutAssumedRole_canDeleteABankAccountCreatedByItself() { + public void arbitraryUser_canDeleteABankAccountCreatedByItself() { // given final var givenBankAccount = givenSomeTemporaryBankAccount("selfregistered-user-drew@hostsharing.org"); // when final var result = jpaAttempt.transacted(() -> { context("selfregistered-user-drew@hostsharing.org", null); - bankaccountRepo.deleteByUuid(givenBankAccount.getUuid()); + bankAccountRepo.deleteByUuid(givenBankAccount.getUuid()); }); // then result.assertSuccessful(); assertThat(jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net", null); - return bankaccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); + return bankAccountRepo.findByOptionalHolderLike(givenBankAccount.getHolder()); }).assertSuccessful().returnedValue()).hasSize(0); } @@ -269,7 +256,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { // when final var result = jpaAttempt.transacted(() -> { context("selfregistered-user-drew@hostsharing.org", null); - return bankaccountRepo.deleteByUuid(givenBankAccount.getUuid()); + return bankAccountRepo.deleteByUuid(givenBankAccount.getUuid()); }); // then @@ -289,7 +276,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { Supplier entitySupplier) { return jpaAttempt.transacted(() -> { context(createdByUser); - return bankaccountRepo.save(entitySupplier.get()); + return bankAccountRepo.save(entitySupplier.get()); }).assertSuccessful().returnedValue(); } @@ -316,10 +303,10 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { @AfterEach void cleanup() { 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 -> { System.out.println("DELETING temporary bankaccount: " + tempPerson.getHolder()); - bankaccountRepo.deleteByUuid(tempPerson.getUuid()); + bankAccountRepo.deleteByUuid(tempPerson.getUuid()); }); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java new file mode 100644 index 00000000..6046674b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java @@ -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 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")); + }); + }); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java index d89da20d..39f06512 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java @@ -176,7 +176,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTest { context("superuser-alex@hostsharing.net"); // when - final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); + final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null); // then allTheseSepaMandatesAreReturned( @@ -192,7 +192,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTest { context("bankaccount-admin@FirstGmbH.example.com"); // when: - final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); + final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null); // then: exactlyTheseSepaMandatesAreReturned( @@ -210,7 +210,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTest { context("superuser-alex@hostsharing.net"); // when - final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); + final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null); // then exactlyTheseSepaMandatesAreReturned( @@ -226,7 +226,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTest { context("bankaccount-admin@ThirdOHG.example.com"); // when - final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); + final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null); // then exactlyTheseSepaMandatesAreReturned(