src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepository.java
@@ -15,9 +15,12 @@ SELECT c FROM HsOfficeBankAccountEntity c WHERE :holder is null OR lower(c.holder) like lower(concat(:holder, '%')) ORDER BY c.holder """) List<HsOfficeBankAccountEntity> findByOptionalHolderLike(String holder); List<HsOfficeBankAccountEntity> findByIbanOrderByIban(String iban); HsOfficeBankAccountEntity save(final HsOfficeBankAccountEntity entity); int deleteByUuid(final UUID uuid); src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateController.java
New 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())); }; } src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepository.java
@@ -15,8 +15,9 @@ SELECT mandate FROM HsOfficeSepaMandateEntity mandate WHERE :iban is null 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); src/main/resources/api-definition/hs-office/api-mappings.yaml
@@ -3,6 +3,7 @@ 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 @@ 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 @@ 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 src/main/resources/api-definition/hs-office/hs-office-sepamandate-schemas.yaml
New 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 src/main/resources/api-definition/hs-office/hs-office-sepamandates-with-uuid.yaml
New 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' src/main/resources/api-definition/hs-office/hs-office-sepamandates.yaml
New 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' src/main/resources/api-definition/hs-office/hs-office.yaml
@@ -60,3 +60,12 @@ /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" src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java
@@ -36,7 +36,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTest { @Autowired HsOfficeBankAccountRepository bankaccountRepo; HsOfficeBankAccountRepository bankAccountRepo; @Autowired RawRbacRoleRepository rawRoleRepo; @@ -60,37 +60,37 @@ 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 @@ 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 @@ } 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 @@ // 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 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 @@ // 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 @@ // 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 @@ Supplier<HsOfficeBankAccountEntity> entitySupplier) { return jpaAttempt.transacted(() -> { context(createdByUser); return bankaccountRepo.save(entitySupplier.get()); return bankAccountRepo.save(entitySupplier.get()); }).assertSuccessful().returnedValue(); } @@ -316,10 +303,10 @@ @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()); }); } src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateControllerAcceptanceTest.java
New 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")); }); }); } } src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java
@@ -176,7 +176,7 @@ context("superuser-alex@hostsharing.net"); // when final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null); // then allTheseSepaMandatesAreReturned( @@ -192,7 +192,7 @@ 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 @@ context("superuser-alex@hostsharing.net"); // when final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null); // then exactlyTheseSepaMandatesAreReturned( @@ -226,7 +226,7 @@ context("bankaccount-admin@ThirdOHG.example.com"); // when final var result = sepaMandateRepo.findSepaMandateByOptionalIBAN(null); final var result = sepaMandateRepo.findSepaMandateByOptionalIban(null); // then exactlyTheseSepaMandatesAreReturned(