profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
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