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.metis.data.MetisDataItem.MetisDataFieldId;
20  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetBase;
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.MoneyWisePortfolio;
27  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePortfolio.MoneyWisePortfolioList;
28  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurity;
29  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityHolding;
30  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityHolding.MoneyWiseSecurityHoldingMap;
31  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransAsset;
32  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransCategory;
33  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction;
34  import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction.MoneyWiseTransactionList;
35  import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseTransCategoryClass;
36  import io.github.tonywasher.joceanus.moneywise.exc.MoneyWiseDataException;
37  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
38  import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
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         return switch (myAsset.getAssetType()) {
322             case DEPOSIT, CASH, PAYEE, LOAN -> false;
323             default -> true;
324         };
325     }
326 
327     /**
328      * Resolve Values.
329      *
330      * @param pDate     the date of the transaction
331      * @param pDebit    the name of the debit object
332      * @param pCredit   the name of the credit object
333      * @param pCategory the name of the category object
334      * @return continue true/false
335      * @throws OceanusException on error
336      */
337     boolean resolveValues(final OceanusDate pDate,
338                           final String pDebit,
339                           final String pCredit,
340                           final String pCategory) throws OceanusException {
341         /* If the Date is null */
342         if (pDate == null) {
343             /* Resolve child values */
344             resolveChildValues(pDebit, pCredit, pCategory);
345             return true;
346         }
347 
348         /* If the date is too late */
349         if (!checkDate(pDate)) {
350             /* reject the transaction */
351             hitEventLimit = true;
352             return false;
353         }
354 
355         /* Note that there is no split */
356         isSplit = Boolean.FALSE;
357         theParent = null;
358 
359         /* Store the Date */
360         theDate = pDate;
361 
362         /* Resolve the names */
363         theLastDebit = theNameMap.get(pDebit);
364         theLastCredit = theNameMap.get(pCredit);
365         theCategory = theCategoryMap.get(pCategory);
366 
367         /* Check resolution */
368         checkResolution(pDebit, pCredit, pCategory);
369 
370         /* If the category is portfolio transfer */
371         if (theCategory.isCategoryClass(MoneyWiseTransCategoryClass.PORTFOLIOXFER)) {
372             /* Adjust maps to reflect the transfer */
373             resolvePortfolioXfer(theData, theLastDebit, theLastCredit);
374         }
375 
376         /* Resolve assets */
377         resolveAssets();
378         return true;
379     }
380 
381     /**
382      * Resolve Child Values.
383      *
384      * @param pDebit    the name of the debit object
385      * @param pCredit   the name of the credit object
386      * @param pCategory the name of the category object
387      * @throws OceanusException on error
388      */
389     private void resolveChildValues(final String pDebit,
390                                     final String pCredit,
391                                     final String pCategory) throws OceanusException {
392         /* Handle no LastParent */
393         if (theLastParent == null) {
394             throw new MoneyWiseDataException(theDate, "Missing parent transaction");
395         }
396 
397         /* Note that there is a split */
398         isSplit = Boolean.TRUE;
399         theParent = theLastParent;
400 
401         /* Resolve the debit and credit */
402         final Object myDebit = pDebit == null
403                 ? theLastDebit
404                 : theNameMap.get(pDebit);
405         final Object myCredit = pCredit == null
406                 ? theLastCredit
407                 : theNameMap.get(pCredit);
408 
409         /* Store last credit and debit */
410         theLastDebit = myDebit;
411         theLastCredit = myCredit;
412 
413         /* Resolve the category */
414         theCategory = theCategoryMap.get(pCategory);
415 
416         /* Check resolution */
417         checkResolution(pDebit, pCredit, pCategory);
418 
419         /* Resolve assets */
420         resolveAssets();
421     }
422 
423     /**
424      * Resolve assets.
425      *
426      * @throws OceanusException on error
427      */
428     private void resolveAssets() throws OceanusException {
429         final boolean isDebitHolding = theLastDebit instanceof MoneyWiseSecurityHolding;
430         final boolean isCreditHolding = theLastCredit instanceof MoneyWiseSecurityHolding;
431 
432         /* Resolve debit and credit */
433         final MoneyWiseTransAsset myDebit = (MoneyWiseTransAsset) theLastDebit;
434         final MoneyWiseTransAsset myCredit = (MoneyWiseTransAsset) theLastCredit;
435 
436         /* Access asset types */
437         final MoneyWiseAssetType myDebitType = myDebit.getAssetType();
438         final MoneyWiseAssetType myCreditType = myCredit.getAssetType();
439 
440         /* Handle non-Asset debit */
441         if (!myDebitType.isBaseAccount()) {
442             /* Use credit as account */
443             isDebitReversed = true;
444 
445             /* Handle non-Asset credit */
446         } else if (!myCreditType.isBaseAccount()) {
447             /* Use debit as account */
448             isDebitReversed = false;
449 
450             /* Handle non-child transfer */
451         } else if (!isSplit) {
452             /* Flip values for StockRightsTaken and LoanInterest */
453             switch (Objects.requireNonNull(theCategory.getCategoryTypeClass())) {
454                 case STOCKRIGHTSISSUE:
455                     /* Use securityHolding as account */
456                     isDebitReversed = !myDebitType.isSecurityHolding();
457                     break;
458                 case LOANINTERESTEARNED:
459                     /* Use credit as account */
460                     isDebitReversed = !theData.newValidityChecks();
461                     break;
462                 case LOANINTERESTCHARGED, WRITEOFF:
463                     /* Use credit as account */
464                     isDebitReversed = theData.newValidityChecks();
465                     break;
466                 default:
467                     /* Use debit as account */
468                     isDebitReversed = false;
469                     break;
470             }
471         } else {
472             /* Access parent assets */
473             final MoneyWiseTransAsset myParAccount = theParent.getAccount();
474             final MoneyWiseTransAsset myParPartner = theParent.getPartner();
475 
476             /* If we match the parent on debit */
477             if (myDebit.equals(myParAccount)) {
478                 /* Use debit as account */
479                 isDebitReversed = false;
480 
481                 /* else if we match credit account */
482             } else if (myCredit.equals(myParAccount)) {
483                 /* Use credit as account */
484                 isDebitReversed = true;
485 
486                 /* else don't match the parent account, so parent must be wrong */
487             } else {
488                 /* Flip parent assets */
489                 theParent.flipAssets();
490 
491                 /* Determine if debit is reversed */
492                 isDebitReversed = !myDebit.equals(myParPartner);
493             }
494         }
495 
496         /* Set up values */
497         if (!isDebitReversed) {
498             /* Use debit as account */
499             theAccount = myDebit;
500             thePartner = myCredit;
501             theDirection = MoneyWiseAssetDirection.TO;
502         } else {
503             /* Use credit as account */
504             theAccount = myCredit;
505             thePartner = myDebit;
506             theDirection = MoneyWiseAssetDirection.FROM;
507         }
508 
509         /* Resolve portfolio */
510         thePortfolio = null;
511         if (isDebitHolding) {
512             thePortfolio = ((MoneyWiseSecurityHolding) theLastDebit).getPortfolio();
513             if (isCreditHolding) {
514                 final MoneyWisePortfolio myPortfolio = ((MoneyWiseSecurityHolding) theLastCredit).getPortfolio();
515                 if (!thePortfolio.equals(myPortfolio)) {
516                     throw new MoneyWiseDataException(theDate, "Inconsistent portfolios");
517                 }
518             }
519         } else if (isCreditHolding) {
520             thePortfolio = ((MoneyWiseSecurityHolding) theLastCredit).getPortfolio();
521         }
522     }
523 
524     /**
525      * Check resolution.
526      *
527      * @param pDebit    the name of the debit object
528      * @param pCredit   the name of the credit object
529      * @param pCategory the name of the category object
530      * @throws OceanusException on error
531      */
532     private void checkResolution(final String pDebit,
533                                  final String pCredit,
534                                  final String pCategory) throws OceanusException {
535         /* Check debit resolution */
536         if (theLastDebit == null) {
537             throw new MoneyWiseDataException(pDebit, "Failed to resolve debit account on " + theDate);
538         }
539 
540         /* Check credit resolution */
541         if (theLastCredit == null) {
542             throw new MoneyWiseDataException(pCredit, "Failed to resolve credit account on " + theDate);
543         }
544 
545         /* Check category resolution */
546         if (theCategory == null) {
547             throw new MoneyWiseDataException(pCategory, "Failed to resolve category on " + theDate);
548         }
549     }
550 
551     /**
552      * Declare asset.
553      *
554      * @param pAsset the asset to declare.
555      * @throws OceanusException on error
556      */
557     void declareAsset(final MoneyWiseAssetBase pAsset) throws OceanusException {
558         /* Access the asset name */
559         final String myName = pAsset.getName();
560 
561         /* Check for name already exists */
562         if (theNameMap.get(myName) != null) {
563             throw new MoneyWiseDataException(pAsset, PrometheusDataItem.ERROR_DUPLICATE);
564         }
565 
566         /* Store the asset */
567         theNameMap.put(myName, pAsset);
568     }
569 
570     /**
571      * Declare category.
572      *
573      * @param pCategory the category to declare.
574      * @throws OceanusException on error
575      */
576     void declareCategory(final MoneyWiseTransCategory pCategory) throws OceanusException {
577         /* Access the asset name */
578         final String myName = pCategory.getName();
579 
580         /* Check for name already exists */
581         if (theCategoryMap.get(myName) != null) {
582             throw new MoneyWiseDataException(pCategory, PrometheusDataItem.ERROR_DUPLICATE);
583         }
584 
585         /* Store the category */
586         theCategoryMap.put(myName, pCategory);
587     }
588 
589     /**
590      * Declare security holding.
591      *
592      * @param pSecurity  the security.
593      * @param pPortfolio the portfolio
594      * @throws OceanusException on error
595      */
596     void declareSecurityHolding(final MoneyWiseSecurity pSecurity,
597                                 final String pPortfolio) throws OceanusException {
598         /* Access the name */
599         final String myName = pSecurity.getName();
600 
601         /* Check for name already exists */
602         if (theNameMap.get(myName) != null) {
603             throw new MoneyWiseDataException(pSecurity, PrometheusDataItem.ERROR_DUPLICATE);
604         }
605 
606         /* Store the asset */
607         theNameMap.put(myName, new MoneyWiseSecurityHoldingDef(pSecurity, pPortfolio));
608     }
609 
610     /**
611      * Declare alias holding.
612      *
613      * @param pName      the security holding name
614      * @param pAlias     the alias name.
615      * @param pPortfolio the portfolio
616      * @throws OceanusException on error
617      */
618     void declareAliasHolding(final String pName,
619                              final String pAlias,
620                              final String pPortfolio) throws OceanusException {
621         /* Check for name already exists */
622         final Object myHolding = theNameMap.get(pAlias);
623         if (!(myHolding instanceof MoneyWiseSecurityHoldingDef myAliased)) {
624             throw new MoneyWiseDataException(pAlias, "Aliased security not found");
625         }
626 
627         /* Store the asset */
628         theNameMap.put(pName, new MoneyWiseSecurityHoldingDef(myAliased.getSecurity(), pPortfolio));
629     }
630 
631     /**
632      * Resolve security holdings.
633      *
634      * @param pData the dataSet
635      */
636     void resolveSecurityHoldings(final MoneyWiseDataSet pData) {
637         /* Access securityHoldingsMap and Portfolio list */
638         final MoneyWiseSecurityHoldingMap myMap = pData.getPortfolios().getSecurityHoldingsMap();
639         final MoneyWisePortfolioList myPortfolios = pData.getPortfolios();
640 
641         /* Loop through the name map */
642         for (Entry<String, Object> myEntry : theNameMap.entrySet()) {
643             /* If this is a security holding definition */
644             final Object myValue = myEntry.getValue();
645             if (myValue instanceof MoneyWiseSecurityHoldingDef myDef) {
646                 /* Access security holding */
647                 final MoneyWisePortfolio myPortfolio = myPortfolios.findItemByName(myDef.getPortfolio());
648                 final MoneyWiseSecurityHolding myHolding = myMap.declareHolding(myPortfolio, myDef.getSecurity());
649 
650                 /* Replace definition in map */
651                 myEntry.setValue(myHolding);
652             }
653         }
654     }
655 
656 
657     /**
658      * Process portfolio transfer.
659      *
660      * @param pData   the dataSet
661      * @param pSource the source asset
662      * @param pTarget the target asset
663      * @throws OceanusException on error
664      */
665     private void resolvePortfolioXfer(final MoneyWiseDataSet pData,
666                                       final Object pSource,
667                                       final Object pTarget) throws OceanusException {
668         /* Target must be portfolio */
669         if (!(pTarget instanceof MoneyWisePortfolio myPortfolio)) {
670             throw new MoneyWiseDataException(pTarget, "Inconsistent portfolios");
671         }
672 
673         final MoneyWiseSecurityHoldingMap myMap = pData.getPortfolios().getSecurityHoldingsMap();
674 
675         /* Loop through the name map */
676         for (Entry<String, Object> myEntry : theNameMap.entrySet()) {
677             /* If this is a security holding definition */
678             final Object myValue = myEntry.getValue();
679             if (myValue instanceof MoneyWiseSecurityHolding myHolding
680                     && (pSource.equals(myHolding) || pSource.equals(myHolding.getPortfolio()))) {
681                 /* Change the holding */
682                 myHolding = myMap.declareHolding(myPortfolio, myHolding.getSecurity());
683 
684                 /* Replace definition in map */
685                 myEntry.setValue(myHolding);
686             }
687         }
688     }
689 
690     /**
691      * Security Holding Definition.
692      */
693     private static final class MoneyWiseSecurityHoldingDef {
694         /**
695          * Security.
696          */
697         private final MoneyWiseSecurity theSecurity;
698 
699         /**
700          * Portfolio.
701          */
702         private final String thePortfolio;
703 
704         /**
705          * Constructor.
706          *
707          * @param pSecurity  the security
708          * @param pPortfolio the portfolio
709          */
710         private MoneyWiseSecurityHoldingDef(final MoneyWiseSecurity pSecurity,
711                                             final String pPortfolio) {
712             /* Store parameters */
713             theSecurity = pSecurity;
714             thePortfolio = pPortfolio;
715         }
716 
717         /**
718          * Obtain security.
719          *
720          * @return the security
721          */
722         public MoneyWiseSecurity getSecurity() {
723             return theSecurity;
724         }
725 
726         /**
727          * Obtain portfolio.
728          *
729          * @return the portfolio
730          */
731         public String getPortfolio() {
732             return thePortfolio;
733         }
734     }
735 }