hs-office-coopshares: add non-negative validation

This commit is contained in:
Michael Hoennig 2022-10-20 14:08:19 +02:00
parent 22bca20c49
commit dc0835fa25
6 changed files with 95 additions and 33 deletions

View File

@ -26,7 +26,6 @@ import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOffic
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION;
@RestController @RestController
public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi { public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopSharesApi {
@Autowired @Autowired
@ -71,7 +70,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
final var uri = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/office/CoopSharesTransactions/{id}") .path("/api/hs/office/coopsharestransactions/{id}")
.buildAndExpand(entityToSave.getUuid()) .buildAndExpand(entityToSave.getUuid())
.toUri(); .toUri();
final var mapped = map(saved, HsOfficeCoopSharesTransactionResource.class); final var mapped = map(saved, HsOfficeCoopSharesTransactionResource.class);
@ -82,7 +81,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
final var violations = new ArrayList<String>(); final var violations = new ArrayList<String>();
validateSubscriptionTransaction(requestBody, violations); validateSubscriptionTransaction(requestBody, violations);
validateCancellationTransaction(requestBody, violations); validateCancellationTransaction(requestBody, violations);
validateSharesCount(requestBody, violations); validateshareCount(requestBody, violations);
if (violations.size() > 0) { if (violations.size() > 0) {
throw new ValidationException("[" + join(", ", violations) + "]"); throw new ValidationException("[" + join(", ", violations) + "]");
} }
@ -92,9 +91,9 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
final HsOfficeCoopSharesTransactionInsertResource requestBody, final HsOfficeCoopSharesTransactionInsertResource requestBody,
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (requestBody.getTransactionType() == SUBSCRIPTION if (requestBody.getTransactionType() == SUBSCRIPTION
&& requestBody.getSharesCount() < 0) { && requestBody.getShareCount() < 0) {
violations.add("for %s, sharesCount must be positive but is \"%d\"".formatted( violations.add("for %s, shareCount must be positive but is \"%d\"".formatted(
requestBody.getTransactionType(), requestBody.getSharesCount())); requestBody.getTransactionType(), requestBody.getShareCount()));
} }
} }
@ -102,18 +101,18 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
final HsOfficeCoopSharesTransactionInsertResource requestBody, final HsOfficeCoopSharesTransactionInsertResource requestBody,
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (requestBody.getTransactionType() == CANCELLATION if (requestBody.getTransactionType() == CANCELLATION
&& requestBody.getSharesCount() > 0) { && requestBody.getShareCount() > 0) {
violations.add("for %s, sharesCount must be negative but is \"%d\"".formatted( violations.add("for %s, shareCount must be negative but is \"%d\"".formatted(
requestBody.getTransactionType(), requestBody.getSharesCount())); requestBody.getTransactionType(), requestBody.getShareCount()));
} }
} }
private static void validateSharesCount( private static void validateshareCount(
final HsOfficeCoopSharesTransactionInsertResource requestBody, final HsOfficeCoopSharesTransactionInsertResource requestBody,
final ArrayList<String> violations) { final ArrayList<String> violations) {
if (requestBody.getSharesCount() == 0) { if (requestBody.getShareCount() == 0) {
violations.add("sharesCount must not be 0 but is \"%d\"".formatted( violations.add("shareCount must not be 0 but is \"%d\"".formatted(
requestBody.getSharesCount())); requestBody.getShareCount()));
} }
} }

View File

@ -18,7 +18,7 @@ components:
format: uuid format: uuid
transactionType: transactionType:
$ref: '#/components/schemas/HsOfficeCoopSharesTransactionType' $ref: '#/components/schemas/HsOfficeCoopSharesTransactionType'
sharesCount: shareCount:
type: integer type: integer
valueDate: valueDate:
type: string type: string
@ -37,7 +37,7 @@ components:
nullable: false nullable: false
transactionType: transactionType:
$ref: '#/components/schemas/HsOfficeCoopSharesTransactionType' $ref: '#/components/schemas/HsOfficeCoopSharesTransactionType'
sharesCount: shareCount:
type: integer type: integer
valueDate: valueDate:
type: string type: string
@ -51,7 +51,7 @@ components:
required: required:
- membershipUuid - membershipUuid
- transactionType - transactionType
- sharesCount - shareCount
- valueDate - valueDate
- reference - reference
additionalProperties: false additionalProperties: false

View File

@ -20,6 +20,33 @@ create table if not exists hs_office_coopsharestransaction
); );
--// --//
-- ============================================================================
--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:--// --changeset hs-office-coopshares-MAIN-TABLE-JOURNAL:1 endDelimiter:--//

View File

@ -92,21 +92,21 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest {
[ [
{ {
"transactionType": "SUBSCRIPTION", "transactionType": "SUBSCRIPTION",
"sharesCount": 4, "shareCount": 4,
"valueDate": "2010-03-15", "valueDate": "2010-03-15",
"reference": "ref 10002-1", "reference": "ref 10002-1",
"comment": "initial subscription" "comment": "initial subscription"
}, },
{ {
"transactionType": "CANCELLATION", "transactionType": "CANCELLATION",
"sharesCount": -2, "shareCount": -2,
"valueDate": "2021-09-01", "valueDate": "2021-09-01",
"reference": "ref 10002-2", "reference": "ref 10002-2",
"comment": "cancelling some" "comment": "cancelling some"
}, },
{ {
"transactionType": "ADJUSTMENT", "transactionType": "ADJUSTMENT",
"sharesCount": 2, "shareCount": 2,
"valueDate": "2022-10-20", "valueDate": "2022-10-20",
"reference": "ref 10002-3", "reference": "ref 10002-3",
"comment": "some adjustment" "comment": "some adjustment"
@ -136,7 +136,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest {
[ [
{ {
"transactionType": "CANCELLATION", "transactionType": "CANCELLATION",
"sharesCount": -2, "shareCount": -2,
"valueDate": "2021-09-01", "valueDate": "2021-09-01",
"reference": "ref 10002-2", "reference": "ref 10002-2",
"comment": "cancelling some" "comment": "cancelling some"
@ -165,7 +165,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest {
{ {
"membershipUuid": "%s", "membershipUuid": "%s",
"transactionType": "SUBSCRIPTION", "transactionType": "SUBSCRIPTION",
"sharesCount": 8, "shareCount": 8,
"valueDate": "2022-10-13", "valueDate": "2022-10-13",
"reference": "temp ref A", "reference": "temp ref A",
"comment": "just some test coop shares transaction" "comment": "just some test coop shares transaction"
@ -181,7 +181,7 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest {
.body("", lenientlyEquals(""" .body("", lenientlyEquals("""
{ {
"transactionType": "SUBSCRIPTION", "transactionType": "SUBSCRIPTION",
"sharesCount": 0, "shareCount": 8,
"valueDate": "2022-10-13", "valueDate": "2022-10-13",
"reference": "temp ref A", "reference": "temp ref A",
"comment": "just some test coop shares transaction" "comment": "just some test coop shares transaction"
@ -195,6 +195,42 @@ class HsOfficeCoopSharesTransactionControllerAcceptanceTest {
location.substring(location.lastIndexOf('/') + 1)); location.substring(location.lastIndexOf('/') + 1));
assertThat(newUserUuid).isNotNull(); 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 @BeforeEach

View File

@ -35,7 +35,7 @@ class HsOfficeCoopSharesTransactionControllerRestTest {
{ {
"membershipUuid": "%s", "membershipUuid": "%s",
"transactionType": "SUBSCRIPTION", "transactionType": "SUBSCRIPTION",
"sharesCount": 8, "shareCount": 8,
"valueDate": "2022-10-13", "valueDate": "2022-10-13",
"reference": "valid reference", "reference": "valid reference",
"comment": "valid comment" "comment": "valid comment"
@ -58,20 +58,20 @@ class HsOfficeCoopSharesTransactionControllerRestTest {
SHARES_COUNT_FOR_SUBSCRIPTION_MUST_BE_POSITIVE( SHARES_COUNT_FOR_SUBSCRIPTION_MUST_BE_POSITIVE(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "SUBSCRIPTION") .with("transactionType", "SUBSCRIPTION")
.with("sharesCount", -1), .with("shareCount", -1),
"[for SUBSCRIPTION, sharesCount must be positive but is \"-1\"]"), "[for SUBSCRIPTION, shareCount must be positive but is \"-1\"]"),
SHARES_COUNT_FOR_CANCELLATION_MUST_BE_NEGATIVE( SHARES_COUNT_FOR_CANCELLATION_MUST_BE_NEGATIVE(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "CANCELLATION") .with("transactionType", "CANCELLATION")
.with("sharesCount", 1), .with("shareCount", 1),
"[for CANCELLATION, sharesCount must be negative but is \"1\"]"), "[for CANCELLATION, shareCount must be negative but is \"1\"]"),
SHARES_COUNT_MUST_NOT_BE_NULL( SHARES_COUNT_MUST_NOT_BE_NULL(
requestBody -> requestBody requestBody -> requestBody
.with("transactionType", "ADJUSTMENT") .with("transactionType", "ADJUSTMENT")
.with("sharesCount", 0), .with("shareCount", 0),
"[sharesCount must not be 0 but is \"0\"]"), "[shareCount must not be 0 but is \"0\"]"),
REFERENCE_MISSING( REFERENCE_MISSING(
requestBody -> requestBody.without("reference"), requestBody -> requestBody.without("reference"),

View File

@ -9,7 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class HsOfficeCoopSharesTransactionEntityTest { class HsOfficeCoopSharesTransactionEntityTest {
final HsOfficeCoopSharesTransactionEntity givenSepaMandate = HsOfficeCoopSharesTransactionEntity.builder() final HsOfficeCoopSharesTransactionEntity givenCoopSharesTransaction = HsOfficeCoopSharesTransactionEntity.builder()
.membership(testMembership) .membership(testMembership)
.reference("some-ref") .reference("some-ref")
.valueDate(LocalDate.parse("2020-01-01")) .valueDate(LocalDate.parse("2020-01-01"))
@ -19,14 +19,14 @@ class HsOfficeCoopSharesTransactionEntityTest {
@Test @Test
void toStringContainsAlmostAllPropertiesAccount() { void toStringContainsAlmostAllPropertiesAccount() {
final var result = givenSepaMandate.toString(); final var result = givenCoopSharesTransaction.toString();
assertThat(result).isEqualTo("CoopShareTransaction(300001, 2020-01-01, SUBSCRIPTION, 4, some-ref)"); assertThat(result).isEqualTo("CoopShareTransaction(300001, 2020-01-01, SUBSCRIPTION, 4, some-ref)");
} }
@Test @Test
void toShortStringContainsOnlyMemberNumberAndSharesCountOnly() { void toShortStringContainsOnlyMemberNumberAndshareCountOnly() {
final var result = givenSepaMandate.toShortString(); final var result = givenCoopSharesTransaction.toShortString();
assertThat(result).isEqualTo("300001+4"); assertThat(result).isEqualTo("300001+4");
} }