HsOfficeCoopAssetsTransactionController.java

package net.hostsharing.hsadminng.hs.office.coopassets;

import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.config.MessageTranslator;
import net.hostsharing.hsadminng.rbac.context.Context;
import net.hostsharing.hsadminng.errors.MultiValidationException;
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.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipRepository;
import net.hostsharing.hsadminng.mapper.StrictMapper;
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
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.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;

import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.ValidationException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.function.BiConsumer;

import static java.util.Optional.ofNullable;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
import static net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionType.REVERSAL;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.CLEARING;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DEPOSIT;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.DISBURSAL;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.LOSS;
import static net.hostsharing.hsadminng.lambda.WithNonNull.withNonNull;

@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "bearerAuth")
public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAssetsApi {

    @Autowired
    private Context context;

    @Autowired
    private StrictMapper mapper;

    @Autowired
    private EntityManagerWrapper emw;

    @Autowired
    private MessageTranslator messageTranslator;

    @Autowired
    private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;

    @Autowired
    private HsOfficeCoopAssetsTransactionRealRepository coopAssetsTransactionRealRepo;

    @Autowired
    private HsOfficeMembershipRepository membershipRepo;

    @Override
    @Transactional(readOnly = true)
    @Timed("app.office.coopAssets.api.getListOfCoopAssets")
    public ResponseEntity<List<HsOfficeCoopAssetsTransactionResource>> getListOfCoopAssets(
            final String assumedRoles,
            final UUID membershipUuid,
            final @DateTimeFormat(iso = ISO.DATE) LocalDate fromValueDate,
            final @DateTimeFormat(iso = ISO.DATE) LocalDate toValueDate) {
        context.assumeRoles(assumedRoles);

        final var visibleUuids = coopAssetsTransactionRepo.findVisibleUuidsByOptionalMembershipUuidAndDateRange(
                membershipUuid,
                fromValueDate,
                toValueDate);
        final var realByUuid = coopAssetsTransactionRealRepo.findByUuidIn(visibleUuids).stream()
                .collect(toMap(HsOfficeCoopAssetsTransactionRealEntity::getUuid, identity()));
        final var entities = visibleUuids.stream().map(realByUuid::get).filter(Objects::nonNull).toList();

        final var resources = mapper.mapList(
                entities,
                HsOfficeCoopAssetsTransactionResource.class,
                REAL_ENTITY_TO_RESOURCE_POSTMAPPER);
        return ResponseEntity.ok(resources);
    }

    @Override
    @Transactional
    @Timed("app.office.coopAssets.api.postNewCoopAssetTransaction")
    public ResponseEntity<HsOfficeCoopAssetsTransactionResource> postNewCoopAssetTransaction(
            final String assumedRoles,
            final HsOfficeCoopAssetsTransactionInsertResource requestBody) {

        context.assumeRoles(assumedRoles);
        validate(requestBody);

        final var entityToSave = mapper.map(
                requestBody,
                HsOfficeCoopAssetsTransactionEntity.class,
                RESOURCE_TO_ENTITY_POSTMAPPER);
        final var saved = coopAssetsTransactionRepo.save(entityToSave);

        final var uri =
                MvcUriComponentsBuilder.fromController(getClass())
                        .path("/api/hs/office/coopassetstransactions/{id}")
                        .buildAndExpand(saved.getUuid())
                        .toUri();
        final var mapped = mapper.map(saved, HsOfficeCoopAssetsTransactionResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
        return ResponseEntity.created(uri).body(mapped);
    }

    @Override
    @Transactional(readOnly = true)
    @Timed("app.office.coopAssets.api.getSingleCoopAssetTransactionByUuid")
    public ResponseEntity<HsOfficeCoopAssetsTransactionResource> getSingleCoopAssetTransactionByUuid(
            final String assumedRoles, final UUID assetTransactionUuid) {

        context.assumeRoles(assumedRoles);

        if (!coopAssetsTransactionRepo.existsByUuid(assetTransactionUuid)) {
            return ResponseEntity.notFound().build();
        }
        final var result = coopAssetsTransactionRealRepo.findByUuid(assetTransactionUuid);
        if (result.isEmpty()) {
            return ResponseEntity.notFound().build();
        }
        final var resource = mapper.map(
                result.get(),
                HsOfficeCoopAssetsTransactionResource.class,
                REAL_ENTITY_TO_RESOURCE_POSTMAPPER);
        return ResponseEntity.ok(resource);

    }

    private void validate(final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
        final var violations = new ArrayList<String>();
        validateDebitTransaction(requestBody, violations);
        validateCreditTransaction(requestBody, violations);
        validateAssetValue(requestBody, violations);
        MultiValidationException.throwIfNotEmpty(violations);
    }

    private void validateDebitTransaction(
            final HsOfficeCoopAssetsTransactionInsertResource requestBody,
            final ArrayList<String> violations) {
        if (List.of(DEPOSIT, HsOfficeCoopAssetsTransactionTypeResource.ADOPTION).contains(requestBody.getTransactionType())
                && requestBody.getAssetValue().signum() < 0) {
            violations.add(messageTranslator.translate(
                    "office.coop-assets.for-transactiontype-{0}-assetvalue-must-be-positive-but-is-{1,number,#0.00}",
                    requestBody.getTransactionType(), requestBody.getAssetValue()));
        }
    }

    private void validateCreditTransaction(
            final HsOfficeCoopAssetsTransactionInsertResource requestBody,
            final ArrayList<String> violations) {
        if (List.of(DISBURSAL, HsOfficeCoopAssetsTransactionTypeResource.TRANSFER, CLEARING, LOSS)
                .contains(requestBody.getTransactionType())
                && requestBody.getAssetValue().signum() > 0) {
            violations.add(messageTranslator.translate(
                    "office.coop-assets.for-transactiontype-{0}-assetvalue-must-be-negative-but-is-{1,number,#0.00}",
                    requestBody.getTransactionType(), requestBody.getAssetValue()));
        }
    }

    private void validateAssetValue(
            final HsOfficeCoopAssetsTransactionInsertResource requestBody,
            final ArrayList<String> violations) {
        if (requestBody.getAssetValue().signum() == 0) {
            violations.add(messageTranslator.translate("office.coop-assets.assetvalue-must-not-be-0"));
        }
    }

    // TODO.refa: this logic needs to get extracted to a service
    final BiConsumer<HsOfficeCoopAssetsTransactionEntity, HsOfficeCoopAssetsTransactionResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
        resource.setMembershipUuid(entity.getMembership().getUuid());
        resource.setMembershipMemberNumber(entity.getMembership().getTaggedMemberNumber());

        withNonNull(
                resource.getReversalAssetTx(), reversalAssetTxResource -> {
                    reversalAssetTxResource.setMembershipUuid(entity.getMembership().getUuid());
                    reversalAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber());
                    reversalAssetTxResource.setRevertedAssetTxUuid(entity.getUuid());
                    withNonNull(
                            entity.getAdoptionAssetTx(), adoptionAssetTx ->
                                    reversalAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid()));
                    withNonNull(
                            entity.getTransferAssetTx(), transferAssetTxResource ->
                                    reversalAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid()));
                });

        withNonNull(
                resource.getRevertedAssetTx(), revertAssetTxResource -> {
                    revertAssetTxResource.setMembershipUuid(entity.getMembership().getUuid());
                    revertAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber());
                    revertAssetTxResource.setReversalAssetTxUuid(entity.getUuid());
                    withNonNull(
                            entity.getRevertedAssetTx().getAdoptionAssetTx(), adoptionAssetTx ->
                                    revertAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid()));
                    withNonNull(
                            entity.getRevertedAssetTx().getTransferAssetTx(), transferAssetTxResource ->
                                    revertAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid()));
                });

        withNonNull(
                resource.getAdoptionAssetTx(), adoptionAssetTxResource -> {
                    adoptionAssetTxResource.setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid());
                    adoptionAssetTxResource.setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber());
                    adoptionAssetTxResource.setTransferAssetTxUuid(entity.getUuid());
                    withNonNull(
                            entity.getAdoptionAssetTx().getReversalAssetTx(), reversalAssetTx ->
                                    adoptionAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid()));
                });

        withNonNull(
                resource.getTransferAssetTx(), transferAssetTxResource -> {
                    resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid());
                    resource.getTransferAssetTx()
                            .setMembershipMemberNumber(entity.getTransferAssetTx().getTaggedMemberNumber());
                    resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid());
                    withNonNull(
                            entity.getTransferAssetTx().getReversalAssetTx(), reversalAssetTx ->
                                    transferAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid()));
                });
    };

    // TODO.impl: read-path twin of ENTITY_TO_RESOURCE_POSTMAPPER over the raw entity; consolidate once extracted to a service
    final BiConsumer<HsOfficeCoopAssetsTransactionRealEntity, HsOfficeCoopAssetsTransactionResource> REAL_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
        resource.setMembershipUuid(entity.getMembership().getUuid());
        resource.setMembershipMemberNumber(entity.getMembership().getTaggedMemberNumber());

        withNonNull(
                resource.getReversalAssetTx(), reversalAssetTxResource -> {
                    reversalAssetTxResource.setMembershipUuid(entity.getMembership().getUuid());
                    reversalAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber());
                    reversalAssetTxResource.setRevertedAssetTxUuid(entity.getUuid());
                    withNonNull(
                            entity.getAdoptionAssetTx(), adoptionAssetTx ->
                                    reversalAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid()));
                    withNonNull(
                            entity.getTransferAssetTx(), transferAssetTxResource ->
                                    reversalAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid()));
                });

        withNonNull(
                resource.getRevertedAssetTx(), revertAssetTxResource -> {
                    revertAssetTxResource.setMembershipUuid(entity.getMembership().getUuid());
                    revertAssetTxResource.setMembershipMemberNumber(entity.getTaggedMemberNumber());
                    revertAssetTxResource.setReversalAssetTxUuid(entity.getUuid());
                    withNonNull(
                            entity.getRevertedAssetTx().getAdoptionAssetTx(), adoptionAssetTx ->
                                    revertAssetTxResource.setAdoptionAssetTxUuid(adoptionAssetTx.getUuid()));
                    withNonNull(
                            entity.getRevertedAssetTx().getTransferAssetTx(), transferAssetTxResource ->
                                    revertAssetTxResource.setTransferAssetTxUuid(transferAssetTxResource.getUuid()));
                });

        withNonNull(
                resource.getAdoptionAssetTx(), adoptionAssetTxResource -> {
                    adoptionAssetTxResource.setMembershipUuid(entity.getAdoptionAssetTx().getMembership().getUuid());
                    adoptionAssetTxResource.setMembershipMemberNumber(entity.getAdoptionAssetTx().getTaggedMemberNumber());
                    adoptionAssetTxResource.setTransferAssetTxUuid(entity.getUuid());
                    withNonNull(
                            entity.getAdoptionAssetTx().getReversalAssetTx(), reversalAssetTx ->
                                    adoptionAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid()));
                });

        withNonNull(
                resource.getTransferAssetTx(), transferAssetTxResource -> {
                    resource.getTransferAssetTx().setMembershipUuid(entity.getTransferAssetTx().getMembership().getUuid());
                    resource.getTransferAssetTx()
                            .setMembershipMemberNumber(entity.getTransferAssetTx().getTaggedMemberNumber());
                    resource.getTransferAssetTx().setAdoptionAssetTxUuid(entity.getUuid());
                    withNonNull(
                            entity.getTransferAssetTx().getReversalAssetTx(), reversalAssetTx ->
                                    transferAssetTxResource.setReversalAssetTxUuid(reversalAssetTx.getUuid()));
                });
    };

    // TODO.refa: this logic needs to get extracted to a service
    final BiConsumer<HsOfficeCoopAssetsTransactionInsertResource, HsOfficeCoopAssetsTransactionEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {

        if (resource.getMembershipUuid() != null) {
            final HsOfficeMembershipEntity membership = ofNullable(emw.find(
                    HsOfficeMembershipEntity.class,
                    resource.getMembershipUuid()))
                    .orElseThrow(() -> new EntityNotFoundException(
                            messageTranslator.translate(
                                    "general.{0}-{1}-not-found", "membership.uuid", resource.getMembershipUuid())));
            entity.setMembership(membership);
        }

        if (entity.getTransactionType() == REVERSAL) {
            if (resource.getRevertedAssetTxUuid() == null) {
                throw new ValidationException(messageTranslator.translate(
                        "office.coop-assets.a-reversal-asset-transaction-requires-specifying-a-revertedassettx-uuid"));
            }
            final var revertedAssetTx = coopAssetsTransactionRepo.findByUuid(resource.getRevertedAssetTxUuid())
                    .orElseThrow(() -> new EntityNotFoundException(messageTranslator.translate(
                            "general.{0}-{1}-not-found",
                            "revertedAssetTx.uuid",
                            resource.getRevertedAssetTxUuid())));
            revertedAssetTx.setReversalAssetTx(entity);
            entity.setRevertedAssetTx(revertedAssetTx);
            if (resource.getAssetValue().negate().compareTo(revertedAssetTx.getAssetValue()) != 0) {
                throw new ValidationException(
                        messageTranslator.translate(
                                "office.coop-assets.given-assetvalue-{0,number,#0.00}-must-be-the-negative-value-of-the-reverted-asset-transaction-{1,number,#0.00}",
                                resource.getAssetValue(), revertedAssetTx.getAssetValue()));
            }

            if (revertedAssetTx.getTransactionType() == HsOfficeCoopAssetsTransactionType.TRANSFER) {
                final var adoptionAssetTx = revertedAssetTx.getAdoptionAssetTx();
                final var adoptionReversalAssetTx = HsOfficeCoopAssetsTransactionEntity.builder()
                        .transactionType(REVERSAL)
                        .membership(adoptionAssetTx.getMembership())
                        .revertedAssetTx(adoptionAssetTx)
                        .assetValue(adoptionAssetTx.getAssetValue().negate())
                        .comment(resource.getComment())
                        .reference(resource.getReference())
                        .valueDate(resource.getValueDate())
                        .build();
                adoptionAssetTx.setReversalAssetTx(adoptionReversalAssetTx);
                adoptionReversalAssetTx.setRevertedAssetTx(adoptionAssetTx);
            }
        }

        if (resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER) {
            final var adoptingMembership = determineAdoptingMembership(resource);
            if ( entity.getMembership() == adoptingMembership) {
                throw new ValidationException(messageTranslator.translate(
                        "office.coop-assets.transferring-and-adopting-membership-must-be-different-but-both-are-{0}",
                        adoptingMembership.getTaggedMemberNumber()));
            }
            final var adoptingAssetTx = createAdoptingAssetTx(entity, adoptingMembership);
            entity.setAdoptionAssetTx(adoptingAssetTx);
        }
    };

    private HsOfficeMembershipEntity determineAdoptingMembership(final HsOfficeCoopAssetsTransactionInsertResource resource) {
        final var adoptingMembershipUuid = resource.getAdoptingMembershipUuid();
        final var adoptingMembershipMemberNumber = resource.getAdoptingMembershipMemberNumber();
        if (adoptingMembershipUuid != null && adoptingMembershipMemberNumber != null) {
            // @formatter:off
            final var message =  messageTranslator.translate(
                    resource.getTransactionType() == HsOfficeCoopAssetsTransactionTypeResource.TRANSFER
                            ? "office.coop-assets.either-{0}-or-{1}-must-be-given-not-both"
                            : "office.coop-assets.neither-{0}-nor-{1}-must-be-given-for-transactiontype-{2}",
                            "adoptingMembership.uuid",
                            "adoptingMembership.memberNumber",
                            resource.getTransactionType());
            // @formatter:on
            throw new ValidationException(message);
        }

        if (adoptingMembershipUuid != null) {
            final var adoptingMembership = membershipRepo.findByUuid(adoptingMembershipUuid);
            return adoptingMembership.orElseThrow(() ->
                    new ValidationException(messageTranslator.translate(
                            "general.{0}-{1}-not-found-or-not-accessible",
                            "adoptingMembership.uuid",
                            adoptingMembershipUuid)));
        }

        if (adoptingMembershipMemberNumber != null) {
            final var adoptingMemberNumber = Integer.valueOf(adoptingMembershipMemberNumber.substring("M-".length()));
            final var adoptingMembership = membershipRepo.findMembershipByMemberNumber(adoptingMemberNumber);
            return adoptingMembership.orElseThrow( () ->
                    new ValidationException(
                            messageTranslator.translate(
                                    "general.{0}-{1}-not-found-or-not-accessible",
                                    "adoptingMembership.memberNumber",
                                    adoptingMembershipMemberNumber)));
        }

        throw new ValidationException(
                messageTranslator.translate(
                        "office.coop-assets.either-{0}-or-{1}-must-be-given-for-transactiontype-{2}",
                        "adoptingMembership.uuid",
                        "adoptingMembership.memberNumber",
                        resource.getTransactionType()
                        ));
    }

    private HsOfficeCoopAssetsTransactionEntity createAdoptingAssetTx(
            final HsOfficeCoopAssetsTransactionEntity transferAssetTxEntity,
            final HsOfficeMembershipEntity adoptingMembership) {
        return HsOfficeCoopAssetsTransactionEntity.builder()
                .membership(adoptingMembership)
                .transactionType(HsOfficeCoopAssetsTransactionType.ADOPTION)
                .transferAssetTx(transferAssetTxEntity)
                .assetValue(transferAssetTxEntity.getAssetValue().negate())
                .comment(transferAssetTxEntity.getComment())
                .reference(transferAssetTxEntity.getReference())
                .valueDate(transferAssetTxEntity.getValueDate())
                .build();
    }
}