View Javadoc
1   /*
2    * MoneyWise: Finance Application
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.moneywise.data.validate;
18  
19  import io.github.tonywasher.joceanus.metis.data.MetisDataDifference;
20  import io.github.tonywasher.joceanus.metis.field.MetisFieldRequired;
21  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetDirection;
22  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetType;
23  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicDataType;
24  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicResource;
25  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDataSet;
26  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDeposit;
27  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseLoan;
28  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePayee;
29  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePortfolio;
30  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurity;
31  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityHolding;
32  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransAsset;
33  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransBase;
34  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransCategory;
35  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransInfoSet;
36  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction;
37  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseDepositCategoryClass;
38  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWisePayeeClass;
39  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWisePortfolioClass;
40  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseSecurityClass;
41  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseTransCategoryClass;
42  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseTransInfoClass;
43  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
44  import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
45  import io.github.tonywasher.joceanus.oceanus.date.OceanusDateRange;
46  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusMoney;
47  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusUnits;
48  import io.github.tonywasher.joceanus.prometheus.data.PrometheusData.PrometheusDataItemCtl;
49  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataItem;
50  import io.github.tonywasher.joceanus.prometheus.views.PrometheusEditSet;
51  
52  import java.util.Currency;
53  import java.util.Objects;
54  
55  /**
56   * Validator for transaction.
57   */
58  public class MoneyWiseValidateTransaction
59          implements MoneyWiseValidateTransCtl {
60      /**
61       * Are we using new validation?
62       */
63      private final boolean newValidation;
64  
65      /**
66       * The infoSet validator.
67       */
68      private final MoneyWiseValidateTransInfoSet theInfoSet;
69  
70      /**
71       * The defaults engine.
72       */
73      private final MoneyWiseValidateTransDefaults theDefaults;
74  
75      /**
76       * Set the editSet.
77       */
78      private PrometheusEditSet theEditSet;
79  
80      /**
81       * Constructor.
82       *
83       * @param pNewValidation true/false
84       */
85      MoneyWiseValidateTransaction(final boolean pNewValidation) {
86          newValidation = pNewValidation;
87          theInfoSet = new MoneyWiseValidateTransInfoSet(pNewValidation);
88          theDefaults = new MoneyWiseValidateTransDefaults(this);
89      }
90  
91      @Override
92      public void setEditSet(final PrometheusEditSet pEditSet) {
93          theEditSet = pEditSet;
94          theInfoSet.storeEditSet(pEditSet);
95      }
96  
97      @Override
98      public PrometheusEditSet getEditSet() {
99          if (theEditSet == null) {
100             throw new IllegalStateException("editSet not set up");
101         }
102         return theEditSet;
103     }
104 
105     @Override
106     public MoneyWiseValidateTransInfoSet getInfoSetValidator() {
107         return theInfoSet;
108     }
109 
110     /**
111      * Should we perform new validity checks?
112      *
113      * @return true/false
114      */
115     public boolean newValidation() {
116         return newValidation;
117     }
118 
119     @Override
120     public void validate(final PrometheusDataItemCtl pTrans) {
121         final MoneyWiseTransaction myTrans = (MoneyWiseTransaction) pTrans;
122         final OceanusDate myDate = myTrans.getDate();
123         final MoneyWiseTransAsset myAccount = myTrans.getAccount();
124         final MoneyWiseTransAsset myPartner = myTrans.getPartner();
125         final MoneyWiseTransCategory myCategory = myTrans.getCategory();
126         final MoneyWiseAssetDirection myDir = myTrans.getDirection();
127         final OceanusMoney myAmount = myTrans.getAmount();
128         final OceanusUnits myAccountUnits = myTrans.getAccountDeltaUnits();
129         final OceanusUnits myPartnerUnits = myTrans.getPartnerDeltaUnits();
130         boolean doCheckCombo = true;
131 
132         /* Header is always valid */
133         if (pTrans.isHeader()) {
134             myTrans.setValidEdit();
135             return;
136         }
137 
138         /* Determine date range to check for */
139         final MoneyWiseDataSet myDataSet = (MoneyWiseDataSet) myTrans.getDataSet();
140         final OceanusDateRange myRange = myDataSet.getDateRange();
141 
142         /* The date must be non-null */
143         if (myDate == null) {
144             myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicResource.MONEYWISEDATA_FIELD_DATE);
145             /* The date must be in-range */
146         } else if (myRange.compareToDate(myDate) != 0) {
147             myTrans.addError(PrometheusDataItem.ERROR_RANGE, MoneyWiseBasicResource.MONEYWISEDATA_FIELD_DATE);
148         }
149 
150         /* Account must be non-null */
151         if (myAccount == null) {
152             myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicResource.TRANSACTION_ACCOUNT);
153             doCheckCombo = false;
154 
155         } else {
156             /* Account must be valid */
157             if (!isValidAccount(myAccount)) {
158                 myTrans.addError(MoneyWiseTransBase.ERROR_COMBO, MoneyWiseBasicResource.TRANSACTION_ACCOUNT);
159                 doCheckCombo = false;
160             }
161         }
162 
163         /* Category must be non-null */
164         if (myCategory == null) {
165             myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicDataType.TRANSCATEGORY);
166             doCheckCombo = false;
167 
168             /* Category must be valid for Account */
169         } else if (doCheckCombo
170                 && !isValidCategory(myAccount, myCategory)) {
171             myTrans.addError(MoneyWiseTransBase.ERROR_COMBO, MoneyWiseBasicDataType.TRANSCATEGORY);
172             doCheckCombo = false;
173         }
174 
175         /* Direction must be non-null */
176         if (myDir == null) {
177             myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicResource.TRANSACTION_DIRECTION);
178             doCheckCombo = false;
179 
180             /* Direction must be valid for Account */
181         } else if (doCheckCombo
182                 && !isValidDirection(myAccount, myCategory, myDir)) {
183             myTrans.addError(MoneyWiseTransBase.ERROR_COMBO, MoneyWiseBasicResource.TRANSACTION_DIRECTION);
184             doCheckCombo = false;
185         }
186 
187         /* Partner must be non-null */
188         if (myPartner == null) {
189             myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicResource.TRANSACTION_PARTNER);
190 
191         } else {
192             /* Partner must be valid for Account */
193             if (doCheckCombo
194                     && !isValidPartner(myAccount, myCategory, myPartner)) {
195                 myTrans.addError(MoneyWiseTransBase.ERROR_COMBO, MoneyWiseBasicResource.TRANSACTION_PARTNER);
196             }
197         }
198 
199         /* If money is null */
200         if (myAmount == null) {
201             /* Check that it must be null */
202             if (!needsNullAmount(myTrans)) {
203                 myTrans.addError(PrometheusDataItem.ERROR_MISSING, MoneyWiseBasicResource.TRANSACTION_AMOUNT);
204             }
205 
206             /* else non-null money */
207         } else {
208             /* Check that it must be null */
209             if (needsNullAmount(myTrans)) {
210                 myTrans.addError(PrometheusDataItem.ERROR_EXIST, MoneyWiseBasicResource.TRANSACTION_AMOUNT);
211             }
212 
213             /* Money must not be negative */
214             if (!myAmount.isPositive()) {
215                 myTrans.addError(PrometheusDataItem.ERROR_NEGATIVE, MoneyWiseBasicResource.TRANSACTION_AMOUNT);
216             }
217 
218             /* Check that amount is correct currency */
219             if (myAccount != null) {
220                 final Currency myCurrency = myAccount.getCurrency();
221                 if (!myAmount.getCurrency().equals(myCurrency)) {
222                     myTrans.addError(MoneyWiseTransBase.ERROR_CURRENCY, MoneyWiseBasicResource.TRANSACTION_AMOUNT);
223                 }
224             }
225         }
226 
227         /* Cannot have PartnerUnits if securities are identical */
228         if (myAccountUnits != null
229                 && myPartnerUnits != null
230                 && MetisDataDifference.isEqual(myAccount, myPartner)) {
231             myTrans.addError(MoneyWiseTransaction.ERROR_CIRCULAR, MoneyWiseTransInfoSet.getFieldForClass(MoneyWiseTransInfoClass.PARTNERDELTAUNITS));
232         }
233 
234         /* If we have a category and an infoSet */
235         if (myCategory != null
236                 && myTrans.getInfoSet() != null) {
237             /* Validate the InfoSet */
238             theInfoSet.validate(myTrans.getInfoSet());
239         }
240 
241         /* Set validation flag */
242         if (!myTrans.hasErrors()) {
243             myTrans.setValidEdit();
244         }
245     }
246 
247     /**
248      * Determines whether an event needs a zero amount.
249      *
250      * @param pTrans the transaction
251      * @return true/false
252      */
253     public boolean needsNullAmount(final MoneyWiseTransaction pTrans) {
254         final MoneyWiseTransCategoryClass myClass = pTrans.getCategoryClass();
255         return myClass != null
256                 && myClass.needsNullAmount();
257     }
258 
259     @Override
260     public boolean isValidAccount(final MoneyWiseTransAsset pAccount) {
261         /* Validate securityHolding */
262         if (pAccount instanceof MoneyWiseSecurityHolding myHolding
263                 && !checkSecurityHolding(myHolding)) {
264             return false;
265         }
266 
267         /* Reject pensions portfolio */
268         if (pAccount instanceof MoneyWisePortfolio myPortfolio
269                 && myPortfolio.getCategoryClass().holdsPensions()) {
270             return false;
271         }
272 
273         /* Check type of account */
274         final MoneyWiseAssetType myType = pAccount.getAssetType();
275         return myType.isBaseAccount() && !pAccount.isHidden();
276     }
277 
278     @Override
279     public boolean isValidCategory(final MoneyWiseTransAsset pAccount,
280                                    final MoneyWiseTransCategory pCategory) {
281         /* Access details */
282         final MoneyWiseAssetType myType = pAccount.getAssetType();
283         final MoneyWiseTransCategoryClass myCatClass = Objects.requireNonNull(pCategory.getCategoryTypeClass());
284 
285         /* Immediately reject hidden categories */
286         if (myCatClass.isHiddenType()) {
287             return false;
288         }
289 
290         /* Switch on the CategoryClass */
291         return switch (myCatClass) {
292             case TAXEDINCOME, GROSSINCOME, RECOVEREDEXPENSES, OTHERINCOME ->
293                 /* Taxed/Other income must be to deposit/cash/loan */
294                     myType.isValued();
295             case PENSIONCONTRIB ->
296                 /* Pension contribution must be to a Pension holding or to a SIPP */
297                     (pAccount instanceof MoneyWiseSecurityHolding myHolding
298                             && myHolding.getSecurity().getCategoryClass().isPension())
299                             || (pAccount instanceof MoneyWisePortfolio myPortfolio
300                             && myPortfolio.isPortfolioClass(MoneyWisePortfolioClass.SIPP));
301             case GIFTEDINCOME, INHERITED ->
302                 /* Inheritance/Gifted must be to asset */
303                     myType.isAsset();
304             case INTEREST ->
305                 /* Account must be deposit or portfolio */
306                     myType.isDeposit() || myType.isPortfolio();
307             case DIVIDEND, SECURITYCLOSURE ->
308                 /* Account must be SecurityHolding */
309                     myType.isSecurityHolding();
310             case BADDEBTCAPITAL, BADDEBTINTEREST ->
311                 /* Account must be peer2Peer */
312                     pAccount instanceof MoneyWiseDeposit myDeposit
313                             && myDeposit.isDepositClass(MoneyWiseDepositCategoryClass.PEER2PEER);
314             case CASHBACK -> checkCashBack(pAccount);
315             case LOYALTYBONUS -> checkLoyaltyBonus(pAccount);
316             case RENTALINCOME, RENTALEXPENSE, ROOMRENTALINCOME ->
317                 /* Account must be property */
318                     pAccount instanceof MoneyWiseSecurityHolding myHolding
319                             && myHolding.getSecurity().isSecurityClass(MoneyWiseSecurityClass.PROPERTY);
320             case UNITSADJUST, SECURITYREPLACE ->
321                 /* Account must be capital */
322                     pAccount.isCapital();
323             case STOCKSPLIT, STOCKTAKEOVER, STOCKDEMERGER, STOCKRIGHTSISSUE ->
324                 /* Account must be shares */
325                     pAccount.isShares();
326             case WRITEOFF, LOANINTERESTEARNED, LOANINTERESTCHARGED, TAXRELIEF -> myType.isLoan();
327             case LOCALTAXES, INCOMETAX -> myType.isValued();
328             case EXPENSE -> myType.isValued() || myType.isAutoExpense();
329             case PORTFOLIOXFER -> pAccount instanceof MoneyWiseSecurityHolding
330                     || pAccount instanceof MoneyWisePortfolio;
331             case TRANSFER -> true;
332 
333             /* Reject other categories */
334             default -> false;
335         };
336     }
337 
338     @Override
339     public boolean isValidDirection(final MoneyWiseTransAsset pAccount,
340                                     final MoneyWiseTransCategory pCategory,
341                                     final MoneyWiseAssetDirection pDirection) {
342         /* TODO relax some of these rules */
343 
344         /* Access details */
345         final MoneyWiseTransCategoryClass myCatClass = pCategory.getCategoryTypeClass();
346 
347         /* Switch on the CategoryClass */
348         return switch (myCatClass) {
349             case TAXEDINCOME, GROSSINCOME ->
350                 /* Cannot refund Taxed Income yet */
351                     newValidation || pDirection.isFrom();
352             case PENSIONCONTRIB ->
353                 /* Cannot refund Pension Contribution */
354                     pDirection.isFrom();
355             case GIFTEDINCOME, INHERITED ->
356                 /* Cannot refund Gifted/Inherited Income yet */
357                     newValidation || pDirection.isFrom();
358             case RENTALINCOME, ROOMRENTALINCOME ->
359                 /* Cannot refund Rental Income */
360                     pDirection.isTo();
361             case RENTALEXPENSE ->
362                 /* Cannot refund Rental Expense */
363                     pDirection.isFrom();
364             case INTEREST ->
365                 /* Cannot refund Interest yet */
366                     newValidation || pDirection.isTo();
367             case DIVIDEND, SECURITYCLOSURE ->
368                 /* Cannot refund Dividend yet */
369                     pDirection.isTo();
370             case LOYALTYBONUS ->
371                 /* Cannot refund loyaltyBonus yet */
372                     newValidation || pDirection.isTo();
373             case WRITEOFF, LOANINTERESTCHARGED ->
374                 /* All need to be TO */
375                     newValidation || pDirection.isTo();
376             case LOANINTERESTEARNED ->
377                 /* All need to be FROM */
378                     newValidation || pDirection.isFrom();
379             case UNITSADJUST, STOCKSPLIT, STOCKDEMERGER, STOCKTAKEOVER, SECURITYREPLACE, PORTFOLIOXFER ->
380                 /* All need to be To */
381                     pDirection.isTo();
382             case null, default -> true;
383         };
384     }
385 
386     @Override
387     public boolean isValidPartner(final MoneyWiseTransAsset pAccount,
388                                   final MoneyWiseTransCategory pCategory,
389                                   final MoneyWiseTransAsset pPartner) {
390         /* Access details */
391         final boolean isRecursive = MetisDataDifference.isEqual(pAccount, pPartner);
392         final MoneyWiseAssetType myPartnerType = pPartner.getAssetType();
393         final MoneyWiseTransCategoryClass myCatClass = Objects.requireNonNull(pCategory.getCategoryTypeClass());
394 
395         /* Immediately reject hidden partners */
396         if (pPartner.isHidden()) {
397             return false;
398         }
399 
400         /* Validate securityHolding */
401         if (pPartner instanceof MoneyWiseSecurityHolding myHolding
402                 && !checkSecurityHolding(myHolding)) {
403             return false;
404         }
405 
406         /* Reject pensions portfolio */
407         if (pPartner instanceof MoneyWisePortfolio myPortfolio
408                 && myPortfolio.getCategoryClass().holdsPensions()) {
409             return false;
410         }
411 
412         /* If this involves auto-expense */
413         if (pAccount.isAutoExpense()
414                 || pPartner.isAutoExpense()) {
415             /* Access account type */
416             final MoneyWiseAssetType myAccountType = pAccount.getAssetType();
417 
418             /* Special processing */
419             return switch (myCatClass) {
420                 case TRANSFER ->
421                     /* Transfer must be to/from deposit/cash/loan */
422                         myPartnerType.isAutoExpense()
423                                 ? myAccountType.isValued()
424                                 : myPartnerType.isValued();
425                 case EXPENSE ->
426                     /* Transfer must be to/from payee */
427                         pPartner instanceof MoneyWisePayee;
428 
429                 /* Auto Expense cannot be used for other categories */
430                 default -> false;
431             };
432         }
433 
434         /* Switch on the CategoryClass */
435         return switch (myCatClass) {
436             case TAXEDINCOME, GROSSINCOME ->
437                 /* Taxed Income must have a Payee that can provide income */
438                     pPartner instanceof MoneyWisePayee myPayee
439                             && myPayee.getCategoryClass().canProvideTaxedIncome();
440             case PENSIONCONTRIB ->
441                 /* Pension Contribution must be a payee that can parent */
442                     pPartner instanceof MoneyWisePayee myPayee
443                             && myPayee.getCategoryClass().canContribPension();
444             case OTHERINCOME, RECOVEREDEXPENSES ->
445                 /* Other Income must have a Payee partner */
446                     pPartner instanceof MoneyWisePayee;
447             case LOCALTAXES ->
448                 /* LocalTaxes must have a Government Payee partner */
449                     pPartner instanceof MoneyWisePayee myPayee
450                             && myPayee.isPayeeClass(MoneyWisePayeeClass.GOVERNMENT);
451             case GIFTEDINCOME, INHERITED ->
452                 /* Gifted/Inherited Income must have an Individual Payee partner */
453                     pPartner instanceof MoneyWisePayee myPayee
454                             && myPayee.isPayeeClass(MoneyWisePayeeClass.INDIVIDUAL);
455             case RENTALINCOME, RENTALEXPENSE, ROOMRENTALINCOME ->
456                 /* RentalIncome/Expense must have a loan partner */
457                     myPartnerType.isLoan();
458             case WRITEOFF, LOANINTERESTEARNED, LOANINTERESTCHARGED ->
459                 /* WriteOff/LoanInterestEarned/Charged must be recursive */
460                     isRecursive;
461             case INTEREST, CASHBACK ->
462                 /* Interest/CashBack is to a valued account */
463                     myPartnerType.isValued();
464             case DIVIDEND -> checkDividend(pAccount, pPartner);
465             case LOYALTYBONUS -> checkLoyaltyBonus(pAccount, pPartner);
466             case BADDEBTCAPITAL, BADDEBTINTEREST -> pPartner instanceof MoneyWisePayee
467                     && MetisDataDifference.isEqual(pPartner, pAccount.getParent());
468             case UNITSADJUST, STOCKSPLIT ->
469                 /* Must be recursive */
470                     isRecursive;
471             case SECURITYREPLACE, STOCKTAKEOVER, STOCKDEMERGER -> checkTakeOver(pAccount, pPartner);
472             case STOCKRIGHTSISSUE -> checkStockRights(pAccount, pPartner);
473             case TRANSFER -> checkTransfer(pAccount, pPartner);
474             case SECURITYCLOSURE -> checkSecurityClosure(pAccount, pPartner);
475             case EXPENSE ->
476                 /* Expense must have a Payee partner */
477                     pPartner instanceof MoneyWisePayee;
478             case INCOMETAX, TAXRELIEF -> pPartner instanceof MoneyWisePayee myPayee
479                     && myPayee.isPayeeClass(MoneyWisePayeeClass.TAXMAN);
480             case PORTFOLIOXFER -> checkPortfolioXfer(pAccount, pPartner);
481             default -> false;
482         };
483     }
484 
485     /**
486      * Check securityHolding.
487      *
488      * @param pHolding the securityHolding
489      * @return valid true/false
490      */
491     private static boolean checkSecurityHolding(final MoneyWiseSecurityHolding pHolding) {
492         /* Access the components */
493         final MoneyWisePortfolio myPortfolio = pHolding.getPortfolio();
494         final MoneyWiseSecurity mySecurity = pHolding.getSecurity();
495 
496         /* If the portfolio can hold pensions */
497         if (myPortfolio.getCategoryClass().holdsPensions()) {
498             /* Can only hold pensions */
499             return mySecurity.getCategoryClass().isPension();
500         }
501 
502         /* cannot be a pension */
503         return !mySecurity.getCategoryClass().isPension();
504     }
505 
506     /**
507      * Check dividend.
508      *
509      * @param pAccount the holding providing the dividend.
510      * @param pPartner the partner
511      * @return valid true/false
512      */
513     private static boolean checkDividend(final MoneyWiseTransAsset pAccount,
514                                          final MoneyWiseTransAsset pPartner) {
515         /* Recursive is allowed */
516         if (MetisDataDifference.isEqual(pAccount, pPartner)) {
517             return true;
518         }
519 
520         /* partner must be valued */
521         return pPartner.getAssetType().isValued();
522     }
523 
524     /**
525      * Check TakeOver.
526      *
527      * @param pAccount the holding being acted on.
528      * @param pPartner the partner
529      * @return valid true/false
530      */
531     private static boolean checkTakeOver(final MoneyWiseTransAsset pAccount,
532                                          final MoneyWiseTransAsset pPartner) {
533         /* Must be holding <-> holding */
534         if (!(pAccount instanceof MoneyWiseSecurityHolding myAccount)
535                 || !(pPartner instanceof MoneyWiseSecurityHolding myPartner)) {
536             return false;
537         }
538 
539         /* Recursive is not allowed */
540         if (MetisDataDifference.isEqual(pAccount, pPartner)) {
541             return false;
542         }
543 
544         /* Portfolios must be the same */
545         if (!MetisDataDifference.isEqual(myAccount.getPortfolio(), myPartner.getPortfolio())) {
546             return false;
547         }
548 
549         /* Security types must be the same */
550         return MetisDataDifference.isEqual(myAccount.getSecurity().getCategory(), myPartner.getSecurity().getCategory());
551     }
552 
553     /**
554      * Check stock rights.
555      *
556      * @param pAccount the account being transferred.
557      * @param pPartner the partner
558      * @return valid true/false
559      */
560     private static boolean checkStockRights(final MoneyWiseTransAsset pAccount,
561                                             final MoneyWiseTransAsset pPartner) {
562         /* If this is security -> portfolio */
563         if (pAccount instanceof MoneyWiseSecurityHolding myHolding
564                 && pPartner instanceof MoneyWisePortfolio) {
565             /* Must be same portfolios */
566             return MetisDataDifference.isEqual(myHolding.getPortfolio(), pPartner);
567         }
568 
569         /* partner must be valued */
570         return pPartner.getAssetType().isValued();
571     }
572 
573     /**
574      * Check cashBack.
575      *
576      * @param pAccount the account providing cashBack.
577      * @return valid true/false
578      */
579     private static boolean checkCashBack(final MoneyWiseTransAsset pAccount) {
580         /* If this is deposit then check whether it can support cashBack */
581         if (pAccount instanceof MoneyWiseDeposit myDeposit) {
582             return myDeposit.getCategoryClass().canCashBack();
583         }
584 
585         /* If this is loan then check whether it can support cashBack */
586         if (pAccount instanceof MoneyWiseLoan myLoan) {
587             return myLoan.getCategoryClass().canCashBack();
588         }
589 
590         /* not allowed */
591         return false;
592     }
593 
594     /**
595      * Check loyalty bonus.
596      *
597      * @param pAccount the account providing bonus.
598      * @return valid true/false
599      */
600     private boolean checkLoyaltyBonus(final MoneyWiseTransAsset pAccount) {
601         /* If this is deposit then check whether it can support loyaltyBonus */
602         if (pAccount instanceof MoneyWiseDeposit myDeposit) {
603             return newValidation
604                     || myDeposit.getCategoryClass().canLoyaltyBonus();
605         }
606 
607         /* must be portfolio */
608         return pAccount instanceof MoneyWisePortfolio;
609     }
610 
611     /**
612      * Check loyalty bonus.
613      *
614      * @param pAccount the account providing bonus.
615      * @param pPartner the partner
616      * @return valid true/false
617      */
618     private static boolean checkLoyaltyBonus(final MoneyWiseTransAsset pAccount,
619                                              final MoneyWiseTransAsset pPartner) {
620         /* If this is portfolio -> security holding */
621         if (pAccount instanceof MoneyWisePortfolio
622                 && pPartner instanceof MoneyWiseSecurityHolding myHolding) {
623             /* Must be same portfolios */
624             return MetisDataDifference.isEqual(myHolding.getPortfolio(), pAccount);
625         }
626 
627         /* must be recursive */
628         return MetisDataDifference.isEqual(pAccount, pPartner);
629     }
630 
631     /**
632      * Check transfer.
633      *
634      * @param pAccount the account being transferred.
635      * @param pPartner the partner
636      * @return valid true/false
637      */
638     private static boolean checkTransfer(final MoneyWiseTransAsset pAccount,
639                                          final MoneyWiseTransAsset pPartner) {
640         /* Must not be recursive */
641         if (MetisDataDifference.isEqual(pAccount, pPartner)) {
642             return false;
643         }
644 
645         /* If this is security -> portfolio */
646         if (pAccount instanceof MoneyWiseSecurityHolding myHolding
647                 && pPartner instanceof MoneyWisePortfolio) {
648             /* Must be same portfolios */
649             if (!MetisDataDifference.isEqual(myHolding.getPortfolio(), pPartner)) {
650                 return false;
651             }
652         }
653 
654         /* If this is security <- portfolio */
655         if (pPartner instanceof MoneyWiseSecurityHolding myHolding
656                 && pAccount instanceof MoneyWisePortfolio) {
657             /* Must be same portfolios */
658             if (!MetisDataDifference.isEqual(myHolding.getPortfolio(), pAccount)) {
659                 return false;
660             }
661         }
662 
663         /* partner must be asset */
664         return pPartner.getAssetType().isAsset();
665     }
666 
667     /**
668      * Check securityClosure.
669      *
670      * @param pAccount the account being closed.
671      * @param pPartner the partner
672      * @return valid true/false
673      */
674     private static boolean checkSecurityClosure(final MoneyWiseTransAsset pAccount,
675                                                 final MoneyWiseTransAsset pPartner) {
676         /* Must not be recursive */
677         if (MetisDataDifference.isEqual(pAccount, pPartner)) {
678             return false;
679         }
680 
681         /* partner must be valued */
682         return pPartner.getAssetType().isValued();
683     }
684 
685     /**
686      * Check portfolioXfer.
687      *
688      * @param pAccount the account being transferred.
689      * @param pPartner the partner
690      * @return valid true/false
691      */
692     private static boolean checkPortfolioXfer(final MoneyWiseTransAsset pAccount,
693                                               final MoneyWiseTransAsset pPartner) {
694         /* Partner must be portfolio */
695         if (!(pPartner instanceof MoneyWisePortfolio)) {
696             return false;
697         }
698 
699         /* If account is portfolio */
700         if (pAccount instanceof MoneyWisePortfolio) {
701             /* Cannot be recursive */
702             if (MetisDataDifference.isEqual(pAccount, pPartner)) {
703                 return false;
704             }
705 
706             /* Must be same currency */
707             return MetisDataDifference.isEqual(pAccount.getAssetCurrency(), pPartner.getAssetCurrency());
708         }
709 
710         /* If account is security holding */
711         if (pAccount instanceof MoneyWiseSecurityHolding myHolding) {
712             /* Must be different portfolios */
713             return !MetisDataDifference.isEqual(myHolding.getPortfolio(), pPartner);
714         }
715 
716         /* Not allowed */
717         return false;
718     }
719 
720     /**
721      * Determine if an infoSet class is required.
722      *
723      * @param pTrans the transaction
724      * @param pClass the infoSet class
725      * @return the status
726      */
727     public MetisFieldRequired isClassRequired(final MoneyWiseTransaction pTrans,
728                                               final MoneyWiseTransInfoClass pClass) {
729         theInfoSet.storeInfoSet(pTrans.getInfoSet());
730         return theInfoSet.isClassRequired(pClass);
731     }
732 
733     @Override
734     public void autoCorrect(final MoneyWiseTransaction pItem) throws OceanusException {
735         theDefaults.autoCorrect(pItem);
736     }
737 
738     @Override
739     public MoneyWiseTransaction buildTransaction(final Object pKey) {
740         return theDefaults.buildTransaction(pKey);
741     }
742 
743     @Override
744     public void setRange(final OceanusDateRange pRange) {
745         theDefaults.setRange(pRange);
746     }
747 
748     @Override
749     public OceanusDateRange getRange() {
750         return theDefaults.getRange();
751     }
752 }