coop-assets-transaction-reverse-entry (#37)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #37
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-04-10 12:44:56 +02:00
parent 48f4cf8ed6
commit f5de2a8850
12 changed files with 253 additions and 55 deletions

View File

@ -2,8 +2,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets;
import net.hostsharing.hsadminng.context.Context; 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.api.HsOfficeCoopAssetsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionResource;
import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
@ -13,11 +12,13 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.ValidationException; import jakarta.validation.ValidationException;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiConsumer;
import static java.lang.String.join; import static java.lang.String.join;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*;
@ -63,8 +64,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
context.define(currentUser, assumedRoles); context.define(currentUser, assumedRoles);
validate(requestBody); validate(requestBody);
final var entityToSave = mapper.map(requestBody, HsOfficeCoopAssetsTransactionEntity.class); final var entityToSave = mapper.map(requestBody, HsOfficeCoopAssetsTransactionEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = coopAssetsTransactionRepo.save(entityToSave); final var saved = coopAssetsTransactionRepo.save(entityToSave);
final var uri = final var uri =
@ -131,4 +131,10 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
} }
} }
} final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if ( resource.getReverseEntryUuid() != null ) {
entity.setAdjustedAssetTx(coopAssetsTransactionRepo.findByUuid(resource.getReverseEntryUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] reverseEntityUuid %s not found".formatted(resource.getReverseEntryUuid()))));
}
};
};

View File

@ -50,6 +50,7 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacO
.withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue) .withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue)
.withProp(HsOfficeCoopAssetsTransactionEntity::getReference) .withProp(HsOfficeCoopAssetsTransactionEntity::getReference)
.withProp(HsOfficeCoopAssetsTransactionEntity::getComment) .withProp(HsOfficeCoopAssetsTransactionEntity::getComment)
.withProp(at -> ofNullable(at.getAdjustedAssetTx()).map(HsOfficeCoopAssetsTransactionEntity::toShortString).orElse(null))
.quotedValues(false); .quotedValues(false);
@Id @Id
@ -93,6 +94,12 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacO
@Column(name = "comment") @Column(name = "comment")
private String comment; private String comment;
/**
* Optionally, the UUID of the corresponding transaction for an adjustment transaction.
*/
@OneToOne
@JoinColumn(name = "adjustedassettxuuid")
private HsOfficeCoopAssetsTransactionEntity adjustedAssetTx;
public String getTaggedMemberNumber() { public String getTaggedMemberNumber() {
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-?????"); return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-?????");

View File

@ -19,7 +19,13 @@ import org.hibernate.annotations.JoinFormula;
import org.hibernate.annotations.NotFound; import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version; import jakarta.persistence.Version;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import java.io.IOException; import java.io.IOException;

View File

@ -32,6 +32,30 @@ components:
type: string type: string
comment: comment:
type: string type: string
adjustedAssetTx:
$ref: '#/components/schemas/HsOfficeAdjustedCoopAssetsTransaction'
HsOfficeAdjustedCoopAssetsTransaction:
description:
Similar to `HsOfficeCoopAssetsTransaction` but without the `reverseEntry`,
otherwise the JSON would be recursive.
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: HsOfficeCoopAssetsTransactionInsert:
type: object type: object
@ -54,6 +78,9 @@ components:
maxLength: 48 maxLength: 48
comment: comment:
type: string type: string
reverseEntryUuid:
type: string
format: uuid
required: required:
- membershipUuid - membershipUuid
- transactionType - transactionType

View File

@ -17,17 +17,29 @@ CREATE CAST (character varying as HsOfficeCoopAssetsTransactionType) WITH INOUT
create table if not exists hs_office_coopassetstransaction create table if not exists hs_office_coopassetstransaction
( (
uuid uuid unique references RbacObject (uuid) initially deferred, uuid uuid unique references RbacObject (uuid) initially deferred,
version int not null default 0, version int not null default 0,
membershipUuid uuid not null references hs_office_membership(uuid), membershipUuid uuid not null references hs_office_membership(uuid),
transactionType HsOfficeCoopAssetsTransactionType not null, transactionType HsOfficeCoopAssetsTransactionType not null,
valueDate date not null, valueDate date not null,
assetValue money, assetValue money not null,
reference varchar(48), reference varchar(48) not null,
comment varchar(512) adjustedAssetTxUuid uuid unique REFERENCES hs_office_coopassetstransaction(uuid) DEFERRABLE INITIALLY DEFERRED,
comment varchar(512)
); );
--// --//
-- ============================================================================
--changeset hs-office-coopassets-BUSINESS-RULES:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
alter table hs_office_coopassetstransaction
add constraint hs_office_coopassetstransaction_reverse_entry_missing
check ( transactionType = 'ADJUSTMENT' and adjustedAssetTxUuid is not null
or transactionType <> 'ADJUSTMENT' and adjustedAssetTxUuid is null);
--//
-- ============================================================================ -- ============================================================================
--changeset hs-office-coopassets-ASSET-VALUE-CONSTRAINT:1 endDelimiter:--// --changeset hs-office-coopassets-ASSET-VALUE-CONSTRAINT:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
@ -40,9 +52,9 @@ declare
totalAssetValue money; totalAssetValue money;
begin begin
select sum(cat.assetValue) select sum(cat.assetValue)
from hs_office_coopassetstransaction cat from hs_office_coopassetstransaction cat
where cat.membershipUuid = forMembershipUuid where cat.membershipUuid = forMembershipUuid
into currentAssetValue; into currentAssetValue;
totalAssetValue := currentAssetValue + newAssetValue; totalAssetValue := currentAssetValue + newAssetValue;
if totalAssetValue::numeric < 0 then if totalAssetValue::numeric < 0 then
raise exception '[400] coop assets transaction would result in a negative balance of assets'; raise exception '[400] coop assets transaction would result in a negative balance of assets';
@ -53,9 +65,9 @@ end; $$;
alter table hs_office_coopassetstransaction alter table hs_office_coopassetstransaction
add constraint hs_office_coopassets_positive add constraint hs_office_coopassets_positive
check ( checkAssetsByMembershipUuid(membershipUuid, assetValue) ); check ( checkAssetsByMembershipUuid(membershipUuid, assetValue) );
--// --//
-- ============================================================================ -- ============================================================================
--changeset hs-office-coopassets-MAIN-TABLE-JOURNAL:1 endDelimiter:--// --changeset hs-office-coopassets-MAIN-TABLE-JOURNAL:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------

View File

@ -14,11 +14,13 @@ create or replace procedure createHsOfficeCoopAssetsTransactionTestData(
) )
language plpgsql as $$ language plpgsql as $$
declare declare
currentTask varchar; currentTask varchar;
membership hs_office_membership; membership hs_office_membership;
lossEntryUuid uuid;
begin begin
currentTask = 'creating coopAssetsTransaction test-data ' || givenPartnerNumber || givenMemberNumberSuffix; currentTask = 'creating coopAssetsTransaction test-data ' || givenPartnerNumber || givenMemberNumberSuffix;
execute format('set local hsadminng.currentTask to %L', currentTask); execute format('set local hsadminng.currentTask to %L', currentTask);
SET CONSTRAINTS ALL DEFERRED;
call defineContext(currentTask); call defineContext(currentTask);
select m.uuid select m.uuid
@ -29,12 +31,14 @@ begin
into membership; into membership;
raise notice 'creating test coopAssetsTransaction: %', givenPartnerNumber || givenMemberNumberSuffix; raise notice 'creating test coopAssetsTransaction: %', givenPartnerNumber || givenMemberNumberSuffix;
lossEntryUuid := uuid_generate_v4();
insert insert
into hs_office_coopassetstransaction(uuid, membershipuuid, transactiontype, valuedate, assetvalue, reference, comment) into hs_office_coopassetstransaction(uuid, membershipuuid, transactiontype, valuedate, assetvalue, reference, comment, adjustedAssetTxUuid)
values values
(uuid_generate_v4(), membership.uuid, 'DEPOSIT', '2010-03-15', 320.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-1', 'initial deposit'), (uuid_generate_v4(), membership.uuid, 'DEPOSIT', '2010-03-15', 320.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-1', 'initial deposit', null),
(uuid_generate_v4(), membership.uuid, 'DISBURSAL', '2021-09-01', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-2', 'partial disbursal'), (uuid_generate_v4(), membership.uuid, 'DISBURSAL', '2021-09-01', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-2', 'partial disbursal', null),
(uuid_generate_v4(), membership.uuid, 'ADJUSTMENT', '2022-10-20', 128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some adjustment'); (lossEntryUuid, membership.uuid, 'DEPOSIT', '2022-10-20', 128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some loss', null),
(uuid_generate_v4(), membership.uuid, 'ADJUSTMENT', '2022-10-21', -128.00, 'ref '||givenPartnerNumber || givenMemberNumberSuffix||'-3', 'some adjustment', lossEntryUuid);
end; $$; end; $$;
--// --//

View File

@ -19,9 +19,11 @@ import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.DEPOSIT;
import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid; import static net.hostsharing.test.IsValidUuidMatcher.isUuidValid;
import static net.hostsharing.test.JsonMatcher.lenientlyEquals; import static net.hostsharing.test.JsonMatcher.lenientlyEquals;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -69,7 +71,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
.then().log().all().assertThat() .then().log().all().assertThat()
.statusCode(200) .statusCode(200)
.contentType("application/json") .contentType("application/json")
.body("", hasSize(9)); // @formatter:on .body("", hasSize(12)); // @formatter:on
} }
@Test @Test
@ -104,10 +106,17 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
"comment": "partial disbursal" "comment": "partial disbursal"
}, },
{ {
"transactionType": "ADJUSTMENT", "transactionType": "DEPOSIT",
"assetValue": 128.00, "assetValue": 128.00,
"valueDate": "2022-10-20", "valueDate": "2022-10-20",
"reference": "ref 1000202-3", "reference": "ref 1000202-3",
"comment": "some loss"
},
{
"transactionType": "ADJUSTMENT",
"assetValue": -128.00,
"valueDate": "2022-10-21",
"reference": "ref 1000202-3",
"comment": "some adjustment" "comment": "some adjustment"
} }
] ]
@ -188,9 +197,77 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
.extract().header("Location"); // @formatter:on .extract().header("Location"); // @formatter:on
// finally, the new coopAssetsTransaction can be accessed under the generated UUID // finally, the new coopAssetsTransaction can be accessed under the generated UUID
final var newUserUuid = UUID.fromString( final var newAssetTxUuid = UUID.fromString(
location.substring(location.lastIndexOf('/') + 1)); location.substring(location.lastIndexOf('/') + 1));
assertThat(newUserUuid).isNotNull(); assertThat(newAssetTxUuid).isNotNull();
}
@Test
void globalAdmin_canAddCoopAssetsAdjustmentTransaction() {
context.define("superuser-alex@hostsharing.net");
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101);
final var givenTransaction = jpaAttempt.transacted(() -> {
// TODO.impl: introduce something like transactedAsSuperuser / transactedAs("...", ...)
context.define("superuser-alex@hostsharing.net");
return coopAssetsTransactionRepo.save(HsOfficeCoopAssetsTransactionEntity.builder()
.transactionType(DEPOSIT)
.valueDate(LocalDate.of(2022, 10, 20))
.membership(givenMembership)
.assetValue(new BigDecimal("256.00"))
.reference("test ref")
.build());
}).assertSuccessful().assertNotNull().returnedValue();
toCleanup(HsOfficeCoopAssetsTransactionRawEntity.class, givenTransaction.getUuid());
final var location = RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON)
.body("""
{
"membershipUuid": "%s",
"transactionType": "ADJUSTMENT",
"assetValue": %s,
"valueDate": "2022-10-30",
"reference": "test ref adjustment",
"comment": "some coop assets adjustment transaction",
"reverseEntryUuid": "%s"
}
""".formatted(
givenMembership.getUuid(),
givenTransaction.getAssetValue().negate().toString(),
givenTransaction.getUuid()))
.port(port)
.when()
.post("http://localhost/api/hs/office/coopassetstransactions")
.then().log().all().assertThat()
.statusCode(201)
.contentType(ContentType.JSON)
.body("uuid", isUuidValid())
.body("", lenientlyEquals("""
{
"transactionType": "ADJUSTMENT",
"assetValue": -256.00,
"valueDate": "2022-10-30",
"reference": "test ref adjustment",
"comment": "some coop assets adjustment transaction",
"adjustedAssetTx": {
"transactionType": "DEPOSIT",
"assetValue": 256.00,
"valueDate": "2022-10-20",
"reference": "test ref"
}
}
""".formatted(givenTransaction.getUuid())))
.header("Location", startsWith("http://localhost"))
.extract().header("Location"); // @formatter:on
// finally, the new coopAssetsTransaction can be accessed under the generated UUID
final var newAssetTxUuid = UUID.fromString(
location.substring(location.lastIndexOf('/') + 1));
assertThat(newAssetTxUuid).isNotNull();
toCleanup(HsOfficeCoopAssetsTransactionRawEntity.class, newAssetTxUuid);
} }
@Test @Test
@ -199,7 +276,7 @@ class HsOfficeCoopAssetsTransactionControllerAcceptanceTest extends ContextBased
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101); final var givenMembership = membershipRepo.findMembershipByMemberNumber(1000101);
final var location = RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
.header("current-user", "superuser-alex@hostsharing.net") .header("current-user", "superuser-alex@hostsharing.net")
.contentType(ContentType.JSON) .contentType(ContentType.JSON)

View File

@ -16,14 +16,36 @@ class HsOfficeCoopAssetsTransactionEntityUnitTest {
.valueDate(LocalDate.parse("2020-01-01")) .valueDate(LocalDate.parse("2020-01-01"))
.transactionType(HsOfficeCoopAssetsTransactionType.DEPOSIT) .transactionType(HsOfficeCoopAssetsTransactionType.DEPOSIT)
.assetValue(new BigDecimal("128.00")) .assetValue(new BigDecimal("128.00"))
.comment("some comment")
.build(); .build();
final HsOfficeCoopAssetsTransactionEntity givenCoopAssetAdjustmentTransaction = HsOfficeCoopAssetsTransactionEntity.builder()
.membership(TEST_MEMBERSHIP)
.reference("some-ref")
.valueDate(LocalDate.parse("2020-01-15"))
.transactionType(HsOfficeCoopAssetsTransactionType.ADJUSTMENT)
.assetValue(new BigDecimal("-128.00"))
.comment("some comment")
.adjustedAssetTx(givenCoopAssetTransaction)
.build();
final HsOfficeCoopAssetsTransactionEntity givenEmptyCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder().build(); final HsOfficeCoopAssetsTransactionEntity givenEmptyCoopAssetsTransaction = HsOfficeCoopAssetsTransactionEntity.builder().build();
@Test @Test
void toStringContainsAlmostAllPropertiesAccount() { void toStringContainsAllNonNullProperties() {
final var result = givenCoopAssetTransaction.toString(); final var result = givenCoopAssetTransaction.toString();
assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref)"); assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment)");
}
@Test
void toStringWithReverseEntryContainsReverseEntry() {
givenCoopAssetTransaction.setAdjustedAssetTx(givenCoopAssetAdjustmentTransaction);
final var result = givenCoopAssetTransaction.toString();
assertThat(result).isEqualTo("CoopAssetsTransaction(M-1000101: 2020-01-01, DEPOSIT, 128.00, some-ref, some comment, M-1000101:-128.00)");
} }
@Test @Test

View File

@ -0,0 +1,18 @@
package net.hostsharing.hsadminng.hs.office.coopassets;
import lombok.NoArgsConstructor;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.UUID;
@Entity
@Table(name = "hs_office_coopassetstransaction")
@NoArgsConstructor
public class HsOfficeCoopAssetsTransactionRawEntity {
@Id
private UUID uuid;
}

View File

@ -127,7 +127,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
class FindAllCoopAssetsTransactions { class FindAllCoopAssetsTransactions {
@Test @Test
public void globalAdmin_anViewAllCoopAssetsTransactions() { public void globalAdmin_canViewAllCoopAssetsTransactions() {
// given // given
context("superuser-alex@hostsharing.net"); context("superuser-alex@hostsharing.net");
@ -138,19 +138,22 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
null); null);
// then // then
allTheseCoopAssetsTransactionsAreReturned( exactlyTheseCoopAssetsTransactionsAreReturned(
result, result,
"CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)", "CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)",
"CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", "CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)",
"CoopAssetsTransaction(M-1000101: 2022-10-20, ADJUSTMENT, 128.00, ref 1000101-3, some adjustment)", "CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss)",
"CoopAssetsTransaction(M-1000101: 2022-10-21, ADJUSTMENT, -128.00, ref 1000101-3, some adjustment, M-1000101:+128.00)",
"CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", "CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)",
"CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)",
"CoopAssetsTransaction(M-1000202: 2022-10-20, ADJUSTMENT, 128.00, ref 1000202-3, some adjustment)", "CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss)",
"CoopAssetsTransaction(M-1000202: 2022-10-21, ADJUSTMENT, -128.00, ref 1000202-3, some adjustment, M-1000202:+128.00)",
"CoopAssetsTransaction(M-1000303: 2010-03-15, DEPOSIT, 320.00, ref 1000303-1, initial deposit)", "CoopAssetsTransaction(M-1000303: 2010-03-15, DEPOSIT, 320.00, ref 1000303-1, initial deposit)",
"CoopAssetsTransaction(M-1000303: 2021-09-01, DISBURSAL, -128.00, ref 1000303-2, partial disbursal)", "CoopAssetsTransaction(M-1000303: 2021-09-01, DISBURSAL, -128.00, ref 1000303-2, partial disbursal)",
"CoopAssetsTransaction(M-1000303: 2022-10-20, ADJUSTMENT, 128.00, ref 1000303-3, some adjustment)"); "CoopAssetsTransaction(M-1000303: 2022-10-20, DEPOSIT, 128.00, ref 1000303-3, some loss)",
"CoopAssetsTransaction(M-1000303: 2022-10-21, ADJUSTMENT, -128.00, ref 1000303-3, some adjustment, M-1000303:+128.00)");
} }
@Test @Test
@ -166,11 +169,12 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
null); null);
// then // then
allTheseCoopAssetsTransactionsAreReturned( exactlyTheseCoopAssetsTransactionsAreReturned(
result, result,
"CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)", "CoopAssetsTransaction(M-1000202: 2010-03-15, DEPOSIT, 320.00, ref 1000202-1, initial deposit)",
"CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)", "CoopAssetsTransaction(M-1000202: 2021-09-01, DISBURSAL, -128.00, ref 1000202-2, partial disbursal)",
"CoopAssetsTransaction(M-1000202: 2022-10-20, ADJUSTMENT, 128.00, ref 1000202-3, some adjustment)"); "CoopAssetsTransaction(M-1000202: 2022-10-20, DEPOSIT, 128.00, ref 1000202-3, some loss)",
"CoopAssetsTransaction(M-1000202: 2022-10-21, ADJUSTMENT, -128.00, ref 1000202-3, some adjustment, M-1000202:+128.00)");
} }
@Test @Test
@ -207,7 +211,8 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase
result, result,
"CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)", "CoopAssetsTransaction(M-1000101: 2010-03-15, DEPOSIT, 320.00, ref 1000101-1, initial deposit)",
"CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)", "CoopAssetsTransaction(M-1000101: 2021-09-01, DISBURSAL, -128.00, ref 1000101-2, partial disbursal)",
"CoopAssetsTransaction(M-1000101: 2022-10-20, ADJUSTMENT, 128.00, ref 1000101-3, some adjustment)"); "CoopAssetsTransaction(M-1000101: 2022-10-20, DEPOSIT, 128.00, ref 1000101-3, some loss)",
"CoopAssetsTransaction(M-1000101: 2022-10-21, ADJUSTMENT, -128.00, ref 1000101-3, some adjustment, M-1000101:+128.00)");
} }
} }

View File

@ -387,16 +387,16 @@ public class ImportOfficeData extends ContextBasedTest {
assertThat(toFormattedString(coopAssets)).isEqualToIgnoringWhitespace(""" assertThat(toFormattedString(coopAssets)).isEqualToIgnoringWhitespace("""
{ {
30000=CoopAssetsTransaction(M-1001700: 2000-12-06, DEPOSIT, 1280.00, for subscription A), 30000=CoopAssetsTransaction(M-1001700: 2000-12-06, DEPOSIT, 1280.00, legacy data import, for subscription A),
31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, for subscription B), 31000=CoopAssetsTransaction(M-1002000: 2000-12-06, DEPOSIT, 128.00, legacy data import, for subscription B),
32000=CoopAssetsTransaction(M-1001700: 2005-01-10, DEPOSIT, 2560.00, for subscription C), 32000=CoopAssetsTransaction(M-1001700: 2005-01-10, DEPOSIT, 2560.00, legacy data import, for subscription C),
33001=CoopAssetsTransaction(M-1001700: 2005-01-10, TRANSFER, -512.00, for transfer to 10), 33001=CoopAssetsTransaction(M-1001700: 2005-01-10, TRANSFER, -512.00, legacy data import, for transfer to 10),
33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, for transfer from 7), 33002=CoopAssetsTransaction(M-1002000: 2005-01-10, ADOPTION, 512.00, legacy data import, for transfer from 7),
34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, for cancellation D), 34001=CoopAssetsTransaction(M-1002000: 2016-12-31, CLEARING, -8.00, legacy data import, for cancellation D),
34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, for cancellation D), 34002=CoopAssetsTransaction(M-1002000: 2016-12-31, DISBURSAL, -100.00, legacy data import, for cancellation D),
34003=CoopAssetsTransaction(M-1002000: 2016-12-31, LOSS, -20.00, for cancellation D), 34003=CoopAssetsTransaction(M-1002000: 2016-12-31, LOSS, -20.00, legacy data import, for cancellation D),
35001=CoopAssetsTransaction(M-1909000: 2024-01-15, DEPOSIT, 128.00, for subscription E), 35001=CoopAssetsTransaction(M-1909000: 2024-01-15, DEPOSIT, 128.00, legacy data import, for subscription E),
35002=CoopAssetsTransaction(M-1909000: 2024-01-20, ADJUSTMENT, -128.00, chargeback for subscription E) 35002=CoopAssetsTransaction(M-1909000: 2024-01-20, ADJUSTMENT, -128.00, legacy data import, chargeback for subscription E, M-1909000:+128.00)
} }
"""); """);
} }
@ -849,8 +849,20 @@ public class ImportOfficeData extends ContextBasedTest {
.transactionType(assetTypeMapping.get(rec.getString("action"))) .transactionType(assetTypeMapping.get(rec.getString("action")))
.assetValue(rec.getBigDecimal("amount")) .assetValue(rec.getBigDecimal("amount"))
.comment(rec.getString("comment")) .comment(rec.getString("comment"))
.reference("legacy data import") // TODO.spec: or use value from comment column?
.build(); .build();
if (assetTransaction.getTransactionType() == HsOfficeCoopAssetsTransactionType.ADJUSTMENT) {
final var negativeValue = assetTransaction.getAssetValue().negate();
final var adjustedAssetTx = coopAssets.values().stream().filter(a ->
a.getTransactionType() != HsOfficeCoopAssetsTransactionType.ADJUSTMENT &&
a.getMembership() == assetTransaction.getMembership() &&
a.getAssetValue().equals(negativeValue))
.findAny()
.orElseThrow(() -> new IllegalStateException("cannot determine asset reverse entry for adjustment " + assetTransaction));
assetTransaction.setAdjustedAssetTx(adjustedAssetTx);
}
coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction); coopAssets.put(rec.getInteger("member_asset_id"), assetTransaction);
}); });
} }

View File

@ -64,8 +64,10 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
return merged; return merged;
} }
public UUID toCleanup(final Class<? extends RbacObject> entityClass, final UUID uuidToCleanup) { // TODO.test: back to `Class<? extends RbacObject> entityClass` but delete on raw table
out.println("toCleanup(" + entityClass.getSimpleName() + ", " + uuidToCleanup); // remove HsOfficeCoopAssetsTransactionRawEntity, which is not needed anymore after this change
public UUID toCleanup(final Class entityClass, final UUID uuidToCleanup) {
out.println("toCleanup(" + entityClass.getSimpleName() + ", " + uuidToCleanup + ")");
entitiesToCleanup.put(uuidToCleanup, entityClass); entitiesToCleanup.put(uuidToCleanup, entityClass);
return uuidToCleanup; return uuidToCleanup;
} }
@ -120,7 +122,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
} }
if (initialRbacObjects != null){ if (initialRbacObjects != null){
assertNoNewRbackObjectsRolesAndGrantsLeaked(); assertNoNewRbacObjectsRolesAndGrantsLeaked();
} }
initialTestDataValidated = false; initialTestDataValidated = false;
@ -170,7 +172,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup"); out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup");
cleanupTemporaryTestData(); cleanupTemporaryTestData();
deleteLeakedRbacObjects(); deleteLeakedRbacObjects();
long rbacObjectCount = assertNoNewRbackObjectsRolesAndGrantsLeaked(); long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked();
out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount); out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount);
} }
@ -180,7 +182,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
final var caughtException = jpaAttempt.transacted(() -> { final var caughtException = jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net", null); context.define("superuser-alex@hostsharing.net", null);
em.remove(em.getReference(entityClass, uuid)); em.remove(em.getReference(entityClass, uuid));
out.println("DELETING temporary " + entityClass.getSimpleName() + "#" + uuid + " successful"); out.println("DELETING temporary " + entityClass.getSimpleName() + "#" + uuid + " generated");
}).caughtException(); }).caughtException();
if (caughtException != null) { if (caughtException != null) {
out.println("DELETING temporary " + entityClass.getSimpleName() + "#" + uuid + " failed: " + caughtException); out.println("DELETING temporary " + entityClass.getSimpleName() + "#" + uuid + " failed: " + caughtException);
@ -188,7 +190,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
}); });
} }
private long assertNoNewRbackObjectsRolesAndGrantsLeaked() { private long assertNoNewRbacObjectsRolesAndGrantsLeaked() {
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net"); context.define("superuser-alex@hostsharing.net");
assertEqual(initialRbacObjects, allRbacObjects()); assertEqual(initialRbacObjects, allRbacObjects());