explanation.md
46,987 bytes
| 1 | # SplitApp — Reisikulude haldamise rakendus |
|---|---|
| 2 | |
| 3 | ## Ülevaade |
| 4 | |
| 5 | SplitApp on ASP.NET Core 10.0 veebirakendus grupireisi kulude jagamiseks ja haldamiseks. Rakendus võimaldab kasutajatel luua reise, kutsuda sõpru, lisada kulusid paindliku jagamisega, hallata eelarvet, teha küsitlusi, pidada soovinimekirja ja arveldada võlgu optimeeritud algoritmiga. Rakendus kasutab **Clean Architecture**'t: sõltuvused liiguvad sissepoole Domain-i, interfejsid elavad Domain-kihis (`App.Domain/Contracts/`) ja `App.DAL.EF` on "plugin", mis neid implementeerib. `App.BLL` sõltub ainult Domain-abstraktsioonidest ja WebApp kontrollerid kasutavad ainult BLL teenuseid — mitte kunagi `IAppUnitOfWork`-i ega repositore otse. |
| 6 | |
| 7 | Projekt on tehtud TalTech kursuse "Web Applications with C#" **Personal Project — Phase 1** raames. |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## 0. Nõuete täitmine (Assignment requirements) |
| 12 | |
| 13 | Phase 1 ülesande järgi peavad olemas olema järgmised asjad. Alljärgnevas tabelis on iga nõue, selle täitmise staatus ja konkreetne asukoht koodis. |
| 14 | |
| 15 | | # | Nõue | Staatus | Kus näha | |
| 16 | |---|------|---------|----------| |
| 17 | | 1 | Domeenikujundus: min 10 mõtestatud entiteeti | ✅ **16 entiteeti** | [App.Domain/](SplitApp/App.Domain/) — Trip, Expense, ExpenseSplit, BudgetCategory, Currency, TripParticipant, TripPoll, TripPollOption, TripPollVote, TripWishlistItem, TripWishlistVote, SplitPreset, SplitPresetMember, TripInvitation, SettlementPlan, SettlementPayment + 3 Identity entiteeti | |
| 18 | | 2 | REST API + versioneerimine + avalikud DTO-d | ✅ | [WebApp/ApiControllers/](SplitApp/WebApp/ApiControllers/), `[ApiVersion("1.0")]`, marsruut `/api/v{version:apiVersion}/[controller]`, DTO-d [App.DTO/v1/](SplitApp/App.DTO/v1/) | |
| 19 | | 3 | Swagger | ✅ | `/swagger` endpoint, [ConfigureSwaggerOptions.cs](SplitApp/WebApp/ConfigureSwaggerOptions.cs) — Bearer auth + versioneerimine integreeritud | |
| 20 | | 4 | Autentimine (JWT) | ✅ | [Program.cs](SplitApp/WebApp/Program.cs) JWT Bearer konfiguratsioon; [AccountController.cs](SplitApp/WebApp/ApiControllers/AccountController.cs) — register, login, refreshtoken, logout | |
| 21 | | 5 | Kliendi UX (MVC, scaffolded) | ✅ | [WebApp/Controllers/](SplitApp/WebApp/Controllers/) — 8 MVC kontrollerit; standardsed CRUD-vaated, tõestavad domeeni toimimise | |
| 22 | | 6 | Admin UX (MVC, Area, kaitstud, kujundatud, ViewModelid, **no ViewBag/ViewData**) | ✅ | [WebApp/Areas/Admin/](SplitApp/WebApp/Areas/Admin/) — 13 kontrollerit, `[Authorize(Roles = "admin")]`, oma sidebar-layout, admin.css, custom Dashboard. **0 ViewData/ViewBag kasutust** — grep kontrollitud | |
| 23 | | 7 | UI tõlked (i18n, .resx) | ✅ EN + ET | [App.Resources/](SplitApp/App.Resources/) — Shared.resx, Common.resx, Domain/* (Trip, Expense, Currency, BudgetCategory jne) | |
| 24 | | 8 | Andmebaasi tõlked (LangStr) | ✅ | [Base.Domain/LangStr.cs](SplitApp/Base.Domain/LangStr.cs); kasutatud `Currency.Name` ja `BudgetCategory.Name` väljadel — JSON-ina PostgreSQL-is | |
| 25 | | 9 | IDOR kaitse (kasutaja näeb ainult oma andmeid) | ✅ | Kaitse elab BLL teenustes: iga meetod, mis puudutab reisi-andmeid, võtab `Guid userId` ja kontrollib osaleja/organiseerija staatust sees. WebApp kontrollerid ei pääse UoW-le üldse — kontrolli vahele jätta on võimatu | |
| 26 | | 10 | CI/CD deploy (äpp + DB) | ✅ | `.gitlab-ci.yml` — `docker compose up --build` `main` harul; `Dockerfile` multi-stage; `docker-compose.yml` — app + PostgreSQL 16 + persistent volume + automaatne migreerimine ja seeding | |
| 27 | | 11 | Admin pole lihtsalt scaffold — "designed, nice, good to use" | ✅ | Eraldi admin layout (sidebar + topbar), [admin.css](SplitApp/WebApp/wwwroot/css/admin.css) — metric cards, status badges, timeline feed, empty states; Dashboard custom statistikaga (Top Active Trips, Biggest Expenses, User Activity 7d/30d, Top Active Users, Activity Feed) | |
| 28 | |
| 29 | **Lisaks** (pole nõutud, aga olemas): |
| 30 | - Repository pattern ([Base.Contracts/IBaseRepository.cs](SplitApp/Base.Contracts/IBaseRepository.cs) + entiteedispetsiifilised repositoryd) |
| 31 | - Unit of Work pattern (`IAppUnitOfWork`) |
| 32 | - Service layer äriloogika jaoks (`SettlementService` greedy algoritm, `ExpenseService` 4 split-meetodit, `InvitationService` token-põhised kutsed, `PollService` hääletamise toggle) |
| 33 | - Manuaalsed DTO mapperid (ei kasuta AutoMapper-it) |
| 34 | |
| 35 | --- |
| 36 | |
| 37 | ## 1. Arhitektuur — Clean Architecture |
| 38 | |
| 39 | Projekt kasutab **Clean Architecture**'t. Sõltuvused liiguvad **sissepoole** (Dependency Inversion Principle): kõik kihid sõltuvad Domain-ist (või millestki sisemisest), mitte väliskihtidest. |
| 40 | |
| 41 | ### Kursuse loengu (architecture1) võtmelause |
| 42 | |
| 43 | *"The entire difference between N-tier and Clean Architecture is who owns the interfaces."* — *"Move `IPersonRepository` from DAL into Domain, and your dependency arrow flips."* |
| 44 | |
| 45 | Meie projekt järgib seda põhimõtet: |
| 46 | - **`IAppUnitOfWork` ja 10 repository-interfejsi elavad [App.Domain/Contracts/](SplitApp/App.Domain/Contracts/)-is**, mitte DAL-is. |
| 47 | - **`App.DAL.EF`** (infrastructure) implementeerib neid interfejse — on "plugin" Domain-kihi peal. |
| 48 | - **`App.BLL.csproj`** ei viita enam `App.DAL.EF`-ile — ainult `App.Domain`-ile ja `App.DTO`-le. |
| 49 | - **WebApp kontrollerid** (kõik 23: client MVC + API + Admin) **ei kasuta** `IAppUnitOfWork`-i ega repositore otse — ainult BLL teenuseid. |
| 50 | |
| 51 | ### Sõltuvuse graaf |
| 52 | |
| 53 | ``` |
| 54 | Base.Contracts (IBaseEntity, IBaseRepository, IUnitOfWork) |
| 55 | ▲ |
| 56 | │ |
| 57 | Base.Domain (BaseEntity, LangStr) |
| 58 | ▲ |
| 59 | │ |
| 60 | App.Domain ← SEES |
| 61 | + Contracts/ (IAppUnitOfWork, 10 × I*Repository) |
| 62 | ▲ |
| 63 | ┌──────┴──────┐ |
| 64 | │ │ |
| 65 | App.DAL.EF App.DTO |
| 66 | (implem.) │ |
| 67 | │ |
| 68 | App.BLL (Services — sõltub AINULT Domain+DTO) |
| 69 | ▲ |
| 70 | │ |
| 71 | WebApp (Controllers — kasutavad BLL teenuseid) |
| 72 | (DAL viide ainult Program.cs DI jaoks |
| 73 | → AddDalServices() extension method) |
| 74 | ``` |
| 75 | |
| 76 | **Clean-i võtmeomadused** (verifitseeritavad): |
| 77 | - `grep "using App.DAL.EF" App.BLL/` → **0 tulemust** |
| 78 | - `grep "IAppUnitOfWork\|_uow\." WebApp/Controllers/ WebApp/ApiControllers/ WebApp/Areas/` → **0 tulemust** |
| 79 | - `App.BLL.csproj` refs: ainult `App.Domain`, `App.DTO` |
| 80 | - `App.DAL.EF` viitab Domain-i interfejsidele ja implementeerib neid (`AppUnitOfWork : IAppUnitOfWork` Domain-ist) |
| 81 | |
| 82 | ### Projekti kihid |
| 83 | |
| 84 | ``` |
| 85 | Base.Contracts ← Geneerilised liidesed (IBaseEntity, IBaseRepository, IUnitOfWork) |
| 86 | Base.Domain ← Base-entiteedid (BaseEntity, LangStr) |
| 87 | Base.Helpers ← JWT genereerimine/valideerimine |
| 88 | App.Domain ← 16 domeeni entiteeti + 8 enum-i + Contracts/ (IAppUnitOfWork + I*Repository) |
| 89 | App.DAL.EF ← AppDbContext, AppUnitOfWork, repository-implementatsioonid, migratsioonid, |
| 90 | ServiceCollectionExtensions.AddDalServices() |
| 91 | App.DTO ← DTO-d (v1/) + manuaalsed Mapper klassid (ei kasuta AutoMapper-it) |
| 92 | App.BLL ← Application-kiht, teenused koos äriloogikaga |
| 93 | Services/ — Trip, Expense, Settlement, Invitation, Poll, |
| 94 | BudgetCategory, Wishlist, SplitPreset |
| 95 | Services/Admin/ — 12 admin-teenust (üks iga admin-sektsiooni jaoks) |
| 96 | App.Resources ← .resx tõlkefailid (EN + ET) |
| 97 | WebApp ← MVC + API + Admin kontrollerid, vaated, ViewModelid, Program.cs |
| 98 | ``` |
| 99 | |
| 100 | ### Miks Clean Architecture? |
| 101 | |
| 102 | - **Dependency Inversion** — WebApp kontrollerid sõltuvad BLL liidestest, BLL sõltub Domain liidestest. Kui tahame DAL-i vahetada (nt MongoDB), asendame ainult `App.DAL.EF` — ülejäänud projekt ei muutu. |
| 103 | - **Testitavus** — iga teenust ja repositoryd saab mockida, sest kõik sõltuvused on interface'id ja elavad sees (Domain-is). BLL-i teste saab kirjutada ilma päris andmebaasita. |
| 104 | - **Separation of Concerns** — äriloogika (settlement algoritm, expense splitting, token-kutsed) elab ainult BLL-is; andmeligipääs ainult DAL-is; HTTP-mure ainult WebApp-is. |
| 105 | - **IDOR kaitse tsentraliseeritud** — iga BLL teenuse meetod, mis puudutab reisi-andmeid, võtab vastu `Guid userId` parameetri ja kontrollib osaleja/organiseerija staatust teenuse sees. Kontroller ei saa kogemata kontrolli vahele jätta. |
| 106 | |
| 107 | ### `App.DAL.EF` kui plugin |
| 108 | |
| 109 | Kuigi DAL sõltub Domain-ist (järgides Clean reeglit), jääb DAL "väliseks" Domain-i suhtes. Program.cs kutsub `builder.Services.AddDalServices(connectionString)` — üks composition-root rida — mis registreerib `AppDbContext` ja `IAppUnitOfWork → AppUnitOfWork`. WebApp kontrollerid pole teadlikud DAL-i implementatsioonist. See on Clean-i "plugin architecture" omadus. |
| 110 | |
| 111 | --- |
| 112 | |
| 113 | ## 2. Repository ja Unit of Work muster |
| 114 | |
| 115 | ### Repository muster |
| 116 | |
| 117 | Iga entiteet on kättesaadav läbi repository liidese. Geneerilised operatsioonid on defineeritud `IBaseRepository<TEntity>` liideses (`Base.Contracts`): |
| 118 | |
| 119 | - `GetAllAsync()` — kõik kirjed (`Task<IEnumerable<TEntity>>`) |
| 120 | - `GetByIdAsync(Guid id)` — üks kirje ID järgi (`Task<TEntity?>`) |
| 121 | - `Add(entity)` — lisa uus (sünkroonne — tegelik salvestus toimub `SaveChangesAsync()` kaudu) |
| 122 | - `Update(entity)` — uuenda olemasolevat (sünkroonne) |
| 123 | - `RemoveAsync(Guid id)` — kustuta (`Task<TEntity?>`) |
| 124 | - `ExistsAsync(Guid id)` — kontrolli olemasolu (`Task<bool>`) |
| 125 | |
| 126 | Geneerilise baasrepository (`BaseRepository<TEntity>`) peal on ehitatud **entiteedispetsiifilised repositoryd** oma päringumeetoditega. Näiteks `TripRepository`: |
| 127 | |
| 128 | - `GetUserTripsAsync(Guid userId)` — kasutaja reisid koos valuuta ja osalejatega (Include) |
| 129 | - `GetByIdWithDetailsAsync(Guid id)` — reis kõigi seostega |
| 130 | - `RemoveAsync(Guid id)` — **override**, mis teostab kaskaadse kustutamise õiges järjekorras (lapselapsed → lapsed → reis), kuna kõik võõrvõtmed on `DeleteBehavior.Restrict` |
| 131 | |
| 132 | `TripParticipantRepository` on arhitektuuri selgroog — sisaldab `IsParticipantAsync()` ja `IsOrganizerAsync()` meetodeid, mida kasutavad KÕIK kontrollerid autoriseerimiseks. Enne refaktoreerimist oli see loogika kopeeritud igasse kontrollerisse eraldi. |
| 133 | |
| 134 | ### Unit of Work muster |
| 135 | |
| 136 | `IAppUnitOfWork` koondab kõik repositoryd üheks liideseks ja haldab `SaveChangesAsync()` kutsumist: |
| 137 | |
| 138 | ``` |
| 139 | IAppUnitOfWork |
| 140 | ├── Trips (ITripRepository) |
| 141 | ├── Expenses (IExpenseRepository) |
| 142 | ├── TripParticipants (ITripParticipantRepository) |
| 143 | ├── TripInvitations (ITripInvitationRepository) |
| 144 | ├── SettlementPlans (ISettlementPlanRepository) |
| 145 | ├── TripPolls (ITripPollRepository) |
| 146 | ├── TripWishlistItems (ITripWishlistItemRepository) |
| 147 | ├── SplitPresets (ISplitPresetRepository) |
| 148 | ├── BudgetCategories (IBudgetCategoryRepository) |
| 149 | ├── GetRepository<T>() — geneeriliste entiteetide jaoks |
| 150 | └── SaveChangesAsync() — salvestab KÕIK muudatused atomaarselt |
| 151 | ``` |
| 152 | |
| 153 | Kursuse loeng ütleb: *"DbContext already is a Unit of Work."* Meie `AppUnitOfWork` on selle peale ehitatud kiht, mis annab puhta liidese ja peidab EF Core detailid. |
| 154 | |
| 155 | Repositoryd on lazy-initsialiseeritud — luuakse ainult siis, kui neid esimest korda kasutatakse. |
| 156 | |
| 157 | --- |
| 158 | |
| 159 | ## 3. Teenuste kiht (App.BLL) |
| 160 | |
| 161 | Teenused sisaldavad **äriloogikat**, mida kontrollerid ei peaks ise teadma. Teenused sõltuvad `IAppUnitOfWork` liidesest (mitte DbContext-ist otse). |
| 162 | |
| 163 | ### SettlementService — arvelduse äriloogika |
| 164 | |
| 165 | Kõige keerulisem teenus. Põhimeetodid + guarded wrapper'id IDOR kaitseks: |
| 166 | |
| 167 | - **`CalculateBalancesAsync(Guid tripId)`** — arvutab iga osaleja kohta: kui palju on maksnud vs kui palju peab maksma. Kasutab `CurrencyConverter`-it valuuta normaliseerimiseks vaikevaluutasse. Tagastab saldod sorteerituna kahanevas järjekorras. |
| 168 | |
| 169 | - **`CalculateSettlementAsync(Guid tripId, Guid userId)`** — greedy algoritm, mis paardab suurima võlausaldaja suurima võlgnikuga, minimeerides maksete arvu. Kasutab kahte sorteeritud nimekirja (võlausaldajad ja võlgnikud) ning two-pointer lähenemist. Lävi: 0.01m (väldib ümardamisartefakte). Loob `SettlementPayment` kirjed staatusega Pending. |
| 170 | |
| 171 | - **`MarkPaidAsync(Guid paymentId, Guid userId)`** — võlgnik märgib makse tehtuks. Muudab staatust Pending → MarkedPaid, salvestab kuupäeva. Ainult `FromUserId` saab seda teha. Guarded variant `MarkPaidGuardedAsync` kontrollib osalust ja tagastab `(ok, errorCode)` tupli — kontroller tõlgib `"forbidden"` → `Forbid()`. |
| 172 | |
| 173 | - **`ConfirmPaymentAsync(Guid paymentId, Guid userId)`** — võlausaldaja kinnitab makse laekumist. Muudab staatust MarkedPaid → Confirmed. Ainult `ToUserId` saab seda teha (guarded variant `ConfirmPaymentGuardedAsync` jõustab selle). Pärast mutatsiooni laaditakse plaan `SettlementPlanRepository.GetByIdAsync`-iga (koos Payments-iga), et kontrollida kas kõik maksed on kinnitatud. **NB!** DAL on konfigureeritud `QueryTrackingBehavior.NoTrackingWithIdentityResolution`-iga — iga laetud entiteet on detached, mistõttu mutatsioonide salvestamiseks on vajalik selgesõnaline `paymentRepo.Update(payment)` kutse (vastasel juhul `SaveChangesAsync` ei näe muudatust ja andmebaasi ei kirjutata midagi). Plaani ja reisi uuendamine käib **shallow** base-repo kaudu (`_uow.GetRepository<SettlementPlan>()`, `_uow.GetRepository<Trip>()`), mis ei lae AppUser navigatsioonivarasid — muidu `DbSet.Update(plan)` ketaks läbi Payments→FromUser/ToUser graafi ja rikuks Identity ridu (`ConcurrencyStamp`, `SecurityStamp`). Kui kõik maksed on Confirmed → plan.Status = Completed + plan.CompletedAt; kui reis on `Finalizing`, läheb see nüüd `Settled`-iks. Vastasel juhul plan.Status = InProgress. |
| 174 | |
| 175 | ### Reisi elutsükkel (`ETripStatus`) |
| 176 | |
| 177 | `Active → Finalizing → Settled` (+ `Archived`). Organisaator klõpsab **Finalize Trip** — `TripService.FinalizeTripAsync` lukustab kulude muutmise (iga kulu CRUD kontrollib `trip.Status != Active`-it) ja kutsub `CalculateSettlementAsync`-i, mis loob plaani. Reis läheb staatusesse **Finalizing** (mitte enam otse `Settled`, nagu varasem versioon tegi). Kui kedagi pole midagi võlgu ja plaani ei looda, läheb reis kohe `Settled` peale. **Settled** staatus saavutatakse automaatselt alles siis, kui viimane makse on saaja poolt kinnitatud — see toimub `ConfirmPaymentAsync`-is. Reopen on lubatud ainult `Finalizing` seisundis (või tagasiühilduvuse pärast `Settled` seisundis, kui plaan pole veel `Completed`); niipea kui mõni makse on juba kinnitatud, `ReopenTripAsync` tagastab `"payments-confirmed"` veakoodi. |
| 178 | |
| 179 | ### ExpenseService — kulu loomine koos jaotusega |
| 180 | |
| 181 | 2 meetodit: |
| 182 | |
| 183 | - **`CreateExpenseWithSplitsAsync(...)`** — loob kulu ja jaotuse (`ExpenseSplit` kirjed) atomaarselt ühes transaktsioonis. Toetab nelja jagamismeetodit: |
| 184 | - **EqualAll** — võrdselt kõigi aktiivse osaleja vahel. `baseAmount = Math.Floor(total / count * 100) / 100`, ülejääk jaotatakse 0.01 kaupa esimestele. |
| 185 | - **EqualSubset** — sama loogika, aga ainult valitud osalejatele. |
| 186 | - **ExactAmounts** — täpsed summad iga osaleja kohta, otse 1:1 vastendus. |
| 187 | - **Percentages** — `amount = Math.Round(expense.Amount * percentage / 100, 2)`, salvestab nii protsendi kui arvutatud summa. |
| 188 | |
| 189 | - **`DeleteExpenseWithSplitsAsync(Guid expenseId)`** — kaskaadne kustutamine: kõigepealt split-id, siis kulu ise. |
| 190 | |
| 191 | ### InvitationService — kutsete haldus |
| 192 | |
| 193 | 2 meetodit: |
| 194 | |
| 195 | - **`CreateInvitationAsync(Guid tripId, Guid userId)`** — genereerib **krüptograafilise tokeni** (`RandomNumberGenerator.GetBytes(32)` = 256 bitti), teisendab URL-ohutuks Base64-ks (asendab `+`→`-`, `/`→`_`, eemaldab `=`). Kutse aegub 7 päeva pärast. |
| 196 | |
| 197 | - **`AcceptInvitationAsync(string token, Guid userId)`** — valideerib tokeni olemasolu, staatuse (Pending) ja aegumise. Haldab kolme stsenaariumit: |
| 198 | 1. Kasutaja on juba aktiivne osaleja → lihtsalt aktsepteerib kutse |
| 199 | 2. Kasutaja on mitteaktiivne osaleja → taasaktiveerib (IsActive=true, LeftAt=null) |
| 200 | 3. Kasutaja pole osaleja → loob uue `TripParticipant` kirje Participant rolliga |
| 201 | |
| 202 | ### TripService — reisi loomine |
| 203 | |
| 204 | - **`CreateTripAsync(Trip trip, Guid userId)`** — loob reisi ja esimese osaleja (Organizer rolli) atomaarselt. |
| 205 | |
| 206 | ### PollService — küsitluste haldus |
| 207 | |
| 208 | 3 meetodit: |
| 209 | |
| 210 | - **`CreatePollWithOptionsAsync(TripPoll poll, List<string> optionTexts)`** — loob küsitluse koos valikuvariantidega (`TripPollOption` kirjed koos `DisplayOrder`-iga). Filtreerib tühjad variandid välja. |
| 211 | |
| 212 | - **`ToggleVoteAsync(Guid pollId, Guid optionId, Guid userId)`** — hääletamise toggle-loogika. Kui `AllowMultipleVotes = false`, eemaldab kõigepealt kasutaja kõik varasemad hääled selles küsitluses. Ei tee midagi, kui küsitlus on suletud (`ClosedAt != null`). |
| 213 | |
| 214 | - **`DeletePollCascadeAsync(Guid pollId)`** — kaskaadne kustutamine: hääled → variandid → küsitlus. |
| 215 | |
| 216 | --- |
| 217 | |
| 218 | ## 4. DTO-d ja mapperid |
| 219 | |
| 220 | ### DTO-d (Data Transfer Objects) |
| 221 | |
| 222 | DTO-d asuvad `App.DTO/v1/` kaustas. Need on andmekandjad ilma äriloogikita — neid kasutatakse API sisendiks/väljundiks: |
| 223 | |
| 224 | - **Response DTO-d**: `TripDto`, `ExpenseDto`, `BudgetCategoryDto`, `SettlementPlanDto`, `SettlementPaymentDto`, `BalanceDto`, `SettlementSummaryDto`, `CurrencyDto`, `PollDto`, `PollOptionDto`, `WishlistItemDto`, `SplitPresetDto`, `SplitPresetMemberDto`, `InvitationDto`, `TripParticipantDto`, `ExpenseSplitDto` — API tagastab neid, mitte kunagi domeeni entiteete otse |
| 225 | - **Request DTO-d**: `TripCreateDto`, `TripUpdateDto`, `ExpenseCreateDto`, `ExpenseSplitCreateDto`, `BudgetCategoryCreateDto`, `PollCreateDto`, `WishlistItemCreateDto`, `SplitPresetCreateDto`, `InvitationCreateDto` — API võtab neid vastu kasutajalt |
| 226 | - **Identity DTO-d**: `RegisterInfo`, `LoginInfo`, `TokenRefreshInfo`, `LogoutInfo`, `JWTResponse` — autentimise andmevahetuseks |
| 227 | - **Vea DTO**: `RestApiErrorResponse` — standardne veaformaat |
| 228 | |
| 229 | ### Mapperid |
| 230 | |
| 231 | Mapperid asuvad `App.DTO/Mappers/` kaustas. Need on **manuaalsed staatilised klassid** (mitte AutoMapper), mis teisendavad domeeni entiteete DTO-deks: |
| 232 | |
| 233 | - `TripMapper`, `ExpenseMapper`, `SettlementMapper`, `CurrencyMapper`, `BudgetCategoryMapper`, `InvitationMapper`, `PollMapper`, `WishlistMapper`, `SplitPresetMapper` |
| 234 | |
| 235 | Kursuse `desc.md` nõuab: *"Manual mappers (no AutoMapper)"*. Iga mapper on lihtne staatiline meetod, mis kopeerib omadused ühest tüübist teise. |
| 236 | |
| 237 | --- |
| 238 | |
| 239 | ## 5. Domeeni mudelid (App.Domain) |
| 240 | |
| 241 | 16 domeeni entiteeti + 3 Identity entiteeti + 8 enum-i. Kõik entiteedid pärivad `BaseEntity`-lt (Id, CreatedAt, UpdatedAt). `BaseEntity` genereerib `Id` automaatselt (`Guid.NewGuid()`) ja seab ajatemplid UTC-s. |
| 242 | |
| 243 | ### Peamised entiteedid |
| 244 | |
| 245 | | Entiteet | Vastutus | |
| 246 | |----------|----------| |
| 247 | | **Trip** | Keskne entiteet — reis nimi, sihtkoht, kuupäevad, olek, vaikevaluuta | |
| 248 | | **TripParticipant** | Seob kasutaja reisiga, roll (Organizer/Participant), unikaalne (TripId, UserId) | |
| 249 | | **TripInvitation** | Token-põhine kutse reisiga liitumiseks, unikaalne tokeni indeks | |
| 250 | | **Expense** | Üksik kulutus — summa, maksja, kategooria, jagamismeetod | |
| 251 | | **ExpenseSplit** | Ühe osaleja osa konkreetses kulus | |
| 252 | | **BudgetCategory** | Eelarve kategooria reisi-spetsiifiline (nt Food, Transport), nimi on LangStr | |
| 253 | | **SplitPreset** | Salvestatud jagamise mall (nt "Hotelli grupp") | |
| 254 | | **SplitPresetMember** | Üks osaleja preset-is | |
| 255 | | **SettlementPlan** | Arveldusplaan optimeeritud maksetega | |
| 256 | | **SettlementPayment** | Üks makse arveldusplaanis (kahepoolne kinnitus) | |
| 257 | | **Currency** | Valuuta referentsandmed (EUR, USD, GBP, SEK, NOK), nimi on LangStr | |
| 258 | | **TripWishlistItem** | Soovinimekirja element (koht, tegevus, restoran) | |
| 259 | | **TripWishlistVote** | Hääl soovinimekirja elemendile, unikaalne (WishlistItemId, UserId) | |
| 260 | | **TripPoll** | Grupi küsitlus otsuste tegemiseks | |
| 261 | | **TripPollOption** | Küsitluse valikuvariant | |
| 262 | | **TripPollVote** | Hääl küsitluse valikule, unikaalne (PollOptionId, UserId) | |
| 263 | |
| 264 | ### Identity entiteedid |
| 265 | |
| 266 | | Entiteet | Vastutus | |
| 267 | |----------|----------| |
| 268 | | **AppUser** | Pärib `IdentityUser<Guid>`, lisab FirstName ja LastName (max 128) | |
| 269 | | **AppRole** | Pärib `IdentityRole<Guid>` | |
| 270 | | **AppRefreshToken** | JWT refresh token koos rotatsiooniga (eelmine token + aegumisaeg) | |
| 271 | |
| 272 | ### Enum-id |
| 273 | |
| 274 | | Enum | Väärtused | |
| 275 | |------|-----------| |
| 276 | | ETripStatus | Active, Finalizing, Settled, Archived (Finalizing on vahepealne seisund: plaan loodud, maksed käimas, kuid kõik pole veel kinnitatud) | |
| 277 | | ESplitMethod | EqualAll, EqualSubset, ExactAmounts, Percentages | |
| 278 | | EParticipantRole | Organizer, Participant | |
| 279 | | EInvitationStatus | Pending, Accepted, Declined, Expired, Revoked | |
| 280 | | EPaymentStatus | Pending, MarkedPaid, Confirmed | |
| 281 | | ESettlementStatus | Pending, InProgress, Completed | |
| 282 | | EWishlistCategory | Place, Activity, Restaurant, Other | |
| 283 | | EWishlistPriority | MustDo, NiceToHave, Optional | |
| 284 | |
| 285 | --- |
| 286 | |
| 287 | ## 6. Autentimine ja autoriseerimine |
| 288 | |
| 289 | ### JWT Bearer (API) |
| 290 | |
| 291 | 1. Kasutaja registreerib/logib sisse läbi `POST /api/v1/identity/account/login` |
| 292 | 2. Server genereerib JWT tokeni (HS256, claims: userId, rollid, email) ja refresh tokeni |
| 293 | 3. Klient saadab tokeni iga päringuga: `Authorization: Bearer <token>` |
| 294 | 4. ASP.NET middleware valideerib allkirja, aegumist ja väljastajat automaatselt |
| 295 | |
| 296 | Refresh token rotatsioon: vana token märgitakse kasutatud ja antakse uus. Vanal tokenil on 1-minutiline üleminekuperiood. |
| 297 | |
| 298 | ### JWT Helper (Base.Helpers) |
| 299 | |
| 300 | - `GenerateJwt(...)` — loob JWT tokeni `SymmetricSecurityKey` + HMAC-SHA256-ga |
| 301 | - `ValidateJWT(...)` — valideerib allkirja ja väljastajat, **aga mitte aegumist** (`ValidateLifetime = false`) — seda kasutatakse refresh flow's, kus aegunud token on oodatud |
| 302 | |
| 303 | ### Cookie autentimine (MVC) |
| 304 | |
| 305 | MVC vaated kasutavad küpsisepõhist autentimist — ASP.NET Identity haldab sessiooni. `SlidingExpiration` on lubatud. |
| 306 | |
| 307 | ### Rollipõhine autoriseerimine |
| 308 | |
| 309 | **Süsteemi rollid** (Identity): `admin`, `user` — kontrollitakse `[Authorize(Roles = "admin")]` atribuudiga. |
| 310 | |
| 311 | **Reisi rollid** (domeen): `Organizer`, `Participant` — kontrollitakse BLL teenustes (nt `ITripService.IsOrganizerAsync()`), mis omakorda kutsuvad `ITripParticipantRepository.IsOrganizerAsync()`. Kontrollerid ei pääse repository-le ligi otse. |
| 312 | |
| 313 | ### IDOR kaitse |
| 314 | |
| 315 | Iga BLL teenuse meetod, mis puudutab reisi-andmeid, võtab konstruktoris vastu `Guid userId` parameetri ja kontrollib osaleja/organiseerija staatust teenuse sees. Näiteks `TripService.GetByIdWithDetailsAsync(tripId, userId)` kutsub esmalt `IsParticipantAsync`-i — kui false, tagastab `null`, mida kontroller tõlgib `NotFound()`/`Forbid()`-iks. Kuna WebApp kontrollerid ei inject'i `IAppUnitOfWork`-i (see on Clean-reegel — verifitseeritav grep'iga), **kontrollerid ei saa kogemata IDOR-kontrolli vahele jätta** — nad peavad alati minema teenuse kaudu, mis kontrolli teeb. |
| 316 | |
| 317 | --- |
| 318 | |
| 319 | ## 7. ViewModelid |
| 320 | |
| 321 | Kursuse nõue on kasutada **ViewModele** andmete edastamiseks vaadetesse, mitte ViewBag/ViewData'd. Rakenduses on **26 ViewModel klassi**. |
| 322 | |
| 323 | ### Admin ViewModelid (AdminViewModels.cs) |
| 324 | |
| 325 | Admin alal on **21 ViewModel klassi**, mis tagavad järjepideva mustri: |
| 326 | |
| 327 | **Index ViewModelid** (loendite kuvamiseks, filtrite ja otsinguga): |
| 328 | - `AdminTripIndexViewModel`, `AdminExpenseIndexViewModel`, `AdminBudgetCategoryIndexViewModel`, `AdminSettlementPlanIndexViewModel`, `AdminTripParticipantIndexViewModel`, `AdminSettlementPaymentIndexViewModel`, `AdminPollIndexViewModel`, `AdminWishlistIndexViewModel`, `AdminInvitationIndexViewModel`, `AdminCurrencyIndexViewModel`, `AdminSplitPresetIndexViewModel` |
| 329 | |
| 330 | **Form ViewModelid** (loomine/muutmine koos SelectList-idega): |
| 331 | - `AdminTripFormViewModel`, `AdminExpenseFormViewModel`, `AdminBudgetCategoryFormViewModel`, `AdminSettlementPlanFormViewModel`, `AdminTripParticipantFormViewModel`, `AdminPollFormViewModel`, `AdminWishlistFormViewModel` |
| 332 | |
| 333 | **Spetsiaalsed ViewModelid:** |
| 334 | - `AdminDashboardViewModel` — 23 statistikanumbrit + 3 nimekirja (viimased reisid, kulud, kasutajad) |
| 335 | - `AdminEditRolesViewModel` — kasutaja rollide haldamine |
| 336 | - `RoleAssignmentViewModel` — abiklass rollide jaoks |
| 337 | |
| 338 | ### Kliendi ViewModelid (kontrollerite failides) |
| 339 | |
| 340 | 5 ViewModeli on defineeritud otse kontrolleri failides: |
| 341 | |
| 342 | | ViewModel | Kontroller | Eesmärk | |
| 343 | |-----------|-----------|---------| |
| 344 | | `TripIndexViewModel` | TripsController | Reisi loendi element koos rolliga | |
| 345 | | `ExpensesIndexViewModel` | ExpensesController | Kulud koos reisi ja valuuta kontekstiga | |
| 346 | | `BudgetCategoryViewModel` | BudgetController | Kategooria + kulutused + progressiriba arvutused | |
| 347 | | `SettlementBalanceViewModel` | SettlementController | Kasutaja saldo (makstud vs võlgu + NetBalance) | |
| 348 | | `WishlistItemViewModel` | WishlistClientController | Soovinimekiri + hääled + kasutaja hääl | |
| 349 | |
| 350 | ### Andmete edastamise mustrid |
| 351 | |
| 352 | - **Admin ala**: 100% ViewModel-põhine, SelectList-id ViewModeli sees |
| 353 | - **Kliendi kontrollerid**: ViewModeleid kasutatakse peamise andmekandja jaoks; ViewData kasutatakse kontekstandmete jaoks (TripId, TripName, CurrencySymbol, IsOrganizer jne) |
| 354 | - **PollsClientController** ja **MembersController** edastavad domeeni entiteete otse (pole eraldi ViewModeli) |
| 355 | |
| 356 | --- |
| 357 | |
| 358 | ## 8. Tõlked |
| 359 | |
| 360 | ### UI tõlked (.resx failid) |
| 361 | |
| 362 | Staatilised tekstid (nupud, sildid, veateated) on `.resx` failides. Iga fail on kahes keeles: |
| 363 | - `Shared.resx` (inglise) / `Shared.et.resx` (eesti) |
| 364 | - `Common.resx` / `Common.et.resx` — valideerimisteated |
| 365 | - `Domain/*.resx` — vormiväljanimede ja enum-ide tõlked (Trip, Expense, Currency, BudgetCategory, TripParticipant, TripPoll, TripPollOption, TripWishlistItem, SettlementPlan, SettlementPayment, Enums) |
| 366 | |
| 367 | Razor vaadetes: `@Localizer["Save"]` → "Salvesta" (ET) või "Save" (EN). |
| 368 | |
| 369 | Keelevahetaja on navbaris — salvestab keele küpsisesse. |
| 370 | |
| 371 | ### Enum-ide tõlked |
| 372 | |
| 373 | `EnumHelper` (WebApp/Helpers/) kasutab `ResourceManager`-it enum väärtuste lokaliseerimiseks. Võti: `{EnumType}_{Value}` (nt `ETripStatus_Active`), otsitakse `App.Resources.Domain.Enums` ressursist. |
| 374 | |
| 375 | ### Andmebaasi tõlked (LangStr) |
| 376 | |
| 377 | Dünaamiline süsteemne sisu, mida admin haldab, kasutab `LangStr` — `Dictionary<string, string>` salvestatakse JSON-ina PostgreSQL-i: |
| 378 | |
| 379 | ```json |
| 380 | {"en": "Euro", "et": "Euro"} |
| 381 | ``` |
| 382 | |
| 383 | `Currency.Name` ja `BudgetCategory.Name` kasutavad `LangStr`-i. Admin vormis on kaks inputit (Name EN, Name ET). `LangStr.ToString()` tagastab automaatselt kasutaja keeles tõlke, fallback-iga neutraalsele kultuurile ja seejärel vaikekultuurile. |
| 384 | |
| 385 | Kasutaja-loodud sisu (reisi nimed, kulud, soovinimekirja elemendid) **ei kasutata LangStr-i** — see on kasutaja enda tekst, mitte süsteemne referentsandmed. |
| 386 | |
| 387 | --- |
| 388 | |
| 389 | ## 9. API (REST) |
| 390 | |
| 391 | Versioonitud: `/api/v1/...`. Kõik kaitstud endpointid nõuavad JWT Bearer tokenit. Marsruudi muster: `/api/v1/[controller]/[action]`. |
| 392 | |
| 393 | ### Kontrollerid |
| 394 | |
| 395 | | Kontroller | Endpointid | |
| 396 | |-----------|-----------| |
| 397 | | AccountController | register, login, refreshtoken, logout | |
| 398 | | TripsController | CRUD + osalejate info | |
| 399 | | ExpensesController | CRUD + jagamise loomine | |
| 400 | | BudgetCategoriesController | CRUD reisi kategooriatele | |
| 401 | | InvitationsController | kutse loomine, info, accept/decline/revoke | |
| 402 | | WishlistController | CRUD + hääletus + valmis märkimine | |
| 403 | | PollsController | CRUD + hääletus + sulgemine | |
| 404 | | SettlementsController | saldod, arvelduse arvutamine, mark-paid, confirm | |
| 405 | | SplitPresetsController | CRUD jagamismallidele | |
| 406 | | CurrenciesController | valuutade nimekiri | |
| 407 | |
| 408 | Swagger on konfigureeritud JWT Bearer turvameetmega — saab otse brauseris testida tokeniga. |
| 409 | |
| 410 | --- |
| 411 | |
| 412 | ## 10. MVC veebirakendus |
| 413 | |
| 414 | ### Kliendi kontrollerid (8 tk) |
| 415 | |
| 416 | | Kontroller | Peamised tegevused | Autoriseerimismuster | |
| 417 | |-----------|-----------|-----------| |
| 418 | | **HomeController** | Index, Privacy | Avalik (pole `[Authorize]`) | |
| 419 | | **TripsController** | CRUD + detailvaade statistikaga | `[Authorize]` + osaleja kontroll | |
| 420 | | **ExpensesController** | CRUD koos 4 jagamismeetodiga | `[Authorize]` + osaleja kontroll | |
| 421 | | **BudgetController** | Kategooriate haldamine + progressiribad | `[Authorize]` + organizer kontroll muutmisteks | |
| 422 | | **MembersController** | Kutselingi genereerimine, accept, eemaldamine | `[Authorize]` + organizer kontroll | |
| 423 | | **SettlementController** | Saldod, makse märkimine, kinnitamine | `[Authorize]` + osaleja kontroll | |
| 424 | | **PollsClientController** | Loomine, hääletus, sulgemine | `[Authorize]` + osaleja kontroll | |
| 425 | | **WishlistClientController** | CRUD + hääletus + valmis märkimine | `[Authorize]` + osaleja kontroll | |
| 426 | |
| 427 | Reisi kontekstis navigeerimine: Trip Details → nav-grid → Expenses / Budget / Members / Wishlist / Polls / Settlement. |
| 428 | |
| 429 | ### Admin paneel (13 kontrollerit) |
| 430 | |
| 431 | Süsteemiadministraatori vaade `[Authorize(Roles = "admin")]`: |
| 432 | |
| 433 | | Kontroller | Vastutus | |
| 434 | |-----------|----------| |
| 435 | | **DashboardController** | Töölaud statistikaga (AdminDashboardViewModel) | |
| 436 | | **TripsController** | Kõigi reiside CRUD | |
| 437 | | **TripParticipantsController** | Osalejate haldamine | |
| 438 | | **ExpensesController** | Kulude haldamine | |
| 439 | | **BudgetCategoriesController** | Eelarvekategooriate haldamine | |
| 440 | | **CurrenciesController** | Valuutade haldamine mitmekeelsete nimedega | |
| 441 | | **SettlementPlansController** | Arveldusplaanide haldamine | |
| 442 | | **SettlementPaymentsController** | Maksete jälgimine | |
| 443 | | **PollsController** | Küsitluste haldamine | |
| 444 | | **WishlistController** | Soovinimekirja haldamine | |
| 445 | | **SplitPresetsController** | Jagamismallide haldamine | |
| 446 | | **InvitationsController** | Kutsete vaatamine ja haldamine | |
| 447 | | **UsersController** | Kasutajate nimekiri + rollide muutmine | |
| 448 | |
| 449 | Admin link navbaris on nähtav ainult kui `User.IsInRole("admin")` JA kasutaja on sisse logitud. |
| 450 | |
| 451 | --- |
| 452 | |
| 453 | ## 11. Vaated (Views) |
| 454 | |
| 455 | ### Kliendi vaated (34 tk) |
| 456 | |
| 457 | **Jagatud kujunduselemendid (Shared/):** |
| 458 | - `_Layout.cshtml` — peamine kujundusmall, toast-teated TempData kaudu, tinglik admin-link |
| 459 | - `_LoginPartial.cshtml` — sisselogimine/väljalogimine |
| 460 | - `_LanguageSelection.cshtml` — keelevahetaja |
| 461 | - `_ValidationScriptsPartial.cshtml` — kliendipoolne valideerimine |
| 462 | - `Error.cshtml` — vealeht |
| 463 | |
| 464 | **Reisid:** Index, Details (dashboard saldo/eelarve ülevaatega), Create, Edit, Delete |
| 465 | |
| 466 | **Kulud:** Index, Create (jagamismeetodi valik + osalejate valik), Edit, Delete |
| 467 | |
| 468 | **Liikmed:** Index, Invite, InviteGenerated (kutselingi kuvamine), AcceptInvitation, InvitationInvalid |
| 469 | |
| 470 | **Eelarve:** Index (progressiribadega), CreateCategory, EditCategory, DeleteCategory |
| 471 | |
| 472 | **Arveldus:** Index (saldod + maksestaatused) |
| 473 | |
| 474 | **Küsitlused:** Index, Create, Details (hääletus + tulemused) |
| 475 | |
| 476 | **Soovinimekiri:** Index, Create, Edit, Delete |
| 477 | |
| 478 | ### Admin vaated (43+ tk) |
| 479 | |
| 480 | Iga admin kontroller omab standardset CRUD vaadete komplekti (Index, Details, Create, Edit, Delete). Eraldi: |
| 481 | - Dashboard/Index — statistika |
| 482 | - Users/Index — kasutajate nimekiri |
| 483 | - Users/EditRoles — rollide muutmine |
| 484 | |
| 485 | --- |
| 486 | |
| 487 | ## 12. Helperid (WebApp/Helpers) |
| 488 | |
| 489 | ### CurrencyConverter |
| 490 | |
| 491 | Staatiline klass valuutade teisendamiseks. Olemas **kahes kohas**: `WebApp/Helpers/CurrencyConverter.cs` (MVC kontrollerite jaoks) ja `App.BLL/Helpers/CurrencyConverter.cs` (teenuste jaoks). Mõlemad on identsed. |
| 492 | |
| 493 | - Hardcoded kursid EUR baasil: EUR=1.0, USD=0.92, GBP=1.16, SEK=0.087, NOK=0.086 |
| 494 | - Teisendus: summa → EUR → sihtvaluuta, ümardamine 2 kohani |
| 495 | - Tundmatu valuuta korral tagastab 1:1 (fallback) |
| 496 | |
| 497 | ### EnumHelper |
| 498 | |
| 499 | Staatiline klass enum-väärtuste lokaliseeritud nimede saamiseks: |
| 500 | - `GetDisplayName<TEnum>(TEnum value)` — kasutab `ResourceManager`-it (`App.Resources.Domain.Enums`) |
| 501 | - Võtmeformaat: `{EnumType}_{Value}`, fallback: enum väärtuse nimi stringina |
| 502 | |
| 503 | ### InvariantDecimalModelBinderProvider |
| 504 | |
| 505 | Custom model binder, mis lubab kasumi sisendites nii punkti (`.`) kui koma (`,`) kümnendkoha eraldajana. See lahendab probleemi, kus erinevad brauseri lokaadid saadavad erinevaid formaate. |
| 506 | |
| 507 | --- |
| 508 | |
| 509 | ## 13. Infrastruktuur |
| 510 | |
| 511 | ### Docker |
| 512 | |
| 513 | - **Dockerfile** — multi-stage build (SDK 10.0 → runtime ASP.NET 10.0), minimeerib image suurust. Port: 8080. |
| 514 | - **docker-compose.yml** — PostgreSQL 16 + veebirakendus, persistent volume andmebaasile. Hostport: 84 → konteiner 8080. |
| 515 | - Käivitamisel: `docker compose down -v && docker compose up --build` |
| 516 | |
| 517 | ### CORS |
| 518 | |
| 519 | `CorsAllowAll` poliitika — lubab kõik päritolud, päised ja meetodid. Eksponeerib päised: `X-Version`, `X-Version-Created-At`. |
| 520 | |
| 521 | ### Andmebaas (AppDbContext) |
| 522 | |
| 523 | PostgreSQL 16 läbi Npgsql. Konfiguratsioon: |
| 524 | - **SplitQuery** — väldib karteerianist plahvatust (`UseQuerySplittingBehavior`) |
| 525 | - **NoTrackingWithIdentityResolution** — parem jõudlus, aga säilitab entiteetide identiteedi |
| 526 | - **Restrict delete behavior** — kõik võõrvõtmed, kaskaad teostatud manuaalselt repositorys |
| 527 | - **UTC ajatemplid** — custom `UtcDateTimeConverter` kõigile DateTime omadustele |
| 528 | - **LangStr JSON** — `Currency.Name` ja `BudgetCategory.Name` salvestatakse JSON-ina |
| 529 | - **Unikaalsed indeksid** — TripInvitation.Token, (TripParticipant.TripId, UserId), küsitlus- ja soovinimekirja hääled |
| 530 | - **Automaatsed ajatemplid** — `SaveChangesAsync()` override uuendab `CreatedAt`/`UpdatedAt` |
| 531 | |
| 532 | ### Data Protection |
| 533 | |
| 534 | ASP.NET Core Data Protection võtmed salvestatakse andmebaasi (`PersistKeysToDbContext`). |
| 535 | |
| 536 | ### API versioonimine |
| 537 | |
| 538 | Asp.Versioning teek, vaikeversioon 1.0, formaat `'v'VVV` (nt v1.0). |
| 539 | |
| 540 | ### Teenuste registreerimine (Program.cs, DI) |
| 541 | |
| 542 | Kõik teenused on registreeritud **Scoped** elutsükliga: |
| 543 | ``` |
| 544 | IAppUnitOfWork → AppUnitOfWork |
| 545 | ITripService → TripService |
| 546 | IExpenseService → ExpenseService |
| 547 | ISettlementService → SettlementService |
| 548 | IInvitationService → InvitationService |
| 549 | IPollService → PollService |
| 550 | ``` |
| 551 | |
| 552 | ### Marsruutimine |
| 553 | |
| 554 | 1. Admin ala: `{area:exists}/{controller=Dashboard}/{action=Index}/{id?}` |
| 555 | 2. Vaikimisi: `{controller=Home}/{action=Index}/{id?}` |
| 556 | 3. Razor Pages (Identity UI jaoks) |
| 557 | |
| 558 | ### Andmebaasi initsialiseerimine |
| 559 | |
| 560 | Startup ajal (`SetupAppData`): |
| 561 | - Ootab PostgreSQL ühendust (retry loop) |
| 562 | - Konfiguratsioonist loetavad lipud: `DropDatabase`, `MigrateDatabase`, `SeedIdentity`, `SeedData` |
| 563 | |
| 564 | ### Seed andmed |
| 565 | |
| 566 | **Kasutajad:** |
| 567 | - admin@taltech.ee (admin roll) |
| 568 | - user@taltech.ee, alice@taltech.ee, bob@taltech.ee, charlie@taltech.ee, diana@taltech.ee (user roll) |
| 569 | |
| 570 | **Valuutad:** EUR, USD, GBP, SEK, NOK (mitmekeelsete nimedega) |
| 571 | |
| 572 | **Näidisreisid (4 tk):** |
| 573 | 1. **Barcelona Weekend** — 4 osalejat, Active, EUR, 11 kulu, eelarve kategooriad, jagamismallid, küsitlus, soovinimekiri |
| 574 | 2. **London Business Trip** — 3 osalejat, Settled, GBP, 6 kulu, kinnitatud arveldusplaan |
| 575 | 3. **Summer Cabin Getaway** — 5 osalejat, Active, EUR, 7 kulu, pooleliolev arveldus, küsitlus, soovinimekiri, ootel kutse |
| 576 | 4. **NYC Adventure** — 3 osalejat, Archived, USD, 9 kulu, suletud küsitlus |
| 577 | |
| 578 | --- |
| 579 | |
| 580 | ## 14. Staatilised failid ja frontend |
| 581 | |
| 582 | ### CSS |
| 583 | - `wwwroot/css/site.css` — peamine kujundusfail |
| 584 | - `wwwroot/css/splitapp-design.css` — SplitApp-spetsiifilised stiilid |
| 585 | - Bootstrap 5 (teegi kaust) |
| 586 | |
| 587 | ### JavaScript |
| 588 | - `wwwroot/js/site.js` — saidi skriptid |
| 589 | - `wwwroot/js/splitapp.js` — SplitApp-spetsiifilised funktsioonid (toast-teated, jagamismeetodi valik jne) |
| 590 | |
| 591 | ### Teegid (wwwroot/lib/) |
| 592 | - Bootstrap 5, jQuery, Popper.js |
| 593 | |
| 594 | --- |
| 595 | |
| 596 | ## 15. Projekti failid ja sõltuvused |
| 597 | |
| 598 | ### NuGet paketid (WebApp) |
| 599 | |
| 600 | | Pakett | Versioon | Otstarve | |
| 601 | |--------|---------|----------| |
| 602 | | Asp.Versioning.Mvc.ApiExplorer | 8.1.1 | API versioonimine | |
| 603 | | Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.5 | JWT tugi | |
| 604 | | Microsoft.AspNetCore.Identity.EntityFrameworkCore | 10.0.5 | Identity | |
| 605 | | Microsoft.AspNetCore.Identity.UI | 10.0.5 | Identity UI | |
| 606 | | Microsoft.EntityFrameworkCore.Tools | 10.0.5 | EF migratsioonid | |
| 607 | | Npgsql.EntityFrameworkCore.PostgreSQL | 10.0.1 | PostgreSQL tugi | |
| 608 | | Swashbuckle.AspNetCore | 10.1.7 | Swagger/OpenAPI | |
| 609 | |
| 610 | ### Migratsioonid (5 tk) |
| 611 | |
| 612 | 1. `20260328145416_Initial` — esialgne skeem |
| 613 | 2. `20260328161224_AddBaseEntityTimestamps` — CreatedAt/UpdatedAt lisamine |
| 614 | 3. `20260329141138_CurrencyNameToLangStr` — Currency.Name teisendamine LangStr JSON-iks |
| 615 | 4. `20260402104505_RemoveUnusedBudgetCategoryTranslations` — puhastus |
| 616 | 5. `20260410202112_BudgetCategoryNameToLangStr` — BudgetCategory.Name teisendamine LangStr JSON-iks |
| 617 | |
| 618 | --- |
| 619 | |
| 620 | ## 15. Kaitsmise spikker (defense cheat sheet) |
| 621 | |
| 622 | Selle peatüki eesmärk on anda lühikesed, ausad vastused õpetaja tüüpilistele küsimustele. |
| 623 | |
| 624 | ### Küsimus: "Mis arhitektuuri sa kasutasid?" |
| 625 | |
| 626 | **Vastus:** "Clean Architecture'it. Sõltuvused liiguvad sissepoole: `WebApp → App.BLL → App.Domain`, ning `App.DAL.EF` on plugin väljaspool, mis implementeerib Domain-interfejse. Repository- ja UoW-liidesed (`IAppUnitOfWork`, `ITripRepository` jt) elavad [App.Domain/Contracts/](SplitApp/App.Domain/Contracts/)-is. `App.BLL.csproj` ei viita `App.DAL.EF`-ile üldse — dependency inversion on tagatud Domain-interfejside kaudu. WebApp kontrollerid kasutavad ainult BLL teenuseid — ükski kontroller ei inject'i `IAppUnitOfWork`-i." |
| 627 | |
| 628 | ### Küsimus: "Kuidas Clean Architecture sinu projektis välja näeb?" |
| 629 | |
| 630 | **Vastus:** "Kolm põhiomadust, mida saab verifitseerida: |
| 631 | 1. **Interfejsid Domain-is:** `App.Domain/Contracts/IAppUnitOfWork.cs`, `ITripRepository.cs` jne — kokku 11 interfejsi |
| 632 | 2. **DAL on plugin:** `App.DAL.EF/AppUnitOfWork.cs` implementeerib `App.Domain.Contracts.IAppUnitOfWork`-i. Sõltuvus liigub DAL → Domain (väljast sisse) |
| 633 | 3. **WebApp ei näe DAL-i:** `grep IAppUnitOfWork WebApp/Controllers WebApp/ApiControllers WebApp/Areas` → 0 tulemust. DAL-i viidatakse ainult Program.cs-s extension method'i (`AddDalServices(connectionString)`) kaudu |
| 634 | 4. **BLL ei sõltu DAL-ist:** `App.BLL.csproj` viitab ainult `App.Domain`-ile ja `App.DTO`-le" |
| 635 | |
| 636 | ### Küsimus: "Kuidas IDOR kaitse töötab?" |
| 637 | |
| 638 | **Vastus:** "IDOR-loogika elab BLL teenustes (`ITripService`, `IExpenseService` jt). Iga meetod, mis tagastab või muudab reisi-andmeid, võtab konstruktoris vastu `Guid userId` parameetri ja kontrollib osaleja/organiseerija staatust teenuse sees. Näiteks `TripService.GetByIdWithDetailsAsync(tripId, userId)` kutsub esmalt `_uow.TripParticipants.IsParticipantAsync(tripId, userId)` — kui false, tagastab `null`. Kontroller tõlgib `null` → `NotFound()`/`Forbid()`. Nii ei saa kontroller kogemata kontrolli vahele jätta, sest kontrollerid ei pääse ligi UoW-le üldse — ainult teenustele." |
| 639 | |
| 640 | ### Küsimus: "Kuidas settlement algoritm töötab?" |
| 641 | |
| 642 | **Vastus:** "Greedy algoritm kahe sorteeritud nimekirjaga. `CalculateBalancesAsync` arvutab iga osaleja netosaldo (makstud − peab maksma). `CalculateSettlementAsync` jagab need võlausaldajateks (positiivne saldo) ja võlgnikeks (negatiivne), sorteerib kahanevas järjekorras, ja two-pointer'iga paardab suurima võlausaldaja suurima võlgnikuga. See minimeerib maksete arvu. Lävi 0.01€ väldib ümardamisartefakte. Makse lifecycle: Pending → MarkedPaid (võlgnik märgib, ainult FromUser) → Confirmed (võlausaldaja kinnitab, ainult ToUser). Reisi lifecycle: Active → Finalizing (Finalize vajutusel) → Settled (automaatselt siis, kui viimane makse on kinnitatud — seda teeb `ConfirmPaymentAsync` plaani all-confirmed kontrollis). Kui kõik maksed Confirmed, plaani staatus Completed ja reis `Settled`. Tähtis detail: DAL on NoTracking-režiimis, seega iga mutatsioon vajab selget `Update()`-kutset; plaani uuendamine käib shallow base-repo kaudu, et mitte kaskaadida AppUser navigatsioonivaradesse (`ConcurrencyStamp` Identity ridu rikuks)." |
| 643 | |
| 644 | ### Küsimus: "Miks mitte AutoMapper?" |
| 645 | |
| 646 | **Vastus:** "Kursuse `desc.md` nõudis manuaalseid mappereid. Lisaks on manuaalsed mapperid kiiremad (pole reflection'it), debugitavamad (saab breakpointi panna) ja tüübiturvalisemad (kompileerimisaeg error, mitte runtime). DTO struktuurid muutuvad harva, nii et käsitsi kirjutamise vaev on minimaalne." |
| 647 | |
| 648 | ### Küsimus: "Kuidas LangStr töötab andmebaasis?" |
| 649 | |
| 650 | **Vastus:** "`LangStr` on `Dictionary<string, string>`, mis serialiseeritakse JSON-ina PostgreSQL-sse. Näiteks `Currency.Name` on andmebaasis `{\"en\":\"Euro\",\"et\":\"Euro\"}`. `LangStr.ToString()` tagastab kasutaja praeguse kultuuri tõlke, fallback'iga neutraalsele kultuurile ja seejärel vaikekultuurile. Admin vormis on kaks eraldi inputit (Name EN, Name ET), mida kontroller paneb kokku `LangStr` objektiks." |
| 651 | |
| 652 | ### Küsimus: "Milliseid entiteete LangStr kasutab?" |
| 653 | |
| 654 | **Vastus:** "Kaks entiteeti: `Currency.Name` ja `BudgetCategory.Name`. Need on süsteemsed referentsandmed, mida admin haldab ja mida kõik kasutajad näevad. Kasutaja-loodud sisu (reisi nimed, kulu kirjeldused, soovinimekirja elemendid) LangStr-i ei kasuta — need on kasutaja enda tekst omas keeles. Nõue oli '*translations in DB*', mitte '*every field translated*'." |
| 655 | |
| 656 | ### Küsimus: "Miks admin kontrollerites on Admin ViewModelid keerulised?" |
| 657 | |
| 658 | **Vastus:** "Teacher'i nõue oli `no viewbags/viewdata - use viewmodels`. Lõin kolm põhiklassi: |
| 659 | - `AdminPageViewModel` — baasklass `Title` omadusega; iga Index/Form VM pärib sellelt |
| 660 | - `AdminDetailsViewModel<T>` — geneeriline wrapper Details-vaadetele, et domeeni entiteet ei lekiks otse vaatesse |
| 661 | - `AdminDeleteViewModel<T>` — sama Delete jaoks |
| 662 | |
| 663 | Admin layout loeb `Title`-i läbi interface'i cast'i: `(Model as ITitledViewModel)?.Title`. Seetõttu on admin vaates **0** `ViewData`/`ViewBag` kasutust — grep-tööriist kinnitab." |
| 664 | |
| 665 | ### Küsimus: "Kuidas admin Dashboard statistika arvutatakse?" |
| 666 | |
| 667 | **Vastus:** "Kogu agregatsiooniloogika (10+ metrikut, Top Active Trips, Biggest Expenses, User Activity 7d/30d, Activity Feed) elab `IAdminStatsService.GetDashboardStatsAsync()`-is ([App.BLL/Services/Admin/AdminStatsService.cs](SplitApp/App.BLL/Services/Admin/AdminStatsService.cs)). Teenus tagastab `AdminDashboardData` DTO, `DashboardController.Index()` mappib selle `AdminDashboardViewModel`-ile ja kuvab vaates. Kontroller ise on ~30 rida — kogu äriloogika on BLL-is, nagu Clean nõuab." |
| 668 | |
| 669 | ### Küsimus: "Kuidas andmebaasi vahetada oleks, kui tahaksid?" |
| 670 | |
| 671 | **Vastus:** "Tänu Clean Architecture'ile väga lihtne. `App.Domain/Contracts/` sisaldab kõiki repository-interfejse, `App.DAL.EF` on nende implementatsioon EF Core + PostgreSQL peal. DB vahetuseks tuleks: |
| 672 | 1. Luua uus projekt (nt `App.DAL.MongoDB`), mis implementeerib samu interfejse |
| 673 | 2. Muuta `Program.cs` ühte rida: `builder.Services.AddMongoDalServices(...)` asemel praeguse `AddDalServices(...)` |
| 674 | 3. Migreerida andmed |
| 675 | |
| 676 | `App.BLL`, `WebApp` ja `App.Domain` ei muutu — see on Clean-i põhivõit. `App.DAL.EF` on teadlikult plugin, mida saab asendada." |
| 677 | |
| 678 | ### Küsimus: "Miks JWT refresh token rotatsioon on vajalik?" |
| 679 | |
| 680 | **Vastus:** "Turvalisuse pärast: kui rünnaja varastab vana refresh tokeni, ei saa ta seda kasutada, sest see on juba konkreetse kasutaja uue tokeniga asendatud. Meie implementatsioon: iga refresh-kutse genereerib uue access+refresh paari, vana refresh token märgitakse `PreviousToken`-iks ja aegub 1 minuti pärast (üleminekuperiood võrgukatkestuste jaoks)." |
| 681 | |
| 682 | ### Küsimus: "Miks CI/CD lükkab ainult `main` harust?" |
| 683 | |
| 684 | **Vastus:** "Konfigureeritud [.gitlab-ci.yml](.gitlab-ci.yml)-s `only: - main`. See takistab juhuslikke feature-branchide deployment'e. Tootmiseks peab explicitly main-i mergima. Docker compose builditakse uuesti iga push'iga, migratsioonid rakendatakse automaatselt (env `DataInitialization__MigrateDatabase=true`) startup'i ajal." |
| 685 | |
| 686 | ### Küsimus: "Miks 10 enam kui 10 entiteeti?" |
| 687 | |
| 688 | **Vastus:** "Ülesanne nõudis *min 10 meaningful*. Mul on 16, sest reisikulude domeen on loomulikult rikas: lisaks põhitükkidele (Trip, Expense, User) on vajalikud vote-tabelid (TripPollVote, TripWishlistVote), settlement'i kaks kihti (SettlementPlan → SettlementPayment'id), split-preset'i kaks kihti (SplitPreset → SplitPresetMember), eraldi split-kirjed iga kulu jaoks (ExpenseSplit). Ükski pole trivaalne join-tabel — kõigil on omadused (Amount, Percentage, IsInterested, jne)." |
| 689 | |
| 690 | ### Küsimus: "Mis on kõige keerulisem osa projektis?" |
| 691 | |
| 692 | **Vastus:** "`SettlementService.CalculateSettlementAsync()` greedy algoritm koos valuutakonversiooniga. Mitu nüansi: |
| 693 | 1. Iga kulu võib olla erinevas valuutas → `CurrencyConverter` normaliseerib reisi vaikevaluutasse |
| 694 | 2. Ümardamisartefaktid (nt 33.33 + 33.33 + 33.34 = 100.00) — lõpliku osaleja summa on floor'itud, ülejääk 0.01 kaupa esimestele |
| 695 | 3. Two-pointer sorted lists — võlausaldajad kahanevalt, võlgnikud tõusvalt (võlg = negatiivne) |
| 696 | 4. Makse lifecycle kahepoolse kinnitusega (mark-paid → confirm), mitte lihtsalt 'done'" |
| 697 | |
| 698 | ### Küsimus: "Miks mõni asi jääb Domain-is 'saastunud' (Display atribuudid Resources-ile)?" |
| 699 | |
| 700 | **Vastus:** "Teadlik pragmaatiline kompromiss. `App.Domain/*.cs` entiteetidel on jätkuvalt `[Display(ResourceType = typeof(App.Resources.Domain.Trip))]` atribuudid, mis seovad Domain-i Resources-iga. Täielikus Cleanis oleks need atribuudid DTO-des või ViewModelides. Ma teadlikult ei kolinud neid, sest see oleks katkestanud ModelState valideerimise ja nõudnud iga form'i re-testi. Kõik **muud** Clean-põhimõtted (interfejsid Domain-is, DAL plugin, BLL ↛ DAL, WebApp ↛ UoW) on rangelt järgitud." |
| 701 | |
| 702 | ### Küsimus: "Kuidas sõltuvuse inversioon sinu projektis konkreetselt toimib?" |
| 703 | |
| 704 | **Vastus:** "Konkreetne näide. `App.BLL/Services/TripService.cs` deklareerib: |
| 705 | ```csharp |
| 706 | using App.Domain.Contracts; // interfejs Domain-ist |
| 707 | |
| 708 | public class TripService : ITripService { |
| 709 | private readonly IAppUnitOfWork _uow; // Domain-interfejs |
| 710 | public TripService(IAppUnitOfWork uow) { _uow = uow; } |
| 711 | } |
| 712 | ``` |
| 713 | BLL ei tea `App.DAL.EF`-ist midagi. Kompileerimise ajal pole `App.BLL.csproj`-s DAL-i viidet. DI-container ühendab käivitamisel `IAppUnitOfWork` → `AppUnitOfWork` (DAL-ist) tänu `Program.cs` `AddDalServices()` registreerimisele. See ongi dependency inversion — kõrgem kiht (BLL) sõltub abstraktsioonist (Domain), mitte konkreetsest implementatsioonist (DAL)." |
| 714 | |
| 715 | --- |
| 716 | |
| 717 | ## 16. Mida võiks paremini teha |
| 718 | |
| 719 | Ausalt — kohad, kus projekt võiks olla parem: |
| 720 | |
| 721 | 1. **Domain puhastamine** — `App.Domain/*.cs` entiteetidel on jätkuvalt `[Display(ResourceType = typeof(App.Resources.Domain.X))]` atribuudid. Täielikus Cleanis peaksid need olema DTO-del või ViewModelidel. Teadlik pragmaatiline kompromiss ModelState-valideerimise tõttu. |
| 722 | 2. **LangStr laiem kasutus** — praegu ainult 2 entiteedis (`Currency.Name`, `BudgetCategory.Name`). Võiks laieneda `Trip.Name`, `TripPoll.Question`, `SplitPreset.Name` peale. |
| 723 | 3. **Integratsioontestid** — ükshaaval tehtud manuaalne testimine; CI käigus võiks olla `dotnet test` koos in-memory andmebaasiga. Clean Architecture teeb testide kirjutamise lihtsamaks (teenuseid saab mockida läbi Domain-interfejside). |
| 724 | 4. **Valuutakursid** — praegu hardcoded `CurrencyConverter`-is. Reaalses rakenduses peaks need tulema välisest API-st. |
| 725 | 5. **Rate limiting** — puudub. API endpointid on kaitsmata DDoS-i eest. |
| 726 | 6. **Logimine** — lihtne `Console.WriteLine` mitmes kohas (eriti `SetupAppData`). Structured logging Serilog-iga oleks parem. |
| 727 | 7. **WebApp → DAL kompromissviide** — `WebApp.csproj` viitab endiselt `App.DAL.EF`-ile, et `Program.cs` saaks kutsuda `AddDalServices()`. 100% isolatsiooniks oleks vaja eraldi `App.DAL.EF.Bootstrap` projekti, mis on Cleani purist'i jaoks väärt, aga praktiliselt over-engineering. |
| 728 | |
| 729 | Need **ei ole puuduvad nõuded** — need on parandusvõimalused. |
| 730 | |