- Oleksii Nawrocki - PM
- Tomasz Nowak
- Jakub Czesnak
- Dawid Bajek
- Mateusz Tokarz
Trippy to kompleksowa aplikacja mobilna (Android) oparta o architekturę klient-serwer, stworzona z myślą o osobach podróżujących w grupach. Rozwiązuje problem transparentnego zarządzania budżetem i sprawiedliwego rozliczania kosztów podczas wspólnych wyjazdów.
Zamiast żmudnego wpisywania wydatków do arkuszy kalkulacyjnych, Trippy oferuje intuicyjny interfejs, w którym uczestnicy mogą na bieżąco dodawać koszty, określać kto za nie zapłacił i kogo dotyczą. Zaawansowany silnik backendowy dba o bezpieczeństwo danych, przeliczanie bilansów i synchronizację między użytkownikami.
Projekt realizowany jest w architekturze mikrousługowej/monolitycznej z wykorzystaniem konteneryzacji, co ułatwia wdrożenie i utrzymanie spójnego środowiska deweloperskiego.
Architektura bazy danych opiera się na relacyjnym modelu (PostgreSQL) i została zaprojektowana w taki sposób, aby sprawnie zarządzać użytkownikami, wyjazdami oraz skomplikowanymi rozliczeniami. Składa się z czterech głównych obszarów:
- USER: Centralna tabela przechowująca dane kont, zabezpieczone hasła, statusy weryfikacji/blokady oraz preferencje (domyślna waluta).
- ROLE / CURRENCY / TRIP_ROLE: Tabele słownikowe standaryzujące uprawnienia w aplikacji, dostępne waluty oraz role pełnione podczas konkretnego wyjazdu (np. Organizator, Uczestnik).
- TRIP_EVENT: Reprezentuje konkretny wyjazd. Zawiera powiązanie z twórcą (
owner), nazwę, ramy czasowe, ustalony główny budżet oraz główną walutę wyjazdu. - TRIP_PARTICIPANTS: Tabela asocjacyjna łącząca wycieczkę z użytkownikami. Zarządza systemem zaproszeń (
isAccepted), indywidualnym budżetem uczestnika na danym wyjeździe oraz jego specjalną rolą (TRIP_ROLE).
- TRIP_NODE: Kluczowa tabela reprezentująca punkty na mapie wyjazdu, wydarzenia w harmonogramie lub konkretne transakcje. Każdy węzeł posiada czas trwania, koszt (
price), przypisanego autora (reporter) oraz flagęisSeparate, decydującą o tym, czy dany wydatek wchodzi w skład równego podziału kosztów.
- TRIP_POST: Wpisy (notatki, przemyślenia) dodawane przez uczestników do konkretnych węzłów wyjazdu.
- TRIP_PHOTO: Adresy URL zdjęć powiązane z konkretnymi wpisami, pozwalające na tworzenie wirtualnego albumu podróży.
- Frontend (Mobile): Android (Kotlin, Jetpack Compose)
- Backend: Java, Spring Boot, Spring Security
- Baza Danych: PostgreSQL, Flyway / Liquibase
- Infrastruktura: Docker, Docker Compose
Celem MVP jest dostarczenie podstawowego przepływu pracy – od rejestracji, poprzez utworzenie wyjazdu i dodanie wydatków, aż po pokazanie prostego bilansu uczestników. Wersja ta zakłada równy podział kosztów i operowanie na jednej walucie.
Poniżej znajduje się specyfikacja głównych wymagań systemu w fazie MVP.
- Zarządzanie kontem (Autoryzacja): Użytkownik musi mieć możliwość rejestracji i logowania przy użyciu adresu email i hasła (zabezpieczone tokenem JWT).
- Tworzenie podróży (Trips): Zalogowany użytkownik może utworzyć nową podróż (podając jej nazwę oraz ramy czasowe), a także przeglądać listę swoich wyjazdów.
- Zarządzanie uczestnikami: Twórca podróży (Owner) ma możliwość dodawania innych zarejestrowanych w systemie użytkowników do danego wyjazdu.
- Ewidencja wydatków (Expenses): Każdy uczestnik podróży może dodać nowy wydatek, podając kwotę, tytuł wydatku (np. "Paliwo") oraz oznaczając osobę, która założyła pieniądze.
- Podgląd bilansu (Balances): System musi automatycznie obliczać i wyświetlać aktualny bilans dla każdego uczestnika (podział kosztów po równo), pokazując kto ma nadpłatę, a kto i ile jest winien grupie.
- Konteneryzacja środowiska: Cały backend (aplikacja Spring Boot oraz baza PostgreSQL) musi być uruchamiany lokalnie za pomocą jednego polecenia, wykorzystując plik
docker-compose.yml. - Bezpieczeństwo danych: Hasła użytkowników muszą być bezwzględnie haszowane w bazie danych (np. przy użyciu algorytmu BCrypt), a API chronione przed nieautoryzowanym dostępem.
- Responsywność i wydajność mobilna: Aplikacja kliencka na Androida musi działać płynnie, poprawnie obsługiwać stany ładowania/błędów sieciowych i być kompatybilna z urządzeniami z systemem Android 8.0 (API 26) lub nowszym.
W celu zapewnienia spójności, stabilności bazy kodu oraz ujednolicenia procesu integracji, zespół projektowy stosuje rygorystyczne zasady pracy z systemem Git. Proces oparty jest na zdefiniowanej hierarchii gałęzi (branches) oraz obowiązkowych przeglądach kodu (Code Review).
Architektura repozytorium opiera się na dwóch głównych gałęziach o nieograniczonym cyklu życia:
main– Gałąź produkcyjna. Zawiera wyłącznie kod przetestowany, stabilny i gotowy do wdrożenia (wersje wydań / MVP). Bezpośrednie wprowadzanie zmian (tzw. direct commit) do tej gałęzi jest surowo zabronione. Zmiany trafiają tu wyłącznie poprzez proces fuzji (merge).develop– Główna gałąź integracyjna. Agreguje zatwierdzone zmiany z gałęzi roboczych. Pełni funkcję bazy do tworzenia nowych odgałęzień i jest docelowym miejscem fuzji dla nowo zaimplementowanych funkcjonalności.
Każde nowe zadanie (funkcjonalność, poprawka, dokumentacja) wymaga utworzenia dedykowanej, tymczasowej gałęzi roboczej. Nazewnictwo musi być zgodne z formatem TR-XX-typ/krotki-opis-zadania, gdzie opis zapisany jest w formacie kebab-case (małe litery, słowa oddzielone myślnikiem). Natomiast na początku nazwy brancha należy umieścić numer ticketa (np. TR-01), żeby Jira była w stanie automatycznie zmieniać statusy ticketów w zależności od naszych interakcji.
Dopuszczalne prefiksy (typy):
TR-XX-feature/<nazwa>– Implementacja nowej funkcjonalności systemu (np.TR-01-feature/jwt-authentication,TR-01-feature/expense-entity). Gałąź tworzona zdevelop.TR-XX-bugfix/<nazwa>– Usunięcie błędu zidentyfikowanego w środowisku deweloperskim (np.TR-01-bugfix/balance-calculation-error). Gałąź tworzona zdevelop.TR-XX-hotfix/<nazwa>– Krytyczna poprawka błędu na środowisku produkcyjnym. Tworzona bezpośrednio z gałęzimain. Po zakończeniu i weryfikacji prac, gałąź jest włączana (merge) zarówno domain, jak i dodevelop(np.TR-01-hotfix/database-connection-loss).TR-XX-documentation/<nazwa>– Tworzenie, rozbudowa lub aktualizacja dokumentacji technicznej oraz plików konfiguracyjnych (np.TR-01-documentation/api-endpoints,TR-01-documentation/readme-update).TR-XX-refactor/<nazwa>– Restrukturyzacja i optymalizacja istniejącego kodu, niepociągająca za sobą zmian w jego obserwowalnym zachowaniu zewnętrznym (np.TR-01-refactor/user-service-architecture).
Wprowadzanie zmian do głównej linii kodu podlega ustandaryzowanemu procesowi:
- Synchronizacja: Pobranie aktualnego stanu gałęzi bazowej (
git pull origin develop). - Inicjalizacja: Utworzenie nowej gałęzi roboczej zgodnie z konwencją nazewnictwa (
git checkout -b <typ>/<nazwa>). - Implementacja: Wprowadzenie zmian, poprawne sformułowanie wiadomości commitów i ich lokalne zatwierdzenie.
- Publikacja: Przesłanie zmian do zdalnego repozytorium (
git push origin <typ>/<nazwa>). - Żądanie integracji: Utworzenie Pull Requesta (PR) do gałęzi docelowej (standardowo
develop). - Code Review: Obowiązkowa weryfikacja jakości i poprawności kodu przeprowadzona przez minimum jednego, innego członka zespołu.
- Fuzja (Merge): Włączenie kodu do gałęzi docelowej po uzyskaniu akceptacji roboczej (Approval) i pozytywnym przejściu ewentualnych testów zautomatyzowanych.
trippy/
|- backend/
|- mobile/
|- database/
|- .env
|- .gitignore
|- docker-compose.yml
Testy weryfikujące poprawność i bezpieczeństwo tworzenia nowych kont użytkowników.
| Nazwa Metody Testowej | Scenariusz (Given) | Akcja (When) | Oczekiwany Wynik (Then) |
|---|---|---|---|
shouldSuccessfullyRegisterUserAndReturnJwt |
Email jest wolny, hasło poprawne. | Wywołanie register(). |
Generuje JWT. Weryfikuje 1x zapis do DB, 1x zapis tokena i 1x wysyłkę emaila. |
shouldThrowExceptionWhenEmailIsAlreadyTaken |
Baza zwraca istniejącego użytkownika dla podanego emaila. | Wywołanie register(). |
Wyrzuca IllegalArgumentException. Udowadnia, że NIE wykonano zapisu do DB ani nie wysłano maila. |
shouldEnforceSecurityConstraintsOnNewUser |
Poprawne dane wejściowe. | Wywołanie register(). |
Przechwytuje (Captor) obiekt User. Sprawdza czy: hasło jest zaszyfrowane, rola to USER, a isVerified to false. |
shouldGenerateValidVerificationToken |
Poprawne dane wejściowe. | Wywołanie register(). |
Przechwytuje obiekt VerificationToken. Sprawdza, czy token nie jest pusty i ma poprawną datę wygaśnięcia. |
Testy weryfikujące proces uwierzytelniania, napisane w architekturze Fail-Fast.
| Nazwa Metody Testowej | Scenariusz (Given) | Akcja (When) | Oczekiwany Wynik (Then) |
|---|---|---|---|
shouldSuccessfullyAuthenticateAndReturnJwt |
Konto jest zweryfikowane (isVerified=true), poświadczenia są poprawne. |
Wywołanie authenticate(). |
Zwraca token JWT, weryfikuje poprawne odpytanie AuthenticationManager. |
shouldThrowExceptionWhenUserIsNotVerified |
Użytkownik podał dobre hasło, ale konto jest nieaktywne (isVerified=false). |
Wywołanie authenticate(). |
Wyrzuca RuntimeException. Weryfikuje, że JwtService NIGDY nie wydał tokena. |
shouldPropagateExceptionWhenBadCredentials |
AuthManager odrzuca hasło użytkownika. |
Wywołanie authenticate(). |
Wyrzuca BadCredentialsException. Weryfikuje, że w celach optymalizacji w ogóle NIE odpytano bazy danych. |
shouldThrowExceptionWhenUserNotFoundInDatabase |
Hasło przeszło, ale użytkownik nagle zniknął z bazy (Edge case). | Wywołanie authenticate(). |
Wyrzuca NoSuchElementException. Token nie zostaje wygenerowany. |
Testy algorytmów HMAC SHA-256 oraz obsługi JSON Web Tokens (izolowane od kontekstu Springa).
| Nazwa Metody Testowej | Scenariusz (Given) | Akcja (When) | Oczekiwany Wynik (Then) |
|---|---|---|---|
shouldGenerateTokenAndExtractUsername |
Ręcznie wstrzyknięty SecretKey i UserDetails. | Generowanie tokena. | Token nie jest nullem, a wyciągnięty Subject zgadza się z mailem. |
shouldReturnTrueWhenTokenIsValid |
Wygenerowany, świeży token dla danego usera. | Walidacja isTokenValid(). |
Zwraca wartość true. |
shouldReturnFalseWhenTokenBelongsToAnotherUser |
Token usera A sprawdzany na koncie usera B. | Walidacja isTokenValid(). |
Zwraca wartość false. |
shouldThrowExceptionWhenTokenIsExpired |
Czas życia tokena skrócony do 1ms. Wątek uśpiony na 10ms. | Walidacja isTokenValid(). |
Wyrzuca wyjątek ExpiredJwtException. |
Weryfikacja integracji z systemem SMTP przy pomocy zaślepek (Mocks).
| Nazwa Metody Testowej | Scenariusz (Given) | Akcja (When) | Oczekiwany Wynik (Then) |
|---|---|---|---|
shouldSendVerificationEmailWithCorrectContent |
Zdefiniowany adres email docelowy i UUID tokena. | Wywołanie sendVerificationEmail(). |
Przechwytuje obiekt SimpleMailMessage. Sprawdza poprawność nadawcy, odbiorcy, tytułu i gwarantuje obecność linku z tokenem w ciele wiadomości. |
Weryfikacja logiki aktywacji konta przez endpointy REST API.
| Nazwa Metody Testowej | Scenariusz (Given) | Akcja (When) | Oczekiwany Wynik (Then) |
|---|---|---|---|
shouldSuccessfullyVerifyEmail |
W bazie istnieje token, jego data ważności jest poprawna. | GET /api/auth/verify?token=... |
Zwraca HTTP 200 (OK). Flaga użytkownika zmienia się na isVerified=true. Token jest usuwany z bazy. |
shouldReturnBadRequestWhenTokenIsExpired |
W bazie istnieje token, ale jego data wygasła. | GET /api/auth/verify?token=... |
Zwraca HTTP 400 (Bad Request). Flaga użytkownika to wciąż false. |
shouldThrowExceptionWhenTokenDoesNotExist |
Podano zmyślony/błędny token. | GET /api/auth/verify?token=fake |
Wyrzuca RuntimeException ("Nieprawidłowy token"). |
docker-compose up -d --build
docker-compose down -v
