README.md
5,586 bytes
| 1 | # SplitApp — Phase 3 Modular Monolith |
|---|---|
| 2 | |
| 3 | ASP.NET Core 10 modular monolith for splitting trip expenses among participants. Three modules (Users, Trips, Expenses) communicating via MediatR; one deployable; per-module Postgres schemas. Full phase-2 UI surface preserved (101 Razor views, 21 MVC + Admin controllers, identity Razor pages, 10 REST API controllers). |
| 4 | |
| 5 | See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for layout, reference rules, contracts, per-module migration commands, and the `[NotMapped]` cross-module navigation caveat. |
| 6 | |
| 7 | ## Run |
| 8 | |
| 9 | ### With Docker (recommended) |
| 10 | |
| 11 | From the repo root (one level up from this directory): |
| 12 | |
| 13 | ```bash |
| 14 | docker compose up --build |
| 15 | ``` |
| 16 | |
| 17 | **Production deployment:** https://travel.rasmusj.com/ |
| 18 | |
| 19 | Locally, phase 3 listens on **http://localhost:90** (host port `90` → container port `8080`). Each module's migrations are applied automatically on startup. The container is named `phase3`. Postgres uses dedicated schemas `users` / `trips` / `expenses` inside the same `splitapp` database (DB is internal-only — not exposed to host). |
| 20 | |
| 21 | The single `Dockerfile` lives at the repo root and copies from `SplitApp.Modular/`. There is no separate Dockerfile inside this directory — the root one is canonical. |
| 22 | |
| 23 | ### Without Docker |
| 24 | |
| 25 | ```bash |
| 26 | docker compose up -d db |
| 27 | dotnet run --project src/SplitApp.WebApp |
| 28 | ``` |
| 29 | |
| 30 | By default `dotnet run` uses `https://localhost:7133` / `http://localhost:5297` (see `Properties/launchSettings.json`). |
| 31 | |
| 32 | ## URL map |
| 33 | |
| 34 | | URL | Purpose | |
| 35 | |-----|---------| |
| 36 | | `/` | Landing page (MVC) | |
| 37 | | `/Trips`, `/Trips/Create`, `/Trips/Details/{id}`, `/Trips/Edit/{id}`, `/Trips/Delete/{id}` | Trip CRUD | |
| 38 | | `/Members?tripId={id}` and `/Members/AcceptInvitation/{token}` | Trip participants + invitation flow | |
| 39 | | `/Expenses?tripId={id}` (with Create/Edit/Delete) | Trip expenses | |
| 40 | | `/Budget?tripId={id}` (with CreateCategory/EditCategory/DeleteCategory) | Budget categories per trip | |
| 41 | | `/Settlement?tripId={id}` | Balances + settlement plans | |
| 42 | | `/PollsClient?tripId={id}` (Create/Details) | Trip polls | |
| 43 | | `/WishlistClient?tripId={id}` (Create/Edit/Delete) | Trip wishlist | |
| 44 | | `/Identity/Account/Register` | Cookie-based register | |
| 45 | | `/Admin/Dashboard` | Admin home (`admin` role) | |
| 46 | | `/Admin/{Users, Trips, Expenses, BudgetCategories, Currencies, Invitations, Polls, SettlementPlans, SettlementPayments, SplitPresets, TripParticipants, Wishlist}` | Admin CRUD over each entity | |
| 47 | | `/swagger` | Swagger UI listing every module's REST endpoints | |
| 48 | |
| 49 | ## REST API |
| 50 | |
| 51 | | Module | Endpoints | |
| 52 | |--------|-----------| |
| 53 | | Users | `/api/v1/identity/account/{register, login, logout, refreshtokendata}` | |
| 54 | | Trips | `/api/v1/trips`, `/api/v1/budgetcategories`, `/api/v1/invitations`, `/api/v1/polls`, `/api/v1/wishlist` | |
| 55 | | Expenses | `/api/v1/expenses`, `/api/v1/currencies`, `/api/v1/settlements`, `/api/v1/splitpresets` | |
| 56 | |
| 57 | ## Tests |
| 58 | |
| 59 | ```bash |
| 60 | dotnet test |
| 61 | ``` |
| 62 | |
| 63 | **25 tests** across four projects: |
| 64 | |
| 65 | - **Per-module unit tests** — `CurrencyConverter` (5), `LangStr` (7), `IdentityHelpers` JWT round-trips (4) |
| 66 | - **Architecture invariants** (in `tests/SplitApp.WebApp.IntegrationTests/Architecture/`): |
| 67 | - `ModuleBoundaryTests` — no module's `Application`/`Infrastructure`/`Api` may `<ProjectReference>` another module |
| 68 | - `DbContextSchemaIsolationTests` — every `DbContext` exposes `DbSet<T>` only for entities in its own `Domain` project |
| 69 | - `CrossModuleNavigationTests` — cross-module navigation properties allowed only when `[NotMapped]`; the WebApp facade hydrates them in-memory after loading from the owning module's DbContext, so EF never crosses schemas |
| 70 | - **Smoke** — `WebApplicationFactory<Program>` boots the full host in `Testing` env (skipping migrations) and serves `/`, `/Home/Index`, returns 401 on unauthenticated API hit |
| 71 | |
| 72 | A failing architecture test means a developer just violated the modular-monolith invariant. |
| 73 | |
| 74 | ## Module owners |
| 75 | |
| 76 | | Module | Owns | Schema | |
| 77 | |--------|------|--------| |
| 78 | | Users | Identity, JWT issuance, refresh tokens, user profile | `users` | |
| 79 | | Trips | Trips, participants, invitations, polls, wishlists, budget categories | `trips` | |
| 80 | | Expenses | Expenses, splits, settlement plans, settlement payments, split presets, currencies | `expenses` | |
| 81 | |
| 82 | ## Caveat — `[NotMapped]` cross-module navigation properties |
| 83 | |
| 84 | To preserve phase 2's view-rendering parity (BLL DTO factories that read `Trip.DefaultCurrency.Code`, `TripParticipant.User.FirstName`, etc.) the entity classes still declare those navigation properties — annotated `[NotMapped]` so EF never crosses schemas. The `CrossModuleNavigationLoader` in `src/SplitApp.WebApp/Application/Persistence/` populates them in C# after entity load by querying the appropriate module's DbContext. |
| 85 | |
| 86 | Keeping the property *types* on the entities required adding `<ProjectReference>` between Domain projects: |
| 87 | |
| 88 | ``` |
| 89 | Modules/Trips/SplitApp.Modules.Trips.Domain |
| 90 | → Modules/Users/SplitApp.Modules.Users.Domain (for AppUser refs) |
| 91 | → Modules/Expenses/SplitApp.Modules.Expenses.Domain (for Currency refs) |
| 92 | Modules/Expenses/SplitApp.Modules.Expenses.Domain |
| 93 | → Modules/Users/SplitApp.Modules.Users.Domain (for AppUser refs) |
| 94 | ``` |
| 95 | |
| 96 | This bends the strict "no direct references between modules" rule from `phase3.md` at the **Domain** level. **`Application`, `Infrastructure`, and `Api` projects remain isolated** — they never `<ProjectReference>` another module — and continue to use MediatR for actual function calls. Schema isolation, MediatR-only inter-module communication, and per-module DbContext ownership are all preserved at runtime; only the entity *type system* is shared. |
| 97 | |