LCOV - code coverage report
Current view: top level - lib/_classes/storage - app_data.dart Coverage Total Hit
Test: lcov.info Lines: 65.0 % 160 104
Test Date: 2026-11-14 15:08:11 Functions: - 0 0

            Line data    Source code
       1              : // Copyright 2023 The terCAD team. All rights reserved.
       2              : // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.
       3              : 
       4              : import 'dart:collection';
       5              : import 'package:app_finance/_classes/controller/iterator_controller.dart';
       6              : import 'package:app_finance/_classes/herald/app_budget_positive.dart';
       7              : import 'package:app_finance/_classes/herald/app_start_of_month.dart';
       8              : import 'package:app_finance/_classes/herald/app_sync.dart';
       9              : import 'package:app_finance/_classes/math/budget_prediction.dart';
      10              : import 'package:app_finance/_classes/math/goal_recalculation.dart';
      11              : import 'package:app_finance/_classes/math/invoice_recalculation.dart';
      12              : import 'package:app_finance/_classes/storage/history_data.dart';
      13              : import 'package:app_finance/_classes/structure/account_app_data.dart';
      14              : import 'package:app_finance/_classes/structure/bill_app_data.dart';
      15              : import 'package:app_finance/_classes/math/bill_recalculation.dart';
      16              : import 'package:app_finance/_classes/structure/budget_app_data.dart';
      17              : import 'package:app_finance/_classes/math/budget_recalculation.dart';
      18              : import 'package:app_finance/_classes/structure/currency_app_data.dart';
      19              : import 'package:app_finance/_classes/structure/currency/exchange.dart';
      20              : import 'package:app_finance/_classes/structure/goal_app_data.dart';
      21              : import 'package:app_finance/_classes/structure/interface_app_data.dart';
      22              : import 'package:app_finance/_classes/structure/invoice_app_data.dart';
      23              : import 'package:app_finance/_classes/structure/payment_app_data.dart';
      24              : import 'package:app_finance/_classes/structure/summary_app_data.dart';
      25              : import 'package:app_finance/_classes/math/total_recalculation.dart';
      26              : import 'package:app_finance/_classes/storage/transaction_log.dart';
      27              : import 'package:app_finance/_ext/iterable_ext.dart';
      28              : import 'package:material_ui/material_ui.dart';
      29              : import 'package:uuid/uuid.dart';
      30              : 
      31              : enum AppDataType {
      32              :   goals,
      33              :   bills,
      34              :   accounts,
      35              :   budgets,
      36              :   currencies,
      37              :   invoice,
      38              :   payments,
      39              : }
      40              : 
      41              : typedef AppDataGetter = ({
      42              :   List<dynamic> list,
      43              :   double total,
      44              :   InterfaceIterator stream,
      45              : });
      46              : 
      47              : class AppData extends ChangeNotifier {
      48              :   final AppSync appSync;
      49              :   bool isLoading = false;
      50              :   final _hashTable = HashMap<String, dynamic>();
      51              :   final _data = <AppDataType, SummaryAppData>{};
      52              :   final prediction = BudgetPrediction();
      53              : 
      54            2 :   AppData(this.appSync) : super() {
      55            1 :     isLoading = true;
      56            1 :     int startingDay = AppStartOfMonth.get();
      57            2 :     for (var key in AppDataType.values) {
      58            3 :       _data[key] = SummaryAppData(startingDay: startingDay);
      59              :     }
      60            2 :     Exchange(store: this).getDefaultCurrency();
      61            3 :     TransactionLog.load(this).then((_) async => await restate()).then((_) => appSync.follow(AppData, _stream));
      62              :   }
      63              : 
      64            1 :   @override
      65              :   dispose() {
      66            1 :     super.dispose();
      67            2 :     appSync.unfollow(AppData);
      68              :   }
      69              : 
      70            0 :   void _stream(String value) {
      71              :     try {
      72            0 :       TransactionLog.add(this, value, true, true);
      73              :     } catch (e) {
      74              :       //...
      75              :     }
      76              :   }
      77              : 
      78            0 :   Future<void> flush() async {
      79            0 :     isLoading = true;
      80            0 :     int startingDay = AppStartOfMonth.get();
      81            0 :     _hashTable.clear();
      82            0 :     _data.updateAll((key, value) => SummaryAppData(startingDay: startingDay));
      83            0 :     await TransactionLog.load(this);
      84            0 :     await restate();
      85              :   }
      86              : 
      87            0 :   Future<void> restate() async {
      88            0 :     await updateTotals(AppDataType.values);
      89            0 :     isLoading = false;
      90            0 :     notifyListeners();
      91              :   }
      92              : 
      93            1 :   void _set(AppDataType property, dynamic value) {
      94            3 :     _hashTable[value.uuid] = value;
      95            5 :     _data[property]?.add(value.uuid, updatedAt: value.createdAt);
      96            1 :     if (!isLoading) {
      97            1 :       TransactionLog.save(value);
      98            3 :       appSync.send(value.toStream());
      99            1 :       _notify();
     100              :     }
     101              :   }
     102              : 
     103            1 :   void _notify() {
     104            1 :     if (!isLoading) {
     105            4 :       WidgetsBinding.instance.addPostFrameCallback((_) => notifyListeners());
     106              :     }
     107              :   }
     108              : 
     109            1 :   dynamic add(InterfaceAppData value, [String? uuid]) {
     110            2 :     value.uuid = uuid ?? const Uuid().v4();
     111            1 :     _update(null, value);
     112            2 :     return getByUuid(value.uuid!);
     113              :   }
     114              : 
     115            1 :   void update(String uuid, InterfaceAppData value, [bool createIfMissing = false]) {
     116            1 :     var initial = getByUuid(uuid, false);
     117              :     if (initial != null || createIfMissing) {
     118            1 :       _update(initial, value);
     119              :     }
     120              :   }
     121              : 
     122            1 :   Future<void> updateTotals(List<AppDataType> scope) async {
     123            1 :     final accountTotal = getTotal(AppDataType.accounts);
     124            1 :     final exchange = Exchange(store: this);
     125            1 :     int isBudgetPositive = AppBudgetPositive.get();
     126            2 :     final rec = TotalRecalculation(exchange: exchange, isBudgetPositive: isBudgetPositive != 0);
     127            2 :     for (AppDataType type in scope) {
     128            4 :       await rec.updateTotal(type, _data[type], _hashTable);
     129              :     }
     130            1 :     if (scope.contains(AppDataType.accounts)) {
     131            3 :       rec.updateGoals(getList(AppDataType.goals, false), accountTotal, getTotal(AppDataType.accounts));
     132              :     }
     133            1 :     _notify();
     134              :   }
     135              : 
     136            1 :   void _update(InterfaceAppData? initial, InterfaceAppData change) {
     137            4 :     if (change.getType() != AppDataType.budgets && change.getType() != AppDataType.payments) {
     138            4 :       HistoryData.addLog(change.uuid, change, initial?.details ?? 0.0, change.details);
     139              :     }
     140            1 :     switch (change.getType()) {
     141            1 :       case AppDataType.accounts:
     142            1 :         _updateAccount(initial as AccountAppData?, change as AccountAppData);
     143              :         break;
     144            1 :       case AppDataType.bills:
     145            1 :         (change as BillAppData).setState(this);
     146            1 :         _updateBill(initial as BillAppData?, change);
     147              :         break;
     148            1 :       case AppDataType.budgets:
     149            1 :         (change as BudgetAppData).setState(this);
     150            1 :         _updateBudget(initial as BudgetAppData?, change);
     151            3 :         HistoryData.addLog(change.uuid, change, initial?.amountLimit ?? 0.0, change.amountLimit);
     152              :         break;
     153            1 :       case AppDataType.goals:
     154            0 :         _updateGoal(initial as GoalAppData?, change as GoalAppData);
     155              :         break;
     156            1 :       case AppDataType.currencies:
     157            1 :         _updateCurrency(initial as CurrencyAppData?, change as CurrencyAppData);
     158              :         break;
     159            0 :       case AppDataType.invoice:
     160            0 :         (change as InvoiceAppData).setState(this);
     161            0 :         _updateInvoice(initial as InvoiceAppData?, change);
     162              :         break;
     163            0 :       case AppDataType.payments:
     164            0 :         _updatePayment(initial as PaymentAppData?, change as PaymentAppData);
     165              :         break;
     166              :     }
     167              :   }
     168              : 
     169            0 :   void _updateInvoice(InvoiceAppData? initial, InvoiceAppData change) {
     170            0 :     _set(AppDataType.invoice, change);
     171            0 :     AccountAppData? currAccount = getByUuid(change.account, false);
     172              :     AccountAppData? prevAccount;
     173              :     if (initial != null) {
     174            0 :       prevAccount = getByUuid(initial.account, false);
     175              :       if (prevAccount != null) {
     176            0 :         _data[AppDataType.accounts]?.add(initial.account);
     177              :       }
     178              :     }
     179              :     if (currAccount != null) {
     180            0 :       final rec = InvoiceRecalculation(change, initial)..exchange = Exchange(store: this);
     181            0 :       rec.updateAccount(currAccount, prevAccount);
     182            0 :       _data[AppDataType.accounts]?.add(change.account);
     183            0 :       if (change.accountFrom != null) {
     184            0 :         rec.updateAccount(getByUuid(change.accountFrom!, false),
     185            0 :             initial != null && initial.accountFrom != null ? getByUuid(initial.accountFrom!, false) : null, true);
     186            0 :         _data[AppDataType.accounts]?.add(change.accountFrom!);
     187              :       }
     188              :     }
     189            0 :     if (!isLoading) {
     190            0 :       updateTotals([AppDataType.accounts]);
     191              :     }
     192              :   }
     193              : 
     194            1 :   void _updateAccount(AccountAppData? initial, AccountAppData change) {
     195            1 :     _set(AppDataType.accounts, change);
     196            1 :     if (!isLoading) {
     197            2 :       updateTotals([AppDataType.accounts]);
     198              :     }
     199              :   }
     200              : 
     201            1 :   void _updateBill(BillAppData? initial, BillAppData change) {
     202            2 :     AccountAppData? currAccount = getByUuid(change.account, false);
     203              :     AccountAppData? prevAccount;
     204            2 :     BudgetAppData? currBudget = getByUuid(change.category, false);
     205              :     BudgetAppData? prevBudget;
     206            1 :     final exchange = Exchange(store: this);
     207            4 :     change.exchangeAccount = exchange.reform(1.0, change.currency, currAccount?.currency);
     208            4 :     change.exchangeCategory = exchange.reform(1.0, change.currency, currBudget?.currency);
     209              :     if (initial != null) {
     210            0 :       prevAccount = getByUuid(initial.account, false);
     211              :       if (prevAccount != null) {
     212            0 :         _data[AppDataType.accounts]?.add(initial.account);
     213              :       }
     214            0 :       prevBudget = getByUuid(initial.category, false);
     215              :       if (prevBudget != null) {
     216            0 :         _data[AppDataType.budgets]?.add(initial.category);
     217              :       }
     218              :     }
     219            2 :     prediction.add(change);
     220            2 :     final rec = BillRecalculation(change: change, initial: initial)..exchange = exchange;
     221              :     if (currAccount != null) {
     222            1 :       rec.updateAccount(currAccount, prevAccount);
     223            4 :       _data[AppDataType.accounts]?.add(change.account);
     224              :     }
     225              :     if (currBudget != null) {
     226            1 :       rec.updateBudget(currBudget, prevBudget);
     227            4 :       _data[AppDataType.budgets]?.add(change.category);
     228              :     }
     229            1 :     _set(AppDataType.bills, change);
     230            0 :     if (initial?.childOf != null) {
     231            0 :       BillAppData? parent = getByUuid(initial!.childOf!, false);
     232              :       if (parent != null) {
     233            0 :         parent.details -= change.details - (initial.details as double);
     234            0 :         update(initial.childOf!, parent);
     235              :       }
     236              :     }
     237            1 :     if (!isLoading) {
     238            2 :       updateTotals([AppDataType.bills, AppDataType.accounts, AppDataType.budgets]);
     239              :     }
     240              :   }
     241              : 
     242            1 :   void _updateBudget(BudgetAppData? initial, BudgetAppData change) {
     243            1 :     BudgetRecalculation(change: change, initial: initial)
     244            2 :       ..exchange = Exchange(store: this)
     245            1 :       ..updateBudget();
     246            1 :     _set(AppDataType.budgets, change);
     247            1 :     if (!isLoading) {
     248            2 :       updateTotals([AppDataType.budgets]);
     249              :     }
     250              :   }
     251              : 
     252            0 :   void _updateGoal(GoalAppData? initial, GoalAppData change) {
     253            0 :     GoalRecalculation(change: change, initial: initial)
     254            0 :       ..exchange = Exchange(store: this)
     255            0 :       ..updateGoal();
     256            0 :     _set(AppDataType.goals, change);
     257            0 :     if (!isLoading) {
     258            0 :       updateTotals([AppDataType.goals]);
     259              :     }
     260              :   }
     261              : 
     262            1 :   void _updateCurrency(CurrencyAppData? initial, CurrencyAppData change) {
     263            1 :     _set(AppDataType.currencies, change);
     264            1 :     if (!isLoading) {
     265            1 :       updateTotals(AppDataType.values);
     266              :     }
     267              :   }
     268              : 
     269            0 :   void _updatePayment(PaymentAppData? initial, PaymentAppData change) {
     270            0 :     _set(AppDataType.payments, change);
     271              :   }
     272              : 
     273            1 :   AppDataGetter get(AppDataType property) {
     274              :     return (
     275            1 :       list: getList(property),
     276            1 :       total: getTotal(property),
     277            1 :       stream: getStream<InterfaceAppData>(property),
     278              :     );
     279              :   }
     280              : 
     281            1 :   List<dynamic> getList(AppDataType property, [bool isClone = true]) {
     282            3 :     return (_data[property]?.list ?? [])
     283            3 :         .map((uuid) => getByUuid(uuid, isClone))
     284            3 :         .where((element) => !element.hidden)
     285            1 :         .toList();
     286              :   }
     287              : 
     288            1 :   InterfaceIterator getStream<M extends InterfaceAppData>(AppDataType property,
     289              :       {bool inverse = true, double? boundary, Function? filter}) {
     290            2 :     if (_data[property] == null) {
     291            0 :       return IteratorController<num, dynamic, M>(SplayTreeMap<num, dynamic>(), transform: getByUuid);
     292              :     }
     293            4 :     return _data[property]!.origin.toStream<M>(
     294              :           inverse,
     295            1 :           transform: getByUuid,
     296              :           boundary: boundary,
     297            4 :           filter: (M v) => v.hidden || filter?.call(v) == true,
     298              :         );
     299              :   }
     300              : 
     301            0 :   List<dynamic> getActualList(AppDataType property, [bool isClone = true]) {
     302            0 :     return (_data[property]?.listActual ?? [])
     303            0 :         .map((uuid) => getByUuid(uuid, isClone))
     304            0 :         .where((element) => !element.hidden)
     305            0 :         .toList();
     306              :   }
     307              : 
     308            1 :   double getTotal(AppDataType property) {
     309            3 :     return _data[property]?.total ?? 0.0;
     310              :   }
     311              : 
     312            1 :   dynamic getByUuid(String uuid, [bool isClone = true]) {
     313            1 :     if (uuid == '') return null;
     314            5 :     var obj = isClone ? _hashTable[uuid]?.clone() : _hashTable[uuid];
     315            3 :     if (obj is BillAppData || obj is BudgetAppData || obj is InvoiceAppData) {
     316            1 :       obj.setState(this);
     317              :     }
     318              :     return obj;
     319              :   }
     320              : }
        

Generated by: LCOV version 2.0-1