Michael Hoennig
2022-10-20 dc0835fa25dbb8904bbe789bd1789a029405b763
hs-office-coopshares: add non-negative validation
6 files modified
128 ■■■■ changed files
src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java 25 ●●●● patch | view | raw | blame | history
src/main/resources/api-definition/hs-office/hs-office-coopshares-schemas.yaml 6 ●●●● patch | view | raw | blame | history
src/main/resources/db/changelog/310-hs-office-coopshares.sql 27 ●●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java 48 ●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java 14 ●●●● patch | view | raw | blame | history
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityTest.java 8 ●●●● patch | view | raw | blame | history
src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java
@@ -26,7 +26,6 @@
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION;
@RestController
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
    @Autowired
@@ -71,7 +70,7 @@
        final var uri =
                MvcUriComponentsBuilder.fromController(getClass())
                        .path("/api/hs/office/CoopSharesTransactions/{id}")
                        .path("/api/hs/office/coopsharestransactions/{id}")
                        .buildAndExpand(entityToSave.getUuid())
                        .toUri();
        final var mapped = map(saved, HsOfficeCoopSharesTransactionResource.class);
@@ -82,7 +81,7 @@
        final var violations = new ArrayList<String>();
        validateSubscriptionTransaction(requestBody, violations);
        validateCancellationTransaction(requestBody, violations);
        validateSharesCount(requestBody, violations);
        validateshareCount(requestBody, violations);
        if (violations.size() > 0) {
            throw new ValidationException("[" + join(", ", violations) + "]");
        }
@@ -92,9 +91,9 @@
            final HsOfficeCoopSharesTransactionInsertResource requestBody,
            final ArrayList<String> violations) {
        if (requestBody.getTransactionType() == SUBSCRIPTION
                && requestBody.getSharesCount() < 0) {
            violations.add("for %s, sharesCount must be positive but is \"%d\"".formatted(
                    requestBody.getTransactionType(), requestBody.getSharesCount()));
                && requestBody.getShareCount() < 0) {
            violations.add("for %s, shareCount must be positive but is \"%d\"".formatted(
                    requestBody.getTransactionType(), requestBody.getShareCount()));
        }
    }
@@ -102,18 +101,18 @@
            final HsOfficeCoopSharesTransactionInsertResource requestBody,
            final ArrayList<String> violations) {
        if (requestBody.getTransactionType() == CANCELLATION
                && requestBody.getSharesCount() > 0) {
            violations.add("for %s, sharesCount must be negative but is \"%d\"".formatted(
                    requestBody.getTransactionType(), requestBody.getSharesCount()));
                && requestBody.getShareCount() > 0) {
            violations.add("for %s, shareCount must be negative but is \"%d\"".formatted(
                    requestBody.getTransactionType(), requestBody.getShareCount()));
        }
    }
    private static void validateSharesCount(
    private static void validateshareCount(
            final HsOfficeCoopSharesTransactionInsertResource requestBody,
            final ArrayList<String> violations) {
        if (requestBody.getSharesCount() == 0) {
            violations.add("sharesCount must not be 0 but is \"%d\"".formatted(
                    requestBody.getSharesCount()));
        if (requestBody.getShareCount() == 0) {
            violations.add("shareCount must not be 0 but is \"%d\"".formatted(
                    requestBody.getShareCount()));
        }
    }
src/main/resources/api-definition/hs-office/hs-office-coopshares-schemas.yaml
@@ -18,7 +18,7 @@
                    format: uuid
                transactionType:
                    $ref: '#/components/schemas/HsOfficeCoopSharesTransactionType'
                sharesCount:
                shareCount:
                    type: integer
                valueDate:
                   type: string
@@ -37,7 +37,7 @@
                    nullable: false
                transactionType:
                    $ref: '#/components/schemas/HsOfficeCoopSharesTransactionType'
                sharesCount:
                shareCount:
                    type: integer
                valueDate:
                    type: string
@@ -51,7 +51,7 @@
            required:
                - membershipUuid
                - transactionType
                - sharesCount
                - shareCount
                - valueDate
                - reference
            additionalProperties: false
src/main/resources/db/changelog/310-hs-office-coopshares.sql
@@ -20,6 +20,33 @@
);
--//
-- ============================================================================
--changeset hs-office-coopshares-SHARE-COUNT-CONSTRAINT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace function checkSharesByMembershipUuid(forMembershipUuid UUID, newShareCount integer)
returns boolean
language plpgsql as $$
declare
    currentShareCount integer;
    totalShareCount integer;
begin
    select sum(cst.shareCount)
    from hs_office_coopsharestransaction cst
    where cst.membershipUuid = forMembershipUuid
    into currentShareCount;
    totalShareCount := currentShareCount + newShareCount;
    if totalShareCount < 0 then
        raise exception '[400] coop shares transaction would result in a negative number of shares';
    end if;
    return true;
end; $$;
alter table hs_office_coopsharestransaction
    add constraint hs_office_coopshares_positive
        check ( checkSharesByMembershipUuid(membershipUuid, shareCount) );
--//
-- ============================================================================
--changeset hs-office-coopshares-MAIN-TABLE-JOURNAL:1 endDelimiter:--//
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerAcceptanceTest.java
@@ -92,21 +92,21 @@
                        [
                            {
                                "transactionType": "SUBSCRIPTION",
                                "sharesCount": 4,
                                "shareCount": 4,
                                "valueDate": "2010-03-15",
                                "reference": "ref 10002-1",
                                "comment": "initial subscription"
                            },
                            {
                                "transactionType": "CANCELLATION",
                                "sharesCount": -2,
                                "shareCount": -2,
                                "valueDate": "2021-09-01",
                                "reference": "ref 10002-2",
                                "comment": "cancelling some"
                            },
                            {
                                "transactionType": "ADJUSTMENT",
                                "sharesCount": 2,
                                "shareCount": 2,
                                "valueDate": "2022-10-20",
                                "reference": "ref 10002-3",
                                "comment": "some adjustment"
@@ -136,7 +136,7 @@
                        [
                            {
                                "transactionType": "CANCELLATION",
                                "sharesCount": -2,
                                "shareCount": -2,
                                "valueDate": "2021-09-01",
                                "reference": "ref 10002-2",
                                "comment": "cancelling some"
@@ -165,7 +165,7 @@
                               {
                                   "membershipUuid": "%s",
                                   "transactionType": "SUBSCRIPTION",
                                   "sharesCount": 8,
                                   "shareCount": 8,
                                   "valueDate": "2022-10-13",
                                   "reference": "temp ref A",
                                   "comment": "just some test coop shares transaction" 
@@ -181,7 +181,7 @@
                        .body("", lenientlyEquals("""
                            {
                                "transactionType": "SUBSCRIPTION",
                                "sharesCount": 0,
                                "shareCount": 8,
                                "valueDate": "2022-10-13",
                                "reference": "temp ref A",
                                "comment": "just some test coop shares transaction"
@@ -195,6 +195,42 @@
                    location.substring(location.lastIndexOf('/') + 1));
            assertThat(newUserUuid).isNotNull();
        }
        @Test
        void globalAdmin_canNotCancelMoreSharesThanCurrentlySubscribed() {
            context.define("superuser-alex@hostsharing.net");
            final var givenMembership = membershipRepo.findMembershipsByOptionalPartnerUuidAndOptionalMemberNumber(null, 10001)
                    .get(0);
            final var location = RestAssured // @formatter:off
                .given()
                    .header("current-user", "superuser-alex@hostsharing.net")
                    .contentType(ContentType.JSON)
                    .body("""
                           {
                               "membershipUuid": "%s",
                               "transactionType": "CANCELLATION",
                               "shareCount": -80,
                               "valueDate": "2022-10-13",
                               "reference": "temp ref X",
                               "comment": "just some test coop shares transaction"
                             }
                            """.formatted(givenMembership.getUuid()))
                    .port(port)
                .when()
                    .post("http://localhost/api/hs/office/coopsharestransactions")
                .then().log().all().assertThat()
                    .statusCode(400)
                    .contentType(ContentType.JSON)
                    .body("", lenientlyEquals("""
                            {
                                 "status": 400,
                                 "error": "Bad Request",
                                 "message": "ERROR: [400] coop shares transaction would result in a negative number of shares"
                             }
                        """));  // @formatter:on
        }
    }
    @BeforeEach
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionControllerRestTest.java
@@ -35,7 +35,7 @@
            {
               "membershipUuid": "%s",
               "transactionType": "SUBSCRIPTION",
               "sharesCount": 8,
               "shareCount": 8,
               "valueDate": "2022-10-13",
               "reference": "valid reference",
               "comment": "valid comment"
@@ -58,20 +58,20 @@
        SHARES_COUNT_FOR_SUBSCRIPTION_MUST_BE_POSITIVE(
                requestBody -> requestBody
                        .with("transactionType", "SUBSCRIPTION")
                        .with("sharesCount", -1),
                "[for SUBSCRIPTION, sharesCount must be positive but is \"-1\"]"),
                        .with("shareCount", -1),
                "[for SUBSCRIPTION, shareCount must be positive but is \"-1\"]"),
        SHARES_COUNT_FOR_CANCELLATION_MUST_BE_NEGATIVE(
                requestBody -> requestBody
                        .with("transactionType", "CANCELLATION")
                        .with("sharesCount", 1),
                "[for CANCELLATION, sharesCount must be negative but is \"1\"]"),
                        .with("shareCount", 1),
                "[for CANCELLATION, shareCount must be negative but is \"1\"]"),
        SHARES_COUNT_MUST_NOT_BE_NULL(
                requestBody -> requestBody
                        .with("transactionType", "ADJUSTMENT")
                        .with("sharesCount", 0),
                "[sharesCount must not be 0 but is \"0\"]"),
                        .with("shareCount", 0),
                "[shareCount must not be 0 but is \"0\"]"),
        REFERENCE_MISSING(
                requestBody -> requestBody.without("reference"),
src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionEntityTest.java
@@ -9,7 +9,7 @@
class HsOfficeCoopSharesTransactionEntityTest {
    final HsOfficeCoopSharesTransactionEntity givenSepaMandate = HsOfficeCoopSharesTransactionEntity.builder()
    final HsOfficeCoopSharesTransactionEntity givenCoopSharesTransaction = HsOfficeCoopSharesTransactionEntity.builder()
            .membership(testMembership)
            .reference("some-ref")
            .valueDate(LocalDate.parse("2020-01-01"))
@@ -19,14 +19,14 @@
    @Test
    void toStringContainsAlmostAllPropertiesAccount() {
        final var result = givenSepaMandate.toString();
        final var result = givenCoopSharesTransaction.toString();
        assertThat(result).isEqualTo("CoopShareTransaction(300001, 2020-01-01, SUBSCRIPTION, 4, some-ref)");
    }
    @Test
    void toShortStringContainsOnlyMemberNumberAndSharesCountOnly() {
        final var result = givenSepaMandate.toShortString();
    void toShortStringContainsOnlyMemberNumberAndshareCountOnly() {
        final var result = givenCoopSharesTransaction.toShortString();
        assertThat(result).isEqualTo("300001+4");
    }