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