MoneyWiseValidateTransaction.java

/*
 * MoneyWise: Finance Application
 * Copyright 2012-2026. Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package io.github.tonywasher.joceanus.moneywise.data.validate;

import io.github.tonywasher.joceanus.metis.data.MetisDataDifference;
import io.github.tonywasher.joceanus.metis.field.MetisFieldRequired;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetDirection;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetType;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicDataType;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicResource;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDataSet;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDeposit;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseLoan;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePayee;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePortfolio;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurity;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityHolding;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransAsset;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransBase;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransCategory;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransInfoSet;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseDepositCategoryClass;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWisePayeeClass;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWisePortfolioClass;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseSecurityClass;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseTransCategoryClass;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseTransInfoClass;
import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
import io.github.tonywasher.joceanus.oceanus.date.OceanusDateRange;
import io.github.tonywasher.joceanus.oceanus.decimal.OceanusMoney;
import io.github.tonywasher.joceanus.oceanus.decimal.OceanusUnits;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusData.PrometheusDataItemCtl;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataItem;
import io.github.tonywasher.joceanus.prometheus.views.PrometheusEditSet;

import java.util.Currency;
import java.util.Objects;

/**
 * Validator for transaction.
 */
public class MoneyWiseValidateTransaction
        implements MoneyWiseValidateTransCtl {
    /**
     * Are we using new validation?
     */
    private final boolean newValidation;

    /**
     * The infoSet validator.
     */
    private final MoneyWiseValidateTransInfoSet theInfoSet;

    /**
     * The defaults engine.
     */
    private final MoneyWiseValidateTransDefaults theDefaults;

    /**
     * Set the editSet.
     */
    private PrometheusEditSet theEditSet;

    /**
     * Constructor.
     *
     * @param pNewValidation true/false
     */
    MoneyWiseValidateTransaction(final boolean pNewValidation) {
        newValidation = pNewValidation;
        theInfoSet = new MoneyWiseValidateTransInfoSet(pNewValidation);
        theDefaults = new MoneyWiseValidateTransDefaults(this);
    }

    @Override
    public void setEditSet(final PrometheusEditSet pEditSet) {
        theEditSet = pEditSet;
        theInfoSet.storeEditSet(pEditSet);
    }

    @Override
    public PrometheusEditSet getEditSet() {
        if (theEditSet == null) {
            throw new IllegalStateException("editSet not set up");
        }
        return theEditSet;
    }

    @Override
    public MoneyWiseValidateTransInfoSet getInfoSetValidator() {
        return theInfoSet;
    }

    /**
     * Should we perform new validity checks?
     *
     * @return true/false
     */
    public boolean newValidation() {
        return newValidation;
    }

    @Override
    public void validate(final PrometheusDataItemCtl pTrans) {
        final MoneyWiseTransaction myTrans = (MoneyWiseTransaction) pTrans;
        final OceanusDate myDate = myTrans.getDate();
        final MoneyWiseTransAsset myAccount = myTrans.getAccount();
        final MoneyWiseTransAsset myPartner = myTrans.getPartner();
        final MoneyWiseTransCategory myCategory = myTrans.getCategory();
        final MoneyWiseAssetDirection myDir = myTrans.getDirection();
        final OceanusMoney myAmount = myTrans.getAmount();
        final OceanusUnits myAccountUnits = myTrans.getAccountDeltaUnits();
        final OceanusUnits myPartnerUnits = myTrans.getPartnerDeltaUnits();
        boolean doCheckCombo = true;

        /* Header is always valid */
        if (pTrans.isHeader()) {
            myTrans.setValidEdit();
            return;
        }

        /* Determine date range to check for */
        final MoneyWiseDataSet myDataSet = (MoneyWiseDataSet) myTrans.getDataSet();
        final OceanusDateRange myRange = myDataSet.getDateRange();

        /* The date must be non-null */
        if (myDate == null) {
            myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicResource.MONEYWISEDATA_FIELD_DATE);
            /* The date must be in-range */
        } else if (myRange.compareToDate(myDate) != 0) {
            myTrans.addError(PrometheusDataItem.ERROR_RANGE, MoneyWiseBasicResource.MONEYWISEDATA_FIELD_DATE);
        }

        /* Account must be non-null */
        if (myAccount == null) {
            myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicResource.TRANSACTION_ACCOUNT);
            doCheckCombo = false;

        } else {
            /* Account must be valid */
            if (!isValidAccount(myAccount)) {
                myTrans.addError(MoneyWiseTransBase.ERROR_COMBO, MoneyWiseBasicResource.TRANSACTION_ACCOUNT);
                doCheckCombo = false;
            }
        }

        /* Category must be non-null */
        if (myCategory == null) {
            myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicDataType.TRANSCATEGORY);
            doCheckCombo = false;

            /* Category must be valid for Account */
        } else if (doCheckCombo
                && !isValidCategory(myAccount, myCategory)) {
            myTrans.addError(MoneyWiseTransBase.ERROR_COMBO, MoneyWiseBasicDataType.TRANSCATEGORY);
            doCheckCombo = false;
        }

        /* Direction must be non-null */
        if (myDir == null) {
            myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicResource.TRANSACTION_DIRECTION);
            doCheckCombo = false;

            /* Direction must be valid for Account */
        } else if (doCheckCombo
                && !isValidDirection(myAccount, myCategory, myDir)) {
            myTrans.addError(MoneyWiseTransBase.ERROR_COMBO, MoneyWiseBasicResource.TRANSACTION_DIRECTION);
            doCheckCombo = false;
        }

        /* Partner must be non-null */
        if (myPartner == null) {
            myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicResource.TRANSACTION_PARTNER);

        } else {
            /* Partner must be valid for Account */
            if (doCheckCombo
                    && !isValidPartner(myAccount, myCategory, myPartner)) {
                myTrans.addError(MoneyWiseTransBase.ERROR_COMBO, MoneyWiseBasicResource.TRANSACTION_PARTNER);
            }
        }

        /* If money is null */
        if (myAmount == null) {
            /* Check that it must be null */
            if (!needsNullAmount(myTrans)) {
                myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicResource.TRANSACTION_AMOUNT);
            }

            /* else non-null money */
        } else {
            /* Check that it must be null */
            if (needsNullAmount(myTrans)) {
                myTrans.addError(PrometheusDataItem.ERROR_EXIST, MoneyWiseBasicResource.TRANSACTION_AMOUNT);
            }

            /* Money must not be negative */
            if (!myAmount.isPositive()) {
                myTrans.addError(PrometheusDataItem.ERROR_NEGATIVE, MoneyWiseBasicResource.TRANSACTION_AMOUNT);
            }

            /* Check that amount is correct currency */
            if (myAccount != null) {
                final Currency myCurrency = myAccount.getCurrency();
                if (!myAmount.getCurrency().equals(myCurrency)) {
                    myTrans.addError(MoneyWiseTransBase.ERROR_CURRENCY, MoneyWiseBasicResource.TRANSACTION_AMOUNT);
                }
            }
        }

        /* Cannot have PartnerUnits if securities are identical */
        if (myAccountUnits != null
                && myPartnerUnits != null
                && MetisDataDifference.isEqual(myAccount, myPartner)) {
            myTrans.addError(MoneyWiseTransaction.ERROR_CIRCULAR, MoneyWiseTransInfoSet.getFieldForClass(MoneyWiseTransInfoClass.PARTNERDELTAUNITS));
        }

        /* If we have a category and an infoSet */
        if (myCategory != null
                && myTrans.getInfoSet() != null) {
            /* Validate the InfoSet */
            theInfoSet.validate(myTrans.getInfoSet());
        }

        /* Set validation flag */
        if (!myTrans.hasErrors()) {
            myTrans.setValidEdit();
        }
    }

    /**
     * Determines whether an event needs a zero amount.
     *
     * @param pTrans the transaction
     * @return true/false
     */
    public boolean needsNullAmount(final MoneyWiseTransaction pTrans) {
        final MoneyWiseTransCategoryClass myClass = pTrans.getCategoryClass();
        return myClass != null
                && myClass.needsNullAmount();
    }

    @Override
    public boolean isValidAccount(final MoneyWiseTransAsset pAccount) {
        /* Validate securityHolding */
        if (pAccount instanceof MoneyWiseSecurityHolding myHolding
                && !checkSecurityHolding(myHolding)) {
            return false;
        }

        /* Reject pensions portfolio */
        if (pAccount instanceof MoneyWisePortfolio myPortfolio
                && myPortfolio.getCategoryClass().holdsPensions()) {
            return false;
        }

        /* Check type of account */
        final MoneyWiseAssetType myType = pAccount.getAssetType();
        return myType.isBaseAccount() && !pAccount.isHidden();
    }

    @Override
    public boolean isValidCategory(final MoneyWiseTransAsset pAccount,
                                   final MoneyWiseTransCategory pCategory) {
        /* Access details */
        final MoneyWiseAssetType myType = pAccount.getAssetType();
        final MoneyWiseTransCategoryClass myCatClass = Objects.requireNonNull(pCategory.getCategoryTypeClass());

        /* Immediately reject hidden categories */
        if (myCatClass.isHiddenType()) {
            return false;
        }

        /* Switch on the CategoryClass */
        return switch (myCatClass) {
            case TAXEDINCOME, GROSSINCOME, RECOVEREDEXPENSES, OTHERINCOME ->
                /* Taxed/Other income must be to deposit/cash/loan */
                    myType.isValued();
            case PENSIONCONTRIB ->
                /* Pension contribution must be to a Pension holding or to a SIPP */
                    (pAccount instanceof MoneyWiseSecurityHolding myHolding
                            && myHolding.getSecurity().getCategoryClass().isPension())
                            || (pAccount instanceof MoneyWisePortfolio myPortfolio
                            && myPortfolio.isPortfolioClass(MoneyWisePortfolioClass.SIPP));
            case GIFTEDINCOME, INHERITED ->
                /* Inheritance/Gifted must be to asset */
                    myType.isAsset();
            case INTEREST ->
                /* Account must be deposit or portfolio */
                    myType.isDeposit() || myType.isPortfolio();
            case DIVIDEND, SECURITYCLOSURE ->
                /* Account must be SecurityHolding */
                    myType.isSecurityHolding();
            case BADDEBTCAPITAL, BADDEBTINTEREST ->
                /* Account must be peer2Peer */
                    pAccount instanceof MoneyWiseDeposit myDeposit
                            && myDeposit.isDepositClass(MoneyWiseDepositCategoryClass.PEER2PEER);
            case CASHBACK -> checkCashBack(pAccount);
            case LOYALTYBONUS -> checkLoyaltyBonus(pAccount);
            case RENTALINCOME, RENTALEXPENSE, ROOMRENTALINCOME ->
                /* Account must be property */
                    pAccount instanceof MoneyWiseSecurityHolding myHolding
                            && myHolding.getSecurity().isSecurityClass(MoneyWiseSecurityClass.PROPERTY);
            case UNITSADJUST, SECURITYREPLACE ->
                /* Account must be capital */
                    pAccount.isCapital();
            case STOCKSPLIT, STOCKTAKEOVER, STOCKDEMERGER, STOCKRIGHTSISSUE ->
                /* Account must be shares */
                    pAccount.isShares();
            case WRITEOFF, LOANINTERESTEARNED, LOANINTERESTCHARGED, TAXRELIEF -> myType.isLoan();
            case LOCALTAXES, INCOMETAX -> myType.isValued();
            case EXPENSE -> myType.isValued() || myType.isAutoExpense();
            case PORTFOLIOXFER -> pAccount instanceof MoneyWiseSecurityHolding
                    || pAccount instanceof MoneyWisePortfolio;
            case TRANSFER -> true;

            /* Reject other categories */
            default -> false;
        };
    }

    @Override
    public boolean isValidDirection(final MoneyWiseTransAsset pAccount,
                                    final MoneyWiseTransCategory pCategory,
                                    final MoneyWiseAssetDirection pDirection) {
        /* TODO relax some of these rules */

        /* Access details */
        final MoneyWiseTransCategoryClass myCatClass = pCategory.getCategoryTypeClass();

        /* Switch on the CategoryClass */
        return switch (myCatClass) {
            case TAXEDINCOME, GROSSINCOME ->
                /* Cannot refund Taxed Income yet */
                    newValidation || pDirection.isFrom();
            case PENSIONCONTRIB ->
                /* Cannot refund Pension Contribution */
                    pDirection.isFrom();
            case GIFTEDINCOME, INHERITED ->
                /* Cannot refund Gifted/Inherited Income yet */
                    newValidation || pDirection.isFrom();
            case RENTALINCOME, ROOMRENTALINCOME ->
                /* Cannot refund Rental Income */
                    pDirection.isTo();
            case RENTALEXPENSE ->
                /* Cannot refund Rental Expense */
                    pDirection.isFrom();
            case INTEREST ->
                /* Cannot refund Interest yet */
                    newValidation || pDirection.isTo();
            case DIVIDEND, SECURITYCLOSURE ->
                /* Cannot refund Dividend yet */
                    pDirection.isTo();
            case LOYALTYBONUS ->
                /* Cannot refund loyaltyBonus yet */
                    newValidation || pDirection.isTo();
            case WRITEOFF, LOANINTERESTCHARGED ->
                /* All need to be TO */
                    newValidation || pDirection.isTo();
            case LOANINTERESTEARNED ->
                /* All need to be FROM */
                    newValidation || pDirection.isFrom();
            case UNITSADJUST, STOCKSPLIT, STOCKDEMERGER, STOCKTAKEOVER, SECURITYREPLACE, PORTFOLIOXFER ->
                /* All need to be To */
                    pDirection.isTo();
            case null, default -> true;
        };
    }

    @Override
    public boolean isValidPartner(final MoneyWiseTransAsset pAccount,
                                  final MoneyWiseTransCategory pCategory,
                                  final MoneyWiseTransAsset pPartner) {
        /* Access details */
        final boolean isRecursive = MetisDataDifference.isEqual(pAccount, pPartner);
        final MoneyWiseAssetType myPartnerType = pPartner.getAssetType();
        final MoneyWiseTransCategoryClass myCatClass = Objects.requireNonNull(pCategory.getCategoryTypeClass());

        /* Immediately reject hidden partners */
        if (pPartner.isHidden()) {
            return false;
        }

        /* Validate securityHolding */
        if (pPartner instanceof MoneyWiseSecurityHolding myHolding
                && !checkSecurityHolding(myHolding)) {
            return false;
        }

        /* Reject pensions portfolio */
        if (pPartner instanceof MoneyWisePortfolio myPortfolio
                && myPortfolio.getCategoryClass().holdsPensions()) {
            return false;
        }

        /* If this involves auto-expense */
        if (pAccount.isAutoExpense()
                || pPartner.isAutoExpense()) {
            /* Access account type */
            final MoneyWiseAssetType myAccountType = pAccount.getAssetType();

            /* Special processing */
            return switch (myCatClass) {
                case TRANSFER ->
                    /* Transfer must be to/from deposit/cash/loan */
                        myPartnerType.isAutoExpense()
                                ? myAccountType.isValued()
                                : myPartnerType.isValued();
                case EXPENSE ->
                    /* Transfer must be to/from payee */
                        pPartner instanceof MoneyWisePayee;

                /* Auto Expense cannot be used for other categories */
                default -> false;
            };
        }

        /* Switch on the CategoryClass */
        return switch (myCatClass) {
            case TAXEDINCOME, GROSSINCOME ->
                /* Taxed Income must have a Payee that can provide income */
                    pPartner instanceof MoneyWisePayee myPayee
                            && myPayee.getCategoryClass().canProvideTaxedIncome();
            case PENSIONCONTRIB ->
                /* Pension Contribution must be a payee that can parent */
                    pPartner instanceof MoneyWisePayee myPayee
                            && myPayee.getCategoryClass().canContribPension();
            case OTHERINCOME, RECOVEREDEXPENSES ->
                /* Other Income must have a Payee partner */
                    pPartner instanceof MoneyWisePayee;
            case LOCALTAXES ->
                /* LocalTaxes must have a Government Payee partner */
                    pPartner instanceof MoneyWisePayee myPayee
                            && myPayee.isPayeeClass(MoneyWisePayeeClass.GOVERNMENT);
            case GIFTEDINCOME, INHERITED ->
                /* Gifted/Inherited Income must have an Individual Payee partner */
                    pPartner instanceof MoneyWisePayee myPayee
                            && myPayee.isPayeeClass(MoneyWisePayeeClass.INDIVIDUAL);
            case RENTALINCOME, RENTALEXPENSE, ROOMRENTALINCOME ->
                /* RentalIncome/Expense must have a loan partner */
                    myPartnerType.isLoan();
            case WRITEOFF, LOANINTERESTEARNED, LOANINTERESTCHARGED ->
                /* WriteOff/LoanInterestEarned/Charged must be recursive */
                    isRecursive;
            case INTEREST, CASHBACK ->
                /* Interest/CashBack is to a valued account */
                    myPartnerType.isValued();
            case DIVIDEND -> checkDividend(pAccount, pPartner);
            case LOYALTYBONUS -> checkLoyaltyBonus(pAccount, pPartner);
            case BADDEBTCAPITAL, BADDEBTINTEREST -> pPartner instanceof MoneyWisePayee
                    && MetisDataDifference.isEqual(pPartner, pAccount.getParent());
            case UNITSADJUST, STOCKSPLIT ->
                /* Must be recursive */
                    isRecursive;
            case SECURITYREPLACE, STOCKTAKEOVER, STOCKDEMERGER -> checkTakeOver(pAccount, pPartner);
            case STOCKRIGHTSISSUE -> checkStockRights(pAccount, pPartner);
            case TRANSFER -> checkTransfer(pAccount, pPartner);
            case SECURITYCLOSURE -> checkSecurityClosure(pAccount, pPartner);
            case EXPENSE ->
                /* Expense must have a Payee partner */
                    pPartner instanceof MoneyWisePayee;
            case INCOMETAX, TAXRELIEF -> pPartner instanceof MoneyWisePayee myPayee
                    && myPayee.isPayeeClass(MoneyWisePayeeClass.TAXMAN);
            case PORTFOLIOXFER -> checkPortfolioXfer(pAccount, pPartner);
            default -> false;
        };
    }

    /**
     * Check securityHolding.
     *
     * @param pHolding the securityHolding
     * @return valid true/false
     */
    private static boolean checkSecurityHolding(final MoneyWiseSecurityHolding pHolding) {
        /* Access the components */
        final MoneyWisePortfolio myPortfolio = pHolding.getPortfolio();
        final MoneyWiseSecurity mySecurity = pHolding.getSecurity();

        /* If the portfolio can hold pensions */
        if (myPortfolio.getCategoryClass().holdsPensions()) {
            /* Can only hold pensions */
            return mySecurity.getCategoryClass().isPension();
        }

        /* cannot be a pension */
        return !mySecurity.getCategoryClass().isPension();
    }

    /**
     * Check dividend.
     *
     * @param pAccount the holding providing the dividend.
     * @param pPartner the partner
     * @return valid true/false
     */
    private static boolean checkDividend(final MoneyWiseTransAsset pAccount,
                                         final MoneyWiseTransAsset pPartner) {
        /* Recursive is allowed */
        if (MetisDataDifference.isEqual(pAccount, pPartner)) {
            return true;
        }

        /* partner must be valued */
        return pPartner.getAssetType().isValued();
    }

    /**
     * Check TakeOver.
     *
     * @param pAccount the holding being acted on.
     * @param pPartner the partner
     * @return valid true/false
     */
    private static boolean checkTakeOver(final MoneyWiseTransAsset pAccount,
                                         final MoneyWiseTransAsset pPartner) {
        /* Must be holding <-> holding */
        if (!(pAccount instanceof MoneyWiseSecurityHolding myAccount)
                || !(pPartner instanceof MoneyWiseSecurityHolding myPartner)) {
            return false;
        }

        /* Recursive is not allowed */
        if (MetisDataDifference.isEqual(pAccount, pPartner)) {
            return false;
        }

        /* Portfolios must be the same */
        if (!MetisDataDifference.isEqual(myAccount.getPortfolio(), myPartner.getPortfolio())) {
            return false;
        }

        /* Security types must be the same */
        return MetisDataDifference.isEqual(myAccount.getSecurity().getCategory(), myPartner.getSecurity().getCategory());
    }

    /**
     * Check stock rights.
     *
     * @param pAccount the account being transferred.
     * @param pPartner the partner
     * @return valid true/false
     */
    private static boolean checkStockRights(final MoneyWiseTransAsset pAccount,
                                            final MoneyWiseTransAsset pPartner) {
        /* If this is security -> portfolio */
        if (pAccount instanceof MoneyWiseSecurityHolding myHolding
                && pPartner instanceof MoneyWisePortfolio) {
            /* Must be same portfolios */
            return MetisDataDifference.isEqual(myHolding.getPortfolio(), pPartner);
        }

        /* partner must be valued */
        return pPartner.getAssetType().isValued();
    }

    /**
     * Check cashBack.
     *
     * @param pAccount the account providing cashBack.
     * @return valid true/false
     */
    private static boolean checkCashBack(final MoneyWiseTransAsset pAccount) {
        /* If this is deposit then check whether it can support cashBack */
        if (pAccount instanceof MoneyWiseDeposit myDeposit) {
            return myDeposit.getCategoryClass().canCashBack();
        }

        /* If this is loan then check whether it can support cashBack */
        if (pAccount instanceof MoneyWiseLoan myLoan) {
            return myLoan.getCategoryClass().canCashBack();
        }

        /* not allowed */
        return false;
    }

    /**
     * Check loyalty bonus.
     *
     * @param pAccount the account providing bonus.
     * @return valid true/false
     */
    private boolean checkLoyaltyBonus(final MoneyWiseTransAsset pAccount) {
        /* If this is deposit then check whether it can support loyaltyBonus */
        if (pAccount instanceof MoneyWiseDeposit myDeposit) {
            return newValidation
                    || myDeposit.getCategoryClass().canLoyaltyBonus();
        }

        /* must be portfolio */
        return pAccount instanceof MoneyWisePortfolio;
    }

    /**
     * Check loyalty bonus.
     *
     * @param pAccount the account providing bonus.
     * @param pPartner the partner
     * @return valid true/false
     */
    private static boolean checkLoyaltyBonus(final MoneyWiseTransAsset pAccount,
                                             final MoneyWiseTransAsset pPartner) {
        /* If this is portfolio -> security holding */
        if (pAccount instanceof MoneyWisePortfolio
                && pPartner instanceof MoneyWiseSecurityHolding myHolding) {
            /* Must be same portfolios */
            return MetisDataDifference.isEqual(myHolding.getPortfolio(), pAccount);
        }

        /* must be recursive */
        return MetisDataDifference.isEqual(pAccount, pPartner);
    }

    /**
     * Check transfer.
     *
     * @param pAccount the account being transferred.
     * @param pPartner the partner
     * @return valid true/false
     */
    private static boolean checkTransfer(final MoneyWiseTransAsset pAccount,
                                         final MoneyWiseTransAsset pPartner) {
        /* Must not be recursive */
        if (MetisDataDifference.isEqual(pAccount, pPartner)) {
            return false;
        }

        /* If this is security -> portfolio */
        if (pAccount instanceof MoneyWiseSecurityHolding myHolding
                && pPartner instanceof MoneyWisePortfolio) {
            /* Must be same portfolios */
            if (!MetisDataDifference.isEqual(myHolding.getPortfolio(), pPartner)) {
                return false;
            }
        }

        /* If this is security <- portfolio */
        if (pPartner instanceof MoneyWiseSecurityHolding myHolding
                && pAccount instanceof MoneyWisePortfolio) {
            /* Must be same portfolios */
            if (!MetisDataDifference.isEqual(myHolding.getPortfolio(), pAccount)) {
                return false;
            }
        }

        /* partner must be asset */
        return pPartner.getAssetType().isAsset();
    }

    /**
     * Check securityClosure.
     *
     * @param pAccount the account being closed.
     * @param pPartner the partner
     * @return valid true/false
     */
    private static boolean checkSecurityClosure(final MoneyWiseTransAsset pAccount,
                                                final MoneyWiseTransAsset pPartner) {
        /* Must not be recursive */
        if (MetisDataDifference.isEqual(pAccount, pPartner)) {
            return false;
        }

        /* partner must be valued */
        return pPartner.getAssetType().isValued();
    }

    /**
     * Check portfolioXfer.
     *
     * @param pAccount the account being transferred.
     * @param pPartner the partner
     * @return valid true/false
     */
    private static boolean checkPortfolioXfer(final MoneyWiseTransAsset pAccount,
                                              final MoneyWiseTransAsset pPartner) {
        /* Partner must be portfolio */
        if (!(pPartner instanceof MoneyWisePortfolio)) {
            return false;
        }

        /* If account is portfolio */
        if (pAccount instanceof MoneyWisePortfolio) {
            /* Cannot be recursive */
            if (MetisDataDifference.isEqual(pAccount, pPartner)) {
                return false;
            }

            /* Must be same currency */
            return MetisDataDifference.isEqual(pAccount.getAssetCurrency(), pPartner.getAssetCurrency());
        }

        /* If account is security holding */
        if (pAccount instanceof MoneyWiseSecurityHolding myHolding) {
            /* Must be different portfolios */
            return !MetisDataDifference.isEqual(myHolding.getPortfolio(), pPartner);
        }

        /* Not allowed */
        return false;
    }

    /**
     * Determine if an infoSet class is required.
     *
     * @param pTrans the transaction
     * @param pClass the infoSet class
     * @return the status
     */
    public MetisFieldRequired isClassRequired(final MoneyWiseTransaction pTrans,
                                              final MoneyWiseTransInfoClass pClass) {
        theInfoSet.storeInfoSet(pTrans.getInfoSet());
        return theInfoSet.isClassRequired(pClass);
    }

    @Override
    public void autoCorrect(final MoneyWiseTransaction pItem) throws OceanusException {
        theDefaults.autoCorrect(pItem);
    }

    @Override
    public MoneyWiseTransaction buildTransaction(final Object pKey) {
        return theDefaults.buildTransaction(pKey);
    }

    @Override
    public void setRange(final OceanusDateRange pRange) {
        theDefaults.setRange(pRange);
    }

    @Override
    public OceanusDateRange getRange() {
        return theDefaults.getRange();
    }
}