1
0

hs-office-coopassets, no get API endpoints yet

This commit is contained in:
Michael Hoennig
2022-10-21 17:12:55 +02:00
parent 5ada0dae35
commit a39cf73cf0
18 changed files with 1314 additions and 1 deletions

View File

@@ -0,0 +1,118 @@
package net.hostsharing.hsadminng.hs.office.coopassets;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionResource;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
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.validation.Valid;
import javax.validation.ValidationException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static java.lang.String.join;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*;
import static net.hostsharing.hsadminng.mapper.Mapper.map;
@RestController
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {
@Autowired
private Context context;
@Autowired
private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> listCoopAssets(
final String currentUser,
final String assumedRoles,
final UUID membershipUuid,
final @DateTimeFormat(iso = ISO.DATE) LocalDate fromValueDate,
final @DateTimeFormat(iso = ISO.DATE) LocalDate toValueDate) {
context.define(currentUser, assumedRoles);
final var entities = coopAssetsTransactionRepo.findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange(
membershipUuid,
fromValueDate,
toValueDate);
final var resources = Mapper.mapList(entities, HsOfficeCoopAssetsTransactionResource.class);
return ResponseEntity.ok(resources);
}
@Override
@Transactional
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> addCoopAssetsTransaction(
final String currentUser,
final String assumedRoles,
@Valid final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
context.define(currentUser, assumedRoles);
validate(requestBody);
final var entityToSave = map(requestBody, HsOfficeCoopAssetsTransactionEntity.class);
entityToSave.setUuid(UUID.randomUUID());
final var saved = coopAssetsTransactionRepo.save(entityToSave);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/office/coopassetstransactions/{id}")
.buildAndExpand(entityToSave.getUuid())
.toUri();
final var mapped = map(saved, HsOfficeCoopAssetsTransactionResource.class);
return ResponseEntity.created(uri).body(mapped);
}
private void validate(final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
final var violations = new ArrayList<String>();
validateDebitTransaction(requestBody, violations);
validateCreditTransaction(requestBody, violations);
validateAssetValue(requestBody, violations);
if (violations.size() > 0) {
throw new ValidationException("[" + join(", ", violations) + "]");
}
}
private static void validateDebitTransaction(
final HsOfficeCoopAssetsTransactionInsertResource requestBody,
final ArrayList<String> violations) {
if (List.of(DEPOSIT, ADOPTION).contains(requestBody.getTransactionType())
&& requestBody.getAssetValue().signum() < 0) {
violations.add("for %s, assetValue must be positive but is \"%.2f\"".formatted(
requestBody.getTransactionType(), requestBody.getAssetValue()));
}
}
private static void validateCreditTransaction(
final HsOfficeCoopAssetsTransactionInsertResource requestBody,
final ArrayList<String> violations) {
if (List.of(DISBURSAL, TRANSFER, CLEARING, LOSS).contains(requestBody.getTransactionType())
&& requestBody.getAssetValue().signum() > 0) {
violations.add("for %s, assetValue must be negative but is \"%.2f\"".formatted(
requestBody.getTransactionType(), requestBody.getAssetValue()));
}
}
private static void validateAssetValue(
final HsOfficeCoopAssetsTransactionInsertResource requestBody,
final ArrayList<String> violations) {
if (requestBody.getAssetValue().signum() == 0) {
violations.add("assetValue must not be 0 but is \"%.2f\"".formatted(
requestBody.getAssetValue()));
}
}
}

View File

@@ -0,0 +1,75 @@
package net.hostsharing.hsadminng.hs.office.coopassets;
import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType;
import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import javax.persistence.*;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.time.LocalDate;
import java.util.UUID;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Entity
@Table(name = "hs_office_coopassetstransaction_rv")
@TypeDef(
name = "pgsql_enum",
typeClass = PostgreSQLEnumType.class
)
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("CoopAssetsTransaction")
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable {
private static Stringify<HsOfficeCoopAssetsTransactionEntity> stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class)
.withProp(e -> e.getMembership().getMemberNumber())
.withProp(HsOfficeCoopAssetsTransactionEntity::getValueDate)
.withProp(HsOfficeCoopAssetsTransactionEntity::getTransactionType)
.withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue)
.withProp(HsOfficeCoopAssetsTransactionEntity::getReference)
.withSeparator(", ")
.quotedValues(false);
private @Id UUID uuid;
@ManyToOne
@JoinColumn(name = "membershipuuid")
private HsOfficeMembershipEntity membership;
@Column(name = "transactiontype")
@Enumerated(EnumType.STRING)
@Type( type = "pgsql_enum" )
private HsOfficeCoopAssetsTransactionType transactionType;
@Column(name = "valuedate")
private LocalDate valueDate;
@Column(name = "assetvalue")
private BigDecimal assetValue;
@Column(name = "reference")
private String reference;
@Column(name = "comment")
private String comment;
@Override
public String toString() {
return stringify.apply(this);
}
@Override
public String toShortString() {
return membership.getMemberNumber() + new DecimalFormat("+0.00").format(assetValue);
}
}

View File

@@ -0,0 +1,29 @@
package net.hostsharing.hsadminng.hs.office.coopassets;
import net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsOfficeCoopAssetsTransactionRepository extends Repository<HsOfficeCoopAssetsTransactionEntity, UUID> {
Optional<HsOfficeCoopAssetsTransactionEntity> findByUuid(UUID id);
@Query("""
SELECT at FROM HsOfficeCoopAssetsTransactionEntity at
WHERE ( CAST(:membershipUuid AS org.hibernate.type.UUIDCharType) IS NULL OR at.membership.uuid = :membershipUuid)
AND ( CAST(:fromValueDate AS java.time.LocalDate) IS NULL OR (at.valueDate >= :fromValueDate))
AND ( CAST(:toValueDate AS java.time.LocalDate)IS NULL OR (at.valueDate <= :toValueDate))
ORDER BY at.membership.memberNumber, at.valueDate
""")
List<HsOfficeCoopAssetsTransactionEntity> findCoopAssetsTransactionByOptionalMembershipUuidAndDateRange(
UUID membershipUuid, LocalDate fromValueDate, LocalDate toValueDate);
HsOfficeCoopAssetsTransactionEntity save(final HsOfficeCoopAssetsTransactionEntity entity);
long count();
}

View File

@@ -0,0 +1,5 @@
package net.hostsharing.hsadminng.hs.office.coopassets;
public enum HsOfficeCoopAssetsTransactionType {
ADJUSTMENT, DEPOSIT, DISBURSAL, TRANSFER, ADOPTION, CLEARING, LOSS
}

View File

@@ -12,6 +12,7 @@ map:
- type: array => java.util.List
- type: string:uuid => java.util.UUID
- type: string:format => java.lang.String
- type: number:currency => java.math.BigDecimal
paths:
/api/hs/office/partners/{partnerUUID}:

View File

@@ -0,0 +1,63 @@
components:
schemas:
HsOfficeCoopAssetsTransactionType:
type: string
enum:
- ADJUSTMENT
- DEPOSIT
- DISBURSAL
- TRANSFER
- ADOPTION
- CLEARING
- LOSS
HsOfficeCoopAssetsTransaction:
type: object
properties:
uuid:
type: string
format: uuid
transactionType:
$ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType'
assetValue:
type: number
format: currency
valueDate:
type: string
format: date
reference:
type: string
comment:
type: string
HsOfficeCoopAssetsTransactionInsert:
type: object
properties:
membershipUuid:
type: string
format: uuid
nullable: false
transactionType:
$ref: '#/components/schemas/HsOfficeCoopAssetsTransactionType'
assetValue:
type: number
format: currency
valueDate:
type: string
format: date
reference:
type: string
minLength: 6
maxLength: 48
comment:
type: string
required:
- membershipUuid
- transactionType
- assetValue
- valueDate
- reference
additionalProperties: false

View File

@@ -0,0 +1,72 @@
get:
summary: Returns a list of (optionally filtered) cooperative asset transactions.
description: Returns the list of (optionally filtered) cooperative asset transactions which are visible to the current user or any of it's assumed roles.
tags:
- hs-office-coopAssets
operationId: listCoopAssets
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
- name: membershipUuid
in: query
required: false
schema:
type: string
format: uuid
description: Optional UUID of the related membership.
- name: fromValueDate
in: query
required: false
schema:
type: string
format: date
description: Optional value date range start (inclusive).
- name: toValueDate
in: query
required: false
schema:
type: string
format: date
description: Optional value date range end (inclusive).
responses:
"200":
description: OK
content:
'application/json':
schema:
type: array
items:
$ref: './hs-office-coopassets-schemas.yaml#/components/schemas/HsOfficeCoopAssetsTransaction'
"401":
$ref: './error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './error-responses.yaml#/components/responses/Forbidden'
post:
summary: Adds a new cooperative asset transaction.
tags:
- hs-office-coopAssets
operationId: addCoopAssetsTransaction
parameters:
- $ref: './auth.yaml#/components/parameters/currentUser'
- $ref: './auth.yaml#/components/parameters/assumedRoles'
requestBody:
description: A JSON object describing the new cooperative assets transaction.
required: true
content:
application/json:
schema:
$ref: '/hs-office-coopassets-schemas.yaml#/components/schemas/HsOfficeCoopAssetsTransactionInsert'
responses:
"201":
description: Created
content:
'application/json':
schema:
$ref: './hs-office-coopassets-schemas.yaml#/components/schemas/HsOfficeCoopAssetsTransaction'
"401":
$ref: './error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './error-responses.yaml#/components/responses/Forbidden'
"409":
$ref: './error-responses.yaml#/components/responses/Conflict'

View File

@@ -84,3 +84,9 @@ paths:
/api/hs/office/coopsharestransactions:
$ref: "./hs-office-coopshares.yaml"
# Coop Assets Transaction
/api/hs/office/coopassetstransactions:
$ref: "./hs-office-coopassets.yaml"

View File

@@ -0,0 +1,62 @@
--liquibase formatted sql
-- ============================================================================
--changeset hs-office-coopassets-MAIN-TABLE:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
CREATE TYPE HsOfficeCoopAssetsTransactionType AS ENUM ('ADJUSTMENT',
'DEPOSIT',
'DISBURSAL',
'TRANSFER',
'ADOPTION',
'CLEARING',
'LOSS');
CREATE CAST (character varying as HsOfficeCoopAssetsTransactionType) WITH INOUT AS IMPLICIT;
create table if not exists hs_office_coopassetstransaction
(
uuid uuid unique references RbacObject (uuid) initially deferred,
membershipUuid uuid not null references hs_office_membership(uuid),
transactionType HsOfficeCoopAssetsTransactionType not null,
valueDate date not null,
assetValue money,
reference varchar(48),
comment varchar(512)
);
--//
-- ============================================================================
--changeset hs-office-coopassets-ASSET-VALUE-CONSTRAINT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace function checkAssetsByMembershipUuid(forMembershipUuid UUID, newAssetValue money)
returns boolean
language plpgsql as $$
declare
currentAssetValue money;
totalAssetValue money;
begin
select sum(cat.assetValue)
from hs_office_coopassetstransaction cat
where cat.membershipUuid = forMembershipUuid
into currentAssetValue;
totalAssetValue := currentAssetValue + newAssetValue;
if totalAssetValue::numeric < 0 then
raise exception '[400] coop assets transaction would result in a negative balance of assets';
end if;
return true;
end; $$;
alter table hs_office_coopassetstransaction
add constraint hs_office_coopassets_positive
check ( checkAssetsByMembershipUuid(membershipUuid, assetValue) );
--//
-- ============================================================================
--changeset hs-office-coopassets-MAIN-TABLE-JOURNAL:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call create_journal('hs_office_coopassetstransaction');
--//

View File

@@ -0,0 +1,29 @@
### hs_office_coopAssetsTransaction RBAC
```mermaid
flowchart TB
subgraph hsOfficeMembership
direction TB
style hsOfficeMembership fill:#eee
role:hsOfficeMembership.owner[membership.admin]
--> role:hsOfficeMembership.admin[membership.admin]
--> role:hsOfficeMembership.agent[membership.agent]
--> role:hsOfficeMembership.tenant[membership.tenant]
--> role:hsOfficeMembership.guest[membership.guest]
role:hsOfficePartner.agent --> role:hsOfficeMembership.agent
end
subgraph hsOfficeCoopAssetsTransaction
role:hsOfficeMembership.admin
--> perm:hsOfficeCoopAssetsTransaction.create{{coopAssetsTx.create}}
role:hsOfficeMembership.agent
--> perm:hsOfficeCoopAssetsTransaction.view{{coopAssetsTx.view}}
end
```

View File

@@ -0,0 +1,124 @@
--liquibase formatted sql
-- ============================================================================
--changeset hs-office-coopAssetsTransaction-rbac-OBJECT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRelatedRbacObject('hs_office_coopAssetsTransaction');
--//
-- ============================================================================
--changeset hs-office-coopAssetsTransaction-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRoleDescriptors('hsOfficeCoopAssetsTransaction', 'hs_office_coopAssetsTransaction');
--//
-- ============================================================================
--changeset hs-office-coopAssetsTransaction-rbac-ROLES-CREATION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates and updates the permissions for coopAssetsTransaction entities.
*/
create or replace function hsOfficeCoopAssetsTransactionRbacRolesTrigger()
returns trigger
language plpgsql
strict as $$
declare
newHsOfficeMembership hs_office_membership;
begin
select * from hs_office_membership as p where p.uuid = NEW.membershipUuid into newHsOfficeMembership;
if TG_OP = 'INSERT' then
-- Each coopAssetsTransaction entity belong exactly to one membership entity
-- and it makes little sense just to delegate coopAssetsTransaction roles.
-- Therefore, we do not create coopAssetsTransaction roles at all,
-- but instead just assign extra permissions to existing membership-roles.
-- coopassetstransactions cannot be edited nor deleted, just created+viewed
call grantPermissionsToRole(
getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'),
createPermissions(NEW.uuid, array ['view'])
);
else
raise exception 'invalid usage of TRIGGER';
end if;
return NEW;
end; $$;
/*
An AFTER INSERT TRIGGER which creates the role structure for a new customer.
*/
create trigger createRbacRolesForHsOfficeCoopAssetsTransaction_Trigger
after insert
on hs_office_coopAssetsTransaction
for each row
execute procedure hsOfficeCoopAssetsTransactionRbacRolesTrigger();
--//
-- ============================================================================
--changeset hs-office-coopAssetsTransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacIdentityView('hs_office_coopAssetsTransaction',
idNameExpression => 'target.reference');
--//
-- ============================================================================
--changeset hs-office-coopAssetsTransaction-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRestrictedView('hs_office_coopAssetsTransaction', orderby => 'target.reference');
--//
-- ============================================================================
--changeset hs-office-coopAssetsTransaction-rbac-NEW-CoopAssetsTransaction:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a global permission for new-coopAssetsTransaction and assigns it to the hostsharing admins role.
*/
do language plpgsql $$
declare
addCustomerPermissions uuid[];
globalObjectUuid uuid;
globalAdminRoleUuid uuid ;
begin
call defineContext('granting global new-coopAssetsTransaction permission to global admin role', null, null, null);
globalAdminRoleUuid := findRoleId(globalAdmin());
globalObjectUuid := (select uuid from global);
addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-coopassetstransaction']);
call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
end;
$$;
/**
Used by the trigger to prevent the add-customer to current user respectively assumed roles.
*/
create or replace function addHsOfficeCoopAssetsTransactionNotAllowedForCurrentSubjects()
returns trigger
language PLPGSQL
as $$
begin
raise exception '[403] new-coopassetstransaction not permitted for %',
array_to_string(currentSubjects(), ';', 'null');
end; $$;
/**
Checks if the user or assumed roles are allowed to create a new customer.
*/
create trigger hs_office_coopAssetsTransaction_insert_trigger
before insert
on hs_office_coopAssetsTransaction
for each row
when ( not hasAssumedRole() )
execute procedure addHsOfficeCoopAssetsTransactionNotAllowedForCurrentSubjects();
--//

View File

@@ -0,0 +1,44 @@
--liquibase formatted sql
-- ============================================================================
--changeset hs-office-coopAssetsTransaction-TEST-DATA-GENERATOR:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a single coopAssetsTransaction test record.
*/
create or replace procedure createHsOfficeCoopAssetsTransactionTestData(givenMembershipNumber numeric)
language plpgsql as $$
declare
currentTask varchar;
membership hs_office_membership;
begin
currentTask = 'creating coopAssetsTransaction test-data ' || givenMembershipNumber;
execute format('set local hsadminng.currentTask to %L', currentTask);
call defineContext(currentTask);
select m.uuid from hs_office_membership m where m.memberNumber = givenMembershipNumber into membership;
raise notice 'creating test coopAssetsTransaction: %', givenMembershipNumber;
insert
into hs_office_coopassetstransaction(uuid, membershipuuid, transactiontype, valuedate, assetvalue, reference, comment)
values
(uuid_generate_v4(), membership.uuid, 'DEPOSIT', '2010-03-15', 320.00, 'ref '||givenMembershipNumber||'-1', 'initial deposit'),
(uuid_generate_v4(), membership.uuid, 'DISBURSAL', '2021-09-01', -128.00, 'ref '||givenMembershipNumber||'-2', 'partial disbursal'),
(uuid_generate_v4(), membership.uuid, 'ADJUSTMENT', '2022-10-20', 128.00, 'ref '||givenMembershipNumber||'-3', 'some adjustment');
end; $$;
--//
-- ============================================================================
--changeset hs-office-coopAssetsTransaction-TEST-DATA-GENERATION:1 context=dev,tc endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$
begin
call createHsOfficeCoopAssetsTransactionTestData(10001);
call createHsOfficeCoopAssetsTransactionTestData(10002);
call createHsOfficeCoopAssetsTransactionTestData(10003);
end;
$$;

View File

@@ -105,3 +105,9 @@ databaseChangeLog:
file: db/changelog/313-hs-office-coopshares-rbac.sql
- include:
file: db/changelog/318-hs-office-coopshares-test-data.sql
- include:
file: db/changelog/320-hs-office-coopassets.sql
- include:
file: db/changelog/323-hs-office-coopassets-rbac.sql
- include:
file: db/changelog/328-hs-office-coopassets-test-data.sql