V prototypu počítáme s jedním uživatelem a jeho jedním bankovním účtem.
- Zobrazení výpisu plateb (http://localhost:3000)
- fetchnutí z API
- ruční kategorizace libovolné platby (možnou přepsání automatického zařazení)
- rozdělní platby na několik dílčích plateb
- možnost filtrování výpisu
- podle data
- příchozí/odchozí
- Správa pravidel (http://localhost:3000/rules)
- CRUD uživatelem vytvořených
- v základu obsahuje několik předdefinovaných na ukázku
- Statistiky (http://localhost:3000/stats)
- Příjmy a výdaje za časové období
- Příjmy a výdaje v jednotlivých kategoriích
- grafické znázornění
- negrafické znázornění (tabulky)
https://mois-banking.herokuapp.com/v1/api-docs
Data pro tuto aplikaci jsou dostupná skrz AccountID = 6669
Pro bezpečnost nedržíme údaje o připojení k DB v souboru application.properties, ale přímo v konfiguraci projektu v IDEI – před spuštěním projektu je potřeba otevřít Run/Debug Configurations a zadat následující údaje. Pozor na přebytečné mezery na konci.
MORIA_DB_HOST = AdresaVasiDB
MORIA_DB_USERNAME = VaseUsername
MORIA_DB_PASSWORD = VaseHeslo
Pro vývoj je lepší vytvořit si v Run/Debug Configurations ještě druhou spouštěcí konfiguraci
A přepsat tyto parametry:
spring.datasource.url = jdbc:mysql://localhost:3306/moria?useTimezone=true&serverTimezone=GMT%2B8
spring.datasource.username = root
spring.datasource.password = // nechat pole prázdné (pozor na mezery)
V application.properties máme aktuálně nastaveno:
spring.jpa.hibernate.ddl-auto = create
To funguje tak, že při každém spuštění se původní DB dropne a vytvoří znovu. Schéma vytvoří automaticky Hibernate a naplní se daty ze souboru import.sql (defaultní název požadovaný Springem).
Ve Springu je možnost použít:
- buď soubory schema.sql + data.sql na vytvoření a naplnění DB
- nebo vytvoření nechat na Hibernate a použít jen import.sql na naplnění
Všechny možnosti práce s DB:
-
create – Hibernate first drops existing tables, then creates new tables
-
update – the object model created based on the mappings (annotations or XML) is compared with the existing schema, and then Hibernate updates the schema according to the diff. It never deletes the existing tables or columns even if they are no more required by the application
create-drop – similar to create, with the addition that Hibernate will drop the database after all operations are completed. Typically used for unit testing.
validate – Hibernate only validates whether the tables and columns exist, otherwise it throws an exception.
none – this value effectively turns off the DDL generation
Mock data jsou přehledně definovaná a okomentovaná v excelovském souboru 20_radku_testovacich_transakci.xlsx, ve kterém se daté generuje JSON pro posílání přes API a v případě nedostupnosti API jsou stejná data případně k dipozici i přes import.sql.
Aktuálně se v mock datech nalézá:
21 zkušebních transakcí z toho jednu rozdělenou na další 4 dílčí transakce.
- 1 účet uživatele (ID = 6668)
- 2 platební karty
- příjmy celkem 24 000, výdaje celkem 23 000
- 4 typy plateb
- PAYMENT_HOME – převody mezi účty
- CARD – platba kartou
- CASH – výběr hotovosti
- MORTGAGE – splátka hypotéky
- některé platby kategorizované, jiné ne
- 6x kategorizováno ručně, 15x nekategorizováno
5 zkušebních pravidel:
- Každé by mělo zkategorizovat po jedné platbě, s výjímkou pravidla na nákupy v Bille, které by mělo zkategorizovat platby dvě
Jaký přesně je očekávaný průběh kategorizace?
Názvy kategorií neukládáme do DB, protože je to s ohledem na lokalizaci čistější řešení.
S kategoriemi pracujeme podle jejich ID:
- Kategorie odchozích plateb….trojciferné číslo
- Kategorie příchozích plateb….dvouciferné číslo
Všechny kategorie jsou napevno definované v enumu. Ani v prototypu, ani v plné verzi NEpočítáme s tím, že by si uživatelé tvořili vlastní. Výčet kategorií v prototypu není úplný, chybí například vše týkající se investiční sféry. V plné verzi by byly tyto a další kategorie doplněny. A taky pro jednoduchost NEřešíme podkategorie (např. u jízdného dělení na vlak / bus / letadlo).
Kvůli problémům s duplicitami mají kategorie příchozích plateb předponu I_nazevKategorie (I podle slova incoming).
CZ název | EN název | ID |
Jídlo | Food | 111 |
Alkohol | Alcohol | 112 |
Oděvy a móda | Apparel & fashion | 113 |
Nábytek a vybavení domácnosti | Home equipment | 114 |
Pohonné hmoty | Fuel | 115 |
Energie | Utilities | 116 |
Tabák a tisk | Tobacco & press | 117 |
Mobil, TV, internet apod. | Phone / TV / Internet / Etc. | 118 |
Jízdné | Fare | 119 |
Nájem | Rent | 120 |
Nemovitosti | Real estate | 121 |
Sport a volný čas | Sport & leisure | 122 |
Zdraví a krása | Health & beauty | 123 |
Zábava | Entertainment | 124 |
Cestování a ubytování | Travelling & accommodation | 125 |
Elektronika | Electronics | 126 |
Hazard a sázky (loterie, losy, dostihy) | Gambling | 127 |
Splátka půjčky/hypotéky | Loans & mortgages | 128 |
Pojištění | Insurance | 129 |
Dary a dárky | Gifts | 130 |
Jiné | Other | 131 |
Plat / mzda | Salary / wage | 11 |
Důchod | Pension | 12 |
Sociální podpora | Social assistance | 13 |
Podnikání | Business | 14 |
Hazard | Gambling | 15 |
Nájem | Rent | 16 |
Splátka půjčky | Loans | 17 |
Kapesné | Pocket money | 18 |
Dary | Gifts | 19 |
Jiné | Other | 20 |
Nekategorizováno | Uncategorized | 0 |
Bacha – je potřeba vnímat rozdíl mezi kategoriemi Jiné VS Nekategorizováno
- Jiné = platby, co do ostatních kategorií nezapadají, nebo je tam uživatel nechtěl zahrnout.
- Nekategorizováno = platby, které zatím nejsou nikam zařazeny. Typicky nové platby, které teprve čekají na zařazení nebo tvorbu pravidla.
- číslo účtu odesílatele/příjemce (ve tvaru předčíslí, číslo účtu, kod banky)
- směr platby (příchozí/odchozí)
- typ platby (karetní, převod, splátka hypotéky, ...)
- čas provedení platby (ne zaúčtování)
- částku:
- částku pevnou/od
- částku do
- zprávu (fuzzy způsob)
- jméno protistrany
- konst. symbol
- var. symbol
- specifický symbol
- číslo karty
Myšlenka:
- Mám ruleset a v něm jednotlivá pravidla pro zařazení platby do kategorie
- Mám transakci s nějakými parametry. Těch může být víc nebo míň.
- Metoda pro každou transakci projde všechny rulesety a zvyšováním skóre vyjádří míru shody (vhodnosti použití) rulesetu a transakce.
- Výstupy ukládá do TreeMapy ve tvaru <score, categoryID>
- Nejvyšší skóre (tzn. nejvyšší podobnost pravidla s platbou) vyhrává a algoritmus vrátí ID odpovídající kategorie.
Algoritmus:
Pro každou nezařazenou transakci
- Projdi všechny rulesety, který máme uložený
- nejprve ošetři, že na PŘÍCHOZÍ transakci lze aplikovat jen pravidlo určené právě pro PŘÍCHOZÍ transakce, stejně tak se ošetří směr ODCHOZÍ
- pro každý ruleset projdi jednotlivý parametry pravidla a nastav skóre na nulu
- porovnej hodnotu z pravidla s hodnotou z transakce a případně přičti určitou hodnotu* ke skóre
- Jak se porovnávají jednotlivá pravidla?
- Před každým porovnáním se nejdříve zkontroluje, jestli je parametr v pravidle i platbě vyplněn (není null)
- zpráva
- podle směru platby je ošetřeno zpracování buďto payee (přichozí platba) nebo payer (odchozí)
- porovnává se, jestli je celá zpráva v pravidlu obsažena ve zprávě transakce
- pokud není, tak ještě probíhá porovnání pomocí Fuzzy** knihovny
- pokud ani tento způsob nenajde shodu, skóre se nemění
- pokud je nalezena shoda, ke skóre se přičte 1
- číslo účtu
- porovná se, jestli prefix i číslo účtu z transakce obsahuje hodnoty z pravidla
- pokud ano, ke skóre se přičte 2, což nám zajistí, že transakce se kategorizuje výhradně podle tohoto pravidla
- pokud ne, tak se nastaví isBankAccountFilledButDifferent na true a celkové skóre se vynuluje.
- jméno protistrany
- podle typu platby je ošetřeno zpracování buďto MerchantName (platba kartou) nebo PartyDescription (ostatní typy plateb)
- porovnává, jestli je partyName pravidla obsaženo v parametrech transakce
- pokud ano, přičte se ke skóre 1
- pokud ne, skóre zůstává nezměněno
- čas zaúčtování transakce
- k porovnání je použita třída LocalTime
- porovnává, jestli je se čas zaúčtování transakce vyskytuje v rozmezí definovaném v pravidlu
- pokud ano, ke skóre se přičte 1
- pokud ne, skóre se nemění
- částka transakce
- pokud v pravidle není vyplněno částka od a částka do, tak se skore nemění (není podle čeho porovnávat)
- pokud je v pravidle vyplněno pouze částka do a zároveň je transactionValue menší než částka do, tak se ke skóre přičte 1
- pokud je v pravidle vyplněno pouze částka od a zároveň je transactionValue větší než částka od, tak se ke skóre přičte 1
- pokud jsou v pravidle vyplněny částka od i částka do a zároveň se transactionValue vyskytuje v tomto rozmezí. přičte se ke skóre 1
- else skóre se nemění
- číslo karty
- porovnává, jestli číslo karty stejné jako číslo karty definované v pravidle
- pokud je stejné, ke skóre se přičte 2
- pokud není, skóre se nemění
- Konstantní, variabilní, specifický symbol
- u všech tří symbolů se používá stejný princip
- porovnává se, jestli je symbol z pravidla obsažen v symbolu transakce
- pokud ano, ke skóre se přičte 2
- pokud ne, skóre se nemění
- typ transakce
- porovnává se, jestli je typ z pravidla obsažen v typu transakce
- pokud je typ jediným parametrem rulesetu
- pokud ano, ke skóre se přičte 1 (má větší váhu)
- pokud ne, ke skóre se přičte 0,5
- zpráva
- po vyhodnocení všech parametrů pravidla proběhne vyhodnocení podmínky isBankAccountFilledButDifferent * pokud je true, tak se skore nastavi na 0 * pokud je false, neděje se nic a skore zůstává stejné
- Ke každému rulesetu, jehož skóre je >=1 (našla se aspoň 1 shoda s nějakou transakcí) se uloží hodnota skóre do tree mapy ve tvaru <score, categoryID>
- Vyber hodnotu nejvyššího skóre v mapě a podle ní ulož k transakci kategorii
** Poznámka: FuzzySearch.partialRatio nám vrátí hodnotu <0,100>, která udává míru shody dvou Stringů. V algoritmu máme určený threshold 75. Pokud nám Fuzzy vrátí hodnotu větší než 75, považujeme Stringy za shodné.
Aktuálně se provede kategorizace po najetí na adresu http://localhost:8080/plsCategorize, kde se zkategorizují všechny transakce bez categoryID. Další možností je http://localhost:8080/categorize bez výpisu plateb.
Při vytvoření nového pravidla se všechny transakce překategorizují. Při smazání pravidla také.
Kategorie jdou přiřadit i ručně ve FE, takto přiřazené kategorie už nejdou žádným pravidlem přepsat.
Poznamka: Pri jakykoliv praci s cislem uctu, at uz ve smeru FE --> BE nebo BE --> FE, posilejte nejdriv cislo uctu pres jednu z getNormalizedAccountNumber() metodu. Je moznost tam poslat cely objekt TransactionPartyAccount nebo 3 stringy s prefixem, cislem uctu a kodem banky.
Tim zajistime, ze cisla uctu budou vzdycky ve stejnem standardizovanem formatu.
Ve výpisu v konzoli (první dva sloupce by měly odpovídat výpisu)
Platba | Kategorie | Použité pravidlo |
ab02 | 111 | Jméno protistrany |
ab04 | 128 | Typ (hypotéka) |
ab07 | 17 | Číslo účtu |
ab12 | 11 | Číslo účtu + částka v rozmezí |
ab15 | 111 | Jméno protistrany |
ab17 | 124 | Jméno protistrany |
Okometrickou metodou:
-
Grafy by měly vypadat takhle
Na datech:
- kategorizují se pouze platby bez přiřazené kategorie (tzn. napoprvé celkem 15 plateb)
- platbám s už přiřazenou kategorií se kategorie nezmění
-
jeden příjem spadne do kategorie 17 (Loans)
-
jeden příjem spadne do kategorie 11 (Salary / wage)
(zbývající tři příjmy zůstanou nekategorizovány)
-
jeden výdaj spadne do kategorie 128 (Loans & mortgages)
-
jeden výdaj spadne do kategorie 124 (Entertainment)
-
Přes testy:
- Aktuálně test zrcadlí rulesety i transakce z import.sql a je tedy schopem ověřit to víceméně stejně.
spočívá v tom, že máme transakci, kterou chceme dále specifikovat (např. že z výběru 2000 Kč z bankomatu jsme použili 1000 Kč na jídlo, 500 Kč na alkohol a dalších 500 na hazard).
Myšlenka
- Nerozdělená transakce má hodnotu v položce amount.
Pokud má dojít k rozdělení transakce je hodnota amount vynullovana a hodnota presunuta do polozky originValue - Dílčí transakce (ta která rozděluje parentí) vezme všechny parametry parent transakce, kromě id, category id, amountu. Zárověň je ji vyplněna položka parent_id, kterou převezme od parent transakce (id).
- Zároveň je pro FE vytvořena zbytková transakce (pro statistiky), která vezme rozdíl mezi parent original_value a zbylými dílčími transakcemi. Od ostatních transakci se liší tím, že má category_id 0 (Uncategorized).
- V případě smazání dílčí transakce dojde k prepočítání zbytkové transakce (pokud již žádná dílčí transakce kromě zbytkové nezbývá, nastaví se hodnota zbytkové transakce na 0)
Na FE uživatel zadá rozdělení transakce a na na backend pošle id rodičovské transakce, částku dílčí transakce a její kategorii. V případě smazání přichází z FE pouze id transakce pro smazání.
- Přijde nám objekt z FE (parent_id, amount, category_id)
- Nalezeneme parenti transakci a vytvoříme její kopii
- Pokud se jedná o první rozdělení transakce (poznáme podle toho, že má ještě vyplněné value a ne original value) přehodíme parentí transakci zmíněné value do original value
- Nové (dílčí) transakci pak nasetuju hodnoty příchozí z FE, zbytek je stejný jako od parenta
- Vyhledám všechny dílčí transakce patřící parentí transakci a podivam se, jestli nemá nějaká stejné category_id, jako ten objekt, který přišel z FE
- Pokud ne, uložím novou transakci do databáze
- Pokud ano, updatuju amount dane dílčí transakce o součet jejiho amountu a amountu z FE
- Zavolám metodu updateRestOfAmountToOriginalValue
- Pro zadanou dílčí transakci zjistí rozdíl mezi parent transakci (originalValue) a dílčími transakcemi (součet amountu)
- Pokud ještě neexistuje zbytková transakce, vytvoří kopii parent transakce a nasetuje ji předchozí vypočítaný rozdíl, jako value, category_id na 0 (protože je to ten neznámý zbytek - nutné pro statistiky na FE) a parent_id parent transakce (a uloží do db)
- pokud transakce už existuje, je ji pouze aktualizována amount částka
- Na FE vratím parent transakci s listem dílčích transakcí (pro zobrazení na FE)
- Z FE přijde id dílčí transakce pro smazání
- Vytáhnu si z databáze dílčí transakci a smažu ji (servisa potřebuje celou transakci)
- Najdu všechny zbývající dílčí transakce (podle parent_id) a spočítám, kolik jich zbylo
- Pokud je transakce poslední (tzn. mám tam jenom zbytkovou transakci), tak smažu transakci a parent transakci přehodím original_value zpátky do amountu
- V případě, že transakce poslední není, zavolám metodu updateRestOfAmountToOriginalValue, která aktualizuje hodnotu zbytkové transakce (viz. řádky výše)
- Na FE vracíme opět parent transakci
GET | rules/getAll | vytáhne všechna pravidla z databáze a pošle na FE |
POST | rules/create | z FE přijde pravidlo a to se uloží do databáze tak jak přijde |
POST | rules/remove | z FE přijde seznam IDček a podle nich se vymažou daná pravidla z databáze |
PUT | rules/update | z FE přijde upravené pravidlo a to se aktualizuje v databázi |
GET | /transaction | vytáhne všechny transakce z DB, transformuje na dto a pošle na FE |
PUT | /transaction/update | z FE přijde id transakce a jmeno kategorie a podle toho se updatuje transakce v DB, navíc se daná transakce nastaví jako manullyUpdated |
POST | /transaction/split | z FE přijde id parent transakce, id kategorie a částka (BigDecimal), podle toho rozdělí transakci na dílčí transakce (pokud ještě nebyla vytvořena žádná dílčí transakce, vloží krom příchozí dílčí transakce ještě dopočítávací dílčí transakci (pro FE) |
POST | /transaction/removeSplit | Smaže dílčí transakci. Jako parametr bere celou transakci pro smazání |
TransactionsToDtoMapper (přesunuto do Utils) využitý v loadAllTransactions() použivá tuhle logiku:
-
Parametr partyDescription vzdycky obsahuje nazev obchodnika nebo název protistrany nebo cislo uctu protistrany. nemelo by se stat ze bude prazdny nebo null.
V pripade chybejicich informaci bude obsahovat “Unknown”.
-
Regex (regulární výraz) použitý pro získání jednotného tvaru čísla bankovního účtu vymaže nuly před samotným číslem účtu, resp. je nahradí “” (prázdným stringem)
Vysvětlení zástupných znaků:
^ - začne na začátku stringu
0 - hledaný řetězec
+ - 1 nebo více výskytů hledaného řetězce
(?!$) - "negative lookahead" (pokud nejsou na konci řádku)
GET | /fetchTransactions | stáhne všechny transakce z API podle konfigurace v BankingAPIConfiguration interfacu |
GET | /saveTransactions | stáhne vsechny transakce z API a nove (podle ID) uloží do DB |
GET | /saveTransactions/{fromDate}/{toDate} | stáhne transakce z API mezi danymi datumy a nove (podle ID) uloží do DB
Zavolá se kategorizace na stáhnuté transakce |
Testuju více (provázaných) aplikačních vrstev, (často) potřebuju přístup k databázi, typicky testy servisních tříd
RulesetServiceTest
- používá testovací databázi (H2, bylo potřeba definovat v pom.xml)
- by default se databáze rollbackne po každém provedeném testu, takže by nemělo dojít ke konfliktům (pokud třeba v rámci více testů pracuju se stejnými daty)
Testuju jen jednu “vrstvu” (logiku), pokud v průběhu testu potřebuju data, která reálně tahám z databáze, tak si je namockuju (prostě počítám s tím, že data z db mi přijdou, jak očekávám) - v unit testu tedy nepoužívám databázi
CategoryScorerTest
- Princip: vytvořím testovací data, které podstrčím CategoryScoreru a následně assertuju, jestli se mi vrátí očekáváné categoryID
- testovací data vytvořena přes TestUtils
**Poznámka: když budete chtít spustit integrační test, nastavte **spring.jpa.hibernate.ddl-auto=update, kdyby tam bylo create, tak se hibernate snaží najebat import.sql do té testovací databáze a hází to chybové hlášky do konzole. Reálně to tomu testu (asi?) nevadí, ale dělá to akorát matoucí bordel v konzoli :)
- Příchozí/odchozí tuzemsko: číslo a jmeno účtu, platební symboly, zpráva, částka, datum, čas)
- Platba kartou: název obchodníka, částka, datum, čas)
- Výběry: bankomat, částka, datum, čas
Na adrese http://194.182.88.14:8082/ běží Jenkins pro deployment
Joby:
- build-server
- naklonuje moria repo z GH
- spusti maven build
- zpristupni .jar soubor
- build-client
- naklonuje moria repo z GH
- nainstaluje npm
- spusti npm build
- zabali build adresar do tar.gz
- zpristupni archiv
- deploy-server
- Sestavi maven balik (build-server job)
- Posle balik pres SSH na server
- Spusti server jako systemd service
- Server je pak dostupny na http://194.182.88.14:8083
- deploy-client
- Sestavi npm balik (build-client job)
- Posle blaik pres SSH na server
- Spusti deployment
- Xicht je dostupny na http://194.182.88.14:8080