From 07941380ecfd7063b574a6bc8df3764e3714d259 Mon Sep 17 00:00:00 2001 From: dankito Date: Fri, 11 Sep 2020 12:25:05 +0200 Subject: [PATCH] Extracted interfaces for UI model classes Customer, BankAccount and AccountTransaction. So entities can implement these interfaces directly, there's no need for mapping anymore --- docs/res/AppIcon/AppIcons.zip | Bin 0 -> 26966 bytes .../res/AppIcon/AppIcons/icon-ios-1024@1x.png | Bin 0 -> 11734 bytes docs/res/AppIcon/AppIcons/icon-ios-20@1x.png | Bin 0 -> 342 bytes docs/res/AppIcon/AppIcons/icon-ios-20@2x.png | Bin 0 -> 493 bytes docs/res/AppIcon/AppIcons/icon-ios-20@3x.png | Bin 0 -> 687 bytes docs/res/AppIcon/AppIcons/icon-ios-29@1x.png | Bin 0 -> 436 bytes docs/res/AppIcon/AppIcons/icon-ios-29@2x.png | Bin 0 -> 684 bytes docs/res/AppIcon/AppIcons/icon-ios-29@3x.png | Bin 0 -> 926 bytes docs/res/AppIcon/AppIcons/icon-ios-40@1x.png | Bin 0 -> 493 bytes docs/res/AppIcon/AppIcons/icon-ios-40@2x.png | Bin 0 -> 840 bytes docs/res/AppIcon/AppIcons/icon-ios-40@3x.png | Bin 0 -> 1145 bytes docs/res/AppIcon/AppIcons/icon-ios-60@2x.png | Bin 0 -> 1145 bytes docs/res/AppIcon/AppIcons/icon-ios-60@3x.png | Bin 0 -> 1714 bytes docs/res/AppIcon/AppIcons/icon-ios-76@1x.png | Bin 0 -> 828 bytes docs/res/AppIcon/AppIcons/icon-ios-76@2x.png | Bin 0 -> 1537 bytes .../res/AppIcon/AppIcons/icon-ios-83.5@2x.png | Bin 0 -> 1589 bytes docs/res/AppIcon/AppIcons/icon-ios.svg | 21 + docs/res/AppIcon/AppIconsMitB.zip | Bin 0 -> 29471 bytes .../AppIcon/AppIconsMitB/icon-ios-1024@1x.png | Bin 0 -> 13818 bytes .../AppIcon/AppIconsMitB/icon-ios-20@2x.png | Bin 0 -> 619 bytes .../AppIcon/AppIconsMitB/icon-ios-20@3x.png | Bin 0 -> 838 bytes .../AppIcon/AppIconsMitB/icon-ios-29@2x.png | Bin 0 -> 822 bytes .../AppIcon/AppIconsMitB/icon-ios-29@3x.png | Bin 0 -> 1154 bytes .../AppIcon/AppIconsMitB/icon-ios-40@2x.png | Bin 0 -> 1069 bytes .../AppIcon/AppIconsMitB/icon-ios-40@3x.png | Bin 0 -> 1516 bytes .../AppIcon/AppIconsMitB/icon-ios-60@2x.png | Bin 0 -> 1516 bytes .../AppIcon/AppIconsMitB/icon-ios-60@3x.png | Bin 0 -> 2041 bytes .../AppIcon/AppIconsMitB/icon-ios-76@2x.png | Bin 0 -> 1823 bytes .../AppIcon/AppIconsMitB/icon-ios-83.5@2x.png | Bin 0 -> 2000 bytes docs/res/AppIcon/AppIconsMitB/icon-ios.svg | 22 ++ docs/res/icons/BankTransfer.pdf | 362 ++++++++++++++++++ docs/res/icons/gear.fill.pdf | 170 ++++++++ docs/res/icons/new.pdf | 88 +++++ .../persistence/LuceneBankingPersistence.kt | 12 +- .../search/LuceneRemitteeSearcherTest.kt | 6 +- .../json/BankingPersistenceJson/build.gradle | 4 - .../persistence/BankingPersistenceJson.kt | 27 +- .../persistence/mapper/CustomerConverter.kt | 90 ----- .../mapper/CycleAvoidingMappingContext.kt | 30 -- .../persistence/mapper/EntitiesMapper.kt | 136 ------- .../mapper/EntitiesModelCreator.kt | 72 ++++ .../model/AccountTransactionEntity.kt | 80 ++-- .../persistence/model/BankAccountEntity.kt | 48 +-- .../persistence/model/CustomerEntity.kt | 36 +- .../persistence/BankingPersistenceJsonTest.kt | 56 ++- .../banking/ui/android/RouterAndroid.kt | 4 +- .../adapter/AccountTransactionAdapter.kt | 8 +- .../ui/android/adapter/BankAccountsAdapter.kt | 6 +- .../banking/ui/android/di/BankingModule.kt | 25 +- .../ui/android/dialogs/EnterTanDialog.kt | 6 +- .../ui/android/dialogs/TransferMoneyDialog.kt | 4 +- .../banking/ui/android/views/DrawerView.kt | 10 +- .../javafx/dialogs/mainwindow/MainWindow.kt | 9 +- .../dankito/banking/ui/javafx/RouterJavaFx.kt | 4 +- .../AccountTransactionsControlView.kt | 2 +- .../controls/AccountTransactionsTable.kt | 16 +- .../controls/AccountTransactionsView.kt | 18 +- .../ui/javafx/controls/AccountsTreeView.kt | 4 +- .../cashtransfer/TransferMoneyDialog.kt | 10 +- .../ui/javafx/dialogs/tan/EnterTanDialog.kt | 4 +- .../javafx/model/AccountsAccountTreeItem.kt | 4 +- .../model/AccountsBankAccountTreeItem.kt | 4 +- .../ui/javafx/model/AccountsRootTreeItem.kt | 6 +- .../persistence/IBankingPersistence.kt | 12 +- .../persistence/NoOpBankingPersistence.kt | 12 +- .../banking/ui/BankingClientCallback.kt | 4 +- .../net/dankito/banking/ui/IBankingClient.kt | 8 +- .../banking/ui/IBankingClientCreator.kt | 4 +- .../kotlin/net/dankito/banking/ui/IRouter.kt | 5 +- .../banking/ui/model/AccountTransaction.kt | 99 ++--- .../dankito/banking/ui/model/BankAccount.kt | 70 ++-- .../net/dankito/banking/ui/model/Customer.kt | 55 +-- .../banking/ui/model/IAccountTransaction.kt | 76 ++++ .../dankito/banking/ui/model/IBankAccount.kt | 56 +++ .../net/dankito/banking/ui/model/ICustomer.kt | 56 +++ .../banking/ui/model/MessageLogEntry.kt | 2 +- .../ui/model/mapper/DefaultModelCreator.kt | 66 ++++ .../banking/ui/model/mapper/IModelCreator.kt | 57 +++ .../parameters/GetTransactionsParameter.kt | 4 +- .../ui/model/parameters/TransferMoneyData.kt | 13 +- .../ui/model/responses/AddAccountResponse.kt | 12 +- .../responses/GetTransactionsResponse.kt | 8 +- .../banking/ui/presenter/BankingPresenter.kt | 113 +++--- .../dankito/banking/BankingPresenterSwift.kt | 3 +- .../BankingiOSApp.xcodeproj/project.pbxproj | 12 +- .../Preview Content/PreviewData.swift | 2 +- .../BankingiOSApp/extensions/Extensions.swift | 29 +- .../BankingiOSApp/persistence/AppData.swift | 6 +- .../CoreDataBankingPersistence.swift | 12 +- .../persistence/EnterTanState.swift | 4 +- .../BankingiOSApp/persistence/Mapper.swift | 20 +- .../BankingiOSApp/ui/Messages.swift | 2 +- .../BankingiOSApp/ui/SwiftUiRouter.swift | 2 +- .../dialogs/AccountTransactionsDialog.swift | 20 +- .../ui/dialogs/AccountsDialog.swift | 2 +- .../ui/dialogs/AddAccountDialog.swift | 2 +- .../dialogs/BankAccountSettingsDialog.swift | 8 +- .../ui/dialogs/BankSettingsDialog.swift | 14 +- .../ui/dialogs/EnterTanDialog.swift | 2 +- .../ui/dialogs/SettingsDialog.swift | 8 +- .../ui/dialogs/TransferMoneyDialog.swift | 5 +- .../ui/views/IconedTitleView.swift | 4 +- .../AccountTransactionListItem.swift | 8 +- .../ui/views/listitems/AllBanksListItem.swift | 2 +- .../views/listitems/BankAccountListItem.swift | 2 +- .../ui/views/listitems/BankListItem.swift | 6 +- .../ui/views/tan/TanProcedurePicker.swift | 4 +- .../dankito/banking/fints4kBankingClient.kt | 25 +- .../banking/fints4kBankingClientCreator.kt | 8 +- .../banking/mapper/fints4kModelMapper.kt | 37 +- .../net/dankito/banking/HbciCallback.kt | 9 +- .../dankito/banking/hbci4jBankingClient.kt | 28 +- .../banking/hbci4jBankingClientCreator.kt | 11 +- .../banking/model/AccountCredentials.kt | 4 +- .../banking/util/AccountTransactionMapper.kt | 28 +- .../dankito/banking/util/hbci4jModelMapper.kt | 34 +- 116 files changed, 1631 insertions(+), 868 deletions(-) create mode 100644 docs/res/AppIcon/AppIcons.zip create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-1024@1x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-20@1x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-20@2x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-20@3x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-29@1x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-29@2x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-29@3x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-40@1x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-40@2x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-40@3x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-60@2x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-60@3x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-76@1x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-76@2x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios-83.5@2x.png create mode 100644 docs/res/AppIcon/AppIcons/icon-ios.svg create mode 100644 docs/res/AppIcon/AppIconsMitB.zip create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-1024@1x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-20@2x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-20@3x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-29@2x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-29@3x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-40@2x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-40@3x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-60@2x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-60@3x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-76@2x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios-83.5@2x.png create mode 100644 docs/res/AppIcon/AppIconsMitB/icon-ios.svg create mode 100644 docs/res/icons/BankTransfer.pdf create mode 100644 docs/res/icons/gear.fill.pdf create mode 100644 docs/res/icons/new.pdf delete mode 100644 persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/CustomerConverter.kt delete mode 100644 persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/CycleAvoidingMappingContext.kt delete mode 100644 persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/EntitiesMapper.kt create mode 100644 persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/EntitiesModelCreator.kt create mode 100644 ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/IAccountTransaction.kt create mode 100644 ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/IBankAccount.kt create mode 100644 ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/ICustomer.kt create mode 100644 ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/mapper/DefaultModelCreator.kt create mode 100644 ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/mapper/IModelCreator.kt diff --git a/docs/res/AppIcon/AppIcons.zip b/docs/res/AppIcon/AppIcons.zip new file mode 100644 index 0000000000000000000000000000000000000000..2607c14b0324862381489cbad289359a2b27ee76 GIT binary patch literal 26966 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 KUmqJ@-Te>UlI!09 literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIcons/icon-ios-1024@1x.png b/docs/res/AppIcon/AppIcons/icon-ios-1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..8acdd23634b57c5f2103becd1f3dc97cd0efff7c GIT binary patch literal 11734 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%0Dl0 zk3Ja-^Zluz^~}|l3fd5ZF0n z9zDrk(CKF%zRP(L4>N^;eZ=*dpq0!06}V~AdN0C>Z|z!_-##!ZswOnjY>Cl;)8=wMTHTA3NRC34(c-w|8S!DDW!#CNPBvPsIp*}oB)*Q9Bq z*1e{}h^0=L*~eT}Uqe=0G$xx>DCc&<+U8x(xXtb0^}n|0q1pBbv>?HJI@KcCBGG6r zU2D+mnU4N^9^Qe){NT+`VaiNQbDs4kS_$TgIS=UDBky!S6wV>k_$TY8h&L)lAKFz( zj;{Uw!#M54sc9=I)aF+USdE>T_M{O4b(vrklMsDrFUoXrk#WLIY?BOPxI{o?^&UgA zZND=#D1(|e8H=GrU#2b+)*9TA;a_Bsk%FDX^+L544mE8`5LE3w!g_(!Hz)J5gzR=% z+h>w6t@MAw2w++t^%$_L^o})Ch`hO|w4G?UU9awTHdZXB$Zo%lZ+PUh zkF5BRXrXeClAr*e%85aKbu9D9aYX8$-XE*Ual!`1u^PfL@-J5cc(a`Nwz(QFUGB;W zx_aF{;-TRg)-GBG%d1nqJV8{b#iJbu)`MLRNPlh?*R?lNs1<5|WIdh|#!6?;Ezu4! z2&={Q2F?w56zh0|t!Ktum_=f%E>$<#Gu6;Ha5$&6>^e1a=-S zfyYlGEn)`b_n|jRz#EAQw1tg44T7+dr$G=l@-zs-MxG`D;r}jAgD*q#B_4gsPxxg` S>}S!t&;4R~s^|pq!T$irZp&@} literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIcons/icon-ios-20@1x.png b/docs/res/AppIcon/AppIcons/icon-ios-20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..5a82f6a2743ef3a63fc1bb62aa42c5146b19518b GIT binary patch literal 342 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VXMsm#F#`j)FbFd;%$g$s6l5$8 za(7}_cTVOdki(Mh=IGO`b6mK! zMY#{U3D|U2@Nu6{&U~mUvPLDsQ)G*$&y_zDOD9j!o21l~AjiX>mcB1j&fDNc;1LZ* z!`J+cnsLU{MK9zmVLG46c}kUW8tV+PqfcKs?yj7`&sQwPd*l7G$kYn|71xg(_^Z5n zt0b?6e2j``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBxh78+$B+ufw=;abnH(jK|KFSyd?jo8q(g@$ zbhPk=weR}kRNKPQ)OS&x>(VK^yzXiVE)EV9+Tyij>#nmsyB?^f)#|uBV?TUu z-~Y6-d9`hc5z*f+9FKTW+@m0>>~WMM$gnBo&*U4PByic6*D z)sInGTV9=gJD+=9THA&xeHwMmW^P8~cxR~hjI|An~?VrawGw|A>#f z_P>8-@#-f>&c)T$?aFm2;Vuj@bHYEWRlWX^AqsVYlm*NhN%h?l^=bF~()miKuYtNrlT zaVh?@w?A*!o0P8Qe@9-)1IbA#w!qk7@O1TaS?83{1OS51(klP} literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIcons/icon-ios-20@3x.png b/docs/res/AppIcon/AppIcons/icon-ios-20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..36907fc97de30ff7fc31f397acd0a3dc0a4033ce GIT binary patch literal 687 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$;i8oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di3`{PbE{-7;jBjVgdW!@~9RHv0R({QB?}VPD z&Mn-F^Mdkw4ouK%iqmV#`(+S#C7t6J+g+Ae5l4X%Ro8^-WJfm9)K__}JAX?}?$9lfP&y+k_^gkk*u7<^@}WC|iSJ70 z6o}vId8{!-HsQmH-hX!9$HRoAU+q^~W1=M69#Q`I$-cWHYIp80H(PpXfl*QAoHaMB zFZGlJ@4G3X7MCfqIai%qdg1NuRb`^L`Ik-Pxh;Lz!@lm*d>y}aPoH}GC2rcCc(1VV z5Tj_3udlqVhL8C|rOTJ<3~OI6&`5fq#O=TTYrtjY%-uij?&t;ucLK6U(wL7~2 literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIcons/icon-ios-29@1x.png b/docs/res/AppIcon/AppIcons/icon-ios-29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..130586432c86f0c3328874e38a23e77603721b3d GIT binary patch literal 436 zcmV;l0ZaagP)M3Kl}F#7Y|*Q`%Tt_@4y-f=$HM(nd&;S_P5#2_}B#a+h0o z_xxydZw)~S>53aF&~W~QmGNXO8o%-ptQ z9}1g7t~2|PtTCvv@8__V0%jyCGWRr|4e>IV28pYEV3CjI6{hW$41RnRp->30y|t>p z)h_GU**!GIG(t&EfG)H?ZSRR-_?}{J{%hIUNgJiLB1-GU^fzRF`v(;b|4D8lIjDpF zO2$4F&qNLc1}~QmhvbyvE*7X(A~2TD*T+VJvh2%=@gZ&3uIk1fRU+;-=%76dRV~x$ e>9xDVe4{t*l4|Gb@G5Em0000Zuq=S?&v8hZa=hmUMel@jhc4tR z=ixVkqQ;{r@#j~<6GOpTndjmcUR)}`$tmeEHK|8&7!D7%geK<)>%>Mxx<9qT^vpS_ zmr0XRm_7IyRtjGQ+Po>tOCuDVmy>QHiQ+7Y6GpSy*)5vG1qv#REMUgGKdC0Yt2k=% z^v2YotfTn|dX)-UR3*)TNY{(%T)gTUXXo}zb+6ke8tj_&@%y(yuYrlz5Qbt8kqtLq{%4O z;RZ?~@t@a~^S}QSf$J@n{rFvOYh`W^4*UzOtLtOh>uJ)nLImJ`_aTzW#wM}a;rvu& z!@vy@<%WoILqxeDqTCQsZir|@Ts0PXIL;$gTt$;$BZ-#I73i~IS$F$Brq6B%C$=!~ z-dHQgt=jm`&MgQ2ZvWtzoMi|lITrg;G^qw(foXtJO)3Sm4DI+|j4!OrZn<^Y_uihb z(AAl)T-#@to}_ITfPhx=Eg&;Qlp7+-4H4yrh;l``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di49v!!E{-7;jBoGU^$t#CIR5ee&1Ys8*M|70 zX}?lZR62OI)n(#^*z&vO7jyqRi!1#wFS+Y_z4WT9fQahbLvapDCN93SZ_c({YjgYN zymOaptbc5p_2=1v`|QW=983IMeeT@td1oE#k2y&Aa5ydA;nL;3gcL%=k=bfeNx>T9 zH}UP)^uCpGe4NqTs-L=J-N*IbKlXeqf4b|U1^d^FRcH897nlAN)K)ZSzIFWH^=l`1 zh3;KZeb-qcas2XB)s5M@DY}f=TjeS><9D7pH}%88{SzdAWj%Z~)k5!!=|hE^l9}lS z0{3>juiVx!aSgZHE4H)$oY`f|%%}faoPDrzMd9R=$L>jbs_k5@_O?qBd9`dzo> zyqESRefU=TWsaZX#13}3=(xC}KaSY6wY^t1|Ih4mhd1hqLe*RGIn(yeKlF!Z;g1cH z%Qo!1`1m7@$mfk+O?o4%g{?EqS-{!na&J#4xyZ2LicFXVFc>&=o zvm#Dsew_Yli};T9&)@9dyRypopyY&;b5==Rd-X2w@PVhs36Itle=l^>5p(tx(k(RM z%DvDbGTo!FlO#e)J#XW-#;SK&y@&PwoSYV3*W4+!-A0^`=gW;=+1zCJdpV~6uC2D3 z%WGmQDU`0=0cj6)vF*$xI31BR(af^Q{0IaH!XbZ zB+Sjj+hhf``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBxh78+$B+ufw=;abnH(jK|KFSyd?jo8q(g@$ zbhPk=weR}kRNKPQ)OS&x>(VK^yzXiVE)EV9+Tyij>#nmsyB?^f)#|uBV?TUu z-~Y6-d9`hc5z*f+9FKTW+@m0>>~WMM$gnBo&*U4PByic6*D z)sInGTV9=gJD+=9THA&xeHwMmW^P8~cxR~hjI|An~?VrawGw|A>#f z_P>8-@#-f>&c)T$?aFm2;Vuj@bHYEWRlWX^AqsVYlm*NhN%h?l^=bF~()miKuYtNrlT zaVh?@w?A*!o0P8Qe@9-)1IbA#w!qk7@O1TaS?83{1OS51(klP} literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIcons/icon-ios-40@2x.png b/docs/res/AppIcon/AppIcons/icon-ios-40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cc9c951e1daef5ea84a74f96a1178ec26e15d35e GIT binary patch literal 840 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di3`}=DT^vIy7~kIV&K3@oIQH?oif?e{BG(tb zt3tX&xkRO6mnd}X5MXgMpZ4mIzmn1q$v<3=PHk0)IO1)7$#vtluAnU|Hga$6yl_YD znUBxwl1qM*)bpi2{&bviXBp3b&u3@89Puy;6v~S7;5gKvz{X;z(81`)!;$15z}zGu za0G`?sVBQ-$%PXR^*i?P{M{+}{pXdczbv%7zl+}*F0Lp%%rnjTX2iu6 zJHEdy{+Cy)-H%-kMW45weg5Rem&wvb-2e z_b)rU!8(-LKiPqYBZzayO~>b%w?de1eKtB9-0qf{*Z->W!4nskzP^@J`XZP)y zar$Rp`ex(HMYU>?)pPajpPakf{w}Ebmf?oqr}Pc?R{wu_=P`4TS1!NNbA7k+-teuP zLMEv9Z(Ey7WIA9k)?v~Y1>i%^NJ|&`K$56pTJz?FsoxOX$CsrNi zj7fbyB~9(}ubJE2RbBR>&qdYAYI;nv z3&NLMEj@k5u5z3Dw#fRKORSwV3cFt>U#d6U7WtpMt-YzKt8wL1xTo(pWwOU{Mf303 zw2Qy`Kzx7h?KImB$7y?_HRMUKHfB5IO`r`(M-AiYsVL# z_tDFkT|Rk*;hmFAuwX2H*29;7VETofTPkd)p6@uJ%KT`3tIF=MpSs5mDtM zOPF1wLSo#hBlhpSYj%E$dw;Z3aOPf9<>rgZqV@T;kEiR{RO$1JCqH6RXKFvUTs7m- zwL`DJsV1-;-uLqL@zd_}=Da#_e68z}aCYYatHVn2Nl%r}-Bb zCe7-6%{%qnj>Ug_=Q=DXboZNnuRO;@tGDAz$;mHU3pbwq=NC5l{I!dhGiQ4`Z#91B zN>+lwmEO|r3*4lpYwk_3{wm-7Y-)6n;l!Mp$bT-M`fsG!7L*xl>`eJ9e`H~HY0Cbo z>l`-zyF2|z-e2*lucKt+!@aLhtNHz|J16Ij$KH~)eapS)A2IuFZ1L7J?PWmh#Nwa7 zr|dVd&P{vxn0@6grRAT#YuX2wvfZCKd%bp%(nj_2=kGeX=R}^m@cNur!j0(;Q&nX* zS*JZ)*QPRkrHw+#TE5sx4(SbER}E`xV~)A`hy2<5;}m~nM9)*@ z-3{5HO^@SDaQP3GI*80mdKI;Vst0G}uX AMF0Q* literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIcons/icon-ios-60@2x.png b/docs/res/AppIcon/AppIcons/icon-ios-60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0e62f72a2e627c690d6e8c3591281bedde940be6 GIT binary patch literal 1145 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q{^b&H|6fVg?3oVGw3ym^DWND9BhG zbBR>&qdYAYI;nv z3&NLMEj@k5u5z3Dw#fRKORSwV3cFt>U#d6U7WtpMt-YzKt8wL1xTo(pWwOU{Mf303 zw2Qy`Kzx7h?KImB$7y?_HRMUKHfB5IO`r`(M-AiYsVL# z_tDFkT|Rk*;hmFAuwX2H*29;7VETofTPkd)p6@uJ%KT`3tIF=MpSs5mDtM zOPF1wLSo#hBlhpSYj%E$dw;Z3aOPf9<>rgZqV@T;kEiR{RO$1JCqH6RXKFvUTs7m- zwL`DJsV1-;-uLqL@zd_}=Da#_e68z}aCYYatHVn2Nl%r}-Bb zCe7-6%{%qnj>Ug_=Q=DXboZNnuRO;@tGDAz$;mHU3pbwq=NC5l{I!dhGiQ4`Z#91B zN>+lwmEO|r3*4lpYwk_3{wm-7Y-)6n;l!Mp$bT-M`fsG!7L*xl>`eJ9e`H~HY0Cbo z>l`-zyF2|z-e2*lucKt+!@aLhtNHz|J16Ij$KH~)eapS)A2IuFZ1L7J?PWmh#Nwa7 zr|dVd&P{vxn0@6grRAT#YuX2wvfZCKd%bp%(nj_2=kGeX=R}^m@cNur!j0(;Q&nX* zS*JZ)*QPRkrHw+#TE5sx4(SbER}E`xV~)A`hy2<5;}m~nM9)*@ z-3{5HO^@SDaQP3GI*80mdKI;Vst0G}uX AMF0Q* literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIcons/icon-ios-60@3x.png b/docs/res/AppIcon/AppIcons/icon-ios-60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..61aead055cb27cb10d1ad37946a85eee820eb857 GIT binary patch literal 1714 zcmd6o`#Tc~7{^z!a!*drHgoGybP8E&!_ZMoi_}@2ncEy`A-QC2<0#I<(IG{tP_D`4 zAhZp!t(NPKhVc|;v*l7pVHTT_+s^u}&L42jd7jVvexL9Ayg$6Ze4jV{w6}+jrlBSP z0MJ1ncf+W(;Y)1;s=9M8@SsXGVvhR~0RSz7FSQBqumG$YZX#kljsoiXj6SN2TBz$u zR{(&TrL`8U4gdf>&~C10<2Fe|K@n#tMx1#m`gKKnQ@*{mHR){AP76nzy!2S!4O?&m z7>d_Zs43LoEJ(_F3z)e#IVI2m44vL+32r=BZcl2~qSkhfz)8JIx(pdv)-y$2Dl%EwLr44$17WT?xHkdY9gf$HLJ1f131?8iOri0GyV&g12-F!4&x~TRLfW) z=(?kcL#_Vjwo;|7qH8z})n5~kCvxx)B-jOCsdW=tEfJxuN*{B@h}lBt;8cQi#JwL)x(!n%Ml4CiE<7NGlSB`9h=YY(eiXIzm!B}!(f(6` zaa`o7Ss5rNN75%}1+rlw7t4|rD-k)OpeC=FQEd>3#NtjrLy+m(x`f%WnZVY|L}`JI z)rw-Kg(hP7iu&2e+onyEQX`$rNe(L~W96|yrDQg1*ekV1YyXH3cZhoALE*jXNr#&C z0muw%s|T_@@9rn=NcKtZj*OLMrpFgMo*o@J`!4>B8SGes^J2$2GHt8p_(jvsTVQnO ztdV1O#$lSanmGysLrU38SX-KQ{^s-&`sa5P)84M;6xG6-CA?+lksDgJ;ZdDFX8Kny zEOlMr;c{D?SJ3Zcpf1#KPTvP*ws|z`92yFN*s53Q34}uxm4Lv+=En2R&m&jv?OM`6 zWic-yq8g4xpQ1E3?w0U%b-q{of1o4t4t#E0y=}y(}o$a!7~Wx2oAwTXNS2*B#oPmWe!2&i{@qBN1VL8 z&zkdOA3>p3;USj1{A69I;qpsN-z{JfXcW16*B4gf!+4Tt=lJ3Nz)O0osN3o2O(m9E zwSRz;9gZ+m8%7kroy_!`sA~-^E~eJAAqZ7MkmG4%ZDPD#eYd($uPayZzF@FoBIsAB zegK~*eTB0b4#1a}MzfUxR4!}%?Vg zheSRKZ}L=JG>4m_5SJIBuUmsH(iqoH9V`ZRmrzMm<8PDtkKq0bmX>3(<{j&{0(xNG Ww&4CSq)+u?0BCn_x4NT0C;tuG7}s+E literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIcons/icon-ios-76@1x.png b/docs/res/AppIcon/AppIcons/icon-ios-76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..1b4730dc9c32c7277faf06447e3df041053eabba GIT binary patch literal 828 zcmeAS@N?(olHy`uVBq!ia0vp^J|N7&1|*M957Y)yoCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di3``e2T^vIy7~kI6n=M=@bNu7`*m+u+!qcU6 zxfWfy(A9L&k=@aq^{A_h42xi)$RB$a@o;s)gC~!kJm^@ma(0yVRwt(|oK2alo_##` zd+FM5RnI2z|5;bAJ72uT?9mF3 zWjz*`ufG46{%Jw=*K6(E+RFrwD0FZYq*z_#o)q1CS-(r~_2IK;%a=sF^2qh9KjP4I zci+T!Z=(`d=|+C~a8dtl^t^BBOUo8-NY>1fEqeUxy7OCmeVED$bmpz* z&^ecDLqxoPT~}>Ln6{_tGS63~;J@$1^`)bqZ%Xnt6F$eO&=HW$wD%H!(b2<)n(Z_u z=$`SomGta(QeV~f4eu6dUVGk>{_99w@Tc~@m!r>p%=i`?y)S(4>0Jf?X2|Hj`R=n` z-~7qP$=8Ga{c``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di46M^UT^vIy7~kH#pC6nmbNu7`OmnM8Rx=JQ zNhwM$VZLbaC5f}JZ$?Iw{kkvbteflG|2KASpHY$i$kgM?jD$rjNjIihY!+}k)RLN< z(DZv@&fQy&E6hx^-wp!qL;2GfC$Yg-3Kg;59*6IwlW{OapLqwPw~dThT9HlY&$u1;-X5RSp{YX z*H53$vszDWKes;np1VF1<0T{g+zdBgpX{s>m$q^C+hbl#k?)IKS(THv3H#e;p)y-P_zJF)Y)~lx$ z@6O`VZ7Kb9caH3u&r`jFPu{uGDnAV*_Vwm+^|yb6YhQky^*;GmzV+v;tk%_aNA{eE zDt}m9yg-$6_0tHU>t@d)jDMe+W*M>R)BKZ96k>pD0gR^L(n< zZqvX`zyF8z>3F^S_u6=QZoz{Bqxs?YA6p->_;KywmtPl7v{XJwlGn-7kN$S`Sl`~( zvsufQ|MK2z_h-l2h2gJT-rE}PSpDky96z^p)8n-wl*95LH-N%5$GV6 z@M24vMZk@0IRoE^A-d;n=CjZ;XP}$Cy=G0_Yo!epiN9Cp=LDp#UG!5;FWkmnKK$pM zMW%w;T^*feg*^LiX1{&A`*LY}*2KQGp0VkZckKJ|sajhs{7mzSU;V#t zM8^nUlRmX)LQYdwjIq(a$-eFfs#;S29$FOkaFbZ#{Om_nE8qFPdEk(LR(t(f)!%uK zuP@Kds5m6kd6$(fJZYIGBBj%^#5y$l{AaZT0iFMuAN`%aPG{Z524IQJ;OXk;vd$@? F2>?VYzfJ%E literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIcons/icon-ios-83.5@2x.png b/docs/res/AppIcon/AppIcons/icon-ios-83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..917a2a631c92e363894f8800270a6819774302a5 GIT binary patch literal 1589 zcmeAS@N?(olHy`uVBq!ia0vp^%R!if4M>)V`Md#AoCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di46LU-T^vIy7~kIY_K!)GIsWngOygEV(Iw`$ z6cZf06n$rNlsF2T=k<6lw)XYClaX}ihf!G!d2X0Ld>&vx0uyy_dnMc1ecErvTTPI!lTwLjpY4B4t1aX+pcyZ-iQz2v42VHC8dXY%EfS&#d+{yuTq;<);!wpTxQ?cLM!`CiJx znjmJa;@c;$2fy4iw|Lj)TnWd=cW0lOzJ2ogJ@)f2&#eS%D)#sspVwDeYV!W~z0~(L zKFs$%ey{O+Q+I?j5*gjAmr%Ee+EDU+6)QV;=zaAlpU8)0w-%keblxjBe%poED*vsI zD>B;qLjLbLr|V)?rkkIEvu$%y6Qc%Xw6g;6Si-^X3w0p=#Rem%j@xfWB09DvUKT5 zyT4CtPp9$UNQ>QJk<=W|6REd7MXt*!`)pl~oY(j1hu++}B{*S$-lmY~gI!AluW)@- zei8lNF5Q=XyBT*;JWJsY%g{m=UnQUCUsjr}_Fta+3!+Ok1aoesfv61WW8Fbn)qR};_O-TjO8n>pP%_3xBaO@ z_vVFb<4%Q?-d^}V%uD&jHKx+<|1&F=K6+c!?fvbcd;Bz}zo*T1cWj8Rz0qp<12vo$M|cP%Ip@`eB3^Y3&2qPsgoHTn;1 zt$G}@{_Ea5c{g){L${xQIxQzH?VR5Kio10`zpks)`DEw!CQ2}0)lsa_iT-F>^usBx en-*yNV&CsHZE?r))yBY5p25@A&t;ucLK6T?yZ?&- literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIcons/icon-ios.svg b/docs/res/AppIcon/AppIcons/icon-ios.svg new file mode 100644 index 00000000..be6d0dff --- /dev/null +++ b/docs/res/AppIcon/AppIcons/icon-ios.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/res/AppIcon/AppIconsMitB.zip b/docs/res/AppIcon/AppIconsMitB.zip new file mode 100644 index 0000000000000000000000000000000000000000..7b1b3fcfe9b2456f4ee0435ce1b0b4f8904a1c72 GIT binary patch literal 29471 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 literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios-1024@1x.png b/docs/res/AppIcon/AppIconsMitB/icon-ios-1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..f6d8ac2e076fa638454ea9aedb4099a11e5a3d14 GIT binary patch literal 13818 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 J>#c4V{|h!ngsuPp literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios-20@2x.png b/docs/res/AppIcon/AppIconsMitB/icon-ios-20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..32cf9dad52223492e1fcd6f9506e03cc9f40e3a2 GIT binary patch literal 619 zcmV-x0+juUP)Au8w)6di&teS)CR5cEy@5K$39P*KpK@X!mDND^Jt zlP{F7yUjC_tRlKPyWnAdu*{<4%+F_@XJ%Oiiy;GDjEXKt3W_2HMUjG{NI_AgpeS80 z*aL?c98fUgbFo(Cm!<)!w2nkl=WQx&AfIpjoYGWqD$v8PdT5eJw8$r^XLqL;qdpQ+ z&E8&LXm4EvPo9~$;mmVQ3uulAn{P1++!32lb*jw!%tEDN4q{PG;6 zXi#xb4pN(7g+m486m6d*6gjg{ePr#%as<7a%JYx-uVBK;X&{1SLE4zVa3Pa~7f z;p8mbbUa1A`Vd44iXsI?k%FQ~K~bcjD2}~ZVAyiT{{c5>wQ-Pon8pAA002ovPDHLk FV1k@O319#K literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios-20@3x.png b/docs/res/AppIcon/AppIconsMitB/icon-ios-20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..eb47edf14af775946ecf0c64421f71e03a1de190 GIT binary patch literal 838 zcmV-M1G)T(P)^K`mGj ziS*J$X)V4qD|R<8JI=G~Zn8->-Rtuc~Y+iPf`R94&KhF%2V6H@9bRaT}7(y`b z2*JD~1oMs%%sWCb?+C%XBLwq~5X?J5Fz;j_1spjn!SQ1Xy~^||gQiN}b*DyXvtVnx z1vT%n1*W;bqc|zbA>LHM8B`Dk9mF>}FdXDwT$;_&Ha76{27!W6__LP={Z1=vHxenr z7pBLcNZVdu6ke~?;lsy9;62x`slb9tqmKc&I;X;;$Gf5XB{GLGY6~|sJBmr8M*Q8F z1Y;yOs!hA~n^PD?j6_i1IEawWHcY0qMpJf~rmhPb&D}fWzI)D`R%mKBd-^Ue?b>F= zg5w!O6xXRt4+w~kHY+6gJFwDyq0(N@*-AraG386NL<1?sDz4nHQ7bVu;<-@`?#* zBGD5M+^hco5gssp+%f&qEvQs2c(ZE8CzkB{%^o5N#^oihCC1uPK%QtjI?AheY(Q`W;cXst z1InK_Ap%4ae@x>XA((fBVBQgec}EE59U+)^gkatgf_X;><{doK8mSYHzX8e&F`svf QTL1t607*qoM6N<$f@yttwg3PC literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios-29@2x.png b/docs/res/AppIcon/AppIconsMitB/icon-ios-29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b704ce5dc76a56a7293d104db41d6ba564d42f4 GIT binary patch literal 822 zcmV-61Ihe}P)kh^7z|cJ9!yXLpj830fwg>%h0wHkMZm)OC}V zZ3MxWD4&cMbviL!7km7~BE^x+nq0z?6MW;GhAG;ZJO!|77i^SFPJ-0)pF4Y{oN z@sSmyGo_5x^W6{~g`Svpx`+~}7(IAYbJjVm+#z}BV2X0d*zmrm&wk?QNU%Eil|I)` zhlN&kIVU+cFP_Ue+c=cF!PWHLbOW17q?Z@39#0N6sjZ1NG#J%1jDO);9a^7hw}Cd2 z!%b`ZAmuc-JcU0uUOz%1u95e|%~2Ez15O(sAH`r{7eom(qF$F99?92}I)!L_e%6Vk z>heWBZogaw8qulg_4>}$_ArGn7a(50I%XH8x%npIKp;_f<(#9Onz*sud3}m2w zGt%oOrrztm?iZuRmnh%tdcl(eek#CE1o)mb`usFe`(775+L!3_7rzh&#)k|N2iC*d zsMxvwxoV)e+@^a?dwYXAZehL!llrXuwt{>z>%07*qoM6N<$g2l{% AUjP6A literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios-29@3x.png b/docs/res/AppIcon/AppIconsMitB/icon-ios-29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd1d0a984db5eaa2734f1dde268847b9682f18b GIT binary patch literal 1154 zcmeAS@N?(olHy`uVBq!ia0vp^;ULVx1|$nl+{^(|oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBuDGX*V@L(#+c{_bFBeE0pWoMe_CT9HW24Vv zA(ue@Ju(VOduFcqnC7#i=+flQ|Lx-aYH_xb`bugsHJn=PLVOZD0yYZliBgN1Z&ts( zyz~21-rf1T&CaW5zPM%dV)=f%^Pj7)`7GR+IF*HERXdA>l6c1nR;8lGju-_BnH48h zfBfYtZ4t4$chhO1!20ShrT32IxEg(VU0!zc-1^lb{g3B4m&}QoR$+8!(dpN(zPzxv z{&(^Y|3BG9)4Ja3Oh1}G{e=JPM`st!sBI}JSuyq3?^nCsjw(N|_iJGFyJr^?yd*N> z$K~|;FaIyIta>@|^kQ{utyc@b?C3pfa`w!-tGYiQEfCk6_HcJI``I%&J!cZKogQ}= zm9Lts*Z=cmuZinc1H)^p`bD;1G+8Raz3ynZZcl`(@;|XPQ8Q-uU;h2m<)j%Ovm2*D zIBU+E;-H%nvX_(fx%|@RoR~Ika*CL@x8bzSqMaA_t~85Hvs^eK=(FvQCDMhD!V)SY z9!%W0Yu>6|=exWPc;(D=Vfk~_J!47Vu1!y4CfPi^)V=s!mB!vD%B9N-3f+#^UWpD< za@#uT{o&azbzx#WY#E=qJ<1=o zsxK}15#qIv`^9>;Ei-JgCMYbvrIPkA*=&u?j{3QIcV$*tq^0~YaV<~&^5bN-5wnk; zx6b^SlF}ll&!Kz&ii-)GstbF~{&*s+Pwd;*w%)_uWhUyL7xU)bn|Juw@ltuk%?F;u zNqu?gZJU#`Y|ASp$GpSej`?n#bXNDqym{(dR=)4eZmh|^;CsL=Zyj?O&-HD0l6Bh; zv*$ne6WO#s%hX_J%!}~dcYANO#W)shDh}@0V59RP!z}CR+_$Us8Le2Olhy^EObz*O zB^rJ1*!y$ea#^>Xcx%4n?`h!$mm5{LG~5pkj4Kqf7W^jlEzh?u?P|$^Cq>VmKUG*G zS#o-g*X0y}c}&(z{iF4L6{jDyt51A4`C8hMYxkJE|8BW6M|qpzXP3)GmVR<7r;k6r za$$K?(Ta~%{5osg%6r~VdvQ)~=2FF?>(@>S8O85ek~QB&V{NLqYxT^VI&)5)Dw@Tn z7orv%`m}cCpI={2yY8xbT2r<>sA7FaN&m8Rv6DJom-h+wOFyeh-g9E^ugi<|kFsnv z`Y_`f&nI@S+xzm``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di49tf-T^vIy7~jr`&Ayxi8JUGP;? zTH%xOA|W(!LnB{e({@qm-VC?4sAZQ}fBn?V5m&#xoC{56-Gp z-N`?Hjb)xRGv^$M`&-xYAMO43r*v+b@XR*py?2XEygc6>sbKf@TRSP(=Ix<7FXp}D zm@CV3`Sf+ks^5y6pBmoz*gt3a`O7U2 zPDw;aDy%Vibn9y0tnYcI$37Nb=D)Tg_Tw6$DbI7Zzvf-y__ilKaK}6izdUp03KQ>x zjjJ-|Uex$~Xu%<`kH7CPS9LhK= zUA4~z_g4S!o%86XXoUWwy1>eZX&Y?TZVveQ>Qt~so@Z+1@2N*J&Qxu@^6>LD%gB2# ze+a5>%6?qBXMK3?e1(=}@r^di3J+++KUYYao?j@pBh=~ap`6Z$+h45=)uf95|0-}* z@ZW3JyjcG97O@-*sEWj$K`WXlSks6PpR!e zaIAa zw&{!EL{T<-T)O1(uN6CW4hAiMfBwRue%B(e+uS+jPhRSf fo(WL0%Y=GGdnLJ@L9^Zgb2o#htDnm{r-UW|h56`# literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios-40@3x.png b/docs/res/AppIcon/AppIconsMitB/icon-ios-40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..071a2d9acfdfa1e4fd1ae8c66a694bc0332800b0 GIT binary patch literal 1516 zcmchX`9Bj30LG^#F>|yRHN0LLm9ltwtF>3m>zd|B2^&MMmyKK@yfmX)ULQwhnPaJ3 z{6~q@#>Fw-yfdm`#jGd@KD@b9KcFYB>(^bMmyTN?=tbb z_V3$uM9``6T_{F5ddC6)Ajo&g0q7+McSpHccLy6l{fLfWw*W&?&L{w&DF^g5Kpp^) zC!uXomlEZ~rPBml@WDO?EF%AX8($4+z7SOquDkv~2sf^4H^7t-?_(Zy;A=e7(?~OI zT>P>97lABLk=83;Wg`_1+|(}a;!!MC2oEnt&|A?d3`k1_!Nnz2#J@gzdRn#hh!qII z*yvk1C%yaT9!B>hh-3%{LY4O2N@tb(M;$$=^Pfh6`gm?R>z=Yzp=sZj9jhc}%$5{q z$Qz_}e|Q}-pp!Kf9r&j7e9nbNed?X-|ENIojLoSBr0wS_M`h&?sE2PslFObrdGb7D zgoUP*?b$)86U%sx7ufBLCTdarA^>gGGmlrbnbLj+8+kc7Wr$Ou?BI|zSHUZ2C8y3T zQM}Aec2H#*f|`OMjA5_FrPZoMiIu&?8(Q1V6@KGDqGe8~)XnR-vjJc4 z;mCz$56AK{WIo6%&W|*9~Q^*_;S-Ck=IP}zV(V=i7d27vwYp3IfW33*{}B=4x$nwp44@Sz&xiGc1_Uc zi-14;z6ix`@{F^*ZNB|5 z$Bz2F%P&4r1BSV$d(;*ReAZr+{#7=iJfd`&LPTp?StqQ$FY+l7RNmla)M6@agJzAB$dFOO&GJAE|!@H{;c1qyH;sZ~+{~w<; h%RF8KNUkW~0rY=sT3AXCEZRM10NT#Qw%*1+{a+>8(OUoj literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios-60@2x.png b/docs/res/AppIcon/AppIconsMitB/icon-ios-60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..071a2d9acfdfa1e4fd1ae8c66a694bc0332800b0 GIT binary patch literal 1516 zcmchX`9Bj30LG^#F>|yRHN0LLm9ltwtF>3m>zd|B2^&MMmyKK@yfmX)ULQwhnPaJ3 z{6~q@#>Fw-yfdm`#jGd@KD@b9KcFYB>(^bMmyTN?=tbb z_V3$uM9``6T_{F5ddC6)Ajo&g0q7+McSpHccLy6l{fLfWw*W&?&L{w&DF^g5Kpp^) zC!uXomlEZ~rPBml@WDO?EF%AX8($4+z7SOquDkv~2sf^4H^7t-?_(Zy;A=e7(?~OI zT>P>97lABLk=83;Wg`_1+|(}a;!!MC2oEnt&|A?d3`k1_!Nnz2#J@gzdRn#hh!qII z*yvk1C%yaT9!B>hh-3%{LY4O2N@tb(M;$$=^Pfh6`gm?R>z=Yzp=sZj9jhc}%$5{q z$Qz_}e|Q}-pp!Kf9r&j7e9nbNed?X-|ENIojLoSBr0wS_M`h&?sE2PslFObrdGb7D zgoUP*?b$)86U%sx7ufBLCTdarA^>gGGmlrbnbLj+8+kc7Wr$Ou?BI|zSHUZ2C8y3T zQM}Aec2H#*f|`OMjA5_FrPZoMiIu&?8(Q1V6@KGDqGe8~)XnR-vjJc4 z;mCz$56AK{WIo6%&W|*9~Q^*_;S-Ck=IP}zV(V=i7d27vwYp3IfW33*{}B=4x$nwp44@Sz&xiGc1_Uc zi-14;z6ix`@{F^*ZNB|5 z$Bz2F%P&4r1BSV$d(;*ReAZr+{#7=iJfd`&LPTp?StqQ$FY+l7RNmla)M6@agJzAB$dFOO&GJAE|!@H{;c1qyH;sZ~+{~w<; h%RF8KNUkW~0rY=sT3AXCEZRM10NT#Qw%*1+{a+>8(OUoj literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios-60@3x.png b/docs/res/AppIcon/AppIconsMitB/icon-ios-60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a02004af0c3ea7be7dd0ee74bd27764ec3d1397b GIT binary patch literal 2041 zcmdT_i9ZtzAD@R@i^ow^u7{Mz8JcDZ5yorrxFYOzl;goH=17ckv?z1OTw5jg@i?+- zp?D;0L$nRea%_(Bywh0b+T;1Wf5r3pe2?Gv`v-h~*}u6!Wu#9@0{{RSCr3N?L+|>B zlH!MINf9?aG^tyT*HHifQ1Ks%0Lm**9F`&|cc?9(m8!aW7{tPCuG#cvbh|B|u&FguD#yVDDIz7+>greS$0)ZG#T zHB^<#o851#yh>Dg`@4fe5v>k!NzHJ_e-TzeV)~_{jFIX`EpTQ~?l+N zUuH1P0y`({JD9vSbReE*=Lzs7@^Sr4wU)lV(D?4X-ZZk%Uzpl?Z;b8cy?E_m6+y9I z6z(Bw=9g5FOJ^a?gs)pC82dEnuAJPa01$G>YF|xnXlqP-Wvtr z{`+x)Jv>BO3QjmUFJP^69p4rHV&)+$^s?UdBMfvyV<366(6Z+bk_m02o=H<;RRF6* zCJ`KvD#V~X*a21R(zD)YJ-xhx3ytxiH=g%H5qSQknNi+SzWgRU<-Wp!JVvKQV%Sxs`7Z&@SW6O*rsw1%fvn5UG_Y5UQ|l$u8)Y;C?A_vZ zefh8v(F=^a&E<2jQdY$3D$X)I3;_yRSsgZt7$$@5Qcj1q--p(Y=>O_8!-+ofvtT=0 z$~h-6L!D$Y$*o_OU(cyt@dA66&kw;>iG#eWl}`qUqb$rAg%^)424-20&%R2~fa)p1 zEzfG$M>`NHM#C>+CkZ=f0@~dS!$x$09QGFYF;;zdj*9i~YN8)>)HuS&W4b&2 zV!qc8ectLvj5bGniwvPteHs#HYI{G7scgLsES32dwO3X|nNV1|wfX1m?YQ-2`L8s~ zn;L1`zHy|HQ|-^voFSYHq}qRAQwId4|=!W353a3r9G%KPhP5=zJd{&WuEy$u$W0$6{(tl3*3q6MMwu*|NBsnkpybRSWKl$gXC}JmLMV+ zsr?XqO?OXvOY+JeHoILxA$f*U*lwW2*Kr^>181ImMpX=s(Lk>2P_z!Op)ja9(HK;z z?U&IOz8zFczupLxwHceAJgR}qhQP$&ZkCDMrIXh{wvX2cmceN~SJK>I*7gr6Mcn6{ zc3 zbTDAC3SynQ)qH&LVTK&O zE|K2X_z4_vyd1mZ8He9UqR}u`5JX>PUnqq;miE7lF+*L`3yd-CSgH6+rU;zZ!E4$dGMT8{F?7Lb>apqI8msM{fN(70?peCXchV%PrM+t{T8G`jm4l9!H8OjrV26cQ$; t;i-i-#xM?;UXV17}Q-uKK#@GCwmvWR@=bzzX2s^%n$$o literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios-76@2x.png b/docs/res/AppIcon/AppIconsMitB/icon-ios-76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..38e65c7b99276dd1b44081ff63b9e55b433ef3fd GIT binary patch literal 1823 zcmd5-{XY{317C%^%vn7dtM2R(HJ4eGJ0bNjv30hXx*H=?vUwYsmu9qQq&p-D&j`cg zt%-RVE#_S~swuONkR{}uF?Mca-0r`)&*%H{`F?->{_st9!@7K@F!0sz1xe{&z;X+A_{_n6W`I^cebhT!7D23ghFO)wnj^xs7|B z^{`biMp__!6VqmwmsvMwwja_DB>vVxO!+6F@|Qz`o0c$8jS$_wb-|+5y zx+r?wa_88!@dMSZe1l_~B?F4?f>Z{7%wpJMD<*Afd!wfQRS%7*#8Mjkx`xv5GDBq* zGI@77L2Fal2F}>A#*_65A4IRci-gn9&mm{1#+svo3g7>iGkxSgPR>9#BzDn~kXx&g z_h(87z$aGC@fTH7T%Pq!Owa?=1aDsOLMb#?GF4WVX6@cNnWmvf(X*#SD;xLup@`V9 z|FCd&pmJo=>i3`M^e-wuV{OEvx~ffZPt)+#hDFOygfMPa=q<|-AAiy*#x}k%W_cJE zZsx&CiI$p1uZ;S7-Cp(buRxZuWiat@*0znDt6McQc*oo-t|55cm1j?CIVUn13f1qs zAN)%pmBEPX%?lvk^YY2Ve472vH>pXFcjqO4B`rSSrm)?#x{r#sDH*198oQ88bd%X% ziJB1cWOxmYx0JZA!gy1Lg<CJ+U2psis4ZW_YFF`>(vU+Vh0r=Kw+HM2gc%h3*r>bUhBTW%BaT)#A? z%0$umWzcbsVUD7;thbEM;~kuRy>|z?LZxrlCitS=4czu0lx&cMAx^IXr!94->kj-m z`g+PsJxoD4MCWew`O(gl1hzSqIfZM@n=2)GsDd70j9b<5fFv(q#7v&SC2XOJUJl1f z8w=xhoQp=B&B4rtxM8NsY=7$u+s_~F5f#PZ?c=7s6>ME=O#QuQ6%=8ps5h!O;V51l zYufnagHin-==`*9O`2b@(OPK*7qm13fsYu-Vve`8)ui$8K0zL*&38Q$(Ft7L5Gflh zL7pff<=Q%E$DF0=bGoNGk?Y=w{`%ps%2{Y^K0+~a=<|(A))LvfSu1{M-8K?_MQ-Q{ z+TlOc+v%f8CwEKvuxVv6A=fkfS;j)onn~4vQJJpQWN|+}4c=dh+=k4(e5_`y_@6PnId{qX29-Airmh2gM>IYdOTSRUuSvLXrzo^uvIYGC}H? zWnQu07>$^Da1S!tC1!V6UvkeD$miVmZV(>Bem!Z?$`cL=mb{x{k&c{spJ>NjtOg!) z7^mP*9%;`W&CePXx9@&@)neefmMl3}u{gJ~`LpXZ=?vPP2>zo pvAh92sjH#op_5^8nfT9w{1OUb1~%Rv`c?gB0Q6<76W5WL@E3`YfXM&= literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios-83.5@2x.png b/docs/res/AppIcon/AppIconsMitB/icon-ios-83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a04586caab1ff91e6d2113d137b6bb119c020ce0 GIT binary patch literal 2000 zcmd5-`#TegAD(kUn^1E}jLB_r3a5!9r+HdfJ7YB?G?E;bqVg9rd9sQg12z!SR0K`29XMj-*!9LW5EITUcw=^_A7o2?-6 zkp%$c{m^z7U1DUGCcVN`{WMZn;;9p8JQAmm?;4N4)osY3#smw~helB-j+<>KxX`~3~%tz3B;d4JJL$k@1BhC)Wn-8e}T5rzUkv(pp~z7 z$ZCvcUs4!p{GJ70XrtL5nlc(oCyyB|xdfOlTvf=)qO)zD1Ybe%g8GX0TIg!qE#+3e z!?Ze>wO-r$R(c)nWAh4ZWXnx6qfDIkUB^8S+~7fpB<=+2TFI|PuX2+%l^spT5sy(P z=F9`uzxxUcf zlKj3LnO0{u@*5k_L1|{Eh})i5jhJ~a1C`!M{?;V96O-5-QsB6bsrCq6j4fdxLbSPa}(`TTtn~A{*%Vq3NH#5swk;nvVmG3;v8rEE_>Vok%L;#^^`3&g%GDhb({%z1kLhN? zf($$a%G827<+MOU1gAC$a)Eg)qn|ahpsUM5gl$&k7I^$VRbd`lAgvq%-8&A2S~l|d z3TBq>!+LDUbk`WUB%s>?N{no0g|OOpUiWXg^mmJ=%A{HRv@j0;>jzu)hTb>0484x@U1+T>Y-#T9Z6WWzuV!M-DAr6lce`4Q9pH z51K@XCcUCJ`iG-8Du>k>c40is@l@A^Yz&4L$*FgAh4`nvS=3+hyi&z+f%v22P`M!! z7EN~|x zJ9oC7VgtLF{Kk+(d*cY&Cf8360fhSJ&j!0GUbEAzjEnalmArAasd8P8$XN0J0z=Ca!YOQ?rC{aAREwq1e!WJ6uWeSit}+!(q1 z{RnyGmp8AYFY7h=4Wp)81T*0L%tk|W>lhAfKtlERYz$c*3B{G^s|9qY7X4l``@-pT zwhN>*r6_prxfjYL0qkQo`W0!Vctpj|f;eprtf0adCt$>Xetcg78@&@DYZRn^a2n&#>^R*0*~JZoORjA0dO&Z}UXS#k>Xsiu z%5}@zrQ+rAXcm1&&&s&hx(DS%IdF0JJ^Ut{Qps!T$kG_SZ`Q literal 0 HcmV?d00001 diff --git a/docs/res/AppIcon/AppIconsMitB/icon-ios.svg b/docs/res/AppIcon/AppIconsMitB/icon-ios.svg new file mode 100644 index 00000000..2fe78812 --- /dev/null +++ b/docs/res/AppIcon/AppIconsMitB/icon-ios.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/res/icons/BankTransfer.pdf b/docs/res/icons/BankTransfer.pdf new file mode 100644 index 00000000..94d9188b --- /dev/null +++ b/docs/res/icons/BankTransfer.pdf @@ -0,0 +1,362 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 12.840942 cm +0.000000 0.000000 0.000000 scn +1.279276 25.159058 m +0.572751 25.159058 0.000000 24.586306 0.000000 23.879782 c +0.000000 17.483404 l +0.000000 1.279245 l +0.000000 0.572721 0.572751 -0.000031 1.279276 -0.000031 c +5.221627 -0.000031 l +5.221627 2.558521 l +2.558551 2.558521 l +2.558551 17.483404 l +2.558551 22.600506 l +36.028580 22.600506 l +36.028580 20.894806 l +38.587132 20.894806 l +38.587132 23.879782 l +38.587132 24.586306 38.014378 25.159058 37.307854 25.159058 c +1.279276 25.159058 l +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 3.942383 9.056763 cm +0.000000 0.000000 0.000000 scn +1.279276 25.105347 m +36.988125 25.105347 l +37.327412 25.105347 37.652798 24.970568 37.892708 24.730656 c +38.132618 24.490746 38.267403 24.165356 38.267403 23.826071 c +38.267403 10.402944 l +37.452763 10.640730 36.596001 10.804845 35.708851 10.885599 c +35.708851 22.546795 l +2.558552 22.546795 l +2.558552 2.558540 l +23.013508 2.558540 l +22.961720 2.192711 22.935091 1.820532 22.935091 1.443214 c +22.935091 0.952564 22.980118 0.470600 23.066944 -0.000010 c +1.279276 -0.000010 l +0.939991 -0.000010 0.614603 0.134769 0.374692 0.374680 c +0.134781 0.614590 0.000000 0.939981 0.000000 1.279266 c +0.000000 23.826071 l +0.000000 24.165356 0.134781 24.490746 0.374692 24.730656 c +0.614603 24.970568 0.939991 25.105347 1.279276 25.105347 c +h +23.255920 6.382050 m +22.035740 5.566751 20.601198 5.131590 19.133701 5.131590 c +17.166679 5.134298 15.280995 5.916893 13.890100 7.307789 c +12.499204 8.698685 11.716607 10.584367 11.713900 12.551389 c +11.713900 14.018887 12.149064 15.453429 12.964362 16.673609 c +13.779660 17.893787 14.938475 18.844801 16.294266 19.406387 c +17.650057 19.967976 19.141930 20.114914 20.581230 19.828619 c +22.020531 19.542324 23.342613 18.835655 24.380291 17.797979 c +25.417969 16.760302 26.124636 15.438218 26.410931 13.998919 c +26.697226 12.559619 26.550285 11.067746 25.988699 9.711955 c +25.427113 8.356165 24.476101 7.197348 23.255920 6.382050 c +h +21.834467 16.593369 m +21.035038 17.127529 20.095165 17.412636 19.133701 17.412636 c +19.133701 17.414341 l +17.844608 17.412762 16.608788 16.899853 15.697420 15.988167 c +14.786053 15.076480 14.273580 13.840483 14.272451 12.551389 c +14.272451 11.589925 14.557560 10.650051 15.091721 9.850623 c +15.625881 9.051195 16.385103 8.428120 17.273380 8.060183 c +18.161657 7.692245 19.139093 7.595976 20.082083 7.783548 c +21.025072 7.971121 21.891266 8.434109 22.571123 9.113967 c +23.250980 9.793824 23.713966 10.660017 23.901539 11.603006 c +24.089111 12.545996 23.992846 13.523432 23.624908 14.411709 c +23.256971 15.299986 22.633896 16.059208 21.834467 16.593369 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 8.500000 27.750000 cm +0.000000 0.000000 0.000000 scn +0.000000 1.250000 m +0.000000 1.940356 0.559644 2.500000 1.250000 2.500000 c +5.250000 2.500000 l +5.940356 2.500000 6.500000 1.940356 6.500000 1.250000 c +6.500000 0.559644 5.940356 0.000000 5.250000 0.000000 c +1.250000 0.000000 l +0.559644 0.000000 0.000000 0.559644 0.000000 1.250000 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 33.000000 12.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 1.000000 m +0.000000 1.552285 0.447715 2.000000 1.000000 2.000000 c +8.000000 2.000000 l +8.552285 2.000000 9.000000 1.552285 9.000000 1.000000 c +9.000000 1.000000 l +9.000000 0.447715 8.552285 0.000000 8.000000 0.000000 c +1.000000 0.000000 l +0.447715 0.000000 0.000000 0.447715 0.000000 1.000000 c +0.000000 1.000000 l +h +f +n +Q +q +q +-1.000000 -0.000000 -0.000000 1.000000 43.000000 6.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 1.000000 m +0.000000 1.552285 0.447715 2.000000 1.000000 2.000000 c +8.000000 2.000000 l +8.552285 2.000000 9.000000 1.552285 9.000000 1.000000 c +9.000000 1.000000 l +9.000000 0.447715 8.552285 0.000000 8.000000 0.000000 c +1.000000 0.000000 l +0.447715 0.000000 0.000000 0.447715 0.000000 1.000000 c +0.000000 1.000000 l +h +f +n +Q +43.000000 7.000000 m +43.000000 7.552284 42.552284 8.000000 42.000000 8.000000 c +35.000000 8.000000 l +34.447716 8.000000 34.000000 7.552284 34.000000 7.000000 c +34.000000 7.000000 l +34.000000 6.447716 34.447716 6.000000 35.000000 6.000000 c +42.000000 6.000000 l +42.552284 6.000000 43.000000 6.447716 43.000000 7.000000 c +43.000000 7.000000 l +h +W* +n +q +-1.000000 -0.000000 -0.000000 1.000000 43.000000 6.000000 cm +0.000000 0.000000 0.000000 scn +1.000000 1.000000 m +8.000000 1.000000 l +8.000000 3.000000 l +1.000000 3.000000 l +1.000000 1.000000 l +h +8.000000 1.000000 m +1.000000 1.000000 l +1.000000 -1.000000 l +8.000000 -1.000000 l +8.000000 1.000000 l +h +1.000000 1.000000 m +1.000000 1.000000 l +-1.000000 1.000000 l +-1.000000 -0.104569 -0.104570 -1.000000 1.000000 -1.000000 c +1.000000 1.000000 l +h +8.000000 1.000000 m +8.000000 1.000000 l +8.000000 -1.000000 l +9.104569 -1.000000 10.000000 -0.104569 10.000000 1.000000 c +8.000000 1.000000 l +h +8.000000 1.000000 m +8.000000 1.000000 l +10.000000 1.000000 l +10.000000 2.104569 9.104569 3.000000 8.000000 3.000000 c +8.000000 1.000000 l +h +1.000000 3.000000 m +-0.104569 3.000000 -1.000000 2.104569 -1.000000 1.000000 c +1.000000 1.000000 l +1.000000 1.000000 l +1.000000 3.000000 l +h +f +n +Q +Q +q +50.000000 10.000000 m +50.000000 4.477154 44.851273 0.000000 38.500000 0.000000 c +32.148727 0.000000 27.000000 4.477154 27.000000 10.000000 c +27.000000 15.522848 32.148727 20.000000 38.500000 20.000000 c +44.851273 20.000000 50.000000 15.522848 50.000000 10.000000 c +h +W* +n +q +1.000000 0.000000 -0.000000 1.000000 27.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +22.000000 10.000000 m +22.000000 5.156100 17.434797 1.000000 11.500000 1.000000 c +11.500000 -1.000000 l +18.267752 -1.000000 24.000000 3.798204 24.000000 10.000000 c +22.000000 10.000000 l +h +11.500000 1.000000 m +5.565202 1.000000 1.000000 5.156100 1.000000 10.000000 c +-1.000000 10.000000 l +-1.000000 3.798204 4.732249 -1.000000 11.500000 -1.000000 c +11.500000 1.000000 l +h +1.000000 10.000000 m +1.000000 14.843899 5.565202 19.000000 11.500000 19.000000 c +11.500000 21.000000 l +4.732249 21.000000 -1.000000 16.201796 -1.000000 10.000000 c +1.000000 10.000000 l +h +11.500000 19.000000 m +17.434797 19.000000 22.000000 14.843899 22.000000 10.000000 c +24.000000 10.000000 l +24.000000 16.201796 18.267752 21.000000 11.500000 21.000000 c +11.500000 19.000000 l +h +f +n +Q +Q +q +0.707107 -0.707107 0.707107 0.707107 28.118925 3.187651 cm +0.000000 0.000000 0.000000 scn +1.000000 6.949707 m +0.447715 6.949707 0.000000 6.501992 0.000000 5.949707 c +0.000000 5.397422 0.447715 4.949707 1.000000 4.949707 c +1.000000 6.949707 l +h +4.000000 4.949707 m +4.552285 4.949707 5.000000 5.397422 5.000000 5.949707 c +5.000000 6.501992 4.552285 6.949707 4.000000 6.949707 c +4.000000 4.949707 l +h +1.000000 4.949707 m +4.000000 4.949707 l +4.000000 6.949707 l +1.000000 6.949707 l +1.000000 4.949707 l +h +f +n +Q +q +0.707107 0.707107 -0.707107 0.707107 36.499969 1.964506 cm +0.000000 0.000000 0.000000 scn +1.000000 6.949707 m +0.447715 6.949707 0.000000 6.501992 0.000000 5.949707 c +0.000000 5.397422 0.447715 4.949707 1.000000 4.949707 c +1.000000 6.949707 l +h +4.000000 4.949707 m +4.552285 4.949707 5.000000 5.397422 5.000000 5.949707 c +5.000000 6.501992 4.552285 6.949707 4.000000 6.949707 c +4.000000 4.949707 l +h +1.000000 4.949707 m +4.000000 4.949707 l +4.000000 6.949707 l +1.000000 6.949707 l +1.000000 4.949707 l +h +f +n +Q +q +-0.707107 0.707107 -0.707107 -0.707107 47.035614 16.964449 cm +0.000000 0.000000 0.000000 scn +1.000000 6.949707 m +0.447715 6.949707 0.000000 6.501992 0.000000 5.949707 c +0.000000 5.397422 0.447715 4.949707 1.000000 4.949707 c +1.000000 6.949707 l +h +4.000000 4.949707 m +4.552285 4.949707 5.000000 5.397422 5.000000 5.949707 c +5.000000 6.501992 4.552285 6.949707 4.000000 6.949707 c +4.000000 4.949707 l +h +1.000000 4.949707 m +4.000000 4.949707 l +4.000000 6.949707 l +1.000000 6.949707 l +1.000000 4.949707 l +h +f +n +Q +q +-0.707107 -0.707107 0.707107 -0.707107 39.035675 17.499971 cm +0.000000 0.000000 0.000000 scn +1.000000 6.949707 m +0.447715 6.949707 0.000000 6.501992 0.000000 5.949707 c +0.000000 5.397422 0.447715 4.949707 1.000000 4.949707 c +1.000000 6.949707 l +h +4.000000 4.949707 m +4.552285 4.949707 5.000000 5.397422 5.000000 5.949707 c +5.000000 6.501992 4.552285 6.949707 4.000000 6.949707 c +4.000000 4.949707 l +h +1.000000 4.949707 m +4.000000 4.949707 l +4.000000 6.949707 l +1.000000 6.949707 l +1.000000 4.949707 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 8320 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 50.000000 38.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000008410 00000 n +0000008433 00000 n +0000008606 00000 n +0000008680 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +8739 +%%EOF \ No newline at end of file diff --git a/docs/res/icons/gear.fill.pdf b/docs/res/icons/gear.fill.pdf new file mode 100644 index 00000000..6591a0b4 --- /dev/null +++ b/docs/res/icons/gear.fill.pdf @@ -0,0 +1,170 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.596078 0.592157 0.572549 scn +12.000000 0.000000 m +18.627417 0.000000 24.000000 5.372583 24.000000 12.000000 c +24.000000 18.627417 18.627417 24.000000 12.000000 24.000000 c +5.372583 24.000000 0.000000 18.627417 0.000000 12.000000 c +0.000000 5.372583 5.372583 0.000000 12.000000 0.000000 c +h +11.356085 20.726612 m +11.080673 20.706541 10.887666 20.454128 10.923384 20.180305 c +11.042957 19.263630 l +10.397048 19.179354 9.777586 19.010939 9.195930 18.769743 c +8.841808 19.623703 l +8.736031 19.878782 8.442759 20.001123 8.194168 19.880884 c +7.804816 19.692562 7.431699 19.476336 7.077398 19.234867 c +6.849212 19.079351 6.808879 18.764158 6.977139 18.545200 c +7.540419 17.812202 l +7.032850 17.422173 6.577827 16.967148 6.187798 16.459579 c +5.454800 17.022860 l +5.235841 17.191120 4.920649 17.150787 4.765132 16.922600 c +4.523664 16.568302 4.307439 16.195185 4.119116 15.805833 c +3.998877 15.557242 4.121218 15.263969 4.376298 15.158192 c +5.230257 14.804070 l +4.989061 14.222414 4.820646 13.602952 4.736370 12.957043 c +3.819693 13.076614 l +3.545871 13.112333 3.293459 12.919325 3.273387 12.643913 c +3.257878 12.431119 3.250000 12.216384 3.250000 12.000000 c +3.250000 11.783616 3.257878 11.568881 3.273387 11.356086 c +3.293460 11.080674 3.545871 10.887668 3.819694 10.923385 c +4.736370 11.042957 l +4.820646 10.397048 4.989061 9.777586 5.230256 9.195931 c +4.376297 8.841809 l +4.121218 8.736032 3.998877 8.442759 4.119116 8.194168 c +4.307439 7.804817 4.523664 7.431700 4.765132 7.077400 c +4.920649 6.849213 5.235842 6.808880 5.454801 6.977140 c +6.187798 7.540421 l +6.577827 7.032852 7.032850 6.577827 7.540419 6.187798 c +6.977141 5.454802 l +6.808880 5.235844 6.849213 4.920650 7.077399 4.765133 c +7.431699 4.523664 7.804815 4.307440 8.194167 4.119116 c +8.442758 3.998877 8.736031 4.121218 8.841808 4.376299 c +9.195930 5.230255 l +9.777586 4.989059 10.397048 4.820644 11.042957 4.736368 c +10.923386 3.819695 l +10.887667 3.545872 11.080674 3.293459 11.356086 3.273388 c +11.568880 3.257879 11.783616 3.250000 12.000000 3.250000 c +12.216384 3.250000 12.431120 3.257879 12.643914 3.273388 c +12.919326 3.293459 13.112333 3.545872 13.076615 3.819695 c +12.957043 4.736368 l +13.602952 4.820644 14.222414 4.989059 14.804070 5.230255 c +15.158191 4.376299 l +15.263968 4.121218 15.557241 3.998877 15.805832 4.119116 c +16.195183 4.307440 16.568300 4.523664 16.922600 4.765133 c +17.150787 4.920650 17.191120 5.235844 17.022858 5.454802 c +16.459581 6.187798 l +16.967150 6.577827 17.422173 7.032850 17.812202 7.540419 c +18.545198 6.977142 l +18.764156 6.808880 19.079350 6.849213 19.234867 7.077400 c +19.476336 7.431700 19.692560 7.804817 19.880884 8.194167 c +20.001123 8.442758 19.878782 8.736032 19.623701 8.841808 c +18.769745 9.195930 l +19.010941 9.777586 19.179356 10.397049 19.263632 11.042958 c +20.180305 10.923386 l +20.454128 10.887668 20.706541 11.080674 20.726612 11.356086 c +20.742121 11.568881 20.750000 11.783616 20.750000 12.000000 c +20.750000 12.216385 20.742121 12.431121 20.726612 12.643916 c +20.706541 12.919327 20.454128 13.112334 20.180305 13.076616 c +19.263632 12.957044 l +19.179356 13.602953 19.010939 14.222414 18.769745 14.804070 c +19.623701 15.158192 l +19.878782 15.263969 20.001123 15.557241 19.880884 15.805832 c +19.692560 16.195185 19.476336 16.568302 19.234867 16.922602 c +19.079350 17.150787 18.764156 17.191120 18.545198 17.022861 c +17.812202 16.459581 l +17.422173 16.967150 16.967148 17.422173 16.459579 17.812202 c +17.022860 18.545200 l +17.191120 18.764158 17.150787 19.079351 16.922600 19.234867 c +16.568300 19.476336 16.195183 19.692562 15.805832 19.880884 c +15.557241 20.001123 15.263968 19.878782 15.158192 19.623703 c +14.804069 18.769743 l +14.222413 19.010939 13.602951 19.179354 12.957042 19.263630 c +13.076614 20.180307 l +13.112332 20.454130 12.919325 20.706541 12.643913 20.726612 c +12.431118 20.742123 12.216383 20.750000 12.000000 20.750000 c +11.783615 20.750000 11.568880 20.742123 11.356085 20.726612 c +h +12.696518 11.185913 m +18.050842 11.185913 l +17.652849 8.199290 15.095482 5.895348 12.000000 5.895348 c +11.021035 5.895348 10.095892 6.125784 9.275852 6.535372 c +11.844859 10.710011 l +12.026858 11.005757 12.349257 11.185913 12.696518 11.185913 c +h +5.895349 12.000000 m +5.895349 10.045127 6.814215 8.304866 8.243605 7.187559 c +10.807167 11.353347 l +10.988450 11.647931 11.004625 12.015280 10.849936 12.324657 c +8.508228 17.008072 l +6.928618 15.904668 5.895349 14.073000 5.895349 12.000000 c +h +12.000000 18.104652 m +11.138405 18.104652 10.318498 17.926159 9.575241 17.604132 c +11.897492 12.959629 l +12.066884 12.620845 12.413148 12.406842 12.791920 12.406842 c +18.091309 12.406842 l +17.881950 15.588663 15.234796 18.104652 12.000000 18.104652 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 4761 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000004851 00000 n +0000004874 00000 n +0000005047 00000 n +0000005121 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +5180 +%%EOF \ No newline at end of file diff --git a/docs/res/icons/new.pdf b/docs/res/icons/new.pdf new file mode 100644 index 00000000..c7d904f6 --- /dev/null +++ b/docs/res/icons/new.pdf @@ -0,0 +1,88 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.596078 0.592157 0.572549 scn +12.000000 0.000000 m +18.627417 0.000000 24.000000 5.372583 24.000000 12.000000 c +24.000000 18.627417 18.627417 24.000000 12.000000 24.000000 c +5.372583 24.000000 0.000000 18.627417 0.000000 12.000000 c +0.000000 5.372583 5.372583 0.000000 12.000000 0.000000 c +h +11.272544 19.262787 m +11.265584 19.754541 11.632695 20.000000 11.999791 20.000000 c +12.366895 20.000000 12.734310 19.754541 12.727037 19.262787 c +12.727037 12.727312 l +19.262306 12.727312 l +20.245783 12.741233 20.245783 11.258884 19.262306 11.272775 c +12.727037 11.272775 l +12.727037 4.737299 l +12.732529 4.336187 12.412436 4.006634 12.006896 4.000090 c +11.601364 3.993753 11.266726 4.327286 11.272544 4.737299 c +11.272544 11.272775 l +4.737278 11.272775 l +4.336182 11.267284 4.006634 11.587388 4.000089 11.992941 c +3.993754 12.398488 4.327280 12.733130 4.737278 12.727312 c +11.272544 12.727312 l +11.272544 19.262787 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1007 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001097 00000 n +0000001120 00000 n +0000001293 00000 n +0000001367 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1426 +%%EOF \ No newline at end of file diff --git a/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/persistence/LuceneBankingPersistence.kt b/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/persistence/LuceneBankingPersistence.kt index d45a211f..87c99180 100644 --- a/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/persistence/LuceneBankingPersistence.kt +++ b/persistence/LuceneBankingPersistence/src/main/kotlin/net/dankito/banking/persistence/LuceneBankingPersistence.kt @@ -14,9 +14,7 @@ import net.dankito.banking.LuceneConfig.Companion.OtherPartyAccountIdFieldName import net.dankito.banking.LuceneConfig.Companion.OtherPartyBankCodeFieldName import net.dankito.banking.LuceneConfig.Companion.OtherPartyNameFieldName import net.dankito.banking.LuceneConfig.Companion.UsageFieldName -import net.dankito.banking.ui.model.Customer -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.* import net.dankito.banking.util.ISerializer import net.dankito.banking.util.JacksonJsonSerializer import net.dankito.utils.lucene.index.DocumentsWriter @@ -47,7 +45,7 @@ open class LuceneBankingPersistence( protected val fields = FieldBuilder() - override fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List) { + override fun saveOrUpdateAccountTransactions(bankAccount: TypedBankAccount, transactions: List) { val writer = getWriter() transactions.forEach { transaction -> @@ -60,7 +58,7 @@ open class LuceneBankingPersistence( writer.flushChangesToDisk() } - protected open fun createFieldsForAccountTransaction(bankAccount: BankAccount, transaction: AccountTransaction): List { + protected open fun createFieldsForAccountTransaction(bankAccount: TypedBankAccount, transaction: IAccountTransaction): List { return listOf( fields.keywordField(BankAccountIdFieldName, bankAccount.technicalId), fields.nullableFullTextSearchField(OtherPartyNameFieldName, transaction.otherPartyName, true), @@ -79,7 +77,7 @@ open class LuceneBankingPersistence( } - override fun deleteAccount(customer: Customer, allCustomers: List) { + override fun deleteAccount(customer: TypedCustomer, allCustomers: List) { try { deleteAccountTransactions(customer.accounts) } catch (e: Exception) { @@ -89,7 +87,7 @@ open class LuceneBankingPersistence( super.deleteAccount(customer, allCustomers) } - protected open fun deleteAccountTransactions(bankAccounts: List) { + protected open fun deleteAccountTransactions(bankAccounts: List) { val writer = getWriter() val bankAccountIds = bankAccounts.map { it.technicalId } diff --git a/persistence/LuceneBankingPersistence/src/test/kotlin/net/dankito/banking/search/LuceneRemitteeSearcherTest.kt b/persistence/LuceneBankingPersistence/src/test/kotlin/net/dankito/banking/search/LuceneRemitteeSearcherTest.kt index d3681437..12d927f6 100644 --- a/persistence/LuceneBankingPersistence/src/test/kotlin/net/dankito/banking/search/LuceneRemitteeSearcherTest.kt +++ b/persistence/LuceneBankingPersistence/src/test/kotlin/net/dankito/banking/search/LuceneRemitteeSearcherTest.kt @@ -5,12 +5,14 @@ import net.dankito.banking.ui.model.Customer import net.dankito.banking.ui.model.AccountTransaction import net.dankito.banking.ui.model.BankAccount import net.dankito.utils.io.FileUtils +import net.dankito.utils.multiplatform.File +import net.dankito.utils.multiplatform.toBigDecimal +import net.dankito.utils.multiplatform.toDate import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Test import org.mockito.Mockito.mock -import java.io.File import java.math.BigDecimal import java.text.SimpleDateFormat import java.util.* @@ -211,7 +213,7 @@ class LuceneRemitteeSearcherTest { otherPartyName: String = randomString(), otherPartyBankCode: String = randomString(), otherPartyAccountId: String = randomString(), usage: String = randomString()): AccountTransaction { - return AccountTransaction(bankAccount, amount, "EUR", usage, bookingDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, null, bookingDate) + return AccountTransaction(bankAccount, amount.toBigDecimal(), "EUR", usage, bookingDate.toDate(), otherPartyName, otherPartyBankCode, otherPartyAccountId, null, bookingDate.toDate()) } private fun randomString(): String { diff --git a/persistence/json/BankingPersistenceJson/build.gradle b/persistence/json/BankingPersistenceJson/build.gradle index c9eb8ad8..5177cf94 100644 --- a/persistence/json/BankingPersistenceJson/build.gradle +++ b/persistence/json/BankingPersistenceJson/build.gradle @@ -17,10 +17,6 @@ compileTestKotlin { dependencies { implementation project(':BankingUiCommon') - implementation("org.mapstruct:mapstruct:$mapStructVersion") - - kapt("org.mapstruct:mapstruct-processor:$mapStructVersion") - testImplementation "junit:junit:$junitVersion" diff --git a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt index 909a906d..32769af6 100644 --- a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt +++ b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt @@ -1,13 +1,9 @@ package net.dankito.banking.persistence -import net.dankito.banking.persistence.mapper.CustomerConverter import net.dankito.banking.persistence.model.CustomerEntity +import net.dankito.banking.ui.model.* import net.dankito.utils.multiplatform.File -import net.dankito.banking.ui.model.Customer -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount import net.dankito.banking.util.ISerializer -import org.mapstruct.factory.Mappers import java.io.FileOutputStream import java.net.URL @@ -17,39 +13,32 @@ open class BankingPersistenceJson( protected val serializer: ISerializer ) : IBankingPersistence { - protected val mapper = Mappers.getMapper(CustomerConverter::class.java) - - init { jsonFile.absoluteFile.parentFile.mkdirs() } - override fun saveOrUpdateAccount(customer: Customer, allCustomers: List) { + override fun saveOrUpdateAccount(customer: TypedCustomer, allCustomers: List) { saveAllCustomers(allCustomers) } - override fun deleteAccount(customer: Customer, allCustomers: List) { + override fun deleteAccount(customer: TypedCustomer, allCustomers: List) { saveAllCustomers(allCustomers) } - override fun readPersistedAccounts(): List { - val deserializedCustomers = serializer.deserializeListOr(jsonFile, CustomerEntity::class) - - return mapper.mapCustomerEntities(deserializedCustomers) + override fun readPersistedAccounts(): List { + return serializer.deserializeListOr(jsonFile, CustomerEntity::class).map { it as TypedCustomer } } - override fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List) { + override fun saveOrUpdateAccountTransactions(bankAccount: TypedBankAccount, transactions: List) { // done when called saveOrUpdateAccount() // TODO: or also call saveAllCustomers()? } - protected open fun saveAllCustomers(allCustomers: List) { - val mappedCustomers = mapper.mapCustomers(allCustomers) - - serializer.serializeObject(mappedCustomers, jsonFile) + protected open fun saveAllCustomers(allCustomers: List) { + serializer.serializeObject(allCustomers, jsonFile) } diff --git a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/CustomerConverter.kt b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/CustomerConverter.kt deleted file mode 100644 index 2a1936cb..00000000 --- a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/CustomerConverter.kt +++ /dev/null @@ -1,90 +0,0 @@ -package net.dankito.banking.persistence.mapper - -import net.dankito.banking.persistence.model.AccountTransactionEntity -import net.dankito.banking.persistence.model.BankAccountEntity -import net.dankito.banking.persistence.model.CustomerEntity -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount -import net.dankito.banking.ui.model.Customer -import org.mapstruct.* - - -@Mapper -abstract class CustomerConverter { - - // Context is needed to fix cycle dependencies issue - - protected val bankAccountCustomerField = BankAccount::class.java.getDeclaredField(BankAccount::customer.name) - - - init { - bankAccountCustomerField.isAccessible = true - } - - - @Mapping(source = "technicalId", target = "id") - abstract fun mapToEntity(customer: Customer, @Context context: CycleAvoidingMappingContext): CustomerEntity - - @InheritInverseConfiguration - abstract fun mapCustomer(customer: CustomerEntity, @Context context: CycleAvoidingMappingContext): Customer - - abstract fun mapCustomers(customers: List, @Context context: CycleAvoidingMappingContext): List - - open fun mapCustomers(customers: List): List { - // create a new context instance each time as otherwise just cached instance would be taken und BankAccounts and AccountTransactions would never get updated - return mapCustomers(customers, CycleAvoidingMappingContext()) - } - - abstract fun mapCustomerEntities(customers: List, @Context context: CycleAvoidingMappingContext): List - - open fun mapCustomerEntities(customers: List): List { - // create a new context instance each time as otherwise just cached instance would be taken und BankAccounts and AccountTransactions would never get updated - return mapCustomerEntities(customers, CycleAvoidingMappingContext()) - } - - - @Mapping(source = "technicalId", target = "id") - abstract fun mapBankAccount(account: BankAccount, @Context context: CycleAvoidingMappingContext): BankAccountEntity - - @InheritInverseConfiguration - abstract fun mapBankAccount(account: BankAccountEntity, @Context context: CycleAvoidingMappingContext): BankAccount - - abstract fun mapBankAccounts(accounts: List, @Context context: CycleAvoidingMappingContext): List - - abstract fun mapBankAccountEntities(accounts: List, @Context context: CycleAvoidingMappingContext): List - - @AfterMapping - open fun mapBankAccountCustomer(serializedAccount: BankAccountEntity, @MappingTarget account: BankAccount, @Context context: CycleAvoidingMappingContext) { - val mappedCustomer = mapCustomer(serializedAccount.customer, context) - - bankAccountCustomerField.set(account, mappedCustomer) - } - - - - @Mapping(source = "technicalId", target = "id") - abstract fun mapTransaction(transaction: AccountTransaction, @Context context: CycleAvoidingMappingContext): AccountTransactionEntity - - @InheritInverseConfiguration - fun mapTransaction(transaction: AccountTransactionEntity, @Context context: CycleAvoidingMappingContext): AccountTransaction { - val account = mapBankAccount(transaction.bankAccount, context) - - val mappedTransaction = AccountTransaction(account, transaction.amount, transaction.currency, transaction.unparsedUsage, transaction.bookingDate, - transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.bookingText, - transaction.valueDate, transaction.statementNumber, transaction.sequenceNumber, transaction.openingBalance, transaction.closingBalance, - transaction.endToEndReference, transaction.customerReference, transaction.mandateReference, transaction.creditorIdentifier, transaction.originatorsIdentificationCode, - transaction.compensationAmount, transaction.originalAmount, transaction.sepaUsage, transaction.deviantOriginator, transaction.deviantRecipient, - transaction.usageWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement, transaction.currencyType, transaction.bookingKey, - transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution, transaction.supplementaryDetails, - transaction.transactionReferenceNumber, transaction.relatedReferenceNumber) - - mappedTransaction.technicalId = transaction.id - - return mappedTransaction - } - - abstract fun mapTransactions(transactions: List, @Context context: CycleAvoidingMappingContext): List - - abstract fun mapTransactionEntities(transactions: List, @Context context: CycleAvoidingMappingContext): List - -} \ No newline at end of file diff --git a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/CycleAvoidingMappingContext.kt b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/CycleAvoidingMappingContext.kt deleted file mode 100644 index fb1cdc35..00000000 --- a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/CycleAvoidingMappingContext.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.dankito.banking.persistence.mapper - -import org.mapstruct.BeforeMapping -import org.mapstruct.MappingTarget -import org.mapstruct.TargetType -import java.util.* - - -open class CycleAvoidingMappingContext { - - private val knownInstances: MutableMap = IdentityHashMap() - - - /** - * Gets an instance out of this context if it is already mapped. - */ - @BeforeMapping - open fun getMappedInstance(source: Any, @TargetType targetType: Class): T { - return targetType.cast(knownInstances[source]) - } - - /** - * Puts an instance into the cache, so that it can be remembered to avoid endless mapping. - */ - @BeforeMapping - open fun storeMappedInstance(source: Any, @MappingTarget target: Any) { - knownInstances[source] = target - } - -} \ No newline at end of file diff --git a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/EntitiesMapper.kt b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/EntitiesMapper.kt deleted file mode 100644 index b00d1336..00000000 --- a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/EntitiesMapper.kt +++ /dev/null @@ -1,136 +0,0 @@ -package net.dankito.banking.persistence.mapper - -import net.dankito.banking.persistence.model.AccountTransactionEntity -import net.dankito.banking.persistence.model.BankAccountEntity -import net.dankito.banking.persistence.model.CustomerEntity -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount -import net.dankito.banking.ui.model.Customer - - -open class EntitiesMapper { - - open fun mapCustomers(customers: List): List { - return customers.map { mapCustomer(it) } - } - - open fun mapCustomer(customer: Customer): CustomerEntity { - val mappedCustomer = CustomerEntity( - customer.bankCode, customer.customerId, customer.password, customer.finTsServerAddress, - customer.bankName, customer.bic, customer.customerName, customer.userId, customer.iconUrl, - listOf(), customer.supportedTanProcedures, customer.selectedTanProcedure, customer.tanMedia - ) - - mappedCustomer.id = customer.technicalId - mappedCustomer.userSetDisplayName = customer.userSetDisplayName - - mappedCustomer.accounts = mapBankAccounts(customer.accounts, mappedCustomer) - - return mappedCustomer - } - - - open fun mapCustomerEntities(customers: List): List { - return customers.map { mapCustomer(it) } - } - - open fun mapCustomer(customer: CustomerEntity): Customer { - val mappedCustomer = Customer( - customer.bankCode, customer.customerId, customer.password, customer.finTsServerAddress, - customer.bankName, customer.bic, customer.customerName, customer.userId, customer.iconUrl - ) - - mappedCustomer.technicalId = customer.id - mappedCustomer.userSetDisplayName = customer.userSetDisplayName - - mappedCustomer.accounts = mapBankAccounts(customer.accounts, mappedCustomer) - - - mappedCustomer.supportedTanProcedures = customer.supportedTanProcedures - mappedCustomer.selectedTanProcedure = customer.selectedTanProcedure - mappedCustomer.tanMedia = customer.tanMedia - - return mappedCustomer - } - - - open fun mapBankAccounts(transactions: List, customer: CustomerEntity): List { - return transactions.map { mapBankAccount(it, customer) } - } - - open fun mapBankAccount(account: BankAccount, customer: CustomerEntity): BankAccountEntity { - val mappedAccount = BankAccountEntity( - customer, account.identifier, account.accountHolderName, account.iban, account.subAccountNumber, - account.customerId, account.balance, account.currency, account.type, account.productName, - account.accountLimit, account.lastRetrievedTransactionsTimestamp, - account.supportsRetrievingAccountTransactions, account.supportsRetrievingBalance, - account.supportsTransferringMoney, account.supportsInstantPaymentMoneyTransfer - ) - - mappedAccount.id = account.technicalId - mappedAccount.userSetDisplayName = account.userSetDisplayName - - mappedAccount.bookedTransactions = mapTransactions(account.bookedTransactions, mappedAccount) - - return mappedAccount - } - - - open fun mapBankAccounts(transactions: List, customer: Customer): List { - return transactions.map { mapBankAccount(it, customer) } - } - - open fun mapBankAccount(account: BankAccountEntity, customer: Customer): BankAccount { - val mappedAccount = BankAccount( - customer, account.identifier, account.accountHolderName, account.iban, account.subAccountNumber, - account.customerId, account.balance, account.currency, account.type, account.productName, - account.accountLimit, account.lastRetrievedTransactionsTimestamp, - account.supportsRetrievingAccountTransactions, account.supportsRetrievingBalance, - account.supportsTransferringMoney, account.supportsInstantPaymentMoneyTransfer - ) - - mappedAccount.technicalId = account.id - mappedAccount.userSetDisplayName = account.userSetDisplayName - - mappedAccount.bookedTransactions = mapTransactions(account.bookedTransactions, mappedAccount) - - return mappedAccount - } - - - open fun mapTransactions(transactions: List, account: BankAccountEntity): List { - return transactions.map { mapTransaction(it, account) } - } - - open fun mapTransaction(transaction: AccountTransaction, account: BankAccountEntity): AccountTransactionEntity { - return AccountTransactionEntity(account, transaction.amount, transaction.currency, transaction.unparsedUsage, transaction.bookingDate, - transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.bookingText, - transaction.valueDate, transaction.statementNumber, transaction.sequenceNumber, transaction.openingBalance, transaction.closingBalance, - transaction.endToEndReference, transaction.customerReference, transaction.mandateReference, transaction.creditorIdentifier, transaction.originatorsIdentificationCode, - transaction.compensationAmount, transaction.originalAmount, transaction.sepaUsage, transaction.deviantOriginator, transaction.deviantRecipient, - transaction.usageWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement, transaction.currencyType, transaction.bookingKey, - transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution, transaction.supplementaryDetails, - transaction.transactionReferenceNumber, transaction.relatedReferenceNumber, transaction.technicalId) - } - - - open fun mapTransactions(transactions: List, account: BankAccount): List { - return transactions.map { mapTransaction(it, account) } - } - - open fun mapTransaction(transaction: AccountTransactionEntity, account: BankAccount): AccountTransaction { - val mappedTransaction = AccountTransaction(account, transaction.amount, transaction.currency, transaction.unparsedUsage, transaction.bookingDate, - transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.bookingText, - transaction.valueDate, transaction.statementNumber, transaction.sequenceNumber, transaction.openingBalance, transaction.closingBalance, - transaction.endToEndReference, transaction.customerReference, transaction.mandateReference, transaction.creditorIdentifier, transaction.originatorsIdentificationCode, - transaction.compensationAmount, transaction.originalAmount, transaction.sepaUsage, transaction.deviantOriginator, transaction.deviantRecipient, - transaction.usageWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement, transaction.currencyType, transaction.bookingKey, - transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution, transaction.supplementaryDetails, - transaction.transactionReferenceNumber, transaction.relatedReferenceNumber) - - mappedTransaction.technicalId = transaction.id - - return mappedTransaction - } - -} \ No newline at end of file diff --git a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/EntitiesModelCreator.kt b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/EntitiesModelCreator.kt new file mode 100644 index 00000000..c63cbfdb --- /dev/null +++ b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/mapper/EntitiesModelCreator.kt @@ -0,0 +1,72 @@ +package net.dankito.banking.persistence.mapper + +import net.dankito.banking.persistence.model.AccountTransactionEntity +import net.dankito.banking.persistence.model.BankAccountEntity +import net.dankito.banking.persistence.model.CustomerEntity +import net.dankito.banking.ui.model.IAccountTransaction +import net.dankito.banking.ui.model.TypedBankAccount +import net.dankito.banking.ui.model.TypedCustomer +import net.dankito.banking.ui.model.mapper.IModelCreator +import net.dankito.utils.multiplatform.BigDecimal +import net.dankito.utils.multiplatform.Date + + +open class EntitiesModelCreator : IModelCreator { + + override fun createCustomer(bankCode: String, customerId: String, password: String, finTsServerAddress: String, bankName: String, bic: String, + customerName: String, userId: String, iconUrl: String?): TypedCustomer { + + return CustomerEntity(bankCode, customerId, password, finTsServerAddress, bankName, bic, customerName, userId, iconUrl) as TypedCustomer + } + + + override fun createBankAccount(customer: TypedCustomer, productName: String?, identifier: String): TypedBankAccount { + return BankAccountEntity(customer as CustomerEntity, identifier, "", null, null, "", productName = productName) as TypedBankAccount + } + + override fun createTransaction( + bankAccount: TypedBankAccount, + amount: BigDecimal, + currency: String, + unparsedUsage: String, + bookingDate: Date, + otherPartyName: String?, + otherPartyBankCode: String?, + otherPartyAccountId: String?, + bookingText: String?, + valueDate: Date, + statementNumber: Int, + sequenceNumber: Int?, + openingBalance: BigDecimal?, + closingBalance: BigDecimal?, + endToEndReference: String?, + customerReference: String?, + mandateReference: String?, + creditorIdentifier: String?, + originatorsIdentificationCode: String?, + compensationAmount: String?, + originalAmount: String?, + sepaUsage: String?, + deviantOriginator: String?, + deviantRecipient: String?, + usageWithNoSpecialType: String?, + primaNotaNumber: String?, + textKeySupplement: String?, + currencyType: String?, + bookingKey: String, + referenceForTheAccountOwner: String, + referenceOfTheAccountServicingInstitution: String?, + supplementaryDetails: String?, + transactionReferenceNumber: String, + relatedReferenceNumber: String? + ) : IAccountTransaction { + + return AccountTransactionEntity(bankAccount as BankAccountEntity, amount, currency, unparsedUsage, bookingDate, + otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, valueDate, statementNumber, sequenceNumber, + openingBalance, closingBalance, endToEndReference, customerReference, mandateReference, creditorIdentifier, + originatorsIdentificationCode, compensationAmount, originalAmount, sepaUsage, deviantOriginator, deviantRecipient, + usageWithNoSpecialType, primaNotaNumber, textKeySupplement, currencyType, bookingKey, referenceForTheAccountOwner, + referenceOfTheAccountServicingInstitution, supplementaryDetails, transactionReferenceNumber, relatedReferenceNumber) + } + +} \ No newline at end of file diff --git a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/AccountTransactionEntity.kt b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/AccountTransactionEntity.kt index 2d0039d9..df72e5bb 100644 --- a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/AccountTransactionEntity.kt +++ b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/AccountTransactionEntity.kt @@ -2,57 +2,63 @@ package net.dankito.banking.persistence.model import com.fasterxml.jackson.annotation.JsonIdentityInfo import com.fasterxml.jackson.annotation.ObjectIdGenerators +import net.dankito.banking.ui.model.IAccountTransaction import net.dankito.utils.multiplatform.BigDecimal import net.dankito.utils.multiplatform.Date import net.dankito.utils.multiplatform.UUID -@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references +@JsonIdentityInfo(property = "technicalId", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references // had to define all properties as 'var' 'cause MapStruct cannot handle vals open class AccountTransactionEntity( - open var bankAccount: BankAccountEntity, - open var amount: BigDecimal, - open var currency: String, - open var unparsedUsage: String, - open var bookingDate: Date, - open var otherPartyName: String?, - open var otherPartyBankCode: String?, - open var otherPartyAccountId: String?, - open var bookingText: String?, - open var valueDate: Date, - open var statementNumber: Int, - open var sequenceNumber: Int?, - open var openingBalance: BigDecimal?, - open var closingBalance: BigDecimal?, + override var bankAccount: BankAccountEntity, + override var amount: BigDecimal, + override var currency: String, + override var unparsedUsage: String, + override var bookingDate: Date, + override var otherPartyName: String?, + override var otherPartyBankCode: String?, + override var otherPartyAccountId: String?, + override var bookingText: String?, + override var valueDate: Date, + override var statementNumber: Int, + override var sequenceNumber: Int?, + override var openingBalance: BigDecimal?, + override var closingBalance: BigDecimal?, - open var endToEndReference: String?, - open var customerReference: String?, - open var mandateReference: String?, - open var creditorIdentifier: String?, - open var originatorsIdentificationCode: String?, - open var compensationAmount: String?, - open var originalAmount: String?, - open var sepaUsage: String?, - open var deviantOriginator: String?, - open var deviantRecipient: String?, - open var usageWithNoSpecialType: String?, - open var primaNotaNumber: String?, - open var textKeySupplement: String?, + override var endToEndReference: String?, + override var customerReference: String?, + override var mandateReference: String?, + override var creditorIdentifier: String?, + override var originatorsIdentificationCode: String?, + override var compensationAmount: String?, + override var originalAmount: String?, + override var sepaUsage: String?, + override var deviantOriginator: String?, + override var deviantRecipient: String?, + override var usageWithNoSpecialType: String?, + override var primaNotaNumber: String?, + override var textKeySupplement: String?, - open var currencyType: String?, - open var bookingKey: String, - open var referenceForTheAccountOwner: String, - open var referenceOfTheAccountServicingInstitution: String?, - open var supplementaryDetails: String?, + override var currencyType: String?, + override var bookingKey: String, + override var referenceForTheAccountOwner: String, + override var referenceOfTheAccountServicingInstitution: String?, + override var supplementaryDetails: String?, - open var transactionReferenceNumber: String, - open var relatedReferenceNumber: String?, - var id: String = UUID.random().toString() -) { + override var transactionReferenceNumber: String, + override var relatedReferenceNumber: String?, + override var technicalId: String = UUID.random() +) : IAccountTransaction { // for object deserializers internal constructor() : this(BankAccountEntity(), BigDecimal.Zero, "", "", Date(), null, null, null, null, Date(), -1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "", "", null, null, "", null) + constructor(bankAccount: BankAccountEntity, otherPartyName: String?, unparsedUsage: String, amount: BigDecimal, valueDate: Date, bookingText: String?) + : this(bankAccount, amount, "EUR", unparsedUsage, valueDate, otherPartyName, null, null, bookingText, valueDate, 0, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, + null, "", "", null, null, "", null) + } \ No newline at end of file diff --git a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/BankAccountEntity.kt b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/BankAccountEntity.kt index e03dc2b6..56d24334 100644 --- a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/BankAccountEntity.kt +++ b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/BankAccountEntity.kt @@ -2,37 +2,39 @@ package net.dankito.banking.persistence.model import com.fasterxml.jackson.annotation.JsonIdentityInfo import com.fasterxml.jackson.annotation.ObjectIdGenerators -import net.dankito.banking.ui.model.BankAccountType +import net.dankito.banking.ui.model.* import net.dankito.utils.multiplatform.BigDecimal import net.dankito.utils.multiplatform.Date import net.dankito.utils.multiplatform.UUID -@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references +@JsonIdentityInfo(property = "technicalId", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references // had to define all properties as 'var' 'cause MapStruct cannot handle vals (and cannot use Pozo's mapstruct-kotlin as SerializableBankAccountBuilder would fail with @Context) open class BankAccountEntity( - open var customer: CustomerEntity, - open var identifier: String, - open var accountHolderName: String, - open var iban: String?, - open var subAccountNumber: String?, - open var customerId: String, - open var balance: BigDecimal = BigDecimal.Zero, - open var currency: String = "EUR", - open var type: BankAccountType = BankAccountType.Girokonto, - open var productName: String? = null, - open var accountLimit: String? = null, - open var lastRetrievedTransactionsTimestamp: Date? = null, - open var supportsRetrievingAccountTransactions: Boolean = false, - open var supportsRetrievingBalance: Boolean = false, - open var supportsTransferringMoney: Boolean = false, - open var supportsInstantPaymentMoneyTransfer: Boolean = false, - open var bookedTransactions: List = listOf(), - open var unbookedTransactions: List = listOf(), - open var id: String = UUID.random().toString(), - open var userSetDisplayName: String? = null + override var customer: CustomerEntity, + override var identifier: String, + override var accountHolderName: String, + override var iban: String?, + override var subAccountNumber: String?, + override var customerId: String, + override var balance: BigDecimal = BigDecimal.Zero, + override var currency: String = "EUR", + override var type: BankAccountType = BankAccountType.Girokonto, + override var productName: String? = null, + override var accountLimit: String? = null, + override var lastRetrievedTransactionsTimestamp: Date? = null, + override var supportsRetrievingAccountTransactions: Boolean = false, + override var supportsRetrievingBalance: Boolean = false, + override var supportsTransferringMoney: Boolean = false, + override var supportsInstantPaymentMoneyTransfer: Boolean = false, + override var bookedTransactions: List = listOf(), + override var unbookedTransactions: List = listOf(), + override var technicalId: String = UUID.random(), + override var userSetDisplayName: String? = null, + override var haveAllTransactionsBeenFetched: Boolean = false, + override var displayIndex: Int = 0 -) { +) : IBankAccount { internal constructor() : this(CustomerEntity(), "", "", null, null, "") // for object deserializers diff --git a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/CustomerEntity.kt b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/CustomerEntity.kt index 1af860a1..9aa46cc8 100644 --- a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/CustomerEntity.kt +++ b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/model/CustomerEntity.kt @@ -1,30 +1,32 @@ package net.dankito.banking.persistence.model import com.fasterxml.jackson.annotation.* +import net.dankito.banking.ui.model.ICustomer import net.dankito.banking.ui.model.tan.TanMedium import net.dankito.banking.ui.model.tan.TanProcedure import java.util.* -@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references +@JsonIdentityInfo(property = "technicalId", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references // had to define all properties as 'var' 'cause MapStruct cannot handle vals (and cannot use Pozo's mapstruct-kotlin as SerializableCustomerBuilder would fail with @Context) open class CustomerEntity( - var bankCode: String, - var customerId: String, - var password: String, - var finTsServerAddress: String, - var bankName: String, - var bic: String, - var customerName: String, - var userId: String = customerId, - var iconUrl: String? = null, - var accounts: List = listOf(), - var supportedTanProcedures: List = listOf(), - var selectedTanProcedure: TanProcedure? = null, - var tanMedia: List = listOf(), - var id: String = UUID.randomUUID().toString(), - var userSetDisplayName: String? = null -) { + override var bankCode: String, + override var customerId: String, + override var password: String, + override var finTsServerAddress: String, + override var bankName: String, + override var bic: String, + override var customerName: String, + override var userId: String = customerId, + override var iconUrl: String? = null, + override var accounts: List = listOf(), + override var supportedTanProcedures: List = listOf(), + override var selectedTanProcedure: TanProcedure? = null, + override var tanMedia: List = listOf(), + override var technicalId: String = UUID.randomUUID().toString(), + override var userSetDisplayName: String? = null, + override var displayIndex: Int = 0 +) : ICustomer { internal constructor() : this("", "", "", "", "", "", "") // for object deserializers diff --git a/persistence/json/BankingPersistenceJson/src/test/kotlin/net/dankito/banking/persistence/BankingPersistenceJsonTest.kt b/persistence/json/BankingPersistenceJson/src/test/kotlin/net/dankito/banking/persistence/BankingPersistenceJsonTest.kt index 1eb72b0c..62732c25 100644 --- a/persistence/json/BankingPersistenceJson/src/test/kotlin/net/dankito/banking/persistence/BankingPersistenceJsonTest.kt +++ b/persistence/json/BankingPersistenceJson/src/test/kotlin/net/dankito/banking/persistence/BankingPersistenceJsonTest.kt @@ -1,13 +1,9 @@ package net.dankito.banking.persistence -import net.dankito.banking.persistence.mapper.CustomerConverter -import net.dankito.banking.persistence.mapper.CycleAvoidingMappingContext import net.dankito.banking.persistence.model.AccountTransactionEntity import net.dankito.banking.persistence.model.BankAccountEntity import net.dankito.banking.persistence.model.CustomerEntity -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.* import net.dankito.banking.util.JacksonJsonSerializer import net.dankito.utils.multiplatform.BigDecimal import net.dankito.utils.multiplatform.Date @@ -15,7 +11,6 @@ import net.dankito.utils.multiplatform.File import org.assertj.core.api.Assertions.assertThat import org.junit.Assert import org.junit.Test -import org.mapstruct.factory.Mappers import kotlin.random.Random @@ -72,7 +67,7 @@ class BankingPersistenceJsonTest { // when - underTest.saveOrUpdateAccount(customers.first(), customers) + underTest.saveOrUpdateAccount(customers.first() as TypedCustomer, customers.map { it as TypedCustomer }) // then @@ -89,13 +84,13 @@ class BankingPersistenceJsonTest { // when - underTest.saveOrUpdateAccount(customer, listOf(customer)) + underTest.saveOrUpdateAccount(customer as TypedCustomer, listOf(customer).map { it as TypedCustomer }) // then val result = serializer.deserializeListOr(file, CustomerEntity::class) - assertCustomersEqual(result, listOf(customer)) + assertCustomersEqual(result, listOf(customer) as List) } @@ -107,9 +102,8 @@ class BankingPersistenceJsonTest { createCustomer(2), createCustomer(3) ) - val serializableCustomers = Mappers.getMapper(CustomerConverter::class.java).mapCustomers(customers, CycleAvoidingMappingContext()) - serializer.serializeObject(serializableCustomers, file) + serializer.serializeObject(customers, file) // when @@ -117,19 +111,19 @@ class BankingPersistenceJsonTest { // then - assertCustomersEqual(serializableCustomers, result) + assertCustomersEqual(customers, result as List) } - private fun createCustomer(countBankAccounts: Int = 0, customerId: String = CustomerId): Customer { - val result = Customer(BankCode, customerId, Password, FinTsServerAddress, BankName, Bic, CustomerName, UserId, IconUrl) + private fun createCustomer(countBankAccounts: Int = 0, customerId: String = CustomerId): CustomerEntity { + val result = CustomerEntity(BankCode, customerId, Password, FinTsServerAddress, BankName, Bic, CustomerName, UserId, IconUrl) result.accounts = createBankAccounts(countBankAccounts, result) return result } - private fun createBankAccounts(count: Int, customer: Customer): List { + private fun createBankAccounts(count: Int, customer: CustomerEntity): List { val random = Random(System.nanoTime()) return IntRange(1, count).map { accountIndex -> @@ -137,8 +131,8 @@ class BankingPersistenceJsonTest { } } - private fun createBankAccount(productName: String, customer: Customer, countTransactions: Int = 0): BankAccount { - val result = BankAccount(customer, customer.customerId, "AccountHolder", "DE00" + customer.bankCode + customer.customerId, null, + private fun createBankAccount(productName: String, customer: CustomerEntity, countTransactions: Int = 0): BankAccountEntity { + val result = BankAccountEntity(customer, customer.customerId, "AccountHolder", "DE00" + customer.bankCode + customer.customerId, null, customer.customerId, BigDecimal(84.25), productName = productName) result.bookedTransactions = createAccountTransactions(countTransactions, result) @@ -146,14 +140,14 @@ class BankingPersistenceJsonTest { return result } - private fun createAccountTransactions(countTransactions: Int, account: BankAccount): List { + private fun createAccountTransactions(countTransactions: Int, account: BankAccountEntity): List { return IntRange(1, countTransactions).map { transactionIndex -> createAccountTransaction(transactionIndex, account) } } - private fun createAccountTransaction(transactionIndex: Int, account: BankAccount): AccountTransaction { - return AccountTransaction(account, "OtherParty_$transactionIndex", "Usage_$transactionIndex", BigDecimal(transactionIndex.toDouble()), createDate(), null) + private fun createAccountTransaction(transactionIndex: Int, account: BankAccountEntity): AccountTransactionEntity { + return AccountTransactionEntity(account, "OtherParty_$transactionIndex", "Usage_$transactionIndex", BigDecimal(transactionIndex.toDouble()), createDate(), null) } private fun createDate(): Date { @@ -161,11 +155,11 @@ class BankingPersistenceJsonTest { } - private fun assertCustomersEqual(deserializedCustomers: List, customers: List) { + private fun assertCustomersEqual(deserializedCustomers: List, customers: List) { assertThat(deserializedCustomers.size).isEqualTo(customers.size) deserializedCustomers.forEach { deserializedCustomer -> - val customer = customers.firstOrNull { it.technicalId == deserializedCustomer.id } + val customer = customers.firstOrNull { it.technicalId == deserializedCustomer.technicalId } if (customer == null) { Assert.fail("Could not find matching customer for deserialized customer $deserializedCustomer. customers = $customers") @@ -176,7 +170,7 @@ class BankingPersistenceJsonTest { } } - private fun assertCustomersEqual(deserializedCustomer: CustomerEntity, customer: Customer) { + private fun assertCustomersEqual(deserializedCustomer: CustomerEntity, customer: CustomerEntity) { assertThat(deserializedCustomer.bankCode).isEqualTo(customer.bankCode) assertThat(deserializedCustomer.customerId).isEqualTo(customer.customerId) assertThat(deserializedCustomer.password).isEqualTo(customer.password) @@ -191,11 +185,11 @@ class BankingPersistenceJsonTest { assertBankAccountsEqual(deserializedCustomer.accounts, customer.accounts) } - private fun assertBankAccountsEqual(deserializedAccounts: List, accounts: List) { + private fun assertBankAccountsEqual(deserializedAccounts: List, accounts: List) { assertThat(deserializedAccounts.size).isEqualTo(accounts.size) deserializedAccounts.forEach { deserializedAccount -> - val account = accounts.firstOrNull { it.technicalId == deserializedAccount.id } + val account = accounts.firstOrNull { it.technicalId == deserializedAccount.technicalId } if (account == null) { Assert.fail("Could not find matching account for deserialized account $deserializedAccount. accounts = $accounts") @@ -206,9 +200,9 @@ class BankingPersistenceJsonTest { } } - private fun assertBankAccountsEqual(deserializedAccount: BankAccountEntity, account: BankAccount) { + private fun assertBankAccountsEqual(deserializedAccount: BankAccountEntity, account: BankAccountEntity) { // to check if MapStruct created reference correctly - assertThat(deserializedAccount.customer.id).isEqualTo(account.customer.technicalId) + assertThat(deserializedAccount.customer.technicalId).isEqualTo(account.customer.technicalId) assertThat(deserializedAccount.identifier).isEqualTo(account.identifier) assertThat(deserializedAccount.iban).isEqualTo(account.iban) @@ -219,11 +213,11 @@ class BankingPersistenceJsonTest { assertAccountTransactionsEqual(deserializedAccount.bookedTransactions, account.bookedTransactions) } - private fun assertAccountTransactionsEqual(deserializedTransactions: List, transactions: List) { + private fun assertAccountTransactionsEqual(deserializedTransactions: List, transactions: List) { assertThat(deserializedTransactions.size).isEqualTo(transactions.size) deserializedTransactions.forEach { deserializedTransaction -> - val transaction = transactions.firstOrNull { it.technicalId == deserializedTransaction.id } + val transaction = transactions.firstOrNull { it.technicalId == deserializedTransaction.technicalId } if (transaction == null) { Assert.fail("Could not find matching transaction for deserialized transaction $deserializedTransaction. transactions = $transactions") @@ -234,9 +228,9 @@ class BankingPersistenceJsonTest { } } - private fun assertAccountTransactionsEqual(deserializedTransaction: AccountTransactionEntity, transaction: AccountTransaction) { + private fun assertAccountTransactionsEqual(deserializedTransaction: AccountTransactionEntity, transaction: AccountTransactionEntity) { // to check if MapStruct created reference correctly - assertThat(deserializedTransaction.bankAccount.id).isEqualTo(transaction.bankAccount.technicalId) + assertThat(deserializedTransaction.bankAccount.technicalId).isEqualTo(transaction.bankAccount.technicalId) assertThat(deserializedTransaction.otherPartyName).isEqualTo(transaction.otherPartyName) assertThat(deserializedTransaction.unparsedUsage).isEqualTo(transaction.unparsedUsage) diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/RouterAndroid.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/RouterAndroid.kt index 61dc1953..196e5178 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/RouterAndroid.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/RouterAndroid.kt @@ -3,7 +3,7 @@ package net.dankito.banking.ui.android import net.dankito.banking.ui.android.util.CurrentActivityTracker import net.dankito.banking.ui.IRouter import net.dankito.banking.ui.android.dialogs.* -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.tan.EnterTanGeneratorAtcResult import net.dankito.banking.ui.model.tan.EnterTanResult @@ -20,7 +20,7 @@ open class RouterAndroid(protected val activityTracker: CurrentActivityTracker) } } - override fun getTanFromUserFromNonUiThread(customer: Customer, tanChallenge: TanChallenge, presenter: BankingPresenter, callback: (EnterTanResult) -> Unit) { + override fun getTanFromUserFromNonUiThread(customer: TypedCustomer, tanChallenge: TanChallenge, presenter: BankingPresenter, callback: (EnterTanResult) -> Unit) { activityTracker.currentOrNextActivity { activity -> activity.runOnUiThread { EnterTanDialog().show(customer, tanChallenge, activity, false) { result -> diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/adapter/AccountTransactionAdapter.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/adapter/AccountTransactionAdapter.kt index e1abcc77..7eaa22a8 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/adapter/AccountTransactionAdapter.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/adapter/AccountTransactionAdapter.kt @@ -6,7 +6,7 @@ import android.view.View import net.dankito.banking.ui.android.R import net.dankito.banking.ui.android.adapter.viewholder.AccountTransactionViewHolder import net.dankito.banking.ui.android.extensions.showAmount -import net.dankito.banking.ui.model.AccountTransaction +import net.dankito.banking.ui.model.IAccountTransaction import net.dankito.banking.ui.presenter.BankingPresenter import net.dankito.utils.android.extensions.asActivity import net.dankito.utils.android.ui.adapter.ListRecyclerAdapter @@ -14,14 +14,14 @@ import java.text.DateFormat open class AccountTransactionAdapter(protected val presenter: BankingPresenter) - : ListRecyclerAdapter() { + : ListRecyclerAdapter() { companion object { val ValueDateFormat = DateFormat.getDateInstance(DateFormat.SHORT) } - var selectedTransaction: AccountTransaction? = null + var selectedTransaction: IAccountTransaction? = null override fun getListItemLayoutId() = R.layout.list_item_account_transaction @@ -34,7 +34,7 @@ open class AccountTransactionAdapter(protected val presenter: BankingPresenter) return viewHolder } - override fun bindItemToView(viewHolder: AccountTransactionViewHolder, item: AccountTransaction) { + override fun bindItemToView(viewHolder: AccountTransactionViewHolder, item: IAccountTransaction) { viewHolder.txtvwDate.text = ValueDateFormat.format(item.valueDate) val label = if (item.showOtherPartyName) item.otherPartyName else item.bookingText diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/adapter/BankAccountsAdapter.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/adapter/BankAccountsAdapter.kt index 46483671..0cc617b4 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/adapter/BankAccountsAdapter.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/adapter/BankAccountsAdapter.kt @@ -8,11 +8,11 @@ import android.view.ViewGroup import android.widget.ImageView import kotlinx.android.synthetic.main.list_item_bank_account.view.* import net.dankito.banking.ui.android.R -import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.TypedBankAccount import net.dankito.utils.android.ui.adapter.ListAdapter -open class BankAccountsAdapter(bankAccounts: List) : ListAdapter(bankAccounts) { +open class BankAccountsAdapter(bankAccounts: List) : ListAdapter(bankAccounts) { override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? { @@ -30,7 +30,7 @@ open class BankAccountsAdapter(bankAccounts: List) : ListAdapter Unit) { this.customer = customer diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/TransferMoneyDialog.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/TransferMoneyDialog.kt index bc99e0ce..b9d09435 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/TransferMoneyDialog.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/TransferMoneyDialog.kt @@ -27,7 +27,7 @@ import net.dankito.banking.ui.android.listener.ListItemSelectedListener import net.dankito.banking.ui.android.util.StandardAutocompleteCallback import net.dankito.banking.ui.android.util.StandardTextWatcher import net.dankito.banking.search.Remittee -import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.TypedBankAccount import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.responses.BankingClientResponse import net.dankito.banking.ui.presenter.BankingPresenter @@ -52,7 +52,7 @@ open class TransferMoneyDialog : DialogFragment() { } - protected lateinit var bankAccount: BankAccount + protected lateinit var bankAccount: TypedBankAccount protected var preselectedValues: TransferMoneyData? = null diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/DrawerView.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/DrawerView.kt index 38b14ae0..988ed5c6 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/DrawerView.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/DrawerView.kt @@ -20,7 +20,7 @@ import com.mikepenz.materialdrawer.util.removeItemByPosition import com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView import net.dankito.banking.ui.android.R import net.dankito.banking.ui.android.extensions.withIcon -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer import net.dankito.banking.ui.presenter.BankingPresenter import org.slf4j.LoggerFactory @@ -139,7 +139,7 @@ open class DrawerView( }.flatten() } - private fun createAccountDrawerItem(customer: Customer): IDrawerItem<*> { + private fun createAccountDrawerItem(customer: TypedCustomer): IDrawerItem<*> { val accountItem = AccountDrawerItem() .withName(customer.displayName) @@ -160,7 +160,7 @@ open class DrawerView( return accountItem } - private fun createBankAccountsDrawerItems(customer: Customer): List> { + private fun createBankAccountsDrawerItems(customer: TypedCustomer): List> { return customer.accounts.map { bankAccount -> SecondaryDrawerItem() .withName(bankAccount.displayName) @@ -176,13 +176,13 @@ open class DrawerView( return false } - private fun closeDrawerAndEditAccount(customer: Customer) { + private fun closeDrawerAndEditAccount(customer: TypedCustomer) { closeDrawer() editAccount(customer) } - private fun editAccount(customer: Customer) { + private fun editAccount(customer: TypedCustomer) { // TODO: implement editing account (e.g. displayed name etc.) AlertDialog.Builder(activity) diff --git a/ui/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/MainWindow.kt b/ui/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/MainWindow.kt index 60066a33..e7a96376 100755 --- a/ui/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/MainWindow.kt +++ b/ui/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/MainWindow.kt @@ -11,6 +11,7 @@ import net.dankito.banking.ui.presenter.BankingPresenter import net.dankito.banking.util.BankIconFinder import net.dankito.banking.bankfinder.LuceneBankFinder import net.dankito.banking.persistence.LuceneBankingPersistence +import net.dankito.banking.persistence.mapper.EntitiesModelCreator import net.dankito.banking.search.LuceneRemitteeSearcher import net.dankito.banking.util.JacksonJsonSerializer import net.dankito.banking.util.extraction.JavaTextExtractorRegistry @@ -32,6 +33,8 @@ class MainWindow : View(messages["application.title"]) { private val indexFolder = ensureFolderExists(dataFolder, "index") + private val modelCreator = EntitiesModelCreator() + private val serializer = JacksonJsonSerializer() private val tesseractTextExtractor = Tesseract4CommandlineImageTextExtractor(TesseractConfig(listOf(OcrLanguage.English, OcrLanguage.German))) @@ -42,11 +45,11 @@ class MainWindow : View(messages["application.title"]) { tesseractTextExtractor, TikaTextExtractor() ))) - private val presenter = BankingPresenter(fints4kBankingClientCreator(serializer), + private val presenter = BankingPresenter(fints4kBankingClientCreator(modelCreator, serializer), LuceneBankFinder(indexFolder), dataFolder, LuceneBankingPersistence(indexFolder, databaseFolder), - RouterJavaFx(), LuceneRemitteeSearcher(indexFolder), BankIconFinder(), textExtractorRegistry) + RouterJavaFx(), modelCreator, LuceneRemitteeSearcher(indexFolder), BankIconFinder(), textExtractorRegistry) // private val presenter = BankingPresenter(hbci4jBankingClientCreator(), LuceneBankFinder(indexFolder), -// dataFolder, LuceneBankingPersistence(indexFolder, databaseFolder), RouterJavaFx(), LuceneRemitteeSearcher(indexFolder), +// dataFolder, LuceneBankingPersistence(indexFolder, databaseFolder), RouterJavaFx(), modelCreator, LuceneRemitteeSearcher(indexFolder), // BankIconFinder(), textExtractorRegistry) diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/RouterJavaFx.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/RouterJavaFx.kt index e29d5ef6..b6dcb06c 100644 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/RouterJavaFx.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/RouterJavaFx.kt @@ -4,7 +4,7 @@ import net.dankito.banking.ui.IRouter import net.dankito.banking.ui.javafx.dialogs.AddAccountDialog import net.dankito.banking.ui.javafx.dialogs.cashtransfer.TransferMoneyDialog import net.dankito.banking.ui.javafx.dialogs.tan.EnterTanDialog -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.tan.EnterTanGeneratorAtcResult import net.dankito.banking.ui.model.tan.EnterTanResult @@ -22,7 +22,7 @@ open class RouterJavaFx : IRouter { AddAccountDialog(presenter).show(messages["add.account.dialog.title"]) } - override fun getTanFromUserFromNonUiThread(customer: Customer, tanChallenge: TanChallenge, presenter: BankingPresenter, callback: (EnterTanResult) -> Unit) { + override fun getTanFromUserFromNonUiThread(customer: TypedCustomer, tanChallenge: TanChallenge, presenter: BankingPresenter, callback: (EnterTanResult) -> Unit) { FX.runAndWait { EnterTanDialog(customer, tanChallenge, presenter) { result -> callback(result) diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsControlView.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsControlView.kt index 019a1afe..d920b866 100644 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsControlView.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsControlView.kt @@ -141,7 +141,7 @@ open class AccountTransactionsControlView( protected open fun updateAccountTransactions(processingIndicatorButton: ProcessingIndicatorButton) { // TODO: or only update transactions of selected accounts? - presenter.updateAccountsTransactionsAsync { transactions -> + presenter.updateAccountsTransactionsAsync { runLater { processingIndicatorButton.resetIsProcessing() } diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsTable.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsTable.kt index 8372e8b0..9b79bfac 100644 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsTable.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsTable.kt @@ -11,7 +11,7 @@ import javafx.scene.control.TableView import javafx.scene.layout.Priority import javafx.scene.paint.Color import javafx.util.Callback -import net.dankito.banking.ui.model.AccountTransaction +import net.dankito.banking.ui.model.IAccountTransaction import net.dankito.banking.ui.presenter.BankingPresenter import net.dankito.utils.javafx.ui.extensions.ensureOnlyUsesSpaceIfVisible import tornadofx.* @@ -21,8 +21,8 @@ import java.text.DateFormat open class AccountTransactionsTable @JvmOverloads constructor( protected val presenter: BankingPresenter, - transactions: ObservableList = FXCollections.emptyObservableList() -) : TableView(transactions) { + transactions: ObservableList = FXCollections.emptyObservableList() +) : TableView(transactions) { companion object { @@ -38,7 +38,7 @@ open class AccountTransactionsTable @JvmOverloads constructor( protected open fun initUi() { - column(messages["account.transactions.table.column.header.value.date"], AccountTransaction::valueDate) { + column(messages["account.transactions.table.column.header.value.date"], IAccountTransaction::valueDate) { prefWidth = 115.0 cellFormat { @@ -48,7 +48,7 @@ open class AccountTransactionsTable @JvmOverloads constructor( } } - columns.add(TableColumn(messages["account.transactions.table.column.header.usage"]).apply { + columns.add(TableColumn(messages["account.transactions.table.column.header.usage"]).apply { this.cellFormat { contentDisplay = ContentDisplay.GRAPHIC_ONLY @@ -80,8 +80,8 @@ open class AccountTransactionsTable @JvmOverloads constructor( } } - cellValueFactory = Callback { object : ObjectBinding() { - override fun computeValue(): AccountTransaction { + cellValueFactory = Callback { object : ObjectBinding() { + override fun computeValue(): IAccountTransaction { return it.value } @@ -90,7 +90,7 @@ open class AccountTransactionsTable @JvmOverloads constructor( weightedWidth(4.0) }) - columns.add(TableColumn(messages["account.transactions.table.column.header.amount"]).apply { + columns.add(TableColumn(messages["account.transactions.table.column.header.amount"]).apply { prefWidth = 85.0 this.cellFormat { diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsView.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsView.kt index 7a906bb3..b80ec828 100644 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsView.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountTransactionsView.kt @@ -8,8 +8,8 @@ import javafx.scene.input.ContextMenuEvent import javafx.scene.input.MouseButton import javafx.scene.input.MouseEvent import net.dankito.banking.ui.javafx.dialogs.JavaFxDialogService -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.IAccountTransaction +import net.dankito.banking.ui.model.TypedBankAccount import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.responses.GetTransactionsResponse import net.dankito.banking.ui.presenter.BankingPresenter @@ -24,7 +24,7 @@ open class AccountTransactionsView(private val presenter: BankingPresenter) : Vi protected val balance = SimpleStringProperty("") - protected val transactionsToDisplay = FXCollections.observableArrayList(listOf()) + protected val transactionsToDisplay = FXCollections.observableArrayList(listOf()) protected var currentMenu: ContextMenu? = null @@ -56,7 +56,7 @@ open class AccountTransactionsView(private val presenter: BankingPresenter) : Vi } - protected open fun tableClicked(event: MouseEvent, selectedItem: AccountTransaction?) { + protected open fun tableClicked(event: MouseEvent, selectedItem: IAccountTransaction?) { if (event.button == MouseButton.PRIMARY || event.button == MouseButton.MIDDLE) { currentMenu?.hide() } @@ -79,7 +79,7 @@ open class AccountTransactionsView(private val presenter: BankingPresenter) : Vi } } - protected open fun createContextMenuForItems(selectedItem: AccountTransaction): ContextMenu { + protected open fun createContextMenuForItems(selectedItem: IAccountTransaction): ContextMenu { val contextMenu = ContextMenu() contextMenu.apply { @@ -105,21 +105,21 @@ open class AccountTransactionsView(private val presenter: BankingPresenter) : Vi return contextMenu } - protected open fun showTransactionDetailsDialog(transaction: AccountTransaction) { + protected open fun showTransactionDetailsDialog(transaction: IAccountTransaction) { // TODO: // presenter.showTransactionDetailsWindow(transaction.item) } - protected open fun newTransferToSameRemittee(transaction: AccountTransaction) { + protected open fun newTransferToSameRemittee(transaction: IAccountTransaction) { presenter.showTransferMoneyDialog(TransferMoneyData.fromAccountTransactionWithoutAmountAndUsage(transaction)) } - protected open fun newTransferWithSameData(transaction: AccountTransaction) { + protected open fun newTransferWithSameData(transaction: IAccountTransaction) { presenter.showTransferMoneyDialog(TransferMoneyData.fromAccountTransaction(transaction)) } - protected open fun handleSelectedBankAccountsChanged(selectedBankAccounts: List) { + protected open fun handleSelectedBankAccountsChanged(selectedBankAccounts: List) { runLater { isAccountSelected.value = selectedBankAccounts.isNotEmpty() diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountsTreeView.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountsTreeView.kt index fbaa34a2..b939dcf6 100644 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountsTreeView.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/controls/AccountsTreeView.kt @@ -10,13 +10,13 @@ import javafx.scene.input.KeyCode import net.dankito.banking.ui.javafx.dialogs.JavaFxDialogService import net.dankito.banking.ui.javafx.model.AccountsAccountTreeItem import net.dankito.banking.ui.javafx.model.AccountsRootTreeItem -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer import net.dankito.banking.ui.presenter.BankingPresenter import tornadofx.* import tornadofx.FX.Companion.messages -open class AccountsTreeView(customers: ObservableList, protected val presenter: BankingPresenter) +open class AccountsTreeView(customers: ObservableList, protected val presenter: BankingPresenter) : TreeView(AccountsRootTreeItem(customers)) { protected var currentMenu: ContextMenu? = null diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/cashtransfer/TransferMoneyDialog.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/cashtransfer/TransferMoneyDialog.kt index 906d81c7..6bcf6431 100644 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/cashtransfer/TransferMoneyDialog.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/cashtransfer/TransferMoneyDialog.kt @@ -12,7 +12,7 @@ import javafx.scene.image.ImageView import javafx.scene.layout.Priority import kotlinx.coroutines.* import net.dankito.banking.ui.javafx.dialogs.JavaFxDialogService -import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.TypedBankAccount import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.responses.BankingClientResponse import net.dankito.banking.ui.presenter.BankingPresenter @@ -49,7 +49,7 @@ open class TransferMoneyDialog @JvmOverloads constructor( protected val bankAccountsSupportingTransferringMoney = FXCollections.observableArrayList(presenter.bankAccounts.filter { it.supportsTransferringMoney }) - protected val selectedBankAccount = SimpleObjectProperty(preselectedValues?.account ?: bankAccountsSupportingTransferringMoney.firstOrNull()) + protected val selectedBankAccount = SimpleObjectProperty(preselectedValues?.account ?: bankAccountsSupportingTransferringMoney.firstOrNull()) protected val showBankAccounts = SimpleBooleanProperty(bankAccountsSupportingTransferringMoney.size > 1) @@ -268,7 +268,7 @@ open class TransferMoneyDialog @JvmOverloads constructor( } - private fun selectedBankAccountChanged(newValue: BankAccount?) { + private fun selectedBankAccountChanged(newValue: TypedBankAccount?) { supportsInstantPayment.value = newValue?.supportsInstantPaymentMoneyTransfer ?: false if (supportsInstantPayment.value == false) { @@ -345,7 +345,7 @@ open class TransferMoneyDialog @JvmOverloads constructor( } protected open fun transferMoney() { - remitteeBank.value?.let { remitteeBank -> + remitteeBank.value?.let { val bankAccount = selectedBankAccount.value val data = TransferMoneyData( @@ -366,7 +366,7 @@ open class TransferMoneyDialog @JvmOverloads constructor( } } - protected open fun handleTransferMoneyResultOnUiThread(bankAccount: BankAccount, transferData: TransferMoneyData, response: BankingClientResponse) { + protected open fun handleTransferMoneyResultOnUiThread(bankAccount: TypedBankAccount, transferData: TransferMoneyData, response: BankingClientResponse) { val currency = bankAccount.currency if (response.isSuccessful) { diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/tan/EnterTanDialog.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/tan/EnterTanDialog.kt index 99bd53d1..806198f4 100644 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/tan/EnterTanDialog.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/tan/EnterTanDialog.kt @@ -10,7 +10,7 @@ import javafx.scene.text.FontWeight import net.dankito.banking.ui.javafx.dialogs.tan.controls.ChipTanFlickerCodeView import net.dankito.banking.ui.javafx.dialogs.JavaFxDialogService import net.dankito.banking.ui.javafx.dialogs.tan.controls.TanImageView -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer import net.dankito.banking.ui.model.responses.BankingClientResponse import net.dankito.banking.ui.model.tan.* import net.dankito.banking.ui.presenter.BankingPresenter @@ -20,7 +20,7 @@ import tornadofx.* open class EnterTanDialog( - protected val customer: Customer, + protected val customer: TypedCustomer, protected val challenge: TanChallenge, protected val presenter: BankingPresenter, protected val tanEnteredCallback: (EnterTanResult) -> Unit diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsAccountTreeItem.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsAccountTreeItem.kt index d60246e2..a7e19a67 100755 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsAccountTreeItem.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsAccountTreeItem.kt @@ -2,10 +2,10 @@ package net.dankito.banking.ui.javafx.model import javafx.scene.Node import javafx.scene.image.ImageView -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer -open class AccountsAccountTreeItem(val customer: Customer) : AccountsTreeItemBase(customer.displayName) { +open class AccountsAccountTreeItem(val customer: TypedCustomer) : AccountsTreeItemBase(customer.displayName) { companion object { private const val IconSize = 16.0 diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsBankAccountTreeItem.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsBankAccountTreeItem.kt index 02c8ed54..1ca6fced 100755 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsBankAccountTreeItem.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsBankAccountTreeItem.kt @@ -1,6 +1,6 @@ package net.dankito.banking.ui.javafx.model -import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.TypedBankAccount -open class AccountsBankAccountTreeItem(val bankAccount: BankAccount) : AccountsTreeItemBase(bankAccount.displayName) \ No newline at end of file +open class AccountsBankAccountTreeItem(val bankAccount: TypedBankAccount) : AccountsTreeItemBase(bankAccount.displayName) \ No newline at end of file diff --git a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsRootTreeItem.kt b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsRootTreeItem.kt index afe36f63..215cae6f 100755 --- a/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsRootTreeItem.kt +++ b/ui/BankingJavaFxControls/src/main/kotlin/net/dankito/banking/ui/javafx/model/AccountsRootTreeItem.kt @@ -2,13 +2,13 @@ package net.dankito.banking.ui.javafx.model import javafx.collections.ListChangeListener import javafx.collections.ObservableList -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer import tornadofx.FX.Companion.messages import tornadofx.get import tornadofx.runLater -open class AccountsRootTreeItem(customers: ObservableList) : AccountsTreeItemBase(messages["accounts.view.all.accounts"]) { +open class AccountsRootTreeItem(customers: ObservableList) : AccountsTreeItemBase(messages["accounts.view.all.accounts"]) { init { setAccounts(customers) @@ -18,7 +18,7 @@ open class AccountsRootTreeItem(customers: ObservableList) : AccountsT }) } - protected open fun setAccounts(customers: List) { + protected open fun setAccounts(customers: List) { isExpanded = customers.isNotEmpty() children.setAll(customers.map { AccountsAccountTreeItem(it) }) diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/IBankingPersistence.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/IBankingPersistence.kt index b51b2e33..350ee25d 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/IBankingPersistence.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/IBankingPersistence.kt @@ -1,21 +1,19 @@ package net.dankito.banking.persistence +import net.dankito.banking.ui.model.* import net.dankito.utils.multiplatform.File -import net.dankito.banking.ui.model.Customer -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount interface IBankingPersistence { - fun saveOrUpdateAccount(customer: Customer, allCustomers: List) + fun saveOrUpdateAccount(customer: TypedCustomer, allCustomers: List) - fun deleteAccount(customer: Customer, allCustomers: List) + fun deleteAccount(customer: TypedCustomer, allCustomers: List) - fun readPersistedAccounts(): List + fun readPersistedAccounts(): List - fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List) + fun saveOrUpdateAccountTransactions(bankAccount: TypedBankAccount, transactions: List) fun saveUrlToFile(url: String, file: File) diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/NoOpBankingPersistence.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/NoOpBankingPersistence.kt index c607a575..939c2221 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/NoOpBankingPersistence.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/NoOpBankingPersistence.kt @@ -1,27 +1,25 @@ package net.dankito.banking.persistence -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.* import net.dankito.utils.multiplatform.File open class NoOpBankingPersistence : IBankingPersistence { - override fun saveOrUpdateAccount(customer: Customer, allCustomers: List) { + override fun saveOrUpdateAccount(customer: TypedCustomer, allCustomers: List) { } - override fun deleteAccount(customer: Customer, allCustomers: List) { + override fun deleteAccount(customer: TypedCustomer, allCustomers: List) { } - override fun readPersistedAccounts(): List { + override fun readPersistedAccounts(): List { return listOf() } - override fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List) { + override fun saveOrUpdateAccountTransactions(bankAccount: TypedBankAccount, transactions: List) { } diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/BankingClientCallback.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/BankingClientCallback.kt index f7e85ccc..614103d3 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/BankingClientCallback.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/BankingClientCallback.kt @@ -1,6 +1,6 @@ package net.dankito.banking.ui -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer import net.dankito.banking.ui.model.tan.EnterTanGeneratorAtcResult import net.dankito.banking.ui.model.tan.EnterTanResult import net.dankito.banking.ui.model.tan.TanChallenge @@ -9,7 +9,7 @@ import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium interface BankingClientCallback { - fun enterTan(customer: Customer, tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) + fun enterTan(customer: TypedCustomer, tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) /** * This method gets called for chipTan TAN generators when the bank asks the customer to synchronize her/his TAN generator. diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IBankingClient.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IBankingClient.kt index 1a122888..8dab3924 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IBankingClient.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IBankingClient.kt @@ -1,8 +1,6 @@ package net.dankito.banking.ui -import net.dankito.banking.ui.model.BankAccount -import net.dankito.banking.ui.model.Customer -import net.dankito.banking.ui.model.MessageLogEntry +import net.dankito.banking.ui.model.* import net.dankito.banking.ui.model.parameters.GetTransactionsParameter import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.responses.AddAccountResponse @@ -18,7 +16,7 @@ interface IBankingClient { fun addAccountAsync(callback: (AddAccountResponse) -> Unit) fun getTransactionsAsync( - bankAccount: BankAccount, + bankAccount: TypedBankAccount, parameter: GetTransactionsParameter, callback: (GetTransactionsResponse) -> Unit ) @@ -26,6 +24,6 @@ interface IBankingClient { fun transferMoneyAsync(data: TransferMoneyData, callback: (BankingClientResponse) -> Unit) - fun dataChanged(customer: Customer) + fun dataChanged(customer: TypedCustomer) } \ No newline at end of file diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IBankingClientCreator.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IBankingClientCreator.kt index bab34274..381bf098 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IBankingClientCreator.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IBankingClientCreator.kt @@ -1,14 +1,14 @@ package net.dankito.banking.ui import net.dankito.utils.multiplatform.File -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer import net.dankito.banking.util.IAsyncRunner interface IBankingClientCreator { fun createClient( - customer: Customer, + customer: TypedCustomer, dataFolder: File, asyncRunner: IAsyncRunner, callback: BankingClientCallback diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IRouter.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IRouter.kt index 202b6d19..ebc99d00 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IRouter.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/IRouter.kt @@ -1,7 +1,6 @@ package net.dankito.banking.ui -import net.dankito.banking.ui.model.Customer -import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.TypedCustomer import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.tan.EnterTanGeneratorAtcResult import net.dankito.banking.ui.model.tan.EnterTanResult @@ -14,7 +13,7 @@ interface IRouter { fun showAddAccountDialog(presenter: BankingPresenter) - fun getTanFromUserFromNonUiThread(customer: Customer, tanChallenge: TanChallenge, presenter: BankingPresenter, callback: (EnterTanResult) -> Unit) + fun getTanFromUserFromNonUiThread(customer: TypedCustomer, tanChallenge: TanChallenge, presenter: BankingPresenter, callback: (EnterTanResult) -> Unit) fun getAtcFromUserFromNonUiThread(tanMedium: TanGeneratorTanMedium, callback: (EnterTanGeneratorAtcResult) -> Unit) diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/AccountTransaction.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/AccountTransaction.kt index 95fae62b..0e69c2fd 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/AccountTransaction.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/AccountTransaction.kt @@ -7,49 +7,44 @@ import net.dankito.utils.multiplatform.DateFormatter open class AccountTransaction( - open val bankAccount: BankAccount, - open val amount: BigDecimal, - open val currency: String, - open val unparsedUsage: String, - open val bookingDate: Date, - open val otherPartyName: String?, - open val otherPartyBankCode: String?, - open val otherPartyAccountId: String?, - open val bookingText: String?, - open val valueDate: Date, - open val statementNumber: Int, - open val sequenceNumber: Int?, - open val openingBalance: BigDecimal?, - open val closingBalance: BigDecimal?, + override val bankAccount: TypedBankAccount, + override val amount: BigDecimal, + override val currency: String, + override val unparsedUsage: String, + override val bookingDate: Date, + override val otherPartyName: String?, + override val otherPartyBankCode: String?, + override val otherPartyAccountId: String?, + override val bookingText: String?, + override val valueDate: Date, + override val statementNumber: Int, + override val sequenceNumber: Int?, + override val openingBalance: BigDecimal?, + override val closingBalance: BigDecimal?, - open val endToEndReference: String?, - open val customerReference: String?, - open val mandateReference: String?, - open val creditorIdentifier: String?, - open val originatorsIdentificationCode: String?, - open val compensationAmount: String?, - open val originalAmount: String?, - open val sepaUsage: String?, - open val deviantOriginator: String?, - open val deviantRecipient: String?, - open val usageWithNoSpecialType: String?, - open val primaNotaNumber: String?, - open val textKeySupplement: String?, + override val endToEndReference: String?, + override val customerReference: String?, + override val mandateReference: String?, + override val creditorIdentifier: String?, + override val originatorsIdentificationCode: String?, + override val compensationAmount: String?, + override val originalAmount: String?, + override val sepaUsage: String?, + override val deviantOriginator: String?, + override val deviantRecipient: String?, + override val usageWithNoSpecialType: String?, + override val primaNotaNumber: String?, + override val textKeySupplement: String?, - open val currencyType: String?, - open val bookingKey: String, - open val referenceForTheAccountOwner: String, - open val referenceOfTheAccountServicingInstitution: String?, - open val supplementaryDetails: String?, - - open val transactionReferenceNumber: String, - open val relatedReferenceNumber: String? -) { - - companion object { - val IdDateFormat = DateFormatter("yyyy.MM.dd") - } + override val currencyType: String?, + override val bookingKey: String, + override val referenceForTheAccountOwner: String, + override val referenceOfTheAccountServicingInstitution: String?, + override val supplementaryDetails: String?, + override val transactionReferenceNumber: String, + override val relatedReferenceNumber: String? +) : IAccountTransaction { // for object deserializers internal constructor() : this(BankAccount(), null, "", BigDecimal.Zero, Date(), null) @@ -69,29 +64,7 @@ open class AccountTransaction( 0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "", "", null, null, "", null) - open var technicalId: String = buildTransactionIdentifier() - - open val transactionIdentifier: String - get() = buildTransactionIdentifier() - - protected fun buildTransactionIdentifier() : String { - if (bankAccount != null) { - return "${bankAccount.technicalId} ${IdDateFormat.format(bookingDate)} ${IdDateFormat.format(valueDate)} $amount $currency $unparsedUsage $otherPartyName $otherPartyBankCode $otherPartyAccountId" - } - else { // happens for derived classes during initialization. These have to set technicalId after initialization by themselves - return " ${IdDateFormat.format(bookingDate)} ${IdDateFormat.format(valueDate)} $amount $currency $unparsedUsage $otherPartyName $otherPartyBankCode $otherPartyAccountId" - } - } - - - open val showOtherPartyName: Boolean - get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO - - open val canCreateMoneyTransferFrom: Boolean - get() = otherPartyAccountId != null && bankAccount.supportsTransferringMoney - - open val usage: String - get() = sepaUsage ?: unparsedUsage + override var technicalId: String = buildTransactionIdentifier() override fun equals(other: Any?): Boolean { diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/BankAccount.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/BankAccount.kt index 1a263eac..3b3df0a0 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/BankAccount.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/BankAccount.kt @@ -7,69 +7,47 @@ import kotlin.jvm.JvmOverloads open class BankAccount @JvmOverloads constructor( - open val customer: Customer, - open var identifier: String, - open var accountHolderName: String, - open var iban: String?, - open var subAccountNumber: String?, - open var customerId: String, - open var balance: BigDecimal = BigDecimal.Zero, - open var currency: String = "EUR", - open var type: BankAccountType = BankAccountType.Girokonto, - open var productName: String? = null, - open var accountLimit: String? = null, - open var lastRetrievedTransactionsTimestamp: Date? = null, - open var supportsRetrievingAccountTransactions: Boolean = false, - open var supportsRetrievingBalance: Boolean = false, - open var supportsTransferringMoney: Boolean = false, - open var supportsInstantPaymentMoneyTransfer: Boolean = false, - open var bookedTransactions: List = listOf(), - open var unbookedTransactions: List = listOf() -) : OrderedDisplayable { + override val customer: TypedCustomer, + override var identifier: String, + override var accountHolderName: String, + override var iban: String?, + override var subAccountNumber: String?, + override var customerId: String, + override var balance: BigDecimal = BigDecimal.Zero, + override var currency: String = "EUR", + override var type: BankAccountType = BankAccountType.Girokonto, + override var productName: String? = null, + override var accountLimit: String? = null, + override var lastRetrievedTransactionsTimestamp: Date? = null, + override var supportsRetrievingAccountTransactions: Boolean = false, + override var supportsRetrievingBalance: Boolean = false, + override var supportsTransferringMoney: Boolean = false, + override var supportsInstantPaymentMoneyTransfer: Boolean = false, + override var bookedTransactions: List = listOf(), + override var unbookedTransactions: List = listOf() +) : TypedBankAccount { internal constructor() : this(Customer(), null, "") // for object deserializers /* convenience constructors for languages not supporting default values */ - constructor(customer: Customer, productName: String?, identifier: String) : this(customer, productName, identifier, BankAccountType.Girokonto) + constructor(customer: TypedCustomer, productName: String?, identifier: String) : this(customer, productName, identifier, BankAccountType.Girokonto) - constructor(customer: Customer, productName: String?, identifier: String, type: BankAccountType = BankAccountType.Girokonto, balance: BigDecimal = BigDecimal.Zero) + constructor(customer: TypedCustomer, productName: String?, identifier: String, type: BankAccountType = BankAccountType.Girokonto, balance: BigDecimal = BigDecimal.Zero) : this(customer, identifier, "", null, null, "", balance, "EUR", type, productName) - open var technicalId: String = UUID.random() + override var technicalId: String = UUID.random() - open var haveAllTransactionsBeenFetched: Boolean = false + override var haveAllTransactionsBeenFetched: Boolean = false - open var userSetDisplayName: String? = null - - override val displayName: String - get() { - return userSetDisplayName ?: productName ?: subAccountNumber ?: identifier - } + override var userSetDisplayName: String? = null override var displayIndex: Int = 0 - open fun addBookedTransactions(retrievedBookedTransactions: List) { - val uniqueTransactions = this.bookedTransactions.toMutableSet() - - uniqueTransactions.addAll(retrievedBookedTransactions) - - this.bookedTransactions = uniqueTransactions.toList() - } - - open fun addUnbookedTransactions(retrievedUnbookedTransactions: List) { - val uniqueUnbookedTransactions = this.unbookedTransactions.toMutableSet() - - uniqueUnbookedTransactions.addAll(retrievedUnbookedTransactions) - - this.unbookedTransactions = uniqueUnbookedTransactions.toList() - } - - override fun toString(): String { return "$accountHolderName ($identifier)" } diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/Customer.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/Customer.kt index cbac8a6e..bf71546f 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/Customer.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/Customer.kt @@ -1,26 +1,22 @@ package net.dankito.banking.ui.model -import net.dankito.utils.multiplatform.BigDecimal -import net.dankito.utils.multiplatform.sum import net.dankito.banking.ui.model.tan.TanMedium -import net.dankito.banking.ui.model.tan.TanMediumStatus import net.dankito.banking.ui.model.tan.TanProcedure -import net.dankito.banking.util.sortedByDisplayIndex import net.dankito.utils.multiplatform.UUID open class Customer( - open var bankCode: String, - open var customerId: String, - open var password: String, - open var finTsServerAddress: String, - open var bankName: String, - open var bic: String, - open var customerName: String, - open var userId: String = customerId, - open var iconUrl: String? = null, - open var accounts: List = listOf() -) : OrderedDisplayable { + override var bankCode: String, + override var customerId: String, + override var password: String, + override var finTsServerAddress: String, + override var bankName: String, + override var bic: String, + override var customerName: String, + override var userId: String = customerId, + override var iconUrl: String? = null, + override var accounts: List = listOf() +) : TypedCustomer { internal constructor() : this("", "", "", "", "", "", "") // for object deserializers @@ -31,40 +27,23 @@ open class Customer( : this(bankCode, customerId, password, finTsServerAddress, "", "", "") - open var technicalId: String = UUID.random() + override var technicalId: String = UUID.random() - open var supportedTanProcedures: List = listOf() + override var supportedTanProcedures: List = listOf() - open var selectedTanProcedure: TanProcedure? = null + override var selectedTanProcedure: TanProcedure? = null - open var tanMedia: List = listOf() - - open val tanMediaSorted: List - get() = tanMedia.sortedByDescending { it.status == TanMediumStatus.Used } + override var tanMedia: List = listOf() - open var userSetDisplayName: String? = null - - override val displayName: String - get() = userSetDisplayName ?: bankName + override var userSetDisplayName: String? = null override var displayIndex: Int = 0 - open val accountsSorted: List - get() = accounts.sortedByDisplayIndex() - - - open val balance: BigDecimal - get() = accounts.map { it.balance }.sum() - - open val transactions: List - get() = accounts.flatMap { it.bookedTransactions } - - override fun toString(): String { - return "$customerName ($customerId)" + return "$bankName $customerId" } } \ No newline at end of file diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/IAccountTransaction.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/IAccountTransaction.kt new file mode 100644 index 00000000..07262874 --- /dev/null +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/IAccountTransaction.kt @@ -0,0 +1,76 @@ +package net.dankito.banking.ui.model + +import net.dankito.utils.multiplatform.BigDecimal +import net.dankito.utils.multiplatform.Date +import net.dankito.utils.multiplatform.DateFormatter + + +interface IAccountTransaction { + + companion object { + val IdDateFormat = DateFormatter("yyyy.MM.dd") + } + + + val bankAccount: IBankAccount<*> + val amount: BigDecimal + val currency: String + val unparsedUsage: String + val bookingDate: Date + val otherPartyName: String? + val otherPartyBankCode: String? + val otherPartyAccountId: String? + val bookingText: String? + val valueDate: Date + val statementNumber: Int + val sequenceNumber: Int? + val openingBalance: BigDecimal? + val closingBalance: BigDecimal? + + val endToEndReference: String? + val customerReference: String? + val mandateReference: String? + val creditorIdentifier: String? + val originatorsIdentificationCode: String? + val compensationAmount: String? + val originalAmount: String? + val sepaUsage: String? + val deviantOriginator: String? + val deviantRecipient: String? + val usageWithNoSpecialType: String? + val primaNotaNumber: String? + val textKeySupplement: String? + + val currencyType: String? + val bookingKey: String + val referenceForTheAccountOwner: String + val referenceOfTheAccountServicingInstitution: String? + val supplementaryDetails: String? + + val transactionReferenceNumber: String + val relatedReferenceNumber: String? + + + var technicalId: String + + + val showOtherPartyName: Boolean + get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO + + val canCreateMoneyTransferFrom: Boolean + get() = otherPartyAccountId != null && bankAccount.supportsTransferringMoney + + val usage: String + get() = sepaUsage ?: unparsedUsage + + + fun buildTransactionIdentifier() : String { + if (bankAccount != null) { + return "${bankAccount.technicalId} ${IdDateFormat.format(bookingDate)} ${IdDateFormat.format(valueDate)} $amount $currency $unparsedUsage $otherPartyName $otherPartyBankCode $otherPartyAccountId" + } + else { // happens for derived classes during initialization. These have to set technicalId after initialization by themselves + return " ${IdDateFormat.format(bookingDate)} ${IdDateFormat.format(valueDate)} $amount $currency $unparsedUsage $otherPartyName $otherPartyBankCode $otherPartyAccountId" + } + } + +} \ No newline at end of file diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/IBankAccount.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/IBankAccount.kt new file mode 100644 index 00000000..d1ffa9f4 --- /dev/null +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/IBankAccount.kt @@ -0,0 +1,56 @@ +package net.dankito.banking.ui.model + +import net.dankito.utils.multiplatform.BigDecimal +import net.dankito.utils.multiplatform.Date + + +typealias TypedBankAccount = IBankAccount + + +interface IBankAccount : OrderedDisplayable { + val customer: ICustomer<*, *> + var identifier: String + var accountHolderName: String + var iban: String? + var subAccountNumber: String? + var customerId: String + var balance: BigDecimal + var currency: String + var type: BankAccountType + var productName: String? + var accountLimit: String? + var lastRetrievedTransactionsTimestamp: Date? + var supportsRetrievingAccountTransactions: Boolean + var supportsRetrievingBalance: Boolean + var supportsTransferringMoney: Boolean + var supportsInstantPaymentMoneyTransfer: Boolean + var bookedTransactions: List + var unbookedTransactions: List + var technicalId: String + var haveAllTransactionsBeenFetched: Boolean + var userSetDisplayName: String? + + + override val displayName: String + get() { + return userSetDisplayName ?: productName ?: subAccountNumber ?: identifier + } + + + fun addBookedTransactions(retrievedBookedTransactions: List) { + val uniqueTransactions = this.bookedTransactions.toMutableSet() + + uniqueTransactions.addAll(retrievedBookedTransactions) + + this.bookedTransactions = uniqueTransactions.toList() + } + + fun addUnbookedTransactions(retrievedUnbookedTransactions: List) { + val uniqueUnbookedTransactions = this.unbookedTransactions.toMutableSet() + + uniqueUnbookedTransactions.addAll(retrievedUnbookedTransactions) + + this.unbookedTransactions = uniqueUnbookedTransactions.toList() + } + +} \ No newline at end of file diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/ICustomer.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/ICustomer.kt new file mode 100644 index 00000000..29ed0a76 --- /dev/null +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/ICustomer.kt @@ -0,0 +1,56 @@ +package net.dankito.banking.ui.model + +import net.dankito.banking.ui.model.tan.TanMedium +import net.dankito.banking.ui.model.tan.TanMediumStatus +import net.dankito.banking.ui.model.tan.TanProcedure +import net.dankito.banking.util.sortedByDisplayIndex +import net.dankito.utils.multiplatform.BigDecimal +import net.dankito.utils.multiplatform.sum + + +typealias TypedCustomer = ICustomer, IAccountTransaction> + + +interface ICustomer, TAccountTransaction: IAccountTransaction> : OrderedDisplayable { + + var bankCode: String + var customerId: String + var password: String + var finTsServerAddress: String + + var bankName: String + var bic: String + var customerName: String + var userId: String + + var iconUrl: String? + + var accounts: List + + var supportedTanProcedures: List + var selectedTanProcedure: TanProcedure? + var tanMedia: List + + var userSetDisplayName: String? + + var technicalId: String + + + override val displayName: String + get() = userSetDisplayName ?: bankName + + + val accountsSorted: List + get() = accounts.sortedByDisplayIndex() + + + val balance: BigDecimal + get() = accounts.map { it.balance }.sum() + + val transactions: List + get() = accounts.flatMap { it.bookedTransactions } + + val tanMediaSorted: List + get() = tanMedia.sortedByDescending { it.status == TanMediumStatus.Used } + +} \ No newline at end of file diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/MessageLogEntry.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/MessageLogEntry.kt index ff5050d6..abed10af 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/MessageLogEntry.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/MessageLogEntry.kt @@ -6,7 +6,7 @@ import net.dankito.utils.multiplatform.Date open class MessageLogEntry( val message: String, val time: Date, - val customer: Customer + val customer: TypedCustomer ) { override fun toString(): String { diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/mapper/DefaultModelCreator.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/mapper/DefaultModelCreator.kt new file mode 100644 index 00000000..50d2091a --- /dev/null +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/mapper/DefaultModelCreator.kt @@ -0,0 +1,66 @@ +package net.dankito.banking.ui.model.mapper + +import net.dankito.banking.ui.model.* +import net.dankito.utils.multiplatform.BigDecimal +import net.dankito.utils.multiplatform.Date + + +open class DefaultModelCreator : IModelCreator { + + override fun createCustomer(bankCode: String, customerId: String, password: String, finTsServerAddress: String, bankName: String, bic: String, + customerName: String, userId: String, iconUrl: String?): TypedCustomer { + + return Customer(bankCode, customerId, password, finTsServerAddress, bankName, bic, customerName, userId, iconUrl) + } + + + override fun createBankAccount(customer: TypedCustomer, productName: String?, identifier: String): TypedBankAccount { + return BankAccount(customer, productName, identifier) + } + + override fun createTransaction( + bankAccount: TypedBankAccount, + amount: BigDecimal, + currency: String, + unparsedUsage: String, + bookingDate: Date, + otherPartyName: String?, + otherPartyBankCode: String?, + otherPartyAccountId: String?, + bookingText: String?, + valueDate: Date, + statementNumber: Int, + sequenceNumber: Int?, + openingBalance: BigDecimal?, + closingBalance: BigDecimal?, + endToEndReference: String?, + customerReference: String?, + mandateReference: String?, + creditorIdentifier: String?, + originatorsIdentificationCode: String?, + compensationAmount: String?, + originalAmount: String?, + sepaUsage: String?, + deviantOriginator: String?, + deviantRecipient: String?, + usageWithNoSpecialType: String?, + primaNotaNumber: String?, + textKeySupplement: String?, + currencyType: String?, + bookingKey: String, + referenceForTheAccountOwner: String, + referenceOfTheAccountServicingInstitution: String?, + supplementaryDetails: String?, + transactionReferenceNumber: String, + relatedReferenceNumber: String? + ) : IAccountTransaction { + + return AccountTransaction(bankAccount, amount, currency, unparsedUsage, bookingDate, + otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, valueDate, statementNumber, sequenceNumber, + openingBalance, closingBalance, endToEndReference, customerReference, mandateReference, creditorIdentifier, + originatorsIdentificationCode, compensationAmount, originalAmount, sepaUsage, deviantOriginator, deviantRecipient, + usageWithNoSpecialType, primaNotaNumber, textKeySupplement, currencyType, bookingKey, referenceForTheAccountOwner, + referenceOfTheAccountServicingInstitution, supplementaryDetails, transactionReferenceNumber, relatedReferenceNumber) + } + +} \ No newline at end of file diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/mapper/IModelCreator.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/mapper/IModelCreator.kt new file mode 100644 index 00000000..3de107cb --- /dev/null +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/mapper/IModelCreator.kt @@ -0,0 +1,57 @@ +package net.dankito.banking.ui.model.mapper + +import net.dankito.banking.ui.model.* +import net.dankito.utils.multiplatform.BigDecimal +import net.dankito.utils.multiplatform.Date + + +interface IModelCreator { + + fun createCustomer(bankCode: String, customerId: String, password: String, finTsServerAddress: String, bankName: String, bic: String, + customerName: String = "", userId: String = customerId, iconUrl: String? = null): TypedCustomer + + + fun createBankAccount(customer: TypedCustomer, productName: String?, identifier: String) : TypedBankAccount + + + fun createTransaction( + bankAccount: TypedBankAccount, + amount: BigDecimal, + currency: String, + unparsedUsage: String, + bookingDate: Date, + otherPartyName: String?, + otherPartyBankCode: String?, + otherPartyAccountId: String?, + bookingText: String?, + valueDate: Date, + statementNumber: Int, + sequenceNumber: Int?, + openingBalance: BigDecimal?, + closingBalance: BigDecimal?, + + endToEndReference: String?, + customerReference: String?, + mandateReference: String?, + creditorIdentifier: String?, + originatorsIdentificationCode: String?, + compensationAmount: String?, + originalAmount: String?, + sepaUsage: String?, + deviantOriginator: String?, + deviantRecipient: String?, + usageWithNoSpecialType: String?, + primaNotaNumber: String?, + textKeySupplement: String?, + + currencyType: String?, + bookingKey: String, + referenceForTheAccountOwner: String, + referenceOfTheAccountServicingInstitution: String?, + supplementaryDetails: String?, + + transactionReferenceNumber: String, + relatedReferenceNumber: String? + ) : IAccountTransaction + +} \ No newline at end of file diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/parameters/GetTransactionsParameter.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/parameters/GetTransactionsParameter.kt index 222c7733..0f6f8aa4 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/parameters/GetTransactionsParameter.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/parameters/GetTransactionsParameter.kt @@ -1,7 +1,7 @@ package net.dankito.banking.ui.model.parameters import net.dankito.utils.multiplatform.Date -import net.dankito.banking.ui.model.AccountTransaction +import net.dankito.banking.ui.model.IAccountTransaction open class GetTransactionsParameter( @@ -9,7 +9,7 @@ open class GetTransactionsParameter( val fromDate: Date? = null, val toDate: Date? = null, val abortIfTanIsRequired: Boolean = false, - val retrievedChunkListener: ((List) -> Unit)? = null + val retrievedChunkListener: ((List) -> Unit)? = null ) { constructor() : this(true, null, null) // for Java diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/parameters/TransferMoneyData.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/parameters/TransferMoneyData.kt index 9f336196..c3a0a263 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/parameters/TransferMoneyData.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/parameters/TransferMoneyData.kt @@ -1,12 +1,11 @@ package net.dankito.banking.ui.model.parameters +import net.dankito.banking.ui.model.* import net.dankito.utils.multiplatform.BigDecimal -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount open class TransferMoneyData( - val account: BankAccount, + val account: TypedBankAccount, val creditorName: String, val creditorIban: String, val creditorBic: String, @@ -17,9 +16,9 @@ open class TransferMoneyData( companion object { - fun fromAccountTransactionWithoutAmountAndUsage(transaction: AccountTransaction): TransferMoneyData { + fun fromAccountTransactionWithoutAmountAndUsage(transaction: IAccountTransaction): TransferMoneyData { return TransferMoneyData( - transaction.bankAccount, + transaction.bankAccount as TypedBankAccount, transaction.otherPartyName ?: "", transaction.otherPartyAccountId ?: "", transaction.otherPartyBankCode ?: "", @@ -28,9 +27,9 @@ open class TransferMoneyData( ) } - fun fromAccountTransaction(transaction: AccountTransaction): TransferMoneyData { + fun fromAccountTransaction(transaction: IAccountTransaction): TransferMoneyData { return TransferMoneyData( - transaction.bankAccount, + transaction.bankAccount as TypedBankAccount, transaction.otherPartyName ?: "", transaction.otherPartyAccountId ?: "", transaction.otherPartyBankCode ?: "", diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/responses/AddAccountResponse.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/responses/AddAccountResponse.kt index e56815cb..8962823c 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/responses/AddAccountResponse.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/responses/AddAccountResponse.kt @@ -1,19 +1,17 @@ package net.dankito.banking.ui.model.responses +import net.dankito.banking.ui.model.* import net.dankito.utils.multiplatform.BigDecimal -import net.dankito.banking.ui.model.Customer -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount open class AddAccountResponse( isSuccessful: Boolean, errorToShowToUser: String?, - val customer: Customer, + val customer: TypedCustomer, val supportsRetrievingTransactionsOfLast90DaysWithoutTan: Boolean = false, - val bookedTransactionsOfLast90Days: Map> = mapOf(), - val unbookedTransactionsOfLast90Days: Map> = mapOf(), - val balances: Map = mapOf(), + val bookedTransactionsOfLast90Days: Map> = mapOf(), + val unbookedTransactionsOfLast90Days: Map> = mapOf(), + val balances: Map = mapOf(), userCancelledAction: Boolean = false ) : BankingClientResponse(isSuccessful, errorToShowToUser, userCancelledAction) { diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/responses/GetTransactionsResponse.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/responses/GetTransactionsResponse.kt index 9ef13977..2ea46a37 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/responses/GetTransactionsResponse.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/model/responses/GetTransactionsResponse.kt @@ -1,15 +1,15 @@ package net.dankito.banking.ui.model.responses import net.dankito.utils.multiplatform.BigDecimal -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.IAccountTransaction +import net.dankito.banking.ui.model.TypedBankAccount open class GetTransactionsResponse( - val bankAccount: BankAccount, + val bankAccount: TypedBankAccount, isSuccessful: Boolean, errorToShowToUser: String?, - val bookedTransactions: List = listOf(), + val bookedTransactions: List = listOf(), val unbookedTransactions: List = listOf(), val balance: BigDecimal? = null, userCancelledAction: Boolean = false, diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt index e256ad08..221f3125 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt @@ -15,6 +15,8 @@ import net.dankito.banking.bankfinder.BankInfo import net.dankito.banking.search.IRemitteeSearcher import net.dankito.banking.search.NoOpRemitteeSearcher import net.dankito.banking.search.Remittee +import net.dankito.banking.ui.model.mapper.DefaultModelCreator +import net.dankito.banking.ui.model.mapper.IModelCreator import net.dankito.banking.ui.model.moneytransfer.ExtractTransferMoneyDataFromPdfResult import net.dankito.banking.ui.model.moneytransfer.ExtractTransferMoneyDataFromPdfResultType import net.dankito.banking.ui.model.parameters.GetTransactionsParameter @@ -30,12 +32,13 @@ import net.dankito.utils.multiplatform.log.LoggerFactory import kotlin.collections.ArrayList -open class BankingPresenter( +open class BankingPresenter constructor( protected val bankingClientCreator: IBankingClientCreator, protected val bankFinder: IBankFinder, protected val dataFolder: File, protected val persister: IBankingPersistence, protected val router: IRouter, + protected val modelCreator: IModelCreator = DefaultModelCreator(), protected val remitteeSearcher: IRemitteeSearcher = NoOpRemitteeSearcher(), protected val bankIconFinder: IBankIconFinder = NoOpBankIconFinder(), protected val textExtractorRegistry: ITextExtractorRegistry = NoOpTextExtractorRegistry(), @@ -56,25 +59,25 @@ open class BankingPresenter( } - protected val bankingClientsForAccounts = mutableMapOf() + protected val bankingClientsForAccounts = mutableMapOf() - protected var selectedBankAccountsField = mutableListOf() + protected var selectedBankAccountsField = mutableListOf() protected var selectedAccountType = SelectedAccountType.AllAccounts protected var saveAccountOnNextEnterTanInvocation = false - protected val accountsChangedListeners = mutableListOf<(List) -> Unit>() + protected val accountsChangedListeners = mutableListOf<(List) -> Unit>() protected val retrievedAccountTransactionsResponseListeners = mutableListOf<(GetTransactionsResponse) -> Unit>() - protected val selectedBankAccountsChangedListeners = mutableListOf<(List) -> Unit>() + protected val selectedBankAccountsChangedListeners = mutableListOf<(List) -> Unit>() protected val callback: BankingClientCallback = object : BankingClientCallback { - override fun enterTan(customer: Customer, tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) { + override fun enterTan(customer: TypedCustomer, tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) { if (saveAccountOnNextEnterTanInvocation) { persistAccount(customer) saveAccountOnNextEnterTanInvocation = false @@ -131,14 +134,14 @@ open class BankingPresenter( } } - protected open fun addClientForAccount(customer: Customer, client: IBankingClient) { + protected open fun addClientForAccount(customer: TypedCustomer, client: IBankingClient) { bankingClientsForAccounts.put(customer, client) } // TODO: move BankInfo out of fints4k - open fun addAccountAsync(bankInfo: BankInfo, customerId: String, pin: String, callback: (AddAccountResponse) -> Unit) { - val customer = Customer(bankInfo.bankCode, customerId, pin, bankInfo.pinTanAddress ?: "", bankInfo.name, bankInfo.bic, "") + open fun addAccountAsync(bankInfo: BankInfo, customerId: String, password: String, callback: (AddAccountResponse) -> Unit) { + val customer = modelCreator.createCustomer(bankInfo.bankCode, customerId, password, bankInfo.pinTanAddress ?: "", bankInfo.name, bankInfo.bic, "") val newClient = bankingClientCreator.createClient(customer, dataFolder, asyncRunner, this.callback) @@ -180,7 +183,7 @@ open class BankingPresenter( } } - protected open fun findIconForBankAsync(customer: Customer) { + protected open fun findIconForBankAsync(customer: TypedCustomer) { bankIconFinder.findIconForBankAsync(customer.bankName) { bankIconUrl -> bankIconUrl?.let { try { @@ -192,7 +195,7 @@ open class BankingPresenter( } } - protected open fun handleFindIconForBankResult(customer: Customer, bankIconUrl: String) { + protected open fun handleFindIconForBankResult(customer: TypedCustomer, bankIconUrl: String) { val bankIconFile = saveBankIconToDisk(customer, bankIconUrl) var iconFilePath = bankIconFile.getAbsolutePath() @@ -208,7 +211,7 @@ open class BankingPresenter( callAccountsChangedListeners() } - protected open fun saveBankIconToDisk(customer: Customer, bankIconUrl: String): File { + protected open fun saveBankIconToDisk(customer: TypedCustomer, bankIconUrl: String): File { val bankIconsDir = File(dataFolder, "bank_icons") bankIconsDir.mkdirs() @@ -239,7 +242,7 @@ open class BankingPresenter( } - open fun deleteAccount(customer: Customer) { + open fun deleteAccount(customer: TypedCustomer) { val wasSelected = isSingleSelectedAccount(customer) or // either account or one of its bank accounts is currently selected (customer.accounts.firstOrNull { isSingleSelectedBankAccount(it) } != null) @@ -264,7 +267,7 @@ open class BankingPresenter( } - open fun fetchAllAccountTransactionsAsync(customer: Customer, + open fun fetchAllAccountTransactionsAsync(customer: TypedCustomer, callback: (GetTransactionsResponse) -> Unit) { customer.accounts.forEach { bankAccount -> @@ -274,13 +277,13 @@ open class BankingPresenter( } } - open fun fetchAllAccountTransactionsAsync(bankAccount: BankAccount, + open fun fetchAllAccountTransactionsAsync(bankAccount: TypedBankAccount, callback: (GetTransactionsResponse) -> Unit) { fetchAccountTransactionsAsync(bankAccount, null, false, callback) } - open fun fetchAccountTransactionsAsync(bankAccount: BankAccount, fromDate: Date?, abortIfTanIsRequired: Boolean = false, + open fun fetchAccountTransactionsAsync(bankAccount: TypedBankAccount, fromDate: Date?, abortIfTanIsRequired: Boolean = false, callback: (GetTransactionsResponse) -> Unit) { getBankingClientForAccount(bankAccount.customer)?.let { client -> @@ -319,7 +322,7 @@ open class BankingPresenter( } } - protected open fun updateBanksAccountsTransactionsAsync(accounts: List, abortIfTanIsRequired: Boolean = false, callback: (GetTransactionsResponse) -> Unit) { + protected open fun updateBanksAccountsTransactionsAsync(accounts: List, abortIfTanIsRequired: Boolean = false, callback: (GetTransactionsResponse) -> Unit) { accounts.forEach { bankAccount -> if (bankAccount.supportsRetrievingAccountTransactions) { updateBankAccountTransactionsAsync(bankAccount, abortIfTanIsRequired, callback) @@ -327,7 +330,7 @@ open class BankingPresenter( } } - protected open fun updateBankAccountTransactionsAsync(bankAccount: BankAccount, abortIfTanIsRequired: Boolean, callback: (GetTransactionsResponse) -> Unit) { + protected open fun updateBankAccountTransactionsAsync(bankAccount: TypedBankAccount, abortIfTanIsRequired: Boolean, callback: (GetTransactionsResponse) -> Unit) { val fromDate = bankAccount.lastRetrievedTransactionsTimestamp?.let { Date(it.millisSinceEpoch - OneDayMillis) } // one day before last received transactions fetchAccountTransactionsAsync(bankAccount, fromDate, abortIfTanIsRequired, callback) @@ -347,7 +350,7 @@ open class BankingPresenter( callRetrievedAccountTransactionsResponseListener(response) } - protected open fun receivedAccountsTransactionChunk(bankAccount: BankAccount, accountTransactionsChunk: List) { + protected open fun receivedAccountsTransactionChunk(bankAccount: TypedBankAccount, accountTransactionsChunk: List) { if (accountTransactionsChunk.isNotEmpty()) { bankAccount.addBookedTransactions(accountTransactionsChunk) @@ -370,7 +373,7 @@ open class BankingPresenter( persistAccountTransactions(bankAccount, response.bookedTransactions, response.unbookedTransactions) } - protected open fun updateBalance(bankAccount: BankAccount, balance: BigDecimal) { + protected open fun updateBalance(bankAccount: TypedBankAccount, balance: BigDecimal) { bankAccount.balance = balance persistAccount(bankAccount.customer) @@ -388,25 +391,25 @@ open class BankingPresenter( } } - open fun accountDisplayIndexUpdated(account: Customer) { + open fun accountDisplayIndexUpdated(account: TypedCustomer) { persistAccount(account) } - open fun accountUpdated(bank: Customer) { + open fun accountUpdated(bank: TypedCustomer) { persistAccount(bank) getBankingClientForAccount(bank)?.dataChanged(bank) } - open fun accountUpdated(account: BankAccount) { + open fun accountUpdated(account: TypedBankAccount) { persistAccount(account.customer) } - protected open fun persistAccount(customer: Customer) { - persister.saveOrUpdateAccount(customer, customers) + protected open fun persistAccount(customer: ICustomer<*, *>) { + persister.saveOrUpdateAccount(customer as TypedCustomer, customers) } - protected open fun persistAccountTransactions(bankAccount: BankAccount, bookedTransactions: List, unbookedTransactions: List) { + protected open fun persistAccountTransactions(bankAccount: TypedBankAccount, bookedTransactions: List, unbookedTransactions: List) { persister.saveOrUpdateAccountTransactions(bankAccount, bookedTransactions) // TODO: someday also persist unbooked transactions @@ -513,11 +516,11 @@ open class BankingPresenter( } - open fun searchSelectedAccountTransactions(query: String): List { + open fun searchSelectedAccountTransactions(query: String): List { return searchAccountTransactions(query, selectedBankAccountsAccountTransactions) } - open fun searchAccountTransactions(query: String, transactions: List): List { + open fun searchAccountTransactions(query: String, transactions: List): List { val queryLowercase = query.trim().toLowerCase() if (queryLowercase.isEmpty()) { @@ -532,7 +535,7 @@ open class BankingPresenter( } - open fun getMessageLogForAccounts(customers: List): List { + open fun getMessageLogForAccounts(customers: List): List { val logEntries = customers.flatMap { getBankingClientForAccount(it)?.messageLogWithoutSensitiveData ?: listOf() } @@ -556,15 +559,15 @@ open class BankingPresenter( } - protected open fun getBankingClientForAccount(customer: Customer): IBankingClient? { - return bankingClientsForAccounts.get(customer) + protected open fun getBankingClientForAccount(customer: ICustomer<*, *>): IBankingClient? { + return bankingClientsForAccounts.get(customer as TypedCustomer) } - open val selectedBankAccounts: List + open val selectedBankAccounts: List get() = ArrayList(selectedBankAccountsField) - open val selectedBankAccountsAccountTransactions: List + open val selectedBankAccountsAccountTransactions: List get() = getAccountTransactionsForBankAccounts(selectedBankAccounts) open val balanceOfSelectedBankAccounts: BigDecimal @@ -574,12 +577,12 @@ open class BankingPresenter( open val areAllAccountSelected: Boolean get() = selectedAccountType == SelectedAccountType.AllAccounts - open fun isSingleSelectedAccount(customer: Customer): Boolean { + open fun isSingleSelectedAccount(customer: TypedCustomer): Boolean { return selectedAccountType == SelectedAccountType.SingleAccount && selectedBankAccountsField.map { it.customer }.toSet().containsExactly(customer) } - open fun isSingleSelectedBankAccount(bankAccount: BankAccount): Boolean { + open fun isSingleSelectedBankAccount(bankAccount: TypedBankAccount): Boolean { return selectedAccountType == SelectedAccountType.SingleBankAccount && selectedBankAccountsField.containsExactly(bankAccount) } @@ -590,39 +593,39 @@ open class BankingPresenter( setSelectedBankAccounts(bankAccounts) } - open fun selectedAccount(customer: Customer) { + open fun selectedAccount(customer: TypedCustomer) { selectedAccountType = SelectedAccountType.SingleAccount setSelectedBankAccounts(customer.accounts) } - open fun selectedBankAccount(bankAccount: BankAccount) { + open fun selectedBankAccount(bankAccount: TypedBankAccount) { selectedAccountType = SelectedAccountType.SingleBankAccount setSelectedBankAccounts(listOf(bankAccount)) } - protected open fun setSelectedBankAccounts(bankAccounts: List) { + protected open fun setSelectedBankAccounts(bankAccounts: List) { this.selectedBankAccountsField = ArrayList(bankAccounts) // make a copy callSelectedBankAccountsChangedListeners(selectedBankAccountsField) } - open val customers: List + open val customers: List get() = bankingClientsForAccounts.keys.toList() - open val bankAccounts: List + open val bankAccounts: List get() = customers.flatMap { it.accounts } - open val allTransactions: List + open val allTransactions: List get() = getAccountTransactionsForBankAccounts(bankAccounts) open val balanceOfAllAccounts: BigDecimal get() = getBalanceForAccounts(customers) - open val bankAccountsSupportingRetrievingAccountTransactions: List + open val bankAccountsSupportingRetrievingAccountTransactions: List get() = bankAccounts.filter { it.supportsRetrievingAccountTransactions } open val hasBankAccountsSupportingRetrievingAccountTransactions: Boolean @@ -631,12 +634,12 @@ open class BankingPresenter( open val doSelectedBankAccountsSupportRetrievingAccountTransactions: Boolean get() = doBankAccountsSupportRetrievingAccountTransactions(selectedBankAccounts) - open fun doBankAccountsSupportRetrievingAccountTransactions(bankAccounts: List): Boolean { + open fun doBankAccountsSupportRetrievingAccountTransactions(bankAccounts: List): Boolean { return bankAccounts.firstOrNull { it.supportsRetrievingAccountTransactions } != null } - open val bankAccountsSupportingRetrievingBalance: List + open val bankAccountsSupportingRetrievingBalance: List get() = bankAccounts.filter { it.supportsRetrievingBalance } open val hasBankAccountsSupportingRetrievingBalance: Boolean @@ -645,12 +648,12 @@ open class BankingPresenter( open val doSelectedBankAccountsSupportRetrievingBalance: Boolean get() = doBankAccountsSupportRetrievingBalance(selectedBankAccounts) - open fun doBankAccountsSupportRetrievingBalance(bankAccounts: List): Boolean { + open fun doBankAccountsSupportRetrievingBalance(bankAccounts: List): Boolean { return bankAccounts.firstOrNull { it.supportsRetrievingBalance } != null } - open val bankAccountsSupportingTransferringMoney: List + open val bankAccountsSupportingTransferringMoney: List get() = bankAccounts.filter { it.supportsTransferringMoney } open val hasBankAccountsSupportTransferringMoney: Boolean @@ -659,16 +662,16 @@ open class BankingPresenter( open val doSelectedBankAccountsSupportTransferringMoney: Boolean get() = doBankAccountsSupportTransferringMoney(selectedBankAccounts) - open fun doBankAccountsSupportTransferringMoney(bankAccounts: List): Boolean { + open fun doBankAccountsSupportTransferringMoney(bankAccounts: List): Boolean { return bankAccounts.firstOrNull { it.supportsTransferringMoney } != null } - protected open fun getAccountTransactionsForBankAccounts(bankAccounts: Collection): List { + protected open fun getAccountTransactionsForBankAccounts(bankAccounts: Collection): List { return bankAccounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate.millisSinceEpoch } // TODO: someday add unbooked transactions } - protected open fun getBalanceForAccounts(customers: Collection): BigDecimal { + protected open fun getBalanceForAccounts(customers: Collection): BigDecimal { return customers.map { it.balance }.sum() } @@ -677,7 +680,7 @@ open class BankingPresenter( } - open fun getTanMediaForTanProcedure(bank: Customer, tanProcedure: TanProcedure): List { + open fun getTanMediaForTanProcedure(bank: TypedCustomer, tanProcedure: TanProcedure): List { if (ChipTanTanProcedures.contains(tanProcedure.type)) { return bank.tanMediaSorted.filterIsInstance() } @@ -719,11 +722,11 @@ open class BankingPresenter( } - open fun addAccountsChangedListener(listener: (List) -> Unit): Boolean { + open fun addAccountsChangedListener(listener: (List) -> Unit): Boolean { return accountsChangedListeners.add(listener) } - open fun removeAccountsChangedListener(listener: (List) -> Unit): Boolean { + open fun removeAccountsChangedListener(listener: (List) -> Unit): Boolean { return accountsChangedListeners.add(listener) } @@ -751,15 +754,15 @@ open class BankingPresenter( } - open fun addSelectedBankAccountsChangedListener(listener: (List) -> Unit): Boolean { + open fun addSelectedBankAccountsChangedListener(listener: (List) -> Unit): Boolean { return selectedBankAccountsChangedListeners.add(listener) } - open fun removeSelectedBankAccountsChangedListener(listener: (List) -> Unit): Boolean { + open fun removeSelectedBankAccountsChangedListener(listener: (List) -> Unit): Boolean { return selectedBankAccountsChangedListeners.add(listener) } - protected open fun callSelectedBankAccountsChangedListeners(selectedBankAccounts: List) { + protected open fun callSelectedBankAccountsChangedListeners(selectedBankAccounts: List) { val selectedBankAccounts = this.selectedBankAccounts ArrayList(selectedBankAccountsChangedListeners).forEach { diff --git a/ui/BankingUiNativeIntegration/src/iosMain/kotlin/net/dankito/banking/BankingPresenterSwift.kt b/ui/BankingUiNativeIntegration/src/iosMain/kotlin/net/dankito/banking/BankingPresenterSwift.kt index b3fb395d..add17df7 100644 --- a/ui/BankingUiNativeIntegration/src/iosMain/kotlin/net/dankito/banking/BankingPresenterSwift.kt +++ b/ui/BankingUiNativeIntegration/src/iosMain/kotlin/net/dankito/banking/BankingPresenterSwift.kt @@ -5,6 +5,7 @@ import net.dankito.banking.fints.webclient.IWebClient import net.dankito.banking.persistence.IBankingPersistence import net.dankito.banking.search.IRemitteeSearcher import net.dankito.banking.ui.IRouter +import net.dankito.banking.ui.model.mapper.DefaultModelCreator import net.dankito.banking.ui.presenter.BankingPresenter import net.dankito.banking.util.* import net.dankito.banking.util.extraction.NoOpInvoiceDataExtractor @@ -14,7 +15,7 @@ import net.dankito.utils.multiplatform.File class BankingPresenterSwift(dataFolder: File, router: IRouter, webClient: IWebClient, persistence: IBankingPersistence, remitteeSearcher: IRemitteeSearcher, bankIconFinder: IBankIconFinder, serializer: ISerializer, asyncRunner: IAsyncRunner) - : BankingPresenter(fints4kBankingClientCreator(serializer, webClient), InMemoryBankFinder(), dataFolder, persistence, router, + : BankingPresenter(fints4kBankingClientCreator(DefaultModelCreator(), serializer, webClient), InMemoryBankFinder(), dataFolder, persistence, router, DefaultModelCreator(), remitteeSearcher, bankIconFinder, NoOpTextExtractorRegistry(), NoOpInvoiceDataExtractor(), serializer, asyncRunner) { } \ No newline at end of file diff --git a/ui/BankingiOSApp/BankingiOSApp.xcodeproj/project.pbxproj b/ui/BankingiOSApp/BankingiOSApp.xcodeproj/project.pbxproj index 53512726..557be7ec 100644 --- a/ui/BankingiOSApp/BankingiOSApp.xcodeproj/project.pbxproj +++ b/ui/BankingiOSApp/BankingiOSApp.xcodeproj/project.pbxproj @@ -35,6 +35,8 @@ 366FA4E224C4ED6C0094F009 /* EnterTanDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366FA4E124C4ED6C0094F009 /* EnterTanDialog.swift */; }; 366FA4E624C6EBF40094F009 /* EnterTanState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 366FA4E524C6EBF40094F009 /* EnterTanState.swift */; }; 3684EB8B2508F6F00001139E /* SearchBarWithLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3684EB8A2508F6F00001139E /* SearchBarWithLabel.swift */; }; + 3684EB8F250B7F3C0001139E /* BankingUiCommon.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3684EB8E250B7F3C0001139E /* BankingUiCommon.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 3684EB90250B7F560001139E /* BankingUiCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3684EB8E250B7F3C0001139E /* BankingUiCommon.framework */; }; 36B8A4482503D12100C15359 /* ProtectAppSettingsDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A4472503D12100C15359 /* ProtectAppSettingsDialog.swift */; }; 36B8A44B2503D1E800C15359 /* BiometricAuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A44A2503D1E800C15359 /* BiometricAuthenticationService.swift */; }; 36B8A44D2503D96D00C15359 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A44C2503D96D00C15359 /* AuthenticationService.swift */; }; @@ -44,8 +46,6 @@ 36B8A4562503E9B200C15359 /* UIAlertBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A4552503E9B200C15359 /* UIAlertBase.swift */; }; 36B8A4582503EEB600C15359 /* ActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36B8A4572503EEB600C15359 /* ActionSheet.swift */; }; 36BCF85424BA0C54005BEC29 /* BankList.json in Resources */ = {isa = PBXBuildFile; fileRef = 36BCF85324BA0C54005BEC29 /* BankList.json */; }; - 36BCF85824BA4274005BEC29 /* BankingUiCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 36BCF85524BA41EE005BEC29 /* BankingUiCommon.framework */; }; - 36BCF85924BA4274005BEC29 /* BankingUiCommon.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 36BCF85524BA41EE005BEC29 /* BankingUiCommon.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 36BCF85E24BA4DA8005BEC29 /* MultiplatformUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 36BCF85D24BA4DA8005BEC29 /* MultiplatformUtils.framework */; }; 36BCF85F24BA4DA8005BEC29 /* MultiplatformUtils.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 36BCF85D24BA4DA8005BEC29 /* MultiplatformUtils.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 36BCF86324BA5097005BEC29 /* SwiftUiRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BCF86224BA5097005BEC29 /* SwiftUiRouter.swift */; }; @@ -138,8 +138,8 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 3684EB8F250B7F3C0001139E /* BankingUiCommon.framework in Embed Frameworks */, 36BCF86A24BA550D005BEC29 /* BankFinder.framework in Embed Frameworks */, - 36BCF85924BA4274005BEC29 /* BankingUiCommon.framework in Embed Frameworks */, 36BCF85F24BA4DA8005BEC29 /* MultiplatformUtils.framework in Embed Frameworks */, 36FC92D124B39C47002B12E9 /* fints4k.framework in Embed Frameworks */, 36BCF87124BB0F8A005BEC29 /* fints4kBankingClient.framework in Embed Frameworks */, @@ -179,6 +179,8 @@ 366FA4E124C4ED6C0094F009 /* EnterTanDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterTanDialog.swift; sourceTree = ""; }; 366FA4E524C6EBF40094F009 /* EnterTanState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterTanState.swift; sourceTree = ""; }; 3684EB8A2508F6F00001139E /* SearchBarWithLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarWithLabel.swift; sourceTree = ""; }; + 3684EB8C250B7F2B0001139E /* BankingUiCommon.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = BankingUiCommon.framework.dSYM; path = "../BankingUiCommon/build/xcode-frameworks/BankingUiCommon.framework.dSYM"; sourceTree = ""; }; + 3684EB8E250B7F3C0001139E /* BankingUiCommon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = BankingUiCommon.framework; path = "../BankingUiCommon/build/xcode-frameworks/BankingUiCommon.framework"; sourceTree = ""; }; 36B8A4472503D12100C15359 /* ProtectAppSettingsDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectAppSettingsDialog.swift; sourceTree = ""; }; 36B8A44A2503D1E800C15359 /* BiometricAuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricAuthenticationService.swift; sourceTree = ""; }; 36B8A44C2503D96D00C15359 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; @@ -264,8 +266,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3684EB90250B7F560001139E /* BankingUiCommon.framework in Frameworks */, 36BCF86924BA550D005BEC29 /* BankFinder.framework in Frameworks */, - 36BCF85824BA4274005BEC29 /* BankingUiCommon.framework in Frameworks */, 36BCF85E24BA4DA8005BEC29 /* MultiplatformUtils.framework in Frameworks */, 36FC92D024B39C47002B12E9 /* fints4k.framework in Frameworks */, 36BCF87024BB0F8A005BEC29 /* fints4kBankingClient.framework in Frameworks */, @@ -419,6 +421,8 @@ 36FC928F24B39A05002B12E9 = { isa = PBXGroup; children = ( + 3684EB8E250B7F3C0001139E /* BankingUiCommon.framework */, + 3684EB8C250B7F2B0001139E /* BankingUiCommon.framework.dSYM */, 36FC929A24B39A05002B12E9 /* BankingiOSApp */, 36FC92B424B39A08002B12E9 /* BankingiOSAppTests */, 36FC92BF24B39A08002B12E9 /* BankingiOSAppUITests */, diff --git a/ui/BankingiOSApp/BankingiOSApp/Preview Content/PreviewData.swift b/ui/BankingiOSApp/BankingiOSApp/Preview Content/PreviewData.swift index cdd65dd4..4346273f 100644 --- a/ui/BankingiOSApp/BankingiOSApp/Preview Content/PreviewData.swift +++ b/ui/BankingiOSApp/BankingiOSApp/Preview Content/PreviewData.swift @@ -15,7 +15,7 @@ let previewImageTanChallenge = ImageTanChallenge(image: TanImage(mimeType: "imag let previewFlickerCodeTanChallenge = FlickerCodeTanChallenge(flickerCode: FlickerCode(challengeHHD_UC: "", parsedDataSet: "", decodingError: nil), messageToShowToUser: "", tanProcedure: previewTanProcedures[0]) -func createPreviewBanks() -> [Customer] { +func createPreviewBanks() -> [ICustomer] { let bank1 = Customer(bankCode: "", customerId: "", password: "", finTsServerAddress: "", bankName: "Abzockbank", bic: "", customerName: "Marieke Musterfrau", userId: "", iconUrl: "", accounts: []) bank1.accounts = [ diff --git a/ui/BankingiOSApp/BankingiOSApp/extensions/Extensions.swift b/ui/BankingiOSApp/BankingiOSApp/extensions/Extensions.swift index 690b7147..a0d1c90e 100644 --- a/ui/BankingiOSApp/BankingiOSApp/extensions/Extensions.swift +++ b/ui/BankingiOSApp/BankingiOSApp/extensions/Extensions.swift @@ -89,7 +89,32 @@ extension AccountTransaction : Identifiable { } -extension Array where Element == Customer { +func ==(lhs: ICustomer, rhs: ICustomer) -> Bool { + return lhs.technicalId == rhs.technicalId +} + +func !=(lhs: ICustomer, rhs: ICustomer) -> Bool { + return lhs.technicalId != rhs.technicalId +} + +func ==(lhs: IBankAccount, rhs: IBankAccount) -> Bool { + return lhs.technicalId == rhs.technicalId +} + +func !=(lhs: IBankAccount, rhs: IBankAccount) -> Bool { + return lhs.technicalId != rhs.technicalId +} + +func ==(lhs: IAccountTransaction, rhs: IAccountTransaction) -> Bool { + return lhs.technicalId == rhs.technicalId +} + +func !=(lhs: IAccountTransaction, rhs: IAccountTransaction) -> Bool { + return lhs.technicalId != rhs.technicalId +} + + +extension Array where Element == ICustomer { func sumBalances() -> CommonBigDecimal { return CommonBigDecimal(decimal_: self.map { $0.balance.decimal }.sum()) @@ -97,7 +122,7 @@ extension Array where Element == Customer { } -extension Array where Element == AccountTransaction { +extension Array where Element == IAccountTransaction { func sumAmounts() -> CommonBigDecimal { return CommonBigDecimal(decimal_: self.map { $0.amount.decimal }.sum()) diff --git a/ui/BankingiOSApp/BankingiOSApp/persistence/AppData.swift b/ui/BankingiOSApp/BankingiOSApp/persistence/AppData.swift index f4d568f5..98fb3c7d 100644 --- a/ui/BankingiOSApp/BankingiOSApp/persistence/AppData.swift +++ b/ui/BankingiOSApp/BankingiOSApp/persistence/AppData.swift @@ -6,8 +6,8 @@ class AppData : ObservableObject { @Inject private var presenter: BankingPresenterSwift - @Published var banks: [Customer] = [] - @Published var banksSorted: [Customer] = [] + @Published var banks: [ICustomer] = [] + @Published var banksSorted: [ICustomer] = [] @Published var hasAtLeastOneAccountBeenAdded: Bool = false @@ -23,7 +23,7 @@ class AppData : ObservableObject { } - private func setFieldsForBanks(_ banks: [Customer]) { + private func setFieldsForBanks(_ banks: [ICustomer]) { self.banks = presenter.customers self.banksSorted = banks.sortedByDisplayIndex() diff --git a/ui/BankingiOSApp/BankingiOSApp/persistence/CoreDataBankingPersistence.swift b/ui/BankingiOSApp/BankingiOSApp/persistence/CoreDataBankingPersistence.swift index 5450aa7c..707798a2 100644 --- a/ui/BankingiOSApp/BankingiOSApp/persistence/CoreDataBankingPersistence.swift +++ b/ui/BankingiOSApp/BankingiOSApp/persistence/CoreDataBankingPersistence.swift @@ -19,7 +19,7 @@ class CoreDataBankingPersistence: IBankingPersistence, IRemitteeSearcher { } - func saveOrUpdateAccount(customer: Customer, allCustomers: [Customer]) { + func saveOrUpdateAccount(customer: ICustomer, allCustomers: [ICustomer]) { do { let mapped = mapper.map(customer, context) @@ -35,7 +35,7 @@ class CoreDataBankingPersistence: IBankingPersistence, IRemitteeSearcher { } } - private func setIds(_ customer: Customer, _ mappedCustomer: PersistedCustomer) { + private func setIds(_ customer: ICustomer, _ mappedCustomer: PersistedCustomer) { customer.technicalId = mappedCustomer.objectIDAsString for account in customer.accounts { @@ -58,7 +58,7 @@ class CoreDataBankingPersistence: IBankingPersistence, IRemitteeSearcher { } - func readPersistedAccounts_() -> [Customer] { + func readPersistedAccounts_() -> [ICustomer] { var customers: [PersistedCustomer] = [] do { @@ -73,7 +73,7 @@ class CoreDataBankingPersistence: IBankingPersistence, IRemitteeSearcher { return customers.map( { mapper.map($0) } ) } - func deleteAccount(customer: Customer, allCustomers: [Customer]) { + func deleteAccount(customer: ICustomer, allCustomers: [ICustomer]) { do { let mapped = mapper.map(customer, context) @@ -85,7 +85,7 @@ class CoreDataBankingPersistence: IBankingPersistence, IRemitteeSearcher { } } - func saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: [AccountTransaction]) { + func saveOrUpdateAccountTransactions(bankAccount: IBankAccount, transactions: [IAccountTransaction]) { if let persistedAccount = context.objectByID(bankAccount.technicalId) as? PersistedBankAccount { for transaction in transactions { if transaction.technicalId.isCoreDataId == false { // TODO: or also update already persisted transactions? @@ -96,7 +96,7 @@ class CoreDataBankingPersistence: IBankingPersistence, IRemitteeSearcher { transaction.technicalId = mappedTransaction.objectIDAsString } catch { - NSLog("Could not save transaction \(transaction.transactionIdentifier) of account \(bankAccount.displayName): \(error)") + NSLog("Could not save transaction \(transaction.buildTransactionIdentifier()) of account \(bankAccount.displayName): \(error)") } } } diff --git a/ui/BankingiOSApp/BankingiOSApp/persistence/EnterTanState.swift b/ui/BankingiOSApp/BankingiOSApp/persistence/EnterTanState.swift index ce00f270..db576d11 100644 --- a/ui/BankingiOSApp/BankingiOSApp/persistence/EnterTanState.swift +++ b/ui/BankingiOSApp/BankingiOSApp/persistence/EnterTanState.swift @@ -6,14 +6,14 @@ class EnterTanState : Identifiable { let id: Foundation.UUID = UUID() - let customer: Customer + let customer: ICustomer let tanChallenge: TanChallenge let callback: (EnterTanResult) -> Void - init(_ customer: Customer, _ tanChallenge: TanChallenge, _ callback: @escaping (EnterTanResult) -> Void) { + init(_ customer: ICustomer, _ tanChallenge: TanChallenge, _ callback: @escaping (EnterTanResult) -> Void) { self.customer = customer self.tanChallenge = tanChallenge self.callback = callback diff --git a/ui/BankingiOSApp/BankingiOSApp/persistence/Mapper.swift b/ui/BankingiOSApp/BankingiOSApp/persistence/Mapper.swift index dc3454ce..c5c8cfec 100644 --- a/ui/BankingiOSApp/BankingiOSApp/persistence/Mapper.swift +++ b/ui/BankingiOSApp/BankingiOSApp/persistence/Mapper.swift @@ -5,7 +5,7 @@ import BankingUiSwift class Mapper { - func map(_ customer: PersistedCustomer) -> Customer { + func map(_ customer: PersistedCustomer) -> ICustomer { let mapped = Customer(bankCode: map(customer.bankCode), customerId: map(customer.customerId), password: map(customer.password), finTsServerAddress: map(customer.finTsServerAddress), bankName: map(customer.bankName), bic: map(customer.bic), customerName: map(customer.customerName), userId: map(customer.userId), iconUrl: customer.iconUrl, accounts: []) mapped.userSetDisplayName = customer.userSetDisplayName @@ -23,7 +23,7 @@ class Mapper { return mapped } - func map(_ customer: Customer, _ context: NSManagedObjectContext) -> PersistedCustomer { + func map(_ customer: ICustomer, _ context: NSManagedObjectContext) -> PersistedCustomer { let mapped = context.objectByID(customer.technicalId) ?? PersistedCustomer(context: context) mapped.bankCode = customer.bankCode @@ -50,11 +50,11 @@ class Mapper { } - func map(_ customer: Customer, _ accounts: [PersistedBankAccount]?) -> [BankAccount] { + func map(_ customer: ICustomer, _ accounts: [PersistedBankAccount]?) -> [IBankAccount] { return accounts?.map( { map(customer, $0) } ) ?? [] } - func map(_ customer: Customer, _ account: PersistedBankAccount) -> BankAccount { + func map(_ customer: ICustomer, _ account: PersistedBankAccount) -> IBankAccount { let mapped = BankAccount(customer: customer, identifier: map(account.identifier), accountHolderName: map(account.accountHolderName), iban: account.iban, subAccountNumber: account.subAccountNumber, customerId: map(account.customerId), balance: map(account.balance), currency: map(account.currency), type: map(account.type), productName: account.productName, accountLimit: account.accountLimit, lastRetrievedTransactionsTimestamp: map(account.lastRetrievedTransactionsTimestamp), supportsRetrievingAccountTransactions: account.supportsRetrievingAccountTransactions, supportsRetrievingBalance: account.supportsRetrievingBalance, supportsTransferringMoney: account.supportsTransferringMoney, supportsInstantPaymentMoneyTransfer: account.supportsInstantPaymentMoneyTransfer, bookedTransactions: [], unbookedTransactions: []) mapped.haveAllTransactionsBeenFetched = account.haveAllTransactionsBeenFetched @@ -69,11 +69,11 @@ class Mapper { return mapped } - func map(_ customer: PersistedCustomer, _ accounts: [BankAccount], _ context: NSManagedObjectContext) -> [PersistedBankAccount] { + func map(_ customer: PersistedCustomer, _ accounts: [IBankAccount], _ context: NSManagedObjectContext) -> [PersistedBankAccount] { return accounts.map( { map(customer, $0, context) } ) } - func map(_ customer: PersistedCustomer, _ account: BankAccount, _ context: NSManagedObjectContext) -> PersistedBankAccount { + func map(_ customer: PersistedCustomer, _ account: IBankAccount, _ context: NSManagedObjectContext) -> PersistedBankAccount { let mapped = context.objectByID(account.technicalId) ?? PersistedBankAccount(context: context) mapped.customer = customer @@ -136,11 +136,11 @@ class Mapper { } - func map(_ account: BankAccount, _ transactions: Set?) -> [AccountTransaction] { + func map(_ account: IBankAccount, _ transactions: Set?) -> [IAccountTransaction] { return transactions?.map( {map(account, $0) } ) ?? [] } - func map(_ account: BankAccount, _ transaction: PersistedAccountTransaction) -> AccountTransaction { + func map(_ account: IBankAccount, _ transaction: PersistedAccountTransaction) -> IAccountTransaction { let mapped = AccountTransaction(bankAccount: account, amount: map(transaction.amount), currency: map(transaction.currency), unparsedUsage: map(transaction.unparsedUsage), bookingDate: map(transaction.bookingDate), otherPartyName: transaction.otherPartyName, otherPartyBankCode: transaction.otherPartyBankCode, otherPartyAccountId: transaction.otherPartyAccountId, bookingText: transaction.bookingText, valueDate: map(transaction.valueDate), statementNumber: Int32(transaction.statementNumber), sequenceNumber: map(transaction.sequenceNumber), openingBalance: map(transaction.openingBalance), closingBalance: map(transaction.closingBalance), endToEndReference: transaction.endToEndReference, customerReference: transaction.customerReference, mandateReference: transaction.mandateReference, creditorIdentifier: transaction.creditorIdentifier, originatorsIdentificationCode: transaction.originatorsIdentificationCode, compensationAmount: transaction.compensationAmount, originalAmount: transaction.originalAmount, sepaUsage: transaction.sepaUsage, deviantOriginator: transaction.deviantOriginator, deviantRecipient: transaction.deviantRecipient, usageWithNoSpecialType: transaction.usageWithNoSpecialType, primaNotaNumber: transaction.primaNotaNumber, textKeySupplement: transaction.textKeySupplement, currencyType: transaction.currencyType, bookingKey: map(transaction.bookingKey), referenceForTheAccountOwner: map(transaction.referenceForTheAccountOwner), referenceOfTheAccountServicingInstitution: transaction.referenceOfTheAccountServicingInstitution, supplementaryDetails: transaction.supplementaryDetails, transactionReferenceNumber: map(transaction.transactionReferenceNumber), relatedReferenceNumber: transaction.relatedReferenceNumber) mapped.technicalId = transaction.objectIDAsString @@ -149,11 +149,11 @@ class Mapper { } - func map(_ account: PersistedBankAccount, _ transactions: [AccountTransaction], _ context: NSManagedObjectContext) -> [PersistedAccountTransaction] { + func map(_ account: PersistedBankAccount, _ transactions: [IAccountTransaction], _ context: NSManagedObjectContext) -> [PersistedAccountTransaction] { return transactions.map( {map(account, $0, context) } ) } - func map(_ account: PersistedBankAccount, _ transaction: AccountTransaction, _ context: NSManagedObjectContext) -> PersistedAccountTransaction { + func map(_ account: PersistedBankAccount, _ transaction: IAccountTransaction, _ context: NSManagedObjectContext) -> PersistedAccountTransaction { let mapped = context.objectByID(transaction.technicalId) ?? PersistedAccountTransaction(context: context) mapped.account = account diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/Messages.swift b/ui/BankingiOSApp/BankingiOSApp/ui/Messages.swift index dc077755..59918658 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/Messages.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/Messages.swift @@ -11,7 +11,7 @@ extension Message { secondaryButton: .cancel()) } - static func createAskUserToDeleteAccountMessage(_ bank: Customer, _ deleteAccount: @escaping (Customer) -> Void) -> Message { + static func createAskUserToDeleteAccountMessage(_ bank: ICustomer, _ deleteAccount: @escaping (ICustomer) -> Void) -> Message { return Message(title: Text("Really delete account '\(bank.displayName)'?"), message: Text("All data for this account will be permanently deleted locally."), primaryButton: .destructive(Text("Delete"), action: { deleteAccount(bank) } ), diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/SwiftUiRouter.swift b/ui/BankingiOSApp/BankingiOSApp/ui/SwiftUiRouter.swift index 0c16df94..842e8aa3 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/SwiftUiRouter.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/SwiftUiRouter.swift @@ -9,7 +9,7 @@ class SwiftUiRouter : IRouter { } - func getTanFromUserFromNonUiThread(customer: Customer, tanChallenge: TanChallenge, presenter: BankingPresenter, callback: @escaping (EnterTanResult) -> Void) { + func getTanFromUserFromNonUiThread(customer: ICustomer, tanChallenge: TanChallenge, presenter: BankingPresenter, callback: @escaping (EnterTanResult) -> Void) { let enterTanState = EnterTanState(customer, tanChallenge, callback) SceneDelegate.navigateToView(EnterTanDialog(enterTanState)) diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AccountTransactionsDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AccountTransactionsDialog.swift index 4b079fd9..c31f15af 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AccountTransactionsDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AccountTransactionsDialog.swift @@ -9,7 +9,7 @@ struct AccountTransactionsDialog: View { private let title: String - private let allTransactions: [AccountTransaction] + private let allTransactions: [IAccountTransaction] private let balanceOfAllTransactions: CommonBigDecimal @@ -20,10 +20,10 @@ struct AccountTransactionsDialog: View { @State private var showFetchAllTransactionsOverlay: Bool - @State private var accountsForWhichNotAllTransactionsHaveBeenFetched: [BankAccount] + @State private var accountsForWhichNotAllTransactionsHaveBeenFetched: [IBankAccount] - @State private var filteredTransactions: [AccountTransaction] + @State private var filteredTransactions: [IAccountTransaction] @State private var balanceOfFilteredTransactions: CommonBigDecimal @@ -45,7 +45,7 @@ struct AccountTransactionsDialog: View { @Inject private var presenter: BankingPresenterSwift - init(allBanks: [Customer]) { + init(allBanks: [ICustomer]) { let allAccounts = allBanks.flatMap { $0.accounts } self.init("All accounts", allAccounts.flatMap { $0.bookedTransactions }, allBanks.sumBalances(), allAccounts.filter { $0.haveAllTransactionsBeenFetched == false }) @@ -53,19 +53,19 @@ struct AccountTransactionsDialog: View { presenter.selectedAllBankAccounts() } - init(bank: Customer) { + init(bank: ICustomer) { self.init(bank.displayName, bank.accounts.flatMap { $0.bookedTransactions }, bank.balance, bank.accounts.filter { $0.haveAllTransactionsBeenFetched == false }) presenter.selectedAccount(customer: bank) } - init(account: BankAccount) { + init(account: IBankAccount) { self.init(account.displayName, account.bookedTransactions, account.balance, account.haveAllTransactionsBeenFetched ? [] : [account]) presenter.selectedBankAccount(bankAccount: account) } - fileprivate init(_ title: String, _ transactions: [AccountTransaction], _ balance: CommonBigDecimal, _ accountsForWhichNotAllTransactionsHaveBeenFetched: [BankAccount] = []) { + fileprivate init(_ title: String, _ transactions: [IAccountTransaction], _ balance: CommonBigDecimal, _ accountsForWhichNotAllTransactionsHaveBeenFetched: [IBankAccount] = []) { self.title = title self.allTransactions = transactions @@ -74,7 +74,7 @@ struct AccountTransactionsDialog: View { self.balanceOfAllTransactions = balance self._balanceOfFilteredTransactions = State(initialValue: balance) - self.areMoreThanOneBanksTransactionsDisplayed = Set(allTransactions.compactMap { $0.bankAccount }.compactMap { $0.customer }).count > 1 + self.areMoreThanOneBanksTransactionsDisplayed = Set(allTransactions.compactMap { $0.bankAccount }.compactMap { $0.customer as! Customer }).count > 1 _accountsForWhichNotAllTransactionsHaveBeenFetched = State(initialValue: accountsForWhichNotAllTransactionsHaveBeenFetched) _haveAllTransactionsBeenFetched = State(initialValue: accountsForWhichNotAllTransactionsHaveBeenFetched.isEmpty) @@ -173,7 +173,7 @@ struct AccountTransactionsDialog: View { } } - private func fetchAllTransactions(_ accounts: [BankAccount]) { + private func fetchAllTransactions(_ accounts: [IBankAccount]) { accounts.forEach { account in presenter.fetchAllAccountTransactionsAsync(bankAccount: account, callback: self.handleGetAllTransactionsResult) } @@ -224,7 +224,7 @@ struct AccountTransactionsDialog: View { struct AccountTransactionsDialog_Previews: PreviewProvider { static var previews: some View { AccountTransactionsDialog(previewBanks[0].displayName, [ - AccountTransaction(bankAccount: previewBanks[0].accounts[0], amount: CommonBigDecimal(double: 1234.56), currency: "€", unparsedUsage: "Usage", bookingDate: CommonDate(year: 2020, month: 5, day: 7), otherPartyName: "Marieke Musterfrau", otherPartyBankCode: nil, otherPartyAccountId: nil, bookingText: "SEPA Ueberweisung", valueDate: CommonDate(year: 2020, month: 5, day: 7)) + AccountTransaction(bankAccount: previewBanks[0].accounts[0] as! BankAccount, amount: CommonBigDecimal(double: 1234.56), currency: "€", unparsedUsage: "Usage", bookingDate: CommonDate(year: 2020, month: 5, day: 7), otherPartyName: "Marieke Musterfrau", otherPartyBankCode: nil, otherPartyAccountId: nil, bookingText: "SEPA Ueberweisung", valueDate: CommonDate(year: 2020, month: 5, day: 7)) ], CommonBigDecimal(double: 84.12)) } diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AccountsDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AccountsDialog.swift index 49a89d2f..97ea8122 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AccountsDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AccountsDialog.swift @@ -22,7 +22,7 @@ struct AccountsDialog: View { Form { AllBanksListItem(banks: data.banks) - ForEach(data.banks.sortedByDisplayIndex()) { bank in + ForEach(data.banks.sortedByDisplayIndex(), id: \.technicalId) { bank in BankListItem(bank: bank) } diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AddAccountDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AddAccountDialog.swift index abb71bdf..336f396d 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AddAccountDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/AddAccountDialog.swift @@ -85,7 +85,7 @@ struct AddAccountDialog: View { isTryingToAddAccount = true UIApplication.hideKeyboard() - presenter.addAccountAsync(bankInfo: bank, customerId: customerId, pin: password) { (response) in + presenter.addAccountAsync(bankInfo: bank, customerId: customerId, password: password) { (response) in self.handleAddAccountResponse(response) } } diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/BankAccountSettingsDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/BankAccountSettingsDialog.swift index d78aaf5a..8d269cd6 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/BankAccountSettingsDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/BankAccountSettingsDialog.swift @@ -9,7 +9,7 @@ struct BankAccountSettingsDialog: View { @Inject private var presenter: BankingPresenterSwift - private let account: BankAccount + private let account: IBankAccount @State private var displayName: String @@ -21,7 +21,7 @@ struct BankAccountSettingsDialog: View { } - init(_ account: BankAccount) { + init(_ account: IBankAccount) { self.account = account _displayName = State(initialValue: account.displayName) @@ -79,8 +79,8 @@ struct BankAccountSettingsDialog: View { private func donePressed() { if hasUnsavedData { account.userSetDisplayName = displayName - - presenter.accountUpdated(account: account.customer) + + presenter.accountUpdated(account: account) } closeDialog() diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/BankSettingsDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/BankSettingsDialog.swift index e19a1046..95ab0195 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/BankSettingsDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/BankSettingsDialog.swift @@ -10,7 +10,7 @@ struct BankSettingsDialog: View { @Inject private var presenter: BankingPresenterSwift - private let bank: Customer + private let bank: ICustomer @State private var displayName: String @@ -19,7 +19,7 @@ struct BankSettingsDialog: View { @State private var selectedTanProcedure: TanProcedure? - @State private var accountsSorted: [BankAccount] + @State private var accountsSorted: [IBankAccount] @State private var askUserToDeleteAccountOrSaveChangesMessage: Message? = nil @@ -32,7 +32,7 @@ struct BankSettingsDialog: View { } - init(_ bank: Customer) { + init(_ bank: ICustomer) { self.bank = bank _displayName = State(initialValue: bank.displayName) @@ -75,7 +75,7 @@ struct BankSettingsDialog: View { } Section(header: SectionHeaderWithRightAlignedEditButton("Accounts")) { - ForEach(accountsSorted) { account in + ForEach(accountsSorted, id: \.technicalId) { account in NavigationLink(destination: LazyView(BankAccountSettingsDialog(account))) { Text(account.displayName) } @@ -102,7 +102,7 @@ struct BankSettingsDialog: View { func reorderAccounts(from source: IndexSet, to destination: Int) { accountsSorted = accountsSorted.reorder(from: source, to: destination) - presenter.accountUpdated(account: bank) + presenter.accountDisplayIndexUpdated(account: bank) } @@ -110,7 +110,7 @@ struct BankSettingsDialog: View { self.askUserToDeleteAccountOrSaveChangesMessage = Message.createAskUserToDeleteAccountMessage(bank, self.deleteAccount) } - func deleteAccount(bank: Customer) { + func deleteAccount(bank: ICustomer) { presenter.deleteAccount(customer: bank) closeDialog() @@ -135,7 +135,7 @@ struct BankSettingsDialog: View { bank.selectedTanProcedure = selectedTanProcedure - presenter.accountUpdated(account: bank) + presenter.accountUpdated(bank: bank) } closeDialog() diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/EnterTanDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/EnterTanDialog.swift index a464e6ee..5d276e14 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/EnterTanDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/EnterTanDialog.swift @@ -11,7 +11,7 @@ struct EnterTanDialog: View { private var tanChallenge: TanChallenge - private var customer: Customer + private var customer: ICustomer private var customersTanMedia: [TanMedium] = [] diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/SettingsDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/SettingsDialog.swift index 2efbf256..b94cd013 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/SettingsDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/SettingsDialog.swift @@ -18,7 +18,7 @@ struct SettingsDialog: View { Form { Section(header: SectionHeaderWithRightAlignedEditButton("Bank Credentials", isEditButtonEnabled: data.hasAtLeastOneAccountBeenAdded), footer: footer) { - ForEach(data.banksSorted) { bank in + ForEach(data.banksSorted, id: \.technicalId) { bank in NavigationLink(destination: LazyView(BankSettingsDialog(bank))) { IconedTitleView(bank) } @@ -68,11 +68,11 @@ struct SettingsDialog: View { } } - func askUserToDeleteAccount(_ bankToDelete: Customer) { + func askUserToDeleteAccount(_ bankToDelete: ICustomer) { self.askToDeleteAccountMessage = Message.createAskUserToDeleteAccountMessage(bankToDelete, self.deleteAccountWithSecurityChecks) } - func deleteAccountWithSecurityChecks(_ bankToDelete: Customer) { + func deleteAccountWithSecurityChecks(_ bankToDelete: ICustomer) { // don't know why but when deleting last bank application crashes if we don't delete bank async DispatchQueue.main.async { if self.presenter.customers.count == 1 { @@ -88,7 +88,7 @@ struct SettingsDialog: View { } } - private func deleteAccount(_ bankToDelete: Customer) { + private func deleteAccount(_ bankToDelete: ICustomer) { self.presenter.deleteAccount(customer: bankToDelete) } diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/TransferMoneyDialog.swift b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/TransferMoneyDialog.swift index 383dc60a..35e2eb07 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/TransferMoneyDialog.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/dialogs/TransferMoneyDialog.swift @@ -10,7 +10,7 @@ struct TransferMoneyDialog: View { private var showAccounts = false - private var accountsSupportingTransferringMoney: [BankAccount] = [] + private var accountsSupportingTransferringMoney: [IBankAccount] = [] @State private var selectedAccountIndex = 0 @@ -52,7 +52,7 @@ struct TransferMoneyDialog: View { @State private var didJustCorrectEnteredValue = false - private var account: BankAccount? { + private var account: IBankAccount? { if (self.selectedAccountIndex < self.accountsSupportingTransferringMoney.count) { return self.accountsSupportingTransferringMoney[selectedAccountIndex] } @@ -411,6 +411,7 @@ struct TransferMoneyDialog: View { self.transferMoneyResponseMessage = Message(message: Text("Could not transfer \(data.amount) \("€") to \(data.creditorName). Error: \(response.errorToShowToUser ?? "").")) } } + } struct TransferMoneyDialog_Previews: PreviewProvider { diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/views/IconedTitleView.swift b/ui/BankingiOSApp/BankingiOSApp/ui/views/IconedTitleView.swift index 071144a7..d69f0751 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/views/IconedTitleView.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/views/IconedTitleView.swift @@ -13,11 +13,11 @@ struct IconedTitleView: View { private var titleFont: Font? - init(_ bank: Customer, titleFont: Font? = nil) { + init(_ bank: ICustomer, titleFont: Font? = nil) { self.init(accountTitle: bank.displayName, iconUrl: bank.iconUrl, defaultIconName: Styles.AccountFallbackIcon, titleFont: titleFont) } - init(_ account: BankAccount, titleFont: Font? = nil) { + init(_ account: IBankAccount, titleFont: Font? = nil) { self.init(accountTitle: account.displayName, iconUrl: account.customer.iconUrl, defaultIconName: Styles.AccountFallbackIcon, titleFont: titleFont) } diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AccountTransactionListItem.swift b/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AccountTransactionListItem.swift index 4249945f..6d34145c 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AccountTransactionListItem.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AccountTransactionListItem.swift @@ -13,7 +13,7 @@ struct AccountTransactionListItem: View { }() - private let transaction: AccountTransaction + private let transaction: IAccountTransaction private let areMoreThanOneBanksTransactionsDisplayed: Bool @@ -21,7 +21,7 @@ struct AccountTransactionListItem: View { @Inject private var presenter: BankingPresenterSwift - init(_ transaction: AccountTransaction, _ areMoreThanOneBanksTransactionsDisplayed: Bool) { + init(_ transaction: IAccountTransaction, _ areMoreThanOneBanksTransactionsDisplayed: Bool) { self.transaction = transaction self.areMoreThanOneBanksTransactionsDisplayed = areMoreThanOneBanksTransactionsDisplayed @@ -82,7 +82,7 @@ struct AccountTransactionListItem: View { } - private func getTransactionLabel(_ transaction: AccountTransaction) -> String { + private func getTransactionLabel(_ transaction: IAccountTransaction) -> String { if transaction.bookingText?.localizedCaseInsensitiveCompare("Bargeldauszahlung") == ComparisonResult.orderedSame { return transaction.bookingText ?? "" } @@ -98,6 +98,6 @@ struct AccountTransactionListItem: View { struct AccountTransactionListItem_Previews: PreviewProvider { static var previews: some View { - AccountTransactionListItem(AccountTransaction(bankAccount: previewBanks[0].accounts[0], otherPartyName: "Marieke Musterfrau", unparsedUsage: "Vielen Dank für Ihre Mühen", amount: CommonBigDecimal(double: 1234.56), valueDate: CommonDate(year: 2020, month: .march, day_: 27), bookingText: "SEPA Überweisung"), false) + AccountTransactionListItem(AccountTransaction(bankAccount: previewBanks[0].accounts[0] as! BankAccount, otherPartyName: "Marieke Musterfrau", unparsedUsage: "Vielen Dank für Ihre Mühen", amount: CommonBigDecimal(double: 1234.56), valueDate: CommonDate(year: 2020, month: .march, day_: 27), bookingText: "SEPA Überweisung"), false) } } diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AllBanksListItem.swift b/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AllBanksListItem.swift index 6b535ef0..da258eba 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AllBanksListItem.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/AllBanksListItem.swift @@ -4,7 +4,7 @@ import BankingUiSwift struct AllBanksListItem: View { - let banks: [Customer] + let banks: [ICustomer] @State private var navigateToAccountTransactionsDialog = false diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/BankAccountListItem.swift b/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/BankAccountListItem.swift index 036c636f..38db6028 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/BankAccountListItem.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/BankAccountListItem.swift @@ -4,7 +4,7 @@ import BankingUiSwift struct BankAccountListItem : View { - let account: BankAccount + let account: IBankAccount @State private var navigateToAccountTransactionsDialog = false diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/BankListItem.swift b/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/BankListItem.swift index d0943af0..1451d9a6 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/BankListItem.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/views/listitems/BankListItem.swift @@ -4,7 +4,7 @@ import BankingUiSwift struct BankListItem : View { - let bank: Customer + let bank: ICustomer @State private var navigateToAccountTransactionsDialog = false @@ -47,7 +47,7 @@ struct BankListItem : View { } - ForEach(bank.accountsSorted) { account in + ForEach(bank.accountsSorted, id: \.technicalId) { account in BankAccountListItem(account: account) } .padding(.leading, Styles.AccountsIconWidth + Styles.DefaultSpaceBetweenIconAndText) @@ -70,7 +70,7 @@ struct BankListItem : View { ).show() } - private func deleteAccount(_ bank: Customer) { + private func deleteAccount(_ bank: ICustomer) { presenter.deleteAccount(customer: bank) } diff --git a/ui/BankingiOSApp/BankingiOSApp/ui/views/tan/TanProcedurePicker.swift b/ui/BankingiOSApp/BankingiOSApp/ui/views/tan/TanProcedurePicker.swift index c88b91bf..d9adcf06 100644 --- a/ui/BankingiOSApp/BankingiOSApp/ui/views/tan/TanProcedurePicker.swift +++ b/ui/BankingiOSApp/BankingiOSApp/ui/views/tan/TanProcedurePicker.swift @@ -4,7 +4,7 @@ import BankingUiSwift struct TanProcedurePicker: View { - private let bank: Customer + private let bank: ICustomer private let selectedTanProcedureChanged: (TanProcedure) -> Void @@ -27,7 +27,7 @@ struct TanProcedurePicker: View { } - init(_ bank: Customer, _ initiallySelectedTanProcedure: TanProcedure? = nil, selectedTanProcedureChanged: @escaping (TanProcedure) -> Void) { + init(_ bank: ICustomer, _ initiallySelectedTanProcedure: TanProcedure? = nil, selectedTanProcedureChanged: @escaping (TanProcedure) -> Void) { self.bank = bank self.selectedTanProcedureChanged = selectedTanProcedureChanged diff --git a/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/fints4kBankingClient.kt b/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/fints4kBankingClient.kt index 96d6e63b..2e99f5d7 100644 --- a/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/fints4kBankingClient.kt +++ b/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/fints4kBankingClient.kt @@ -2,9 +2,6 @@ package net.dankito.banking import net.dankito.banking.ui.BankingClientCallback import net.dankito.banking.ui.IBankingClient -import net.dankito.banking.ui.model.Customer -import net.dankito.banking.ui.model.BankAccount -import net.dankito.banking.ui.model.MessageLogEntry import net.dankito.banking.ui.model.parameters.GetTransactionsParameter import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.responses.AddAccountResponse @@ -20,13 +17,17 @@ import net.dankito.banking.fints.webclient.IWebClient import net.dankito.banking.fints.webclient.KtorWebClient import net.dankito.banking.extensions.toMoney import net.dankito.banking.fints.response.client.FinTsClientResponse +import net.dankito.banking.ui.model.* +import net.dankito.banking.ui.model.MessageLogEntry +import net.dankito.banking.ui.model.mapper.IModelCreator import net.dankito.banking.util.ISerializer import net.dankito.utils.multiplatform.File import net.dankito.utils.multiplatform.log.LoggerFactory open class fints4kBankingClient( - protected val customer: Customer, + protected val customer: TypedCustomer, + protected val modelCreator: IModelCreator, protected val dataFolder: File, protected val serializer: ISerializer, webClient: IWebClient = KtorWebClient(), @@ -42,7 +43,7 @@ open class fints4kBankingClient( } - protected val mapper = net.dankito.banking.mapper.fints4kModelMapper() + protected val mapper = net.dankito.banking.mapper.fints4kModelMapper(modelCreator) protected var didTryToGetAccountDataFromBank = false @@ -74,7 +75,7 @@ open class fints4kBankingClient( } - override fun getTransactionsAsync(bankAccount: BankAccount, parameter: GetTransactionsParameter, callback: (GetTransactionsResponse) -> Unit) { + override fun getTransactionsAsync(bankAccount: TypedBankAccount, parameter: GetTransactionsParameter, callback: (GetTransactionsResponse) -> Unit) { findAccountForBankAccount(bankAccount) { account, errorMessage -> if (account == null) { callback(GetTransactionsResponse(bankAccount, false, errorMessage)) @@ -90,13 +91,13 @@ open class fints4kBankingClient( } protected open fun doGetTransactionsAsync(parameter: net.dankito.banking.fints.model.GetTransactionsParameter, - account: AccountData, bankAccount: BankAccount, callback: (GetTransactionsResponse) -> Unit) { + account: AccountData, bankAccount: TypedBankAccount, callback: (GetTransactionsResponse) -> Unit) { client.getTransactionsAsync(parameter, account) { response -> handleGetTransactionsResponse(bankAccount, response, callback) } } - protected open fun handleGetTransactionsResponse(bankAccount: BankAccount, response: net.dankito.banking.fints.response.client.GetTransactionsResponse, + protected open fun handleGetTransactionsResponse(bankAccount: TypedBankAccount, response: net.dankito.banking.fints.response.client.GetTransactionsResponse, callback: (GetTransactionsResponse) -> Unit) { val mappedResponse = mapper.mapResponse(bankAccount, response) @@ -132,12 +133,12 @@ open class fints4kBankingClient( } - override fun dataChanged(customer: Customer) { + override fun dataChanged(customer: TypedCustomer) { mapper.mapChangesFromUiToClientModel(customer, bank) } - protected open fun findAccountForBankAccount(bankAccount: BankAccount, findAccountResult: (AccountData?, error: String?) -> Unit) { + protected open fun findAccountForBankAccount(bankAccount: TypedBankAccount, findAccountResult: (AccountData?, error: String?) -> Unit) { val mappedAccount = mapper.findAccountForBankAccount(bank, bankAccount) if (mappedAccount != null) { @@ -157,12 +158,12 @@ open class fints4kBankingClient( } - protected open fun restoreDataOrMapFromUiModel(customer: Customer): BankData { + protected open fun restoreDataOrMapFromUiModel(customer: TypedCustomer): BankData { return restoreData(customer) ?: BankData(customer.bankCode, customer.customerId, customer.password, customer.finTsServerAddress, customer.bic, customer.bankName) } - protected open fun restoreData(customer: Customer): BankData? { + protected open fun restoreData(customer: TypedCustomer): BankData? { try { return serializer.deserializeObject(getFints4kClientDataFile(customer.bankCode, customer.customerId), BankData::class) } catch (e: Exception) { diff --git a/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/fints4kBankingClientCreator.kt b/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/fints4kBankingClientCreator.kt index 1fa31c0b..87f061e0 100644 --- a/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/fints4kBankingClientCreator.kt +++ b/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/fints4kBankingClientCreator.kt @@ -5,25 +5,27 @@ import net.dankito.banking.ui.IBankingClient import net.dankito.banking.ui.IBankingClientCreator import net.dankito.banking.fints.webclient.IWebClient import net.dankito.banking.fints.webclient.KtorWebClient -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer +import net.dankito.banking.ui.model.mapper.IModelCreator import net.dankito.banking.util.IAsyncRunner import net.dankito.banking.util.ISerializer import net.dankito.utils.multiplatform.File open class fints4kBankingClientCreator( + protected val modelCreator: IModelCreator, protected val serializer: ISerializer, protected val webClient: IWebClient = KtorWebClient() ) : IBankingClientCreator { override fun createClient( - customer: Customer, + customer: TypedCustomer, dataFolder: File, asyncRunner: IAsyncRunner, callback: BankingClientCallback ): IBankingClient { - return fints4kBankingClient(customer, dataFolder, serializer, webClient, callback = callback) + return fints4kBankingClient(customer, modelCreator, dataFolder, serializer, webClient, callback = callback) } } \ No newline at end of file diff --git a/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/mapper/fints4kModelMapper.kt b/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/mapper/fints4kModelMapper.kt index 157bf45b..75cec669 100644 --- a/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/mapper/fints4kModelMapper.kt +++ b/ui/fints4kBankingClient/src/commonMain/kotlin/net/dankito/banking/mapper/fints4kModelMapper.kt @@ -11,24 +11,24 @@ import net.dankito.banking.fints.messages.datenelemente.implementierte.signatur. import net.dankito.banking.fints.model.AccountData import net.dankito.banking.fints.model.AccountFeature import net.dankito.banking.fints.model.BankData -import net.dankito.banking.fints.model.Money import net.dankito.banking.fints.response.client.FinTsClientResponse import net.dankito.banking.fints.response.segments.AccountType +import net.dankito.banking.ui.model.mapper.IModelCreator -open class fints4kModelMapper { +open class fints4kModelMapper(protected val modelCreator: IModelCreator) { open fun mapResponse(response: FinTsClientResponse): BankingClientResponse { return BankingClientResponse(response.isSuccessful, mapErrorToShowToUser(response), response.userCancelledAction) } - open fun mapResponse(customer: Customer, response: net.dankito.banking.fints.response.client.AddAccountResponse): AddAccountResponse { + open fun mapResponse(customer: TypedCustomer, response: net.dankito.banking.fints.response.client.AddAccountResponse): AddAccountResponse { val balances = response.balances.mapKeys { findMatchingBankAccount(customer, it.key) }.filter { it.key != null } - .mapValues { (it.value as Money).toBigDecimal() } as Map + .mapValues { it.value.toBigDecimal() } as Map val bookedTransactions = response.bookedTransactions.associateBy { it.account } - val mappedBookedTransactions = mutableMapOf>() + val mappedBookedTransactions = mutableMapOf>() bookedTransactions.keys.forEach { accountData -> findMatchingBankAccount(customer, accountData)?.let { bankAccount -> @@ -44,7 +44,7 @@ open class fints4kModelMapper { response.userCancelledAction) } - open fun mapResponse(bankAccount: BankAccount, response: net.dankito.banking.fints.response.client.GetTransactionsResponse): GetTransactionsResponse { + open fun mapResponse(bankAccount: TypedBankAccount, response: net.dankito.banking.fints.response.client.GetTransactionsResponse): GetTransactionsResponse { return GetTransactionsResponse(bankAccount, response.isSuccessful, mapErrorToShowToUser(response), mapTransactions(bankAccount, response.bookedTransactions), @@ -60,7 +60,7 @@ open class fints4kModelMapper { } - open fun mapBank(customer: Customer, bank: BankData) { + open fun mapBank(customer: TypedCustomer, bank: BankData) { customer.bankCode = bank.bankCode customer.customerId = bank.customerId customer.password = bank.pin @@ -78,7 +78,7 @@ open class fints4kModelMapper { /** * In UI only customerId, password, (bankCode,) and selected TAN procedure can be set */ - open fun mapChangesFromUiToClientModel(customer: Customer, bank: BankData) { + open fun mapChangesFromUiToClientModel(customer: TypedCustomer, bank: BankData) { bank.customerId = customer.customerId bank.pin = customer.password @@ -105,9 +105,10 @@ open class fints4kModelMapper { } - open fun mapBankAccounts(customer: Customer, accountData: List): List { + open fun mapBankAccounts(customer: TypedCustomer, accountData: List): List { return accountData.mapIndexed { index, account -> - val mappedAccount = customer.accounts.firstOrNull { it.identifier == account.accountIdentifier } ?: BankAccount(customer, account.productName, account.accountIdentifier) + val mappedAccount = customer.accounts.firstOrNull { it.identifier == account.accountIdentifier } + ?: modelCreator.createBankAccount(customer, account.productName, account.accountIdentifier) mapBankAccount(mappedAccount, account) @@ -117,7 +118,7 @@ open class fints4kModelMapper { } } - open fun mapBankAccount(account: BankAccount, accountData: AccountData) { + open fun mapBankAccount(account: TypedBankAccount, accountData: AccountData) { account.identifier = accountData.accountIdentifier account.accountHolderName = accountData.accountHolderName account.iban = accountData.iban @@ -182,11 +183,11 @@ open class fints4kModelMapper { } } - open fun findAccountForBankAccount(bank: BankData, bankAccount: BankAccount): AccountData? { + open fun findAccountForBankAccount(bank: BankData, bankAccount: TypedBankAccount): AccountData? { return bank.accounts.firstOrNull { bankAccount.identifier == it.accountIdentifier } } - open fun findMatchingBankAccount(customer: Customer, accountData: AccountData): BankAccount? { + open fun findMatchingBankAccount(customer: TypedCustomer, accountData: AccountData): TypedBankAccount? { return customer.accounts.firstOrNull { it.identifier == accountData.accountIdentifier } } @@ -195,12 +196,12 @@ open class fints4kModelMapper { } - open fun mapTransactions(bankAccount: BankAccount, transactions: List): List { + open fun mapTransactions(bankAccount: TypedBankAccount, transactions: List): List { return transactions.map { mapTransaction(bankAccount, it) } } - open fun mapTransaction(bankAccount: BankAccount, transaction: net.dankito.banking.fints.model.AccountTransaction): AccountTransaction { - return AccountTransaction( + open fun mapTransaction(bankAccount: TypedBankAccount, transaction: net.dankito.banking.fints.model.AccountTransaction): IAccountTransaction { + return modelCreator.createTransaction( bankAccount, transaction.amount.toBigDecimal(), transaction.amount.currency.code, @@ -242,7 +243,7 @@ open class fints4kModelMapper { } - open fun updateTanMediaAndProcedures(account: Customer, bank: BankData) { + open fun updateTanMediaAndProcedures(account: TypedCustomer, bank: BankData) { account.supportedTanProcedures = mapTanProcedures(bank.tanProceduresAvailableForUser) if (bank.isTanProcedureSelected) { @@ -292,7 +293,7 @@ open class fints4kModelMapper { } } - protected open fun findMappedTanProcedure(customer: Customer, tanProcedure: net.dankito.banking.fints.model.TanProcedure): TanProcedure? { + protected open fun findMappedTanProcedure(customer: TypedCustomer, tanProcedure: net.dankito.banking.fints.model.TanProcedure): TanProcedure? { return customer.supportedTanProcedures.firstOrNull { it.bankInternalProcedureCode == tanProcedure.securityFunction.code } } diff --git a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/HbciCallback.kt b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/HbciCallback.kt index 60e7bde3..3260b618 100644 --- a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/HbciCallback.kt +++ b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/HbciCallback.kt @@ -2,13 +2,12 @@ package net.dankito.banking import net.dankito.banking.model.AccountCredentials import net.dankito.banking.ui.BankingClientCallback -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer import net.dankito.banking.ui.model.tan.FlickerCodeTanChallenge import net.dankito.banking.ui.model.tan.ImageTanChallenge import net.dankito.banking.ui.model.tan.TanChallenge import net.dankito.banking.ui.model.tan.TanImage import net.dankito.banking.util.hbci4jModelMapper -import net.dankito.utils.multiplatform.Date import org.kapott.hbci.callback.AbstractHBCICallback import org.kapott.hbci.callback.HBCICallback import org.kapott.hbci.manager.HBCIUtils @@ -24,7 +23,7 @@ import org.slf4j.LoggerFactory */ open class HbciCallback( protected val credentials: AccountCredentials, - protected val customer: Customer, + protected val customer: TypedCustomer, protected val mapper: hbci4jModelMapper, protected val callback: BankingClientCallback ) : AbstractHBCICallback() { @@ -37,7 +36,7 @@ open class HbciCallback( /** * @see org.kapott.hbci.callback.HBCICallback.log */ - override fun log(msg: String, level: Int, date: Date, trace: StackTraceElement) { + override fun log(msg: String?, level: Int, date: java.util.Date?, trace: StackTraceElement?) { // Ausgabe von Log-Meldungen bei Bedarf when (level) { HBCIUtils.LOG_ERR -> log.error(msg) @@ -172,7 +171,7 @@ open class HbciCallback( } - open fun getTanFromUser(customer: Customer, messageToShowToUser: String, challengeHHD_UC: String): String? { + open fun getTanFromUser(customer: TypedCustomer, messageToShowToUser: String, challengeHHD_UC: String): String? { // Wenn per "retData" Daten uebergeben wurden, dann enthalten diese // den fuer chipTAN optisch zu verwendenden Flickercode. // Falls nicht, ist es eine TAN-Abfrage, fuer die keine weiteren diff --git a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClient.kt b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClient.kt index 4ce97fbc..419b4640 100644 --- a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClient.kt +++ b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClient.kt @@ -5,6 +5,7 @@ import net.dankito.banking.model.ConnectResult import net.dankito.banking.ui.BankingClientCallback import net.dankito.banking.ui.IBankingClient import net.dankito.banking.ui.model.* +import net.dankito.banking.ui.model.mapper.IModelCreator import net.dankito.banking.ui.model.parameters.GetTransactionsParameter import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.responses.AddAccountResponse @@ -32,7 +33,8 @@ import java.util.* open class hbci4jBankingClient( - protected val customer: Customer, + protected val customer: TypedCustomer, + modelCreator: IModelCreator, protected val dataFolder: File, protected val asyncRunner: IAsyncRunner = ThreadPoolAsyncRunner(ThreadPool()), protected val callback: BankingClientCallback @@ -51,9 +53,9 @@ open class hbci4jBankingClient( protected val credentials = AccountCredentials(customer) - protected val mapper = hbci4jModelMapper() + protected val mapper = hbci4jModelMapper(modelCreator) - protected val accountTransactionMapper = AccountTransactionMapper() + protected val accountTransactionMapper = AccountTransactionMapper(modelCreator) override val messageLogWithoutSensitiveData: List = listOf() // TODO: implement @@ -86,11 +88,11 @@ open class hbci4jBankingClient( return AddAccountResponse(false, connection.error.getInnerExceptionMessage(), customer) } - protected open fun tryToRetrieveAccountTransactionsForAddedAccounts(customer: Customer): AddAccountResponse { + protected open fun tryToRetrieveAccountTransactionsForAddedAccounts(customer: TypedCustomer): AddAccountResponse { val transactionsOfLast90DaysResponses = mutableListOf() - val balances = mutableMapOf() - val bookedTransactions = mutableMapOf>() - val unbookedTransactions = mutableMapOf>() + val balances = mutableMapOf() + val bookedTransactions = mutableMapOf>() + val unbookedTransactions = mutableMapOf>() customer.accounts.forEach { bankAccount -> if (bankAccount.supportsRetrievingAccountTransactions) { @@ -117,7 +119,7 @@ open class hbci4jBankingClient( * So we simply try to retrieve at accounting entries of the last 90 days and see if a second factor is required * or not. */ - open fun getTransactionsOfLast90DaysAsync(bankAccount: BankAccount, callback: (GetTransactionsResponse) -> Unit) { + open fun getTransactionsOfLast90DaysAsync(bankAccount: TypedBankAccount, callback: (GetTransactionsResponse) -> Unit) { asyncRunner.runAsync { callback(getTransactionsOfLast90Days(bankAccount)) } @@ -130,19 +132,19 @@ open class hbci4jBankingClient( * So we simply try to retrieve at accounting entries of the last 90 days and see if a second factor is required * or not. */ - open fun getTransactionsOfLast90Days(bankAccount: BankAccount): GetTransactionsResponse { + open fun getTransactionsOfLast90Days(bankAccount: TypedBankAccount): GetTransactionsResponse { val ninetyDaysAgo = Date(Date().time - NinetyDaysInMilliseconds) return getTransactions(bankAccount, GetTransactionsParameter(bankAccount.supportsRetrievingBalance, ninetyDaysAgo)) // TODO: implement abortIfTanIsRequired } - override fun getTransactionsAsync(bankAccount: BankAccount, parameter: GetTransactionsParameter, callback: (GetTransactionsResponse) -> Unit) { + override fun getTransactionsAsync(bankAccount: TypedBankAccount, parameter: GetTransactionsParameter, callback: (GetTransactionsResponse) -> Unit) { asyncRunner.runAsync { callback(getTransactions(bankAccount, parameter)) } } - protected open fun getTransactions(bankAccount: BankAccount, parameter: GetTransactionsParameter): GetTransactionsResponse { + protected open fun getTransactions(bankAccount: TypedBankAccount, parameter: GetTransactionsParameter): GetTransactionsResponse { val connection = connect() connection.handle?.let { handle -> @@ -195,7 +197,7 @@ open class hbci4jBankingClient( return GetTransactionsResponse(bankAccount, false, connection.error.getInnerExceptionMessage()) } - protected open fun executeJobsForGetAccountingEntries(handle: HBCIHandler, bankAccount: BankAccount, parameter: GetTransactionsParameter): Triple { + protected open fun executeJobsForGetAccountingEntries(handle: HBCIHandler, bankAccount: TypedBankAccount, parameter: GetTransactionsParameter): Triple { val konto = mapper.mapToKonto(bankAccount) // 1. Auftrag fuer das Abrufen des Saldos erzeugen @@ -271,7 +273,7 @@ open class hbci4jBankingClient( } - override fun dataChanged(customer: Customer) { + override fun dataChanged(customer: TypedCustomer) { if (customer.bankCode != credentials.bankCode || customer.customerId != credentials.customerId || customer.password != credentials.password) { getPassportFile(credentials).delete() } diff --git a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClientCreator.kt b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClientCreator.kt index 43872618..42a9d8e6 100644 --- a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClientCreator.kt +++ b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClientCreator.kt @@ -3,21 +3,24 @@ package net.dankito.banking import net.dankito.banking.ui.BankingClientCallback import net.dankito.banking.ui.IBankingClient import net.dankito.banking.ui.IBankingClientCreator -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer +import net.dankito.banking.ui.model.mapper.IModelCreator import net.dankito.banking.util.IAsyncRunner import net.dankito.utils.multiplatform.File -open class hbci4jBankingClientCreator : IBankingClientCreator { +open class hbci4jBankingClientCreator( + protected val modelCreator: IModelCreator +) : IBankingClientCreator { override fun createClient( - customer: Customer, + customer: TypedCustomer, dataFolder: File, asyncRunner: IAsyncRunner, callback: BankingClientCallback ): IBankingClient { - return hbci4jBankingClient(customer, dataFolder, asyncRunner, callback) + return hbci4jBankingClient(customer, modelCreator, dataFolder, asyncRunner, callback) } } \ No newline at end of file diff --git a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/AccountCredentials.kt b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/AccountCredentials.kt index 88b21eb6..af697e7b 100644 --- a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/AccountCredentials.kt +++ b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/AccountCredentials.kt @@ -1,6 +1,6 @@ package net.dankito.banking.model -import net.dankito.banking.ui.model.Customer +import net.dankito.banking.ui.model.TypedCustomer open class AccountCredentials( @@ -9,6 +9,6 @@ open class AccountCredentials( var password: String ) { - constructor(bank: Customer) : this(bank.bankCode, bank.customerId, bank.password) + constructor(bank: TypedCustomer) : this(bank.bankCode, bank.customerId, bank.password) } \ No newline at end of file diff --git a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/AccountTransactionMapper.kt b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/AccountTransactionMapper.kt index 95fe4e8a..1827eea8 100644 --- a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/AccountTransactionMapper.kt +++ b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/AccountTransactionMapper.kt @@ -1,8 +1,10 @@ package net.dankito.banking.util import net.dankito.banking.fints.transactions.mt940.Mt940Parser -import net.dankito.banking.ui.model.AccountTransaction -import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.TypedBankAccount +import net.dankito.banking.ui.model.IAccountTransaction +import net.dankito.banking.ui.model.mapper.IModelCreator +import net.dankito.utils.multiplatform.toDate import org.kapott.hbci.GV_Result.GVRKUms import org.kapott.hbci.structures.Value import org.slf4j.LoggerFactory @@ -10,7 +12,9 @@ import java.math.BigDecimal import java.text.SimpleDateFormat -open class AccountTransactionMapper { +open class AccountTransactionMapper( + protected val modelCreator: IModelCreator +) { companion object { protected val DateStartString = "DATUM " @@ -24,8 +28,8 @@ open class AccountTransactionMapper { } - open fun mapAccountTransactions(bankAccount: BankAccount, result: GVRKUms): List { - val entries = mutableListOf() + open fun mapAccountTransactions(bankAccount: TypedBankAccount, result: GVRKUms): List { + val entries = mutableListOf() result.dataPerDay.forEach { btag -> btag.lines.forEach { transaction -> @@ -38,17 +42,17 @@ open class AccountTransactionMapper { return entries } - protected open fun mapAccountingEntry(bankAccount: BankAccount, btag: GVRKUms.BTag, transaction: GVRKUms.UmsLine): AccountTransaction { + protected open fun mapAccountingEntry(bankAccount: TypedBankAccount, btag: GVRKUms.BTag, transaction: GVRKUms.UmsLine): IAccountTransaction { val unparsedUsage = transaction.usage.joinToString("") val parsedUsage = Mt940Parser().getUsageParts(unparsedUsage) val statementAndMaySequenceNumber = btag.counter.split('/') - val result = AccountTransaction(bankAccount, - mapValue(transaction.value), transaction.value.curr, unparsedUsage, transaction.bdate, + return modelCreator.createTransaction(bankAccount, + mapValue(transaction.value), transaction.value.curr, unparsedUsage, transaction.bdate.toDate(), transaction.other.name + (transaction.other.name2 ?: ""), transaction.other.bic ?: transaction.other.blz, transaction.other.iban ?: transaction.other.number, - transaction.text, transaction.valuta, + transaction.text, transaction.valuta.toDate(), statementAndMaySequenceNumber[0].toInt(), if (statementAndMaySequenceNumber.size > 1) statementAndMaySequenceNumber[1].toInt() else null, mapValue(btag.start.value), mapValue(btag.end.value), @@ -76,12 +80,10 @@ open class AccountTransactionMapper { "", null ) - - return result } - protected open fun mapValue(value: Value): BigDecimal { - return BigDecimal.valueOf(value.longValue).divide(BigDecimal.valueOf(100)) + protected open fun mapValue(value: Value): net.dankito.utils.multiplatform.BigDecimal { + return net.dankito.utils.multiplatform.BigDecimal(BigDecimal.valueOf(value.longValue).divide(BigDecimal.valueOf(100)).toPlainString()) } } \ No newline at end of file diff --git a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/hbci4jModelMapper.kt b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/hbci4jModelMapper.kt index bc5557cc..314177aa 100644 --- a/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/hbci4jModelMapper.kt +++ b/ui/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/hbci4jModelMapper.kt @@ -1,10 +1,9 @@ package net.dankito.banking.util +import net.dankito.banking.ui.model.* +import net.dankito.banking.ui.model.mapper.IModelCreator import net.dankito.utils.multiplatform.BigDecimal import net.dankito.utils.multiplatform.toBigDecimal -import net.dankito.banking.ui.model.Customer -import net.dankito.banking.ui.model.BankAccount -import net.dankito.banking.ui.model.BankAccountType import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.tan.TanProcedureType import org.kapott.hbci.passport.HBCIPassport @@ -12,9 +11,11 @@ import org.kapott.hbci.structures.Konto import org.kapott.hbci.structures.Value -open class hbci4jModelMapper { +open class hbci4jModelMapper( + protected val modelCreator: IModelCreator +) { - open fun mapToKonto(bankAccount: BankAccount): Konto { + open fun mapToKonto(bankAccount: TypedBankAccount): Konto { val customer = bankAccount.customer val konto = Konto("DE", customer.bankCode, bankAccount.identifier, bankAccount.subAccountNumber) @@ -41,15 +42,26 @@ open class hbci4jModelMapper { } - open fun mapBankAccounts(customer: Customer, bankAccounts: Array, passport: HBCIPassport): List { + open fun mapBankAccounts(customer: TypedCustomer, bankAccounts: Array, passport: HBCIPassport): List { return bankAccounts.map { bankAccount -> val iban = if (bankAccount.iban.isNullOrBlank() == false) bankAccount.iban else passport.upd.getProperty("KInfo.iban") ?: "" - BankAccount(customer, bankAccount.number, - if (bankAccount.name2.isNullOrBlank() == false) bankAccount.name + " " + bankAccount.name2 else bankAccount.name, - iban, bankAccount.subnumber, bankAccount.customerid, BigDecimal.Zero, bankAccount.curr, mapBankAccountType(bankAccount), - null, bankAccount.limit?.value?.let { mapValue(it).toString() }, null, - bankAccount.allowedGVs.contains("HKKAZ"), bankAccount.allowedGVs.contains("HKSAL"), bankAccount.allowedGVs.contains("HKCCS")) + val result = modelCreator.createBankAccount(customer, bankAccount.number, + if (bankAccount.name2.isNullOrBlank() == false) bankAccount.name + " " + bankAccount.name2 else bankAccount.name) + + result.iban = iban + result.subAccountNumber = bankAccount.subnumber + result.customerId = bankAccount.customerid + + result.currency = bankAccount.curr + result.type = mapBankAccountType(bankAccount) + result.accountLimit = bankAccount.limit?.value?.let { mapValue(it).toString() } + + result.supportsRetrievingBalance = bankAccount.allowedGVs.contains("HKSAL") + result.supportsRetrievingAccountTransactions = bankAccount.allowedGVs.contains("HKKAZ") + result.supportsRetrievingBalance = bankAccount.allowedGVs.contains("HKCCS") + + result } }