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.archive;
18  
19  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
20  import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
21  import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataFieldId;
22  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetBase;
23  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetDirection;
24  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetType;
25  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicDataType;
26  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicResource;
27  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDataSet;
28  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePortfolio;
29  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePortfolio.MoneyWisePortfolioList;
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.MoneyWiseSecurityHolding.MoneyWiseSecurityHoldingMap;
33  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransAsset;
34  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransCategory;
35  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction;
36  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction.MoneyWiseTransactionList;
37  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseTransCategoryClass;
38  import io.github.tonywasher.joceanus.moneywise.exc.MoneyWiseDataException;
39  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataItem;
40  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataValues;
41  
42  import java.util.ArrayList;
43  import java.util.HashMap;
44  import java.util.List;
45  import java.util.ListIterator;
46  import java.util.Map;
47  import java.util.Map.Entry;
48  import java.util.Objects;
49  
50  /**
51   * Parent Cache details.
52   */
53  public final class MoneyWiseArchiveCache {
54      /**
55       * DataSet.
56       */
57      private final MoneyWiseDataSet theData;
58  
59      /**
60       * TransactionList.
61       */
62      private final MoneyWiseTransactionList theList;
63  
64      /**
65       * The map of names to assets.
66       */
67      private final Map<String, Object> theNameMap;
68  
69      /**
70       * The map of names->categories.
71       */
72      private final Map<String, MoneyWiseTransCategory> theCategoryMap;
73  
74      /**
75       * The list of years.
76       */
77      private final List<MoneyWiseArchiveYear> theYears;
78  
79      /**
80       * Are we filtering?.
81       */
82      private boolean enableFiltering;
83  
84      /**
85       * Last Parent.
86       */
87      private MoneyWiseTransaction theLastParent;
88  
89      /**
90       * Last Debit.
91       */
92      private Object theLastDebit;
93  
94      /**
95       * Last Credit.
96       */
97      private Object theLastCredit;
98  
99      /**
100      * The parent.
101      */
102     private MoneyWiseTransaction theParent;
103 
104     /**
105      * Split Status.
106      */
107     private boolean isSplit;
108 
109     /**
110      * Resolved Date.
111      */
112     private OceanusDate theDate;
113 
114     /**
115      * AssetPair Id.
116      */
117     private MoneyWiseAssetDirection theDirection;
118 
119     /**
120      * Resolved Account.
121      */
122     private MoneyWiseTransAsset theAccount;
123 
124     /**
125      * Resolved Partner.
126      */
127     private MoneyWiseTransAsset thePartner;
128 
129     /**
130      * Resolved Transaction Category.
131      */
132     private MoneyWiseTransCategory theCategory;
133 
134     /**
135      * Resolved Portfolio.
136      */
137     private MoneyWisePortfolio thePortfolio;
138 
139     /**
140      * Is the Debit reversed?
141      */
142     private boolean isDebitReversed;
143 
144     /**
145      * The last event.
146      */
147     private OceanusDate theLastEvent;
148 
149     /**
150      * Have we hit the lastEvent limit.
151      */
152     private boolean hitEventLimit;
153 
154     /**
155      * Constructor.
156      *
157      * @param pData the dataSet
158      */
159     MoneyWiseArchiveCache(final MoneyWiseDataSet pData) {
160         /* Store lists */
161         theData = pData;
162         theList = theData.getTransactions();
163 
164         /* Create the maps */
165         theNameMap = new HashMap<>();
166         theCategoryMap = new HashMap<>();
167         theYears = new ArrayList<>();
168     }
169 
170     /**
171      * Enable filtering.
172      */
173     void enableFiltering() {
174         enableFiltering = true;
175     }
176 
177     /**
178      * Set lastEvent.
179      *
180      * @param pLastEvent the last event date
181      */
182     void setLastEvent(final OceanusDate pLastEvent) {
183         theLastEvent = pLastEvent;
184     }
185 
186     /**
187      * Check valid date.
188      *
189      * @param pDate the date
190      * @return true/false
191      */
192     boolean checkDate(final OceanusDate pDate) {
193         return theLastEvent == null || theLastEvent.compareTo(pDate) >= 0;
194     }
195 
196     /**
197      * Did we hit the event limit?
198      *
199      * @return true/false
200      */
201     boolean hitEventLimit() {
202         return hitEventLimit;
203     }
204 
205     /**
206      * Add a year to the front of the list.
207      *
208      * @param pName the range name
209      */
210     void addYear(final String pName) {
211         final MoneyWiseArchiveYear myYear = new MoneyWiseArchiveYear(pName);
212         theYears.add(myYear);
213     }
214 
215     /**
216      * Get the iterator.
217      *
218      * @return the iterator
219      */
220     ListIterator<MoneyWiseArchiveYear> getIterator() {
221         return theYears.listIterator();
222     }
223 
224     /**
225      * Obtain the reverse iterator of the years.
226      *
227      * @return the iterator.
228      */
229     ListIterator<MoneyWiseArchiveYear> reverseIterator() {
230         return theYears.listIterator(getNumYears());
231     }
232 
233     /**
234      * Get the number of years.
235      *
236      * @return the number of years
237      */
238     int getNumYears() {
239         return theYears.size();
240     }
241 
242     /**
243      * Build transaction.
244      *
245      * @param pAmount     the amount
246      * @param pReconciled is the transaction reconciled?
247      * @return the new transaction
248      * @throws OceanusException on error
249      */
250     MoneyWiseTransaction buildTransaction(final String pAmount,
251                                           final boolean pReconciled) throws OceanusException {
252         /* Build data values */
253         final PrometheusDataValues myValues = new PrometheusDataValues(MoneyWiseTransaction.OBJECT_NAME);
254         myValues.addValue(MoneyWiseBasicResource.MONEYWISEDATA_FIELD_DATE, theDate);
255         myValues.addValue(MoneyWiseBasicDataType.TRANSCATEGORY, theCategory);
256         myValues.addValue(MoneyWiseBasicResource.TRANSACTION_DIRECTION, theDirection);
257         myValues.addValue(MoneyWiseBasicResource.TRANSACTION_ACCOUNT, theAccount);
258         myValues.addValue(MoneyWiseBasicResource.TRANSACTION_PARTNER, thePartner);
259         myValues.addValue(MoneyWiseBasicResource.TRANSACTION_RECONCILED, pReconciled);
260         if (pAmount != null) {
261             myValues.addValue(MoneyWiseBasicResource.TRANSACTION_AMOUNT, pAmount);
262         }
263 
264         if (filterTransaction(myValues)) {
265             return null;
266         }
267 
268         /* Add the value into the list */
269         final MoneyWiseTransaction myTrans = theList.addValuesItem(myValues);
270 
271         /* If we were not a child */
272         if (!isSplit) {
273             /* Note the last parent */
274             theLastParent = myTrans;
275         }
276 
277         /* return the new transaction */
278         return myTrans;
279     }
280 
281     /**
282      * Is the debit reversed?
283      *
284      * @return true/false
285      */
286     boolean isDebitReversed() {
287         return isDebitReversed;
288     }
289 
290     /**
291      * Is the transaction recursive?
292      *
293      * @return true/false
294      */
295     boolean isRecursive() {
296         return theLastDebit.equals(theLastCredit);
297     }
298 
299     /**
300      * should we filter this transaction?
301      *
302      * @param pTrans the transaction
303      * @return true/false
304      */
305     boolean filterTransaction(final PrometheusDataValues pTrans) {
306         return enableFiltering
307                 && (filterAsset(pTrans, MoneyWiseBasicResource.TRANSACTION_ACCOUNT)
308                 || filterAsset(pTrans, MoneyWiseBasicResource.TRANSACTION_PARTNER));
309     }
310 
311     /**
312      * Should we filter this asset?
313      *
314      * @param pTrans the transaction values
315      * @param pAsset the asset
316      * @return true/false
317      */
318     private boolean filterAsset(final PrometheusDataValues pTrans,
319                                 final MetisDataFieldId pAsset) {
320         final MoneyWiseTransAsset myAsset = pTrans.getValue(pAsset, MoneyWiseTransAsset.class);
321         switch (myAsset.getAssetType()) {
322             case DEPOSIT:
323             case CASH:
324             case PAYEE:
325             case LOAN:
326                 return false;
327             default:
328                 return true;
329         }
330     }
331 
332     /**
333      * Resolve Values.
334      *
335      * @param pDate     the date of the transaction
336      * @param pDebit    the name of the debit object
337      * @param pCredit   the name of the credit object
338      * @param pCategory the name of the category object
339      * @return continue true/false
340      * @throws OceanusException on error
341      */
342     boolean resolveValues(final OceanusDate pDate,
343                           final String pDebit,
344                           final String pCredit,
345                           final String pCategory) throws OceanusException {
346         /* If the Date is null */
347         if (pDate == null) {
348             /* Resolve child values */
349             resolveChildValues(pDebit, pCredit, pCategory);
350             return true;
351         }
352 
353         /* If the date is too late */
354         if (!checkDate(pDate)) {
355             /* reject the transaction */
356             hitEventLimit = true;
357             return false;
358         }
359 
360         /* Note that there is no split */
361         isSplit = Boolean.FALSE;
362         theParent = null;
363 
364         /* Store the Date */
365         theDate = pDate;
366 
367         /* Resolve the names */
368         theLastDebit = theNameMap.get(pDebit);
369         theLastCredit = theNameMap.get(pCredit);
370         theCategory = theCategoryMap.get(pCategory);
371 
372         /* Check resolution */
373         checkResolution(pDebit, pCredit, pCategory);
374 
375         /* If the category is portfolio transfer */
376         if (theCategory.isCategoryClass(MoneyWiseTransCategoryClass.PORTFOLIOXFER)) {
377             /* Adjust maps to reflect the transfer */
378             resolvePortfolioXfer(theData, theLastDebit, theLastCredit);
379         }
380 
381         /* Resolve assets */
382         resolveAssets();
383         return true;
384     }
385 
386     /**
387      * Resolve Child Values.
388      *
389      * @param pDebit    the name of the debit object
390      * @param pCredit   the name of the credit object
391      * @param pCategory the name of the category object
392      * @throws OceanusException on error
393      */
394     private void resolveChildValues(final String pDebit,
395                                     final String pCredit,
396                                     final String pCategory) throws OceanusException {
397         /* Handle no LastParent */
398         if (theLastParent == null) {
399             throw new MoneyWiseDataException(theDate, "Missing parent transaction");
400         }
401 
402         /* Note that there is a split */
403         isSplit = Boolean.TRUE;
404         theParent = theLastParent;
405 
406         /* Resolve the debit and credit */
407         final Object myDebit = pDebit == null
408                 ? theLastDebit
409                 : theNameMap.get(pDebit);
410         final Object myCredit = pCredit == null
411                 ? theLastCredit
412                 : theNameMap.get(pCredit);
413 
414         /* Store last credit and debit */
415         theLastDebit = myDebit;
416         theLastCredit = myCredit;
417 
418         /* Resolve the category */
419         theCategory = theCategoryMap.get(pCategory);
420 
421         /* Check resolution */
422         checkResolution(pDebit, pCredit, pCategory);
423 
424         /* Resolve assets */
425         resolveAssets();
426     }
427 
428     /**
429      * Resolve assets.
430      *
431      * @throws OceanusException on error
432      */
433     private void resolveAssets() throws OceanusException {
434         final boolean isDebitHolding = theLastDebit instanceof MoneyWiseSecurityHolding;
435         final boolean isCreditHolding = theLastCredit instanceof MoneyWiseSecurityHolding;
436 
437         /* Resolve debit and credit */
438         final MoneyWiseTransAsset myDebit = (MoneyWiseTransAsset) theLastDebit;
439         final MoneyWiseTransAsset myCredit = (MoneyWiseTransAsset) theLastCredit;
440 
441         /* Access asset types */
442         final MoneyWiseAssetType myDebitType = myDebit.getAssetType();
443         final MoneyWiseAssetType myCreditType = myCredit.getAssetType();
444 
445         /* Handle non-Asset debit */
446         if (!myDebitType.isBaseAccount()) {
447             /* Use credit as account */
448             isDebitReversed = true;
449 
450             /* Handle non-Asset credit */
451         } else if (!myCreditType.isBaseAccount()) {
452             /* Use debit as account */
453             isDebitReversed = false;
454 
455             /* Handle non-child transfer */
456         } else if (!isSplit) {
457             /* Flip values for StockRightsTaken and LoanInterest */
458             switch (Objects.requireNonNull(theCategory.getCategoryTypeClass())) {
459                 case STOCKRIGHTSISSUE:
460                     /* Use securityHolding as account */
461                     isDebitReversed = !myDebitType.isSecurityHolding();
462                     break;
463                 case LOANINTERESTEARNED:
464                     /* Use credit as account */
465                     isDebitReversed = !theData.newValidityChecks();
466                     break;
467                 case LOANINTERESTCHARGED:
468                 case WRITEOFF:
469                     /* Use credit as account */
470                     isDebitReversed = theData.newValidityChecks();
471                     break;
472                 default:
473                     /* Use debit as account */
474                     isDebitReversed = false;
475                     break;
476             }
477         } else {
478             /* Access parent assets */
479             final MoneyWiseTransAsset myParAccount = theParent.getAccount();
480             final MoneyWiseTransAsset myParPartner = theParent.getPartner();
481 
482             /* If we match the parent on debit */
483             if (myDebit.equals(myParAccount)) {
484                 /* Use debit as account */
485                 isDebitReversed = false;
486 
487                 /* else if we match credit account */
488             } else if (myCredit.equals(myParAccount)) {
489                 /* Use credit as account */
490                 isDebitReversed = true;
491 
492                 /* else don't match the parent account, so parent must be wrong */
493             } else {
494                 /* Flip parent assets */
495                 theParent.flipAssets();
496 
497                 /* Determine if debit is reversed */
498                 isDebitReversed = !myDebit.equals(myParPartner);
499             }
500         }
501 
502         /* Set up values */
503         if (!isDebitReversed) {
504             /* Use debit as account */
505             theAccount = myDebit;
506             thePartner = myCredit;
507             theDirection = MoneyWiseAssetDirection.TO;
508         } else {
509             /* Use credit as account */
510             theAccount = myCredit;
511             thePartner = myDebit;
512             theDirection = MoneyWiseAssetDirection.FROM;
513         }
514 
515         /* Resolve portfolio */
516         thePortfolio = null;
517         if (isDebitHolding) {
518             thePortfolio = ((MoneyWiseSecurityHolding) theLastDebit).getPortfolio();
519             if (isCreditHolding) {
520                 final MoneyWisePortfolio myPortfolio = ((MoneyWiseSecurityHolding) theLastCredit).getPortfolio();
521                 if (!thePortfolio.equals(myPortfolio)) {
522                     throw new MoneyWiseDataException(theDate, "Inconsistent portfolios");
523                 }
524             }
525         } else if (isCreditHolding) {
526             thePortfolio = ((MoneyWiseSecurityHolding) theLastCredit).getPortfolio();
527         }
528     }
529 
530     /**
531      * Check resolution.
532      *
533      * @param pDebit    the name of the debit object
534      * @param pCredit   the name of the credit object
535      * @param pCategory the name of the category object
536      * @throws OceanusException on error
537      */
538     private void checkResolution(final String pDebit,
539                                  final String pCredit,
540                                  final String pCategory) throws OceanusException {
541         /* Check debit resolution */
542         if (theLastDebit == null) {
543             throw new MoneyWiseDataException(pDebit, "Failed to resolve debit account on " + theDate);
544         }
545 
546         /* Check credit resolution */
547         if (theLastCredit == null) {
548             throw new MoneyWiseDataException(pCredit, "Failed to resolve credit account on " + theDate);
549         }
550 
551         /* Check category resolution */
552         if (theCategory == null) {
553             throw new MoneyWiseDataException(pCategory, "Failed to resolve category on " + theDate);
554         }
555     }
556 
557     /**
558      * Declare asset.
559      *
560      * @param pAsset the asset to declare.
561      * @throws OceanusException on error
562      */
563     void declareAsset(final MoneyWiseAssetBase pAsset) throws OceanusException {
564         /* Access the asset name */
565         final String myName = pAsset.getName();
566 
567         /* Check for name already exists */
568         if (theNameMap.get(myName) != null) {
569             throw new MoneyWiseDataException(pAsset, PrometheusDataItem.ERROR_DUPLICATE);
570         }
571 
572         /* Store the asset */
573         theNameMap.put(myName, pAsset);
574     }
575 
576     /**
577      * Declare category.
578      *
579      * @param pCategory the category to declare.
580      * @throws OceanusException on error
581      */
582     void declareCategory(final MoneyWiseTransCategory pCategory) throws OceanusException {
583         /* Access the asset name */
584         final String myName = pCategory.getName();
585 
586         /* Check for name already exists */
587         if (theCategoryMap.get(myName) != null) {
588             throw new MoneyWiseDataException(pCategory, PrometheusDataItem.ERROR_DUPLICATE);
589         }
590 
591         /* Store the category */
592         theCategoryMap.put(myName, pCategory);
593     }
594 
595     /**
596      * Declare security holding.
597      *
598      * @param pSecurity  the security.
599      * @param pPortfolio the portfolio
600      * @throws OceanusException on error
601      */
602     void declareSecurityHolding(final MoneyWiseSecurity pSecurity,
603                                 final String pPortfolio) throws OceanusException {
604         /* Access the name */
605         final String myName = pSecurity.getName();
606 
607         /* Check for name already exists */
608         if (theNameMap.get(myName) != null) {
609             throw new MoneyWiseDataException(pSecurity, PrometheusDataItem.ERROR_DUPLICATE);
610         }
611 
612         /* Store the asset */
613         theNameMap.put(myName, new MoneyWiseSecurityHoldingDef(pSecurity, pPortfolio));
614     }
615 
616     /**
617      * Declare alias holding.
618      *
619      * @param pName      the security holding name
620      * @param pAlias     the alias name.
621      * @param pPortfolio the portfolio
622      * @throws OceanusException on error
623      */
624     void declareAliasHolding(final String pName,
625                              final String pAlias,
626                              final String pPortfolio) throws OceanusException {
627         /* Check for name already exists */
628         final Object myHolding = theNameMap.get(pAlias);
629         if (!(myHolding instanceof MoneyWiseSecurityHoldingDef myAliased)) {
630             throw new MoneyWiseDataException(pAlias, "Aliased security not found");
631         }
632 
633         /* Store the asset */
634         theNameMap.put(pName, new MoneyWiseSecurityHoldingDef(myAliased.getSecurity(), pPortfolio));
635     }
636 
637     /**
638      * Resolve security holdings.
639      *
640      * @param pData the dataSet
641      */
642     void resolveSecurityHoldings(final MoneyWiseDataSet pData) {
643         /* Access securityHoldingsMap and Portfolio list */
644         final MoneyWiseSecurityHoldingMap myMap = pData.getPortfolios().getSecurityHoldingsMap();
645         final MoneyWisePortfolioList myPortfolios = pData.getPortfolios();
646 
647         /* Loop through the name map */
648         for (Entry<String, Object> myEntry : theNameMap.entrySet()) {
649             /* If this is a security holding definition */
650             final Object myValue = myEntry.getValue();
651             if (myValue instanceof MoneyWiseSecurityHoldingDef myDef) {
652                 /* Access security holding */
653                 final MoneyWisePortfolio myPortfolio = myPortfolios.findItemByName(myDef.getPortfolio());
654                 final MoneyWiseSecurityHolding myHolding = myMap.declareHolding(myPortfolio, myDef.getSecurity());
655 
656                 /* Replace definition in map */
657                 myEntry.setValue(myHolding);
658             }
659         }
660     }
661 
662 
663     /**
664      * Process portfolio transfer.
665      *
666      * @param pData   the dataSet
667      * @param pSource the source asset
668      * @param pTarget the target asset
669      * @throws OceanusException on error
670      */
671     private void resolvePortfolioXfer(final MoneyWiseDataSet pData,
672                                       final Object pSource,
673                                       final Object pTarget) throws OceanusException {
674         /* Target must be portfolio */
675         if (!(pTarget instanceof MoneyWisePortfolio myPortfolio)) {
676             throw new MoneyWiseDataException(pTarget, "Inconsistent portfolios");
677         }
678 
679         final MoneyWiseSecurityHoldingMap myMap = pData.getPortfolios().getSecurityHoldingsMap();
680 
681         /* Loop through the name map */
682         for (Entry<String, Object> myEntry : theNameMap.entrySet()) {
683             /* If this is a security holding definition */
684             final Object myValue = myEntry.getValue();
685             if (myValue instanceof MoneyWiseSecurityHolding myHolding) {
686                 /* If this holding needs updating */
687                 if (pSource.equals(myHolding) || pSource.equals(myHolding.getPortfolio())) {
688                     /* Change the holding */
689                     myHolding = myMap.declareHolding(myPortfolio, myHolding.getSecurity());
690 
691                     /* Replace definition in map */
692                     myEntry.setValue(myHolding);
693                 }
694             }
695         }
696     }
697 
698     /**
699      * Security Holding Definition.
700      */
701     private static final class MoneyWiseSecurityHoldingDef {
702         /**
703          * Security.
704          */
705         private final MoneyWiseSecurity theSecurity;
706 
707         /**
708          * Portfolio.
709          */
710         private final String thePortfolio;
711 
712         /**
713          * Constructor.
714          *
715          * @param pSecurity  the security
716          * @param pPortfolio the portfolio
717          */
718         private MoneyWiseSecurityHoldingDef(final MoneyWiseSecurity pSecurity,
719                                             final String pPortfolio) {
720             /* Store parameters */
721             theSecurity = pSecurity;
722             thePortfolio = pPortfolio;
723         }
724 
725         /**
726          * Obtain security.
727          *
728          * @return the security
729          */
730         public MoneyWiseSecurity getSecurity() {
731             return theSecurity;
732         }
733 
734         /**
735          * Obtain portfolio.
736          *
737          * @return the portfolio
738          */
739         public String getPortfolio() {
740             return thePortfolio;
741         }
742     }
743 }